diff --git a/pages/01.home/home.md b/pages/01.home/home.md
index 538555c..811fe27 100644
--- a/pages/01.home/home.md
+++ b/pages/01.home/home.md
@@ -9,5 +9,5 @@ Austin, Texas — fabrication, printing, and maker services.
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.
diff --git a/pages/02.shop/01.Welcome/page.md b/pages/02.shop/01.welcome/page.md
similarity index 100%
rename from pages/02.shop/01.Welcome/page.md
rename to pages/02.shop/01.welcome/page.md
diff --git a/qa/site-qa.sh b/qa/site-qa.sh
index 53e8a8b..5b67479 100755
--- a/qa/site-qa.sh
+++ b/qa/site-qa.sh
@@ -1,88 +1,93 @@
#!/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.
+# 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)); }
-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:-/}"
}
+# 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() {
- 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")
+ 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=$(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:-/}')"
+ 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 < <(find pages -name '*.md')
+ done
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
+ 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' "${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")
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|*.txt|*.xml) ;;
- *) continue ;;
+ *.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 "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"
+ [ "$code" != "200" ] && fail "link $link -> $code"
done
done
- # canonical must be https (reverse-proxy scheme detection)
- can=$(curl -s -m 15 "$BASE/" | grep -oE '