#!/bin/bash ################################################################################ # PWA Health Check Script # Version: 1.1.1 ################################################################################ # This script checks whether a website meets Progressive Web App requirements. # # Usage: # ./pwa-health-check.sh [URL] # # Requirements: # - curl, jq, openssl ################################################################################ set -e set -u # Script version SCRIPT_VERSION="1.1.1" # Configuration TIMEOUT=10 # Color codes RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' MAGENTA='\033[0;35m' NC='\033[0m' # Symbols CHECK_MARK="✓" CROSS_MARK="✗" WARNING="⚠" INFO="ℹ" # Test counters TESTS_TOTAL=0 TESTS_PASSED=0 TESTS_FAILED=0 TESTS_WARNING=0 # Global variables TARGET_URL="" HTML_CONTENT="" MANIFEST_CONTENT="" ################################################################################ # Helper Functions ################################################################################ print_header() { echo -e "\n${MAGENTA}═══════════════════════════════════════${NC}" echo -e "${MAGENTA}$1${NC}" echo -e "${MAGENTA}═══════════════════════════════════════${NC}\n" } print_section() { echo -e "\n${BLUE}━━━ $1 ━━━${NC}\n" } print_pass() { echo -e "${GREEN}${CHECK_MARK}${NC} $1" TESTS_PASSED=$((TESTS_PASSED + 1)) TESTS_TOTAL=$((TESTS_TOTAL + 1)) } print_fail() { echo -e "${RED}${CROSS_MARK}${NC} $1" TESTS_FAILED=$((TESTS_FAILED + 1)) TESTS_TOTAL=$((TESTS_TOTAL + 1)) } print_warning() { echo -e "${YELLOW}${WARNING}${NC} $1" TESTS_WARNING=$((TESTS_WARNING + 1)) TESTS_TOTAL=$((TESTS_TOTAL + 1)) } print_info() { echo -e "${CYAN}${INFO}${NC} $1" } print_detail() { echo -e " ${NC}$1${NC}" } command_exists() { command -v "$1" >/dev/null 2>&1 } ################################################################################ # Dependency Checks ################################################################################ check_dependencies() { local missing_deps=() for cmd in curl jq openssl; do if ! command_exists "$cmd"; then missing_deps+=("$cmd") fi done if [ ${#missing_deps[@]} -gt 0 ]; then echo -e "${RED}Error: Missing required dependencies: ${missing_deps[*]}${NC}" echo "Install with: sudo apt-get install ${missing_deps[*]}" exit 1 fi } normalize_url() { local url="$1" # Add https:// if no protocol specified if [[ ! "$url" =~ ^https?:// ]]; then url="https://$url" fi # Remove trailing slash url="${url%/}" echo "$url" } ################################################################################ # Test 1: HTTPS Check ################################################################################ check_https() { print_section "1. HTTPS Security" if [[ "$TARGET_URL" =~ ^https:// ]]; then if curl -sI --max-time "$TIMEOUT" "$TARGET_URL" >/dev/null 2>&1; then print_pass "Site is served over HTTPS" else print_fail "Cannot connect to site via HTTPS" return 1 fi else print_fail "Site is NOT served over HTTPS - PWA requires HTTPS" print_detail "HTTP is only allowed for localhost during development" return 1 fi } ################################################################################ # Test 2: Fetch HTML ################################################################################ fetch_html() { print_section "2. Fetching Site Content" HTML_CONTENT=$(curl -sL --max-time "$TIMEOUT" "$TARGET_URL" 2>/dev/null) if [ -n "$HTML_CONTENT" ]; then print_pass "Successfully fetched HTML content" else print_fail "Failed to fetch HTML content" return 1 fi } ################################################################################ # Test 3: Manifest Check ################################################################################ check_manifest() { print_section "3. Web App Manifest" # Find manifest link in HTML local manifest_link=$(echo "$HTML_CONTENT" | grep -oP ']*rel="manifest"[^>]*href="\K[^"]+' | head -1) if [ -z "$manifest_link" ]; then print_fail "No manifest link found in HTML" return 1 fi print_pass "Manifest link found: $manifest_link" # Construct full manifest URL local manifest_url if [[ "$manifest_link" =~ ^https?:// ]]; then manifest_url="$manifest_link" elif [[ "$manifest_link" =~ ^/ ]]; then local base_url=$(echo "$TARGET_URL" | grep -oP '^https?://[^/]+') manifest_url="$base_url$manifest_link" else manifest_url="$TARGET_URL/$manifest_link" fi # Fetch manifest MANIFEST_CONTENT=$(curl -sL --max-time "$TIMEOUT" "$manifest_url" 2>/dev/null) if [ -z "$MANIFEST_CONTENT" ]; then print_fail "Failed to fetch manifest file" return 1 fi # Validate JSON if echo "$MANIFEST_CONTENT" | jq empty 2>/dev/null; then print_pass "Manifest is valid JSON" else print_fail "Manifest is not valid JSON" return 1 fi check_manifest_fields } check_manifest_fields() { local manifest="$MANIFEST_CONTENT" # Check name local name=$(echo "$manifest" | jq -r '.name // empty' 2>/dev/null) local short_name=$(echo "$manifest" | jq -r '.short_name // empty' 2>/dev/null) if [ -n "$name" ]; then print_pass "Name: \"$name\"" elif [ -n "$short_name" ]; then print_pass "Short name: \"$short_name\"" else print_fail "Missing 'name' or 'short_name' property" fi # Check icons local icons_count=$(echo "$manifest" | jq '.icons | length' 2>/dev/null || echo "0") if [ "$icons_count" -gt 0 ]; then print_pass "Icons defined: $icons_count icons" # Check for required sizes local has_192=$(echo "$manifest" | jq '.icons[] | select(.sizes | contains("192x192"))' 2>/dev/null) local has_512=$(echo "$manifest" | jq '.icons[] | select(.sizes | contains("512x512"))' 2>/dev/null) if [ -n "$has_192" ]; then print_pass "Has 192x192 icon - required for installability" else print_fail "Missing 192x192 icon - required for PWA" fi if [ -n "$has_512" ]; then print_pass "Has 512x512 icon - required for splash screen" else print_warning "Missing 512x512 icon - recommended for splash screen" fi else print_fail "No icons defined in manifest" fi # Check start_url local start_url=$(echo "$manifest" | jq -r '.start_url // empty' 2>/dev/null) if [ -n "$start_url" ]; then print_pass "Start URL: \"$start_url\"" else print_warning "Missing 'start_url' - recommended" fi # Check display mode local display=$(echo "$manifest" | jq -r '.display // empty' 2>/dev/null) if [ -n "$display" ]; then print_pass "Display mode: \"$display\"" if [[ "$display" =~ ^(standalone|fullscreen|minimal-ui)$ ]]; then print_detail "Good! Display mode is PWA-friendly" fi else print_warning "Missing 'display' property - recommended: standalone" fi # Check theme_color local theme_color=$(echo "$manifest" | jq -r '.theme_color // empty' 2>/dev/null) if [ -n "$theme_color" ]; then print_pass "Theme color: $theme_color" else print_warning "Missing 'theme_color' - recommended" fi # Check background_color local background_color=$(echo "$manifest" | jq -r '.background_color // empty' 2>/dev/null) if [ -n "$background_color" ]; then print_pass "Background color: $background_color" else print_warning "Missing 'background_color' - recommended for splash screen" fi # Check description local description=$(echo "$manifest" | jq -r '.description // empty' 2>/dev/null) if [ -n "$description" ]; then print_pass "Description present" else print_warning "Missing 'description' - recommended" fi } ################################################################################ # Test 4: Service Worker ################################################################################ check_service_worker() { print_section "4. Service Worker" # Common service worker filenames local sw_files=("service-worker.js" "sw.js" "serviceworker.js" "firebase-messaging-sw.js") local found_sw=false local base_url=$(echo "$TARGET_URL" | grep -oP '^https?://[^/]+') # Check if service worker is registered in HTML if echo "$HTML_CONTENT" | grep -q "serviceWorker.register"; then print_pass "Service worker registration found in HTML" else print_warning "No service worker registration found in HTML" fi # Try to fetch common service worker files for sw_file in "${sw_files[@]}"; do local sw_url="$base_url/$sw_file" local status_code=$(curl -s -o /dev/null -w "%{http_code}" --max-time "$TIMEOUT" "$sw_url" 2>/dev/null || echo "000") if [[ "$status_code" =~ ^2 ]]; then print_pass "Service worker file found: $sw_file - HTTP $status_code" found_sw=true # Check service worker content local sw_content=$(curl -sL --max-time "$TIMEOUT" "$sw_url" 2>/dev/null || true) if echo "$sw_content" | grep -q "addEventListener.*fetch"; then print_pass "Service worker has fetch event handler - offline support" else print_warning "Service worker may not handle fetch events - limited offline" fi if echo "$sw_content" | grep -q "addEventListener.*install"; then print_pass "Service worker has install event handler" fi if echo "$sw_content" | grep -q "addEventListener.*activate"; then print_pass "Service worker has activate event handler" fi break fi done if [ "$found_sw" = false ]; then print_warning "Could not locate service worker file" fi } ################################################################################ # Test 5: HTML Meta Tags ################################################################################ check_meta_tags() { print_section "5. HTML Meta Tags" # Viewport meta tag if echo "$HTML_CONTENT" | grep -q ']*name="viewport"'; then print_pass "Viewport meta tag present" else print_fail "Missing viewport meta tag - required for responsive PWA" fi # Theme color meta tag if echo "$HTML_CONTENT" | grep -q ']*name="theme-color"'; then local theme_color=$(echo "$HTML_CONTENT" | grep -oP ']*name="theme-color"[^>]*content="\K[^"]+' | head -1) print_pass "Theme color meta tag: $theme_color" else print_warning "Missing theme-color meta tag - recommended" fi # Description meta tag if echo "$HTML_CONTENT" | grep -q ']*name="description"'; then print_pass "Description meta tag present" else print_warning "Missing description meta tag - recommended for SEO" fi # Apple mobile web app capable if echo "$HTML_CONTENT" | grep -q ']*name="apple-mobile-web-app-capable"'; then print_pass "Apple mobile web app capable tag present" else print_info "Missing apple-mobile-web-app-capable - optional for iOS" fi # Apple touch icon if echo "$HTML_CONTENT" | grep -q ']*rel="apple-touch-icon"'; then print_pass "Apple touch icon present - good for iOS" else print_info "Missing apple-touch-icon - optional for iOS home screen" fi } ################################################################################ # Test 6: Basic Performance ################################################################################ check_performance() { print_section "6. Basic Performance Check" # Measure page load time local start_time=$(date +%s%N) curl -sL --max-time "$TIMEOUT" "$TARGET_URL" >/dev/null 2>&1 local end_time=$(date +%s%N) local load_time_ms=$(( (end_time - start_time) / 1000000 )) if [ "$load_time_ms" -lt 1000 ]; then print_pass "Page load time: ${load_time_ms}ms - Excellent!" elif [ "$load_time_ms" -lt 3000 ]; then print_warning "Page load time: ${load_time_ms}ms - Acceptable, could be faster" else print_fail "Page load time: ${load_time_ms}ms - Too slow for good UX" fi # Check for HTTP/2 local http_version=$(curl -sI --http2 "$TARGET_URL" 2>/dev/null | grep -i "^http/" | head -1) if echo "$http_version" | grep -q "HTTP/2"; then print_pass "HTTP/2 supported - better performance" else print_warning "HTTP/2 not detected - recommended for PWA performance" fi # Check for compression local content_encoding=$(curl -sI -H "Accept-Encoding: gzip, deflate, br" "$TARGET_URL" 2>/dev/null | grep -i "^content-encoding:" | cut -d: -f2 | tr -d ' \r') if [ -n "$content_encoding" ]; then print_pass "Compression enabled: $content_encoding" else print_warning "No compression detected - recommended: gzip or brotli" fi } ################################################################################ # Test 7: Lighthouse PWA Audit (Optional) ################################################################################ run_lighthouse() { print_section "7. Lighthouse PWA Audit - Optional" if ! command_exists lighthouse; then print_info "Lighthouse not installed - skipping detailed PWA audit" print_detail "Install with: npm install -g lighthouse" return 0 fi print_info "Running Lighthouse PWA audit - this may take 30-60 seconds..." # Create temp directory for report local temp_dir=$(mktemp -d) local report_file="$temp_dir/lighthouse-report.json" # Run Lighthouse in headless mode (v13+ removed pwa category, now part of best-practices) if lighthouse "$TARGET_URL" \ --output=json \ --output-path="$report_file" \ --chrome-flags="--headless --no-sandbox --disable-gpu" \ --quiet 2>/dev/null; then # Parse Lighthouse results # In Lighthouse v13+, check best-practices category for PWA-related audits local best_practices_score=$(jq -r '.categories["best-practices"].score * 100' "$report_file" 2>/dev/null || echo "0") if [ "$best_practices_score" != "null" ] && [ -n "$best_practices_score" ]; then # Convert to integer for comparison local score_int=${best_practices_score%.*} if [ "$score_int" -ge 90 ]; then print_pass "Lighthouse Best Practices Score: ${best_practices_score}% - Excellent!" elif [ "$score_int" -ge 70 ]; then print_warning "Lighthouse Best Practices Score: ${best_practices_score}% - Good, but can be improved" else print_fail "Lighthouse Best Practices Score: ${best_practices_score}% - Needs improvement" fi # Show PWA-related audits from best-practices print_detail "PWA-related findings:" # Check for specific PWA audits local pwa_audit_ids=("viewport" "service-worker" "installable-manifest" "splash-screen" "themed-omnibox" "maskable-icon") local found_issues=false for audit_id in "${pwa_audit_ids[@]}"; do local audit_score=$(jq -r ".audits.\"$audit_id\".score" "$report_file" 2>/dev/null) local audit_title=$(jq -r ".audits.\"$audit_id\".title" "$report_file" 2>/dev/null) if [ "$audit_score" != "null" ] && [ "$audit_title" != "null" ]; then if [ "$audit_score" != "1" ]; then found_issues=true if [ "$audit_score" == "0" ]; then echo -e " ${RED}✗${NC} $audit_title" else echo -e " ${YELLOW}⚠${NC} $audit_title - partial pass" fi fi fi done if [ "$found_issues" = false ]; then print_detail "All PWA audits passed!" fi # Show performance metrics local performance_score=$(jq -r '.categories.performance.score * 100' "$report_file" 2>/dev/null || echo "N/A") if [ "$performance_score" != "null" ] && [ "$performance_score" != "N/A" ]; then print_detail "Performance Score: ${performance_score}%" fi local accessibility_score=$(jq -r '.categories.accessibility.score * 100' "$report_file" 2>/dev/null || echo "N/A") if [ "$accessibility_score" != "null" ] && [ "$accessibility_score" != "N/A" ]; then print_detail "Accessibility Score: ${accessibility_score}%" fi local best_practices_score=$(jq -r '.categories["best-practices"].score * 100' "$report_file" 2>/dev/null || echo "N/A") if [ "$best_practices_score" != "null" ] && [ "$best_practices_score" != "N/A" ]; then print_detail "Best Practices Score: ${best_practices_score}%" fi local seo_score=$(jq -r '.categories.seo.score * 100' "$report_file" 2>/dev/null || echo "N/A") if [ "$seo_score" != "null" ] && [ "$seo_score" != "N/A" ]; then print_detail "SEO Score: ${seo_score}%" fi # Offer to save full report print_detail "Full report saved to: $report_file" print_info "View HTML report with: lighthouse $TARGET_URL --view" else print_warning "Could not parse Lighthouse PWA score" fi else print_warning "Lighthouse audit failed or timed out" print_detail "This may happen if Chrome/Chromium is not installed" fi # Clean up (optional - keep for debugging) # rm -rf "$temp_dir" } ################################################################################ # Summary Report ################################################################################ print_summary() { print_header "PWA Health Check Summary" local total=$TESTS_TOTAL local passed=$TESTS_PASSED local failed=$TESTS_FAILED local warnings=$TESTS_WARNING echo -e "Target URL: ${CYAN}$TARGET_URL${NC}\n" echo -e "Results:" echo -e " ${GREEN}Passed:${NC} $passed" echo -e " ${RED}Failed:${NC} $failed" echo -e " ${YELLOW}Warnings:${NC} $warnings" echo -e " ${BLUE}Total:${NC} $total" echo "" # Calculate status if [ "$total" -gt 0 ]; then local pass_percentage=$(( (passed * 100) / total )) if [ "$failed" -eq 0 ] && [ "$pass_percentage" -ge 90 ]; then echo -e "${GREEN}${CHECK_MARK} PWA Status: EXCELLENT${NC}" echo -e "Your site meets all critical PWA requirements and is ready for installation!" elif [ "$failed" -eq 0 ]; then echo -e "${GREEN}${CHECK_MARK} PWA Status: GOOD${NC}" echo -e "Your site meets PWA requirements. Consider addressing warnings." elif [ "$failed" -le 2 ]; then echo -e "${YELLOW}${WARNING} PWA Status: NEEDS IMPROVEMENT${NC}" echo -e "Your site has some PWA features but needs fixes to be installable." else echo -e "${RED}${CROSS_MARK} PWA Status: NOT A PWA${NC}" echo -e "Your site does not meet PWA requirements. Address the failed checks." fi fi echo "" echo -e "${BLUE}${INFO} Next Steps:${NC}" echo -e "1. Fix any failed checks marked with ${RED}✗${NC}" echo -e "2. Address warnings for better user experience marked with ${YELLOW}⚠${NC}" echo -e "3. Test installation on mobile devices" echo -e "4. Test offline functionality" echo -e "5. Review Lighthouse audit results if available above" echo -e "6. Generate full HTML Lighthouse report: lighthouse $TARGET_URL --view" echo "" } ################################################################################ # Main Execution ################################################################################ main() { # Print banner clear echo -e "${MAGENTA}" cat << 'BANNER' ╔═══════════════════════════════════════════════════════════════╗ ║ ║ ║ PWA Health Check Tool v1.1.1 ║ ║ Progressive Web App Validator ║ ║ ║ ╚═══════════════════════════════════════════════════════════════╝ BANNER echo -e "${NC}" # Check dependencies check_dependencies # Get URL from argument or prompt user if [ $# -eq 1 ]; then TARGET_URL=$(normalize_url "$1") else echo -e "${BLUE}Enter the URL to check:${NC}" read -p "> " user_url if [ -z "$user_url" ]; then echo -e "${RED}Error: URL cannot be empty${NC}" exit 1 fi TARGET_URL=$(normalize_url "$user_url") fi echo -e "\n${CYAN}Checking:${NC} $TARGET_URL\n" # Run all checks (don't exit on error) set +e check_https fetch_html check_manifest check_service_worker check_meta_tags check_performance run_lighthouse set -e # Print summary print_summary } # Run main function with all arguments main "$@"