#!/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, # canonical URLs are HTTPS # # Exit non-zero on any failure. Used by Gitea Actions, the local pre-push # hook, and the post-deploy verification in the site's sync job. set -uo pipefail cd "$(dirname "$0")/.." FAIL=0 fail() { echo "FAIL: $*"; FAIL=$((FAIL+1)); } # pages-relative dir -> URL route: lowercase, strip NN. numeric prefixes route_of() { local p=${1#/} p=$(echo "$p" | tr 'A-Z' 'a-z' | sed -E 's@(^|/)[0-9]+[.]@\1@g') printf '%s' "${p:-/}" } # collect route -> file map from the pages tree declare -A ROUTES PFILE while IFS= read -r f; do grep -q '^title:' "$f" || fail "no title frontmatter: $f" d=${f#pages/} # e.g. 02.shop/01.Welcome/page.md d=${d%/*} # 02.shop/01.Welcome d=${d%/page} # (page.md pages keep their folder route) r="/$(route_of "$d")" # Grav's homepage alias: the configured home folder is served at / [ "$r" = "/home" ] && r="/" ROUTES["$r"]=$f PFILE["$r"]=$f done < <(find pages -name '*.md') ROUTES["/"]=${PFILE["home"]:-} repo_checks() { local f r link t for r in "${!ROUTES[@]}"; do f=${ROUTES[$r]} [ -n "$f" ] || continue for link in $(grep -oE '\]\([^)#]+' "$f" | sed -E 's/^\]\(//'); do case "$link" in http*|mailto:*) continue ;; esac t="$r/$link" t=$(awk -v p="$t" 'BEGIN{n=split(p,seg,"/");out="";for(i=1;i<=n;i++){s=tolower(seg[i]);sub(/^[0-9]+\./,"",s);if(s==""||s==".")continue;if(s==".."){sub(/\/[^\/]*$/,"",out)}else{out=out"/"s}};print (out==""?"/":out)}') [ -n "${ROUTES[$t]:-}" ] || fail "broken link '$link' in ${PFILE[$r]} (no route '$t')" done done echo "repo: ${#ROUTES[@]} routes checked" } live_checks() { local BASE=${1%/} local ROUTELIST=("/") SITEMAP=/tmp/qa-sitemap.txt CHECKED=/tmp/qa-checked.txt local r for r in "${!ROUTES[@]}"; do [ "$r" = "/" ] || ROUTELIST+=("/$r"); done : > $SITEMAP; : > $CHECKED for r in $(printf '%s\n' "${ROUTELIST[@]}" | 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 can=$(curl -s -m 15 "$BASE/" | grep -oE '/dev/null | 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|*.xml) continue ;; esac code=$(curl -s -o /dev/null -w '%{http_code}' -m 20 "$link"); sleep 0.1 [ "$code" != "200" ] && fail "link $link -> $code" done done echo "checked ${#ROUTES[@]} routes, $(wc -l < $CHECKED) links/assets" } case "${1:-}" in -l) live_checks "$2" ;; *) repo_checks ;; esac [ $FAIL -eq 0 ] && echo "QA: PASS" || { echo "QA: $FAIL failure(s)"; exit 1; }