Commit 13fa8f0 h
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