#!/bin/bash # site-qa.sh - QA for the Grav Website repo and (with -l BASE) the live site. # ./site-qa.sh repo checks: structure, frontmatter, link targets # ./site-qa.sh -l BASE live checks: every known route returns 200, all # internal links/assets resolve, pages have content # Exit non-zero on any failure. Used by CI and the post-deploy sync job. set -uo pipefail cd "$(dirname "$0")/.." FAIL=0 fail() { echo "FAIL: $*"; FAIL=$((FAIL+1)); } route_of() { # pages-relative dir -> URL route (lowercase, strip NN. prefixes) echo "$1" | tr 'A-Z' 'a-z' | sed -E 's|/([0-9]+\.)|/|g; s|^([0-9]+\.)||' } repo_checks() { declare -A ROUTES while IFS= read -r f; do grep -q '^title:' "$f" || fail "no title frontmatter: $f" d=$(dirname "$f"); r=$(route_of "${d#pages/}") ROUTES["${r:-/}"]=1 done < <(find pages -name '*.md') while IFS= read -r f; do d=$(dirname "$f") for link in $(grep -oE '\]\([^)#]+' "$f" | sed -E 's/^\]\(//'); do case "$link" in http*|mailto:*) continue ;; esac t=$(realpath -m --relative-to=. "$(dirname "$f")/$link" 2>/dev/null) || { fail "unresolvable link $link in $f"; continue; } t=${t#pages}; t=$(route_of "$t") [ -n "${ROUTES[${t:-/}]:-}" ] || fail "broken link '$link' in $f (no route '${t:-/}')" done done < <(find pages -name '*.md') echo "repo: ${#ROUTES[@]} routes checked" } live_checks() { local BASE=${1%/} local ROUTES=("/") while IFS= read -r f; do d=$(dirname "$f"); r=$(route_of "${d#pages/}") ROUTES+=("/${r#/}") done < <(find pages -name '*.md' | grep -v 'pages/01.home/home.md') local SITEMAP=/tmp/qa-sitemap.txt CHECKED=/tmp/qa-checked.txt : > $SITEMAP; : > $CHECKED for r in $(printf '%s\n' "${ROUTES[@]}" | sort -u); do code=$(curl -s -o /dev/null -w '%{http_code}' -m 20 --retry 1 "$BASE$r") if [ "$code" = "200" ]; then echo "$BASE$r" >> $SITEMAP; else fail "route $r -> $code"; fi sleep 0.2 done for url in $(cat $SITEMAP); do body=$(curl -s -m 20 "$url"); sleep 0.2 [ ${#body} -lt 500 ] && fail "page too small ($url, ${#body} bytes)" upath=${url#$BASE}; [ -z "$upath" ] && upath=/ for link in $(echo "$body" | grep -oE 'href="[^"]*"' | sed 's/href="//; s/"$//' | sort -u); do case "$link" in http*|mailto:*|\#*) [[ "$link" == "$BASE"* ]] || continue ;; /*) link="$BASE$link" ;; *) link="$BASE$(realpath -m "$upath/../$link" | sed 's|//\+|/|g')" ;; esac grep -qxF "$link" $CHECKED && continue echo "$link" >> $CHECKED case "$link" in *.css|*.js|*.png|*.jpg|*.jpeg|*.svg|*.ico|*.woff*|*.map|*.webp|*.txt|*.xml) ;; *) continue ;; esac code=$(curl -s -o /dev/null -w '%{http_code}' -m 20 "$link"); sleep 0.1 [ "$code" != "200" ] && fail "asset $link -> $code" done for link in $(echo "$body" | grep -oE 'href="[^"]*"' | sed 's/href="//; s/"$//' | sort -u); do case "$link" in http*|mailto:*|\#*) continue ;; esac case "$link" in /*) L="$BASE$link" ;; *) L="$BASE$(realpath -m "$upath/../$link" | sed 's|//\+|/|g')" ;; esac grep -qxF "$L" $CHECKED && continue echo "$L" >> $CHECKED case "$L" in *.css|*.js|*.png|*.jpg|*.jpeg|*.svg|*.ico|*.woff*|*.map|*.webp|*.txt|*.xml) continue ;; esac code=$(curl -s -o /dev/null -w '%{http_code}' -m 20 "$L"); sleep 0.1 [ "$code" != "200" ] && fail "link $L -> $code" done done # canonical must be https (reverse-proxy scheme detection) can=$(curl -s -m 15 "$BASE/" | grep -oE '