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="/"')