Commit 804ad77 h
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