Commit b6870928c4

b6870928c46c1eec7103746bd3153a671b8d344c

parent: 4d78517a19

Unsigned

cmc <hello@cleberg.net> · 2026-08-11 19:28 UTC

Render the table of contents, and group /blog/ by year

Two of the differences the page-by-page diff against the live site turned up.

The TOC was the larger one: live has one on 171 pages and the org-ssg build had
none. `[html] toc = true` only *exposes* `page.toc` — rendering it is the
layout's job, and base.html never did. It now emits the same
`div#table-of-contents` Emacs does, from a recursive macro over the tree. The
anchors already matched, so every link lands where the live one does. Pages that
say `#+OPTIONS: toc:nil` get an empty tree and render nothing, which is why
/uses/, /now/ and /salary/ have no TOC here either — the same three as live.

/blog/ now breaks its list under year headings, as the live index does, using
`page.year` and minijinja's groupby. `group_by_year` at the top of blog.html
turns it off for one flat list.

After both, /blog/ matches the live page's content exactly, and 171 of 171 TOCs
are accounted for.

Layout: unified · split

content/templates/base.html +24 −1
@@ -10,6 +10,27 @@
10 10
11 Available here: page (.title .url .source .date .date_iso .tags .excerpt .toc 11 Available here: page (.title .url .source .date .date_iso .tags .excerpt .toc
12 .keywords), site, nav, root, stylesheet. See the org-ssg guide on Templates. #} 12 .keywords), site, nav, root, stylesheet. See the org-ssg guide on Templates. #}
13{#- The table of contents, in the shape Emacs exports it. `page.toc` is a tree of
14 {title, anchor, level, children}, and it is empty when the page has no headings or
15 says `#+OPTIONS: toc:nil` — so pages that opt out simply render nothing here. #}
16{%- macro toc_list(entries) -%}
17<ul>
18{%- for e in entries %}
19<li><a href="#{{ e.anchor }}">{{ e.title }}</a>
20{%- if e.children %}{{ toc_list(e.children) }}{% endif %}</li>
21{%- endfor %}
22</ul>
23{%- endmacro %}
24{%- macro toc(entries) -%}
25{%- if entries %}
26<div id="table-of-contents" role="doc-toc">
27<h2>Table of Contents</h2>
28<div id="text-table-of-contents" role="doc-toc">
29{{- toc_list(entries) }}
30</div>
31</div>
32{%- endif %}
33{%- endmacro %}
13<!DOCTYPE html> 34<!DOCTYPE html>
14<html lang="{{ site.language }}"> 35<html lang="{{ site.language }}">
15<head> 36<head>
@@ -59,7 +80,9 @@
59{%- endif %} 80{%- endif %}
60{#- Blog posts add a reply footer inside this block; see post.html, which the 81{#- Blog posts add a reply footer inside this block; see post.html, which the
61 [[pages]] rule in org-ssg.toml selects for everything under blog/. #} 82 [[pages]] rule in org-ssg.toml selects for everything under blog/. #}
62<div>{% block content %}{{ body | safe }}{% endblock %}</div> 83<div>
84{{- toc(page.toc) }}
85{% block content %}{{ body | safe }}{% endblock %}</div>
63</article> 86</article>
64{% endblock %} 87{% endblock %}
65</main> 88</main>
content/templates/blog.html +18
@@ -1,16 +1,34 @@
1{# The blog index at /blog/, mirroring theme/templates/blog.html. 1{# The blog index at /blog/, mirroring theme/templates/blog.html.
2 Separate from list.html because the live page carries its own prose. #} 2 Separate from list.html because the live page carries its own prose. #}
3{% extends "base.html" %} 3{% extends "base.html" %}
4
5{#- Group the list under year headings, as the live site does. Set to false for one flat
6 list; styles.css has .post-list-year either way. `page.year` comes from #+DATE:, and
7 a post without one lands in its own group at the end rather than being dropped. #}
8{%- set group_by_year = true %}
9
4{% block main %} 10{% block main %}
5<h1>{{ page.title }}</h1> 11<h1>{{ page.title }}</h1>
6<p>Use <code>&#8984; F</code> / <code>Ctrl F</code> to search &middot; <a href="{{ root }}feed.xml">RSS Feed</a></p> 12<p>Use <code>&#8984; F</code> / <code>Ctrl F</code> to search &middot; <a href="{{ root }}feed.xml">RSS Feed</a></p>
7<p>Want to browse by topic instead? Head over to the <a href="{{ root }}tags/index.html">tags</a> page.</p> 13<p>Want to browse by topic instead? Head over to the <a href="{{ root }}tags/index.html">tags</a> page.</p>
8<ul class="post-list"> 14<ul class="post-list">
15{%- if group_by_year %}
16{%- for year, posts in pages | groupby("year") | reverse %}
17<li class="post-list-year">{{ year if year else "undated" }}</li>
18{%- for entry in posts %}
19<li class="post-list-item">
20{%- if entry.date_iso %}<time datetime="{{ entry.date_iso }}">{{ entry.date_iso }}</time>{% endif %}
21<a href="{{ root }}{{ entry.url }}">{{ entry.title }}</a>
22</li>
23{%- endfor %}
24{%- endfor %}
25{%- else %}
9{%- for entry in pages %} 26{%- for entry in pages %}
10<li class="post-list-item"> 27<li class="post-list-item">
11{%- if entry.date_iso %}<time datetime="{{ entry.date_iso }}">{{ entry.date_iso }}</time>{% endif %} 28{%- if entry.date_iso %}<time datetime="{{ entry.date_iso }}">{{ entry.date_iso }}</time>{% endif %}
12<a href="{{ root }}{{ entry.url }}">{{ entry.title }}</a> 29<a href="{{ root }}{{ entry.url }}">{{ entry.title }}</a>
13</li> 30</li>
14{%- endfor %} 31{%- endfor %}
32{%- endif %}
15</ul> 33</ul>
16{% endblock %} 34{% endblock %}