fix: lowercase shop page folders (theme menu 404s), absolute home links; QA suite v3 with live crawl + canonical check
https://projects.knownelement.com/issues/928
This commit is contained in:
@@ -9,5 +9,5 @@ Austin, Texas — fabrication, printing, and maker services.
|
|||||||
|
|
||||||
Bookings are open — schedule a shop consultation from the link above.
|
Bookings are open — schedule a shop consultation from the link above.
|
||||||
|
|
||||||
- [Shop Manual](shop) — how the shop runs: areas, tools, equipment, supplies, safety.
|
- [Shop Manual](/shop) — how the shop runs: areas, tools, equipment, supplies, safety.
|
||||||
- [Book a visit](https://booking.startinglineproductions.com) — schedule a shop consultation.
|
- [Book a visit](https://booking.startinglineproductions.com) — schedule a shop consultation.
|
||||||
|
|||||||
+51
-46
@@ -1,88 +1,93 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# site-qa.sh - QA for the Grav Website repo and (with -l BASE) the live site.
|
# 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 repo checks: structure, frontmatter, link targets
|
||||||
# ./site-qa.sh -l BASE live checks: every known route returns 200, all
|
# ./site-qa.sh -l BASE live checks: every known route returns 200, all
|
||||||
# internal links/assets resolve, pages have content
|
# internal links/assets resolve, pages have content,
|
||||||
# Exit non-zero on any failure. Used by CI and the post-deploy sync job.
|
# 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
|
set -uo pipefail
|
||||||
cd "$(dirname "$0")/.."
|
cd "$(dirname "$0")/.."
|
||||||
FAIL=0
|
FAIL=0
|
||||||
fail() { echo "FAIL: $*"; FAIL=$((FAIL+1)); }
|
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]+\.)||'
|
# 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:-/}"
|
||||||
}
|
}
|
||||||
|
|
||||||
repo_checks() {
|
# collect route -> file map from the pages tree
|
||||||
declare -A ROUTES
|
declare -A ROUTES PFILE
|
||||||
while IFS= read -r f; do
|
while IFS= read -r f; do
|
||||||
grep -q '^title:' "$f" || fail "no title frontmatter: $f"
|
grep -q '^title:' "$f" || fail "no title frontmatter: $f"
|
||||||
d=$(dirname "$f"); r=$(route_of "${d#pages/}")
|
d=${f#pages/} # e.g. 02.shop/01.Welcome/page.md
|
||||||
ROUTES["${r:-/}"]=1
|
d=${d%/*} # 02.shop/01.Welcome
|
||||||
done < <(find pages -name '*.md')
|
d=${d%/page} # (page.md pages keep their folder route)
|
||||||
while IFS= read -r f; do
|
r="/$(route_of "$d")"
|
||||||
d=$(dirname "$f")
|
# 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
|
for link in $(grep -oE '\]\([^)#]+' "$f" | sed -E 's/^\]\(//'); do
|
||||||
case "$link" in http*|mailto:*) continue ;; esac
|
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="$r/$link"
|
||||||
t=${t#pages}; t=$(route_of "$t")
|
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 $f (no route '${t:-/}')"
|
[ -n "${ROUTES[$t]:-}" ] || fail "broken link '$link' in ${PFILE[$r]} (no route '$t')"
|
||||||
|
done
|
||||||
done
|
done
|
||||||
done < <(find pages -name '*.md')
|
|
||||||
echo "repo: ${#ROUTES[@]} routes checked"
|
echo "repo: ${#ROUTES[@]} routes checked"
|
||||||
}
|
}
|
||||||
|
|
||||||
live_checks() {
|
live_checks() {
|
||||||
local BASE=${1%/}
|
local BASE=${1%/}
|
||||||
local ROUTES=("/")
|
local ROUTELIST=("/") SITEMAP=/tmp/qa-sitemap.txt CHECKED=/tmp/qa-checked.txt
|
||||||
while IFS= read -r f; do
|
local r
|
||||||
d=$(dirname "$f"); r=$(route_of "${d#pages/}")
|
for r in "${!ROUTES[@]}"; do [ "$r" = "/" ] || ROUTELIST+=("/$r"); done
|
||||||
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
|
: > $SITEMAP; : > $CHECKED
|
||||||
for r in $(printf '%s\n' "${ROUTES[@]}" | sort -u); do
|
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")
|
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
|
if [ "$code" = "200" ]; then echo "$BASE$r" >> $SITEMAP; else fail "route $r -> $code"; fi
|
||||||
sleep 0.2
|
sleep 0.2
|
||||||
done
|
done
|
||||||
|
can=$(curl -s -m 15 "$BASE/" | grep -oE '<link rel="canonical" href="[^"]*"' | head -1)
|
||||||
|
case "$can" in
|
||||||
|
*https://*) : ;;
|
||||||
|
"") fail "no canonical link on home" ;;
|
||||||
|
*) fail "canonical not https: $can" ;;
|
||||||
|
esac
|
||||||
|
local url body upath link code
|
||||||
for url in $(cat $SITEMAP); do
|
for url in $(cat $SITEMAP); do
|
||||||
body=$(curl -s -m 20 "$url"); sleep 0.2
|
body=$(curl -s -m 20 "$url"); sleep 0.2
|
||||||
[ ${#body} -lt 500 ] && fail "page too small ($url, ${#body} bytes)"
|
[ ${#body} -lt 500 ] && fail "page too small ($url, ${#body} bytes)"
|
||||||
upath=${url#$BASE}; [ -z "$upath" ] && upath=/
|
upath=${url#$BASE}; upath=${upath:-/}
|
||||||
for link in $(echo "$body" | grep -oE 'href="[^"]*"' | sed 's/href="//; s/"$//' | sort -u); do
|
for link in $(echo "$body" | grep -oE 'href="[^"]*"' | sed 's/href="//; s/"$//' | sort -u); do
|
||||||
case "$link" in
|
case "$link" in
|
||||||
http*|mailto:*|\#*) [[ "$link" == "$BASE"* ]] || continue ;;
|
http*|mailto:*|\#*) [[ "$link" == "$BASE"* ]] || continue ;;
|
||||||
/*) link="$BASE$link" ;;
|
/*) link="$BASE$link" ;;
|
||||||
*) link="$BASE$(realpath -m "$upath/../$link" | sed 's|//\+|/|g')" ;;
|
*) link="$BASE$(realpath -m "${upath%/}/..${upath%/}/$link" 2>/dev/null | sed 's|//\+|/|g')" ;;
|
||||||
esac
|
esac
|
||||||
grep -qxF "$link" $CHECKED && continue
|
grep -qxF "$link" $CHECKED && continue
|
||||||
echo "$link" >> $CHECKED
|
echo "$link" >> $CHECKED
|
||||||
case "$link" in
|
case "$link" in
|
||||||
*.css|*.js|*.png|*.jpg|*.jpeg|*.svg|*.ico|*.woff*|*.map|*.webp|*.txt|*.xml) ;;
|
*.css|*.js|*.png|*.jpg|*.jpeg|*.svg|*.ico|*.woff*|*.map|*.webp|*.xml) continue ;;
|
||||||
*) continue ;;
|
|
||||||
esac
|
esac
|
||||||
code=$(curl -s -o /dev/null -w '%{http_code}' -m 20 "$link"); sleep 0.1
|
code=$(curl -s -o /dev/null -w '%{http_code}' -m 20 "$link"); sleep 0.1
|
||||||
[ "$code" != "200" ] && fail "asset $link -> $code"
|
[ "$code" != "200" ] && fail "link $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
|
||||||
done
|
done
|
||||||
# canonical must be https (reverse-proxy scheme detection)
|
echo "checked ${#ROUTES[@]} routes, $(wc -l < $CHECKED) links/assets"
|
||||||
can=$(curl -s -m 15 "$BASE/" | grep -oE '<link rel="canonical" href="[^"]*"' | head -1)
|
|
||||||
case "$can" in *https://*) : ;; "") fail "no canonical link on home" ;; *) fail "canonical not https: $can" ;; esac
|
|
||||||
echo "checked $(wc -l < $SITEMAP) routes, $(wc -l < $CHECKED) links/assets"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case "${1:-}" in
|
case "${1:-}" in
|
||||||
|
|||||||
Reference in New Issue
Block a user