From 2ddf787aa743c1d126a29456fff29acf96fe17f9 Mon Sep 17 00:00:00 2001 From: FosterStack Admin <317177128+fosterstack-admin@users.noreply.github.com> Date: Wed, 14 Oct 2026 09:30:14 -0400 Subject: [PATCH 1/3] Site-rot check: a page linked from /guides/ counts as linked from home --- bin/check-site-rot.py | 9 ++++++++- bin/test-check-site-rot.sh | 24 ++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/bin/check-site-rot.py b/bin/check-site-rot.py index 0adcef7..cf53eff 100755 --- a/bin/check-site-rot.py +++ b/bin/check-site-rot.py @@ -227,7 +227,14 @@ def resolve(path): homelinks = {m for m in re.findall(r'href="/([a-z0-9-]+)/"', texts[""])} for p in sorted(homelinks - page_set): bad("home: links /%s/ which is not a page" % p) - for p in sorted(page_set - homelinks): + # The home page links a few guides and /guides/, which lists every guide: a page linked from /guides/ counts as linked from home. + reachable = set(homelinks) + if "guides" in page_set: + if "guides" in homelinks: + reachable |= {m for m in re.findall(r'href="/([a-z0-9-]+)/"', texts.get("guides", ""))} + else: + bad("home: does not link /guides/ (the page that lists every guide)") + for p in sorted(page_set - reachable): bad("home: does not link /%s/ (the Guides list or another section should)" % p) for line in problems: diff --git a/bin/test-check-site-rot.sh b/bin/test-check-site-rot.sh index 75cb2bb..f85028d 100755 --- a/bin/test-check-site-rot.sh +++ b/bin/test-check-site-rot.sh @@ -83,9 +83,33 @@ import sys p, slug = sys.argv[1:3] s = open(p).read().replace('href="/%s/"' % slug, 'href="/"') open(p, "w").write(s) +# a site with a /guides/ page lists every guide there: take the page off that list too +import os +g = os.path.join(os.path.dirname(p), "guides", "index.html") +if os.path.isfile(g): + gs = open(g).read() + open(g, "w").write(gs.replace('href="/%s/"' % slug, 'href="/"')) EOF expect_fail "a page the home page no longer links" "home: does not link" +if [ -f "$here/guides/index.html" ]; then + # a page linked only from /guides/ passes; the home page must still link /guides/ + fresh; python3 - "$site/index.html" "$first" <<'EOF' +import sys +p, slug = sys.argv[1:3] +s = open(p).read() +open(p, "w").write(s.replace('href="/%s/"' % slug, 'href="/"')) +EOF + expect_pass "a page linked only from /guides/ counts as linked from home" + fresh; python3 - "$site/index.html" <<'EOF' +import sys +p = sys.argv[1] +s = open(p).read() +open(p, "w").write(s.replace('href="/guides/"', 'href="/"')) +EOF + expect_fail "a home page that does not link /guides/" "home: does not link /guides/" +fi + fresh; edit "$site/$first/index.html" "REGEX:.*?" "This title is deliberately far too long to be a good page title for a search result" expect_fail "a title over 60 characters" "characters (max 60)" From 6d4fbc5160e3891cd4d94efb81b928ded69feec6 Mon Sep 17 00:00:00 2001 From: FosterStack Admin <317177128+fosterstack-admin@users.noreply.github.com> Date: Wed, 14 Oct 2026 09:37:56 -0400 Subject: [PATCH 2/3] Site-rot self-test: build the /guides/ cases in the throwaway copy so they run on every site --- bin/test-check-site-rot.sh | 48 ++++++++++++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/bin/test-check-site-rot.sh b/bin/test-check-site-rot.sh index f85028d..1bb7043 100755 --- a/bin/test-check-site-rot.sh +++ b/bin/test-check-site-rot.sh @@ -92,23 +92,57 @@ if os.path.isfile(g): EOF expect_fail "a page the home page no longer links" "home: does not link" -if [ -f "$here/guides/index.html" ]; then - # a page linked only from /guides/ passes; the home page must still link /guides/ - fresh; python3 - "$site/index.html" "$first" <<'EOF' +# /guides/ lists the guides. Built in the throwaway copy, so these cases run on every site, with or without a real /guides/. +# The home page links /guides/ and not $first; /guides/ links $first. +with_guides() { + fresh + python3 - "$site" "$first" "$second" <<'EOF' +import os, re, sys +site, first, second = sys.argv[1:4] +real = os.path.join(site, "guides") +if os.path.isdir(real): + import shutil + shutil.rmtree(real) +src = open(os.path.join(site, second, "index.html")).read() +src = re.sub(r".*?", "Guides", src, count=1, flags=re.S) +src = src.replace("/%s/" % second, "/guides/") +src = src.replace("", '

listed guide

' % first, 1) +os.makedirs(real) +open(os.path.join(real, "index.html"), "w").write(src) +home = os.path.join(site, "index.html") +h = open(home).read().replace('href="/%s/"' % first, 'href="/"') +if 'href="/guides/"' not in h: + h = h.replace("", '

All guides

', 1) +open(home, "w").write(h) +sm = os.path.join(site, "sitemap.xml") +m = open(sm).read() +if "/guides/" not in m: + open(sm, "w").write(m.replace("", "https://fosterstack.com/guides/", 1)) +ll = os.path.join(site, "llms.txt") +l = open(ll).read() +if "fosterstack.com/guides/)" not in l: + open(ll, "w").write(l.rstrip("\n") + "\n- [Guides](https://fosterstack.com/guides/): all guides\n") +EOF +} + +with_guides; expect_pass "a page linked only from /guides/ counts as linked from home" + +with_guides; python3 - "$site/guides/index.html" "$first" <<'EOF' import sys p, slug = sys.argv[1:3] s = open(p).read() open(p, "w").write(s.replace('href="/%s/"' % slug, 'href="/"')) EOF - expect_pass "a page linked only from /guides/ counts as linked from home" - fresh; python3 - "$site/index.html" <<'EOF' +expect_fail "a page on neither the home page nor /guides/" "home: does not link /$first/" + +with_guides; python3 - "$site/index.html" <<'EOF' import sys p = sys.argv[1] s = open(p).read() open(p, "w").write(s.replace('href="/guides/"', 'href="/"')) EOF - expect_fail "a home page that does not link /guides/" "home: does not link /guides/" -fi +expect_fail "a home page that does not link /guides/" "home: does not link /guides/" +expect_fail "a /guides/ page the home page does not link lists nothing for home" "home: does not link /$first/" fresh; edit "$site/$first/index.html" "REGEX:.*?" "This title is deliberately far too long to be a good page title for a search result" expect_fail "a title over 60 characters" "characters (max 60)" From bb790c82ecd1c2303c5843bd03a816fc8e7f0bdb Mon Sep 17 00:00:00 2001 From: FosterStack Admin <317177128+fosterstack-admin@users.noreply.github.com> Date: Wed, 14 Oct 2026 09:39:16 -0400 Subject: [PATCH 3/3] Site-rot self-test: keep a real /guides/ page in the fixture and add the listed guide to it --- bin/test-check-site-rot.sh | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/bin/test-check-site-rot.sh b/bin/test-check-site-rot.sh index 1bb7043..8c8fbac 100755 --- a/bin/test-check-site-rot.sh +++ b/bin/test-check-site-rot.sh @@ -100,14 +100,16 @@ with_guides() { import os, re, sys site, first, second = sys.argv[1:4] real = os.path.join(site, "guides") -if os.path.isdir(real): - import shutil - shutil.rmtree(real) -src = open(os.path.join(site, second, "index.html")).read() -src = re.sub(r".*?", "Guides", src, count=1, flags=re.S) -src = src.replace("/%s/" % second, "/guides/") -src = src.replace("", '

listed guide

' % first, 1) -os.makedirs(real) +if os.path.isfile(os.path.join(real, "index.html")): + # a real /guides/ page: keep it, and make sure it lists $first + src = open(os.path.join(real, "index.html")).read() +else: + src = open(os.path.join(site, second, "index.html")).read() + src = re.sub(r".*?", "Guides", src, count=1, flags=re.S) + src = src.replace("/%s/" % second, "/guides/") + os.makedirs(real, exist_ok=True) +if 'href="/%s/"' % first not in src: + src = src.replace("", '

listed guide

' % first, 1) open(os.path.join(real, "index.html"), "w").write(src) home = os.path.join(site, "index.html") h = open(home).read().replace('href="/%s/"' % first, 'href="/"')