ssh git@grit.deadkey.org man

Commit 804ad77 h

Pat Ambrosio · 26-08-26 19:31

An index page is its directory, and cannot clobber the blog index

Changes: 2 files +96 -3

Changes (Side-by-Side)

share/jenny/lib/components.sh +33 -3

render_pages () {
272 esac 272 esac
273 273
274 out="$dist/${rel%.md}.html" 274 out="$dist/${rel%.md}.html"
275
276 # pages/index.md would land on the blog's own index, which is written
277 # earlier in the build -- so the page silently replaced the post list with
278 # itself and the front page stopped being an index at all. Refused rather
279 # than resolved: either file could reasonably be what the author meant,
280 # and guessing would make one of them vanish without saying so.
281 if [ "${rel%.md}" = "index" ]; then
282 echo "$T ! Skipping $pages_dir/$rel: it would overwrite the blog index." >&2
283 echo "$T Name it something else, or put it in a subdirectory." >&2
284 BUILD_FAILED=1
285 continue
286 fi
287
275 outdir=$(dirname "$out") 288 outdir=$(dirname "$out")
276 mkdir -p "$outdir" 289 mkdir -p "$outdir"
277 290
render_page () {
426 done 439 done
427 segs+=("$walk") 440 segs+=("$walk")
428 441
442 # "index" is a filesystem convention, not part of the address: the page is
443 # reached at its directory's URL, so the directory is what the trail should
444 # name. Left in, the last crumb read "index" -- a filename presented to the
445 # reader as a place.
446 # Only a trailing "index" beneath at least one directory. At the top level
447 # there would be nothing left to name, and the page is the site root, which
448 # the site title already links to.
449 if [ "${#segs[@]}" -gt 1 ] && [ "${segs[$(( ${#segs[@]} - 1 ))]}" = "index" ]; then
450 unset 'segs[$(( ${#segs[@]} - 1 ))]'
451 segs=("${segs[@]}")
452 fi
453
429 local i=0 n=${#segs[@]} 454 local i=0 n=${#segs[@]}
430 for seg in "${segs[@]}"; do 455 for seg in "${segs[@]}"; do
431 i=$((i + 1)) 456 i=$((i + 1))
432 acc="${acc:+$acc/}$seg" 457 acc="${acc:+$acc/}$seg"
433 if [ "$i" -eq "$n" ]; then 458 if [ "$i" -eq "$n" ]; then
434 up="$prefix/$acc" 459 up="$prefix/$acc"
460 elif [ -f "$PAGES_DIR/$acc/index.md" ]; then
461 # An intermediate segment with an index.md is a real page, reached at
462 # its directory. Before pages could be indexes every intermediate
463 # segment pointed at the site root, so a trail through a section that
464 # does have a landing page sent the reader home instead.
465 up="$prefix/$acc/"
435 else 466 else
436 # An intermediate segment is a directory, which jenny does not render
437 # an index for. It still names the section, so it points at the site
438 # root rather than a page that does not exist.
467 # A directory jenny renders no index for. It still names the section,
468 # so it points at the site root rather than a page that does not exist.
439 up="$prefix/" 469 up="$prefix/"
440 fi 470 fi
441 crumbs="${crumbs}${up}$(printf '\t')$(printf '%s' "$seg" | tr '-' ' ') 471 crumbs="${crumbs}${up}$(printf '\t')$(printf '%s' "$seg" | tr '-' ' ')

test/regressions.sh +63 -0

assert_file_contains "$al/.dist/index.html" 'padding-right: 1.5em;'
1083 it "the build is clean" 1083 it "the build is clean"
1084 assert_eq "$stderr_output" "" 1084 assert_eq "$stderr_output" ""
1085 1085
1086 suite "an index page is its directory"
1087
1088 # "index" is a filesystem convention, not part of the address: the page is
1089 # reached at its directory's URL. Left in the trail, the last crumb read
1090 # "index" -- a filename presented to the reader as a place.
1091
1092 ix="$TMPROOT/indexpage"
1093 make_blog_min "$ix" 1 10
1094 mkdir -p "$ix/pages/thesis/mdd" "$ix/pages/notes/method"
1095 printf '# The Thesis\n\nBody.\n' > "$ix/pages/thesis/mdd/index.md"
1096 printf '# Artifacts\n\nBody.\n' > "$ix/pages/thesis/mdd/artifacts.md"
1097 printf '# Intro\n\nBody.\n' > "$ix/pages/notes/method/intro.md"
1098 run_jenny_split "$ix"
1099
1100 it "the page renders at its directory"
1101 assert_file_exists "$ix/.dist/thesis/mdd/index.html"
1102
1103 it "the trail does not name the file"
1104 ix_head=$("$GREP" -o '<header>.*</header>' "$ix/.dist/thesis/mdd/index.html" | head -1)
1105 assert_not_contains "$ix_head" '>index<'
1106
1107 it "the directory is the current crumb instead"
1108 assert_contains "$ix_head" 'class="current">mdd</a>'
1109
1110 it "a sibling links back to the directory, not the site root"
1111 # Intermediate segments used to point home because no directory had a page.
1112 # One with an index.md does, and sending the reader home from a section that
1113 # has a landing page is worse than not linking at all.
1114 sib_head=$("$GREP" -o '<header>.*</header>' "$ix/.dist/thesis/mdd/artifacts.html" | head -1)
1115 assert_contains "$sib_head" 'href="../../thesis/mdd/">mdd</a>'
1116
1117 it "a directory with no index still points home"
1118 # Nothing to link to there, so the old behaviour is right and stays.
1119 no_ix=$("$GREP" -o '<header>.*</header>' "$ix/.dist/notes/method/intro.html" | head -1)
1120 assert_contains "$no_ix" 'href="../../">method</a>'
1121
1122 it "a page whose name merely starts with index keeps its crumb"
1123 # The rule fires on a segment named exactly "index"; "indexing" is a page like
1124 # any other and must keep its own trail.
1125 mkdir -p "$ix/pages/solo"
1126 printf '# Indexing\n\nBody.\n' > "$ix/pages/solo/indexing.md"
1127 run_jenny_split "$ix"
1128 idx_like=$("$GREP" -o '<header>.*</header>' "$ix/.dist/solo/indexing.html" | head -1)
1129 assert_contains "$idx_like" 'class="current">indexing</a>'
1130
1131 it "a page at the top of pages/ cannot overwrite the blog index"
1132 # pages/index.md lands on the blog's own index, which is written earlier in the
1133 # build -- so the page silently replaced the post list with itself and the
1134 # front page stopped being an index. Refused rather than resolved: either file
1135 # could be what the author meant, and guessing makes one vanish without saying
1136 # so.
1137 printf '# Root\n\nBody.\n' > "$ix/pages/index.md"
1138 run_jenny_split "$ix"
1139 assert_contains "$stderr_output" 'would overwrite the blog index'
1140 assert_file_contains "$ix/.dist/index.html" 'class="post-link"'
1141 # Removed and rebuilt so the clean-build check below is not reading the
1142 # warning this test provoked on purpose.
1143 rm "$ix/pages/index.md"
1144 run_jenny_split "$ix"
1145
1146 it "the build is clean"
1147 assert_eq "$stderr_output" ""
1148
1086 suite "headings are addressable" 1149 suite "headings are addressable"
1087 1150
1088 # A document long enough to link within needs its headings to be targets. Any 1151 # A document long enough to link within needs its headings to be targets. Any

Raw

commit 804ad77cbb5ebc69681c1505948310591c7f74ff
Author: Pat Ambrosio <cp.ambrosio@gmail.com>
Date:   Wed Aug 26 19:31:18 2026 +0800

    An index page is its directory, and cannot clobber the blog index
    
    "index" is a filesystem convention, not part of the address: the page is
    reached at its directory's URL. Left in the trail the last crumb read
    "index" -- a filename presented to the reader as a place -- so a trailing
    "index" segment is dropped and the directory becomes the current crumb.
    
    Only a trailing one, and only beneath at least one directory: a page named
    "indexing" keeps its own crumb, and a page at the top of pages/ would have
    nothing left to name.
    
    That exposed a second bug in the same walk. Intermediate segments always
    pointed at the site root, on the reasoning that jenny renders no index for
    a directory -- true until a directory could have an index.md. A section
    that does have a landing page now links to it, so the trail through the
    thesis reaches the thesis instead of sending the reader home.
    
    And a third, older and worse: pages/index.md lands on the blog's own
    index, which is written earlier in the build, so the page silently
    replaced the post list with itself and the front page stopped being an
    index at all. It is refused now, with a message naming the file and
    saying what to do -- refused rather than resolved, because either file
    could reasonably be what the author meant and guessing makes one of them
    vanish without saying so.
    
    Verified by resolving every breadcrumb link on every page of the site: 55
    links, none broken.
    
    Tests: the dropped segment, the directory as current crumb, a sibling
    linking back to it, a directory without an index still pointing home, a
    page whose name merely starts with "index", and the refused collision
    leaving the post list intact. Checked by mutation -- restoring the index
    crumb, sending sections home again, allowing the collision -- each caught.
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01WnSAstgnDYdKPwLGv1YZE5

diff --git a/share/jenny/lib/components.sh b/share/jenny/lib/components.sh
index 7f56967..8d822e3 100644
--- a/share/jenny/lib/components.sh
+++ b/share/jenny/lib/components.sh
@@ -272,6 +272,19 @@ render_pages () {
     esac
 
     out="$dist/${rel%.md}.html"
+
+    # pages/index.md would land on the blog's own index, which is written
+    # earlier in the build -- so the page silently replaced the post list with
+    # itself and the front page stopped being an index at all. Refused rather
+    # than resolved: either file could reasonably be what the author meant,
+    # and guessing would make one of them vanish without saying so.
+    if [ "${rel%.md}" = "index" ]; then
+      echo "$T ! Skipping $pages_dir/$rel: it would overwrite the blog index." >&2
+      echo "$T   Name it something else, or put it in a subdirectory." >&2
+      BUILD_FAILED=1
+      continue
+    fi
+
     outdir=$(dirname "$out")
     mkdir -p "$outdir"
 
@@ -426,16 +439,33 @@ render_page () {
   done
   segs+=("$walk")
 
+  # "index" is a filesystem convention, not part of the address: the page is
+  # reached at its directory's URL, so the directory is what the trail should
+  # name. Left in, the last crumb read "index" -- a filename presented to the
+  # reader as a place.
+  # Only a trailing "index" beneath at least one directory. At the top level
+  # there would be nothing left to name, and the page is the site root, which
+  # the site title already links to.
+  if [ "${#segs[@]}" -gt 1 ] && [ "${segs[$(( ${#segs[@]} - 1 ))]}" = "index" ]; then
+    unset 'segs[$(( ${#segs[@]} - 1 ))]'
+    segs=("${segs[@]}")
+  fi
+
   local i=0 n=${#segs[@]}
   for seg in "${segs[@]}"; do
     i=$((i + 1))
     acc="${acc:+$acc/}$seg"
     if [ "$i" -eq "$n" ]; then
       up="$prefix/$acc"
+    elif [ -f "$PAGES_DIR/$acc/index.md" ]; then
+      # An intermediate segment with an index.md is a real page, reached at
+      # its directory. Before pages could be indexes every intermediate
+      # segment pointed at the site root, so a trail through a section that
+      # does have a landing page sent the reader home instead.
+      up="$prefix/$acc/"
     else
-      # An intermediate segment is a directory, which jenny does not render
-      # an index for. It still names the section, so it points at the site
-      # root rather than a page that does not exist.
+      # A directory jenny renders no index for. It still names the section,
+      # so it points at the site root rather than a page that does not exist.
       up="$prefix/"
     fi
     crumbs="${crumbs}${up}$(printf '\t')$(printf '%s' "$seg" | tr '-' ' ')
diff --git a/test/regressions.sh b/test/regressions.sh
index ac5362d..e18a687 100755
--- a/test/regressions.sh
+++ b/test/regressions.sh
@@ -1083,6 +1083,69 @@ assert_file_contains "$al/.dist/index.html" 'padding-right: 1.5em;'
 it "the build is clean"
 assert_eq "$stderr_output" ""
 
+suite "an index page is its directory"
+
+# "index" is a filesystem convention, not part of the address: the page is
+# reached at its directory's URL. Left in the trail, the last crumb read
+# "index" -- a filename presented to the reader as a place.
+
+ix="$TMPROOT/indexpage"
+make_blog_min "$ix" 1 10
+mkdir -p "$ix/pages/thesis/mdd" "$ix/pages/notes/method"
+printf '# The Thesis\n\nBody.\n' > "$ix/pages/thesis/mdd/index.md"
+printf '# Artifacts\n\nBody.\n' > "$ix/pages/thesis/mdd/artifacts.md"
+printf '# Intro\n\nBody.\n' > "$ix/pages/notes/method/intro.md"
+run_jenny_split "$ix"
+
+it "the page renders at its directory"
+assert_file_exists "$ix/.dist/thesis/mdd/index.html"
+
+it "the trail does not name the file"
+ix_head=$("$GREP" -o '<header>.*</header>' "$ix/.dist/thesis/mdd/index.html" | head -1)
+assert_not_contains "$ix_head" '>index<'
+
+it "the directory is the current crumb instead"
+assert_contains "$ix_head" 'class="current">mdd</a>'
+
+it "a sibling links back to the directory, not the site root"
+# Intermediate segments used to point home because no directory had a page.
+# One with an index.md does, and sending the reader home from a section that
+# has a landing page is worse than not linking at all.
+sib_head=$("$GREP" -o '<header>.*</header>' "$ix/.dist/thesis/mdd/artifacts.html" | head -1)
+assert_contains "$sib_head" 'href="../../thesis/mdd/">mdd</a>'
+
+it "a directory with no index still points home"
+# Nothing to link to there, so the old behaviour is right and stays.
+no_ix=$("$GREP" -o '<header>.*</header>' "$ix/.dist/notes/method/intro.html" | head -1)
+assert_contains "$no_ix" 'href="../../">method</a>'
+
+it "a page whose name merely starts with index keeps its crumb"
+# The rule fires on a segment named exactly "index"; "indexing" is a page like
+# any other and must keep its own trail.
+mkdir -p "$ix/pages/solo"
+printf '# Indexing\n\nBody.\n' > "$ix/pages/solo/indexing.md"
+run_jenny_split "$ix"
+idx_like=$("$GREP" -o '<header>.*</header>' "$ix/.dist/solo/indexing.html" | head -1)
+assert_contains "$idx_like" 'class="current">indexing</a>'
+
+it "a page at the top of pages/ cannot overwrite the blog index"
+# pages/index.md lands on the blog's own index, which is written earlier in the
+# build -- so the page silently replaced the post list with itself and the
+# front page stopped being an index. Refused rather than resolved: either file
+# could be what the author meant, and guessing makes one vanish without saying
+# so.
+printf '# Root\n\nBody.\n' > "$ix/pages/index.md"
+run_jenny_split "$ix"
+assert_contains "$stderr_output" 'would overwrite the blog index'
+assert_file_contains "$ix/.dist/index.html" 'class="post-link"'
+# Removed and rebuilt so the clean-build check below is not reading the
+# warning this test provoked on purpose.
+rm "$ix/pages/index.md"
+run_jenny_split "$ix"
+
+it "the build is clean"
+assert_eq "$stderr_output" ""
+
 suite "headings are addressable"
 
 # A document long enough to link within needs its headings to be targets. Any