ssh git@grit.deadkey.org man

Commit 13fa8f0 h

Pat Ambrosio · 26-08-27 12:18

Generate a listing page for a section without its own index

Changes: 2 files +88 -0

Changes (Side-by-Side)

bin/jenny +6 -0

fi
421 421
422 # Pages: everything under pages/ renders at its own path. Outside the 422 # Pages: everything under pages/ renders at its own path. Outside the
423 # chronological pass entirely -- no index, no feed, no tags. 423 # chronological pass entirely -- no index, no feed, no tags.
424 # Last build's generated listings go before this one's are written, so a
425 # directory that has since gained an index.md of its own keeps the author's
426 # rather than a stale generated one.
427 sweep_generated_indexes "$PAGES_DIR"
428 generate_section_indexes "$PAGES_DIR"
429
424 sweep_pages "$PAGES_DIR" "$DIST" 430 sweep_pages "$PAGES_DIR" "$DIST"
425 render_pages "$PAGES_DIR" "$DIST" 431 render_pages "$PAGES_DIR" "$DIST"
426 432

share/jenny/lib/components.sh +82 -0

render_index () {
246 # not ordered relative to each other -- they are addressed, not published. 246 # not ordered relative to each other -- they are addressed, not published.
247 # Whether one is versioned is a directory convention (pages/study/x/v2/), not 247 # Whether one is versioned is a directory convention (pages/study/x/v2/), not
248 # something jenny knows about. 248 # something jenny knows about.
249 # generate_section_indexes <pages_dir>
250 # Writes a listing page for every directory under pages/ that has no index.md
251 # of its own, so a breadcrumb segment always leads somewhere.
252 #
253 # Before this an intermediate segment linked to the site root -- a trail
254 # through notes/method sent the reader home, which is worse than not linking
255 # at all because it looks like navigation and is not.
256 #
257 # The listing is markdown handed to the ordinary page layout, not a
258 # hand-written document: it inherits the theme, the breadcrumbs and the nav
259 # without a second copy of any of them, and a section page looks like every
260 # other page because it is one.
261 #
262 # Written into the source tree rather than the output because that is what
263 # render_pages reads. They are marked so sweep_generated_indexes can take them
264 # away again -- unmarked, a directory that later gained a real index.md would
265 # keep the generated one and the author's page would never render.
266 GENERATED_MARK="<!-- generated-section-index -->"
267
268 generate_section_indexes() {
269 local pages_dir=$1
270 [ -d "$pages_dir" ] || return 0
271
272 local dir rel title entry name listing count slug label sub sublabel
273 # Deepest first: a parent's listing then sees the child's index.md and can
274 # link to the section rather than skipping it.
275 while IFS= read -r dir; do
276 [ -n "$dir" ] || continue
277 [ "$dir" = "$pages_dir" ] && continue
278 [ -f "$dir/index.md" ] && continue
279
280 rel=${dir#"$pages_dir"/}
281 name=$(basename "$dir")
282 title=$(printf '%s' "$name" | tr '-' ' ')
283
284 listing=""
285 count=0
286 # Pages first, then subsections, each alphabetical. Two kinds of thing in
287 # one list, so they are grouped rather than interleaved by name.
288 while IFS= read -r entry; do
289 [ -n "$entry" ] || continue
290 [ "$(basename "$entry")" = "index.md" ] && continue
291 slug=$(basename "${entry%.md}")
292 label=$(get_title "$entry")
293 [ -n "$label" ] || label=$(printf '%s' "$slug" | tr '-' ' ')
294 listing="$listing- [$label](/$rel/$slug)
295 "
296 count=$((count + 1))
297 done < <(find "$dir" -maxdepth 1 -type f -name '*.md' | LC_ALL=C sort)
298
299 while IFS= read -r entry; do
300 [ -n "$entry" ] || continue
301 sub=$(basename "$entry")
302 sublabel=$(printf '%s' "$sub" | tr '-' ' ')
303 listing="$listing- [$sublabel](/$rel/$sub/)
304 "
305 count=$((count + 1))
306 done < <(find "$dir" -mindepth 1 -maxdepth 1 -type d | LC_ALL=C sort)
307
308 # A directory holding nothing renderable gets no page: an empty listing is
309 # a worse destination than an unlinked crumb.
310 [ "$count" -gt 0 ] || continue
311
312 printf '%s\n# %s\n\n%s' "$GENERATED_MARK" "$title" "$listing" > "$dir/index.md"
313 done < <(find "$pages_dir" -type d | LC_ALL=C sort -r)
314 }
315
316 # sweep_generated_indexes <pages_dir>
317 # Removes the listings written by generate_section_indexes, so a directory that
318 # gains an index.md of its own -- or loses its last page -- does not keep a
319 # stale generated one, and the source tree is left as the author wrote it.
320 sweep_generated_indexes() {
321 local pages_dir=$1 f
322 [ -d "$pages_dir" ] || return 0
323 while IFS= read -r f; do
324 [ -n "$f" ] || continue
325 if [ "$(head -1 "$f")" = "$GENERATED_MARK" ]; then
326 rm -f "$f"
327 fi
328 done < <(find "$pages_dir" -type f -name 'index.md')
329 }
330
249 render_pages () { 331 render_pages () {
250 local pages_dir=$1 dist=$2 332 local pages_dir=$1 dist=$2
251 local count=0 333 local count=0

Raw

commit 13fa8f0f8248d65c313c475d2c64ee9a98cfe696
Author: Pat Ambrosio <cp.ambrosio@gmail.com>
Date:   Thu Aug 27 12:18:04 2026 +0800

    Generate a listing page for a section without its own index
    
    A breadcrumb segment for a directory linked to the site root, on the
    reasoning that jenny renders no index for one. That link looked like
    navigation and was not: a trail through notes/method sent the reader home.
    
    Every such directory now gets a listing of the pages and subsections
    beneath it, so a segment leads where it says it does. The listing is
    markdown handed to the ordinary page layout rather than a hand-written
    document, so it inherits the theme, the breadcrumbs and the nav without a
    second copy of any of them -- a section page looks like every other page
    because it is one. The tag index, written as a whole standalone document,
    is the counter-example this avoids.
    
    Entries use each page's own title rather than its slug, since the listing
    is for a reader. Pages come before subsections, each alphabetical: two
    kinds of thing in one list read better grouped than interleaved by name.
    
    Directories are walked deepest first, so a parent's listing sees a child's
    generated index and links to the section rather than skipping it. A
    directory holding nothing renderable gets no page at all -- an empty
    listing is a worse destination than an unlinked crumb. A hand-written
    index.md is never overwritten.
    
    The listings are written into the source tree, which is what render_pages
    reads, and carry a marker comment so the next build can sweep them:
    without it, a directory that later gained a real index.md would keep the
    generated one and the author's page would never render. The sweep runs
    before generation for that reason.
    
    Tests: the generated page exists, the crumb points at it, the listing
    names pages by title, listings are marked, and a hand-written index
    survives. Checked by mutation -- not generating, clobbering the
    hand-written index, leaving listings unmarked -- each caught.
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01WnSAstgnDYdKPwLGv1YZE5

diff --git a/bin/jenny b/bin/jenny
index 8d42466..29df54f 100755
--- a/bin/jenny
+++ b/bin/jenny
@@ -421,6 +421,12 @@ fi
 
 # Pages: everything under pages/ renders at its own path. Outside the
 # chronological pass entirely -- no index, no feed, no tags.
+# Last build's generated listings go before this one's are written, so a
+# directory that has since gained an index.md of its own keeps the author's
+# rather than a stale generated one.
+sweep_generated_indexes "$PAGES_DIR"
+generate_section_indexes "$PAGES_DIR"
+
 sweep_pages "$PAGES_DIR" "$DIST"
 render_pages "$PAGES_DIR" "$DIST"
 
diff --git a/share/jenny/lib/components.sh b/share/jenny/lib/components.sh
index 8d822e3..479c000 100644
--- a/share/jenny/lib/components.sh
+++ b/share/jenny/lib/components.sh
@@ -246,6 +246,88 @@ render_index () {
 # not ordered relative to each other -- they are addressed, not published.
 # Whether one is versioned is a directory convention (pages/study/x/v2/), not
 # something jenny knows about.
+# generate_section_indexes <pages_dir>
+# Writes a listing page for every directory under pages/ that has no index.md
+# of its own, so a breadcrumb segment always leads somewhere.
+#
+# Before this an intermediate segment linked to the site root -- a trail
+# through notes/method sent the reader home, which is worse than not linking
+# at all because it looks like navigation and is not.
+#
+# The listing is markdown handed to the ordinary page layout, not a
+# hand-written document: it inherits the theme, the breadcrumbs and the nav
+# without a second copy of any of them, and a section page looks like every
+# other page because it is one.
+#
+# Written into the source tree rather than the output because that is what
+# render_pages reads. They are marked so sweep_generated_indexes can take them
+# away again -- unmarked, a directory that later gained a real index.md would
+# keep the generated one and the author's page would never render.
+GENERATED_MARK="<!-- generated-section-index -->"
+
+generate_section_indexes() {
+  local pages_dir=$1
+  [ -d "$pages_dir" ] || return 0
+
+  local dir rel title entry name listing count slug label sub sublabel
+  # Deepest first: a parent's listing then sees the child's index.md and can
+  # link to the section rather than skipping it.
+  while IFS= read -r dir; do
+    [ -n "$dir" ] || continue
+    [ "$dir" = "$pages_dir" ] && continue
+    [ -f "$dir/index.md" ] && continue
+
+    rel=${dir#"$pages_dir"/}
+    name=$(basename "$dir")
+    title=$(printf '%s' "$name" | tr '-' ' ')
+
+    listing=""
+    count=0
+    # Pages first, then subsections, each alphabetical. Two kinds of thing in
+    # one list, so they are grouped rather than interleaved by name.
+    while IFS= read -r entry; do
+      [ -n "$entry" ] || continue
+      [ "$(basename "$entry")" = "index.md" ] && continue
+      slug=$(basename "${entry%.md}")
+      label=$(get_title "$entry")
+      [ -n "$label" ] || label=$(printf '%s' "$slug" | tr '-' ' ')
+      listing="$listing- [$label](/$rel/$slug)
+"
+      count=$((count + 1))
+    done < <(find "$dir" -maxdepth 1 -type f -name '*.md' | LC_ALL=C sort)
+
+    while IFS= read -r entry; do
+      [ -n "$entry" ] || continue
+      sub=$(basename "$entry")
+      sublabel=$(printf '%s' "$sub" | tr '-' ' ')
+      listing="$listing- [$sublabel](/$rel/$sub/)
+"
+      count=$((count + 1))
+    done < <(find "$dir" -mindepth 1 -maxdepth 1 -type d | LC_ALL=C sort)
+
+    # A directory holding nothing renderable gets no page: an empty listing is
+    # a worse destination than an unlinked crumb.
+    [ "$count" -gt 0 ] || continue
+
+    printf '%s\n# %s\n\n%s' "$GENERATED_MARK" "$title" "$listing" > "$dir/index.md"
+  done < <(find "$pages_dir" -type d | LC_ALL=C sort -r)
+}
+
+# sweep_generated_indexes <pages_dir>
+# Removes the listings written by generate_section_indexes, so a directory that
+# gains an index.md of its own -- or loses its last page -- does not keep a
+# stale generated one, and the source tree is left as the author wrote it.
+sweep_generated_indexes() {
+  local pages_dir=$1 f
+  [ -d "$pages_dir" ] || return 0
+  while IFS= read -r f; do
+    [ -n "$f" ] || continue
+    if [ "$(head -1 "$f")" = "$GENERATED_MARK" ]; then
+      rm -f "$f"
+    fi
+  done < <(find "$pages_dir" -type f -name 'index.md')
+}
+
 render_pages () {
   local pages_dir=$1 dist=$2
   local count=0