Commit 2ff6947870
2ff694787076f19a71fb0378186033fa937fb278
parent: 768e668f7a
Verified · cmc
cmc <hello@cleberg.net> · 2026-08-11 21:12 UTC
0.19.1: name the footnote back-links
`<a class="footnote-back" href="#fnr-1">↩</a>` has that glyph as its entire
accessible name, so a screen reader announces "left arrow with hook" once per
note and the reader cannot tell which reference any of them returns to. Each one
now carries `aria-label="Back to reference N"`, and the section carries
`aria-label="Footnotes"` — a `<hr>` is a picture of a section break rather than
a section, and the landmark was otherwise announced as an unnamed "section".
No visual change, so no stylesheet has to move.
Also documents template partials in the guide: a snippet that is not really
layout — an invitation to reply, a licence line — is better as its own file, and
the render key follows `{% include %}` so editing one re-renders exactly its
users.
Layout: unified · split
CHANGELOG.md
+7
| @@ -15,6 +15,13 @@ Two conventions worth knowing before reading: |
| 15 | 15 | Versions follow the compatibility promise in the README: config keys, template variables, |
| 16 | 16 | CLI flags and URLs are the stable surface. |
| 17 | 17 | |
| 18 | ## 0.19.1 |
| 19 | |
| 20 | - Footnote back-links carry `aria-label="Back to reference N"`, and the notes section is |
| 21 | labelled. A link whose only visible content is `↩` has that glyph as its whole |
| 22 | accessible name, so a screen reader announced "left arrow with hook" once per note with |
| 23 | no way to tell them apart. |
| 24 | |
| 18 | 25 | ## 0.19.0 |
| 19 | 26 | |
| 20 | 27 | - **Full-content collections.** `include_content = true` gives a listing template each |
Cargo.lock
+1 −1
| @@ -675,7 +675,7 @@ dependencies = [ |
| 675 | 675 | |
| 676 | 676 | [[package]] |
| 677 | 677 | name = "org-ssg" |
| 678 | | version = "0.19.0" |
| 678 | version = "0.19.1" |
| 679 | 679 | dependencies = [ |
| 680 | 680 | "anyhow", |
| 681 | 681 | "blake3", |
Cargo.toml
+1 −1
| @@ -1,6 +1,6 @@ |
| 1 | 1 | [package] |
| 2 | 2 | name = "org-ssg" |
| 3 | | version = "0.19.0" |
| 3 | version = "0.19.1" |
| 4 | 4 | edition = "2021" |
| 5 | 5 | description = "Org-mode static site generator that renders the org element tree straight to HTML" |
| 6 | 6 | license = "MIT" |
docs/guide/04-templates.org
+18
| @@ -77,6 +77,24 @@ page. |
| 77 | 77 | |
| 78 | 78 | Subdirectories work, so ={% include "partials/header.html" %}= does what you expect. |
| 79 | 79 | |
| 80 | ** Partials keep a snippet out of a layout |
| 81 | |
| 82 | A block of content that is not really layout — an invitation to reply, a licence line, a |
| 83 | donation ask — is better as its own file than as a line inside =base.html=: |
| 84 | |
| 85 | #+BEGIN_SRC html |
| 86 | {% extends "base.html" %} |
| 87 | {% block content %} |
| 88 | {{ body | safe }} |
| 89 | {% include "reply.html" %} |
| 90 | {% endblock %} |
| 91 | #+END_SRC |
| 92 | |
| 93 | Emptying =reply.html= removes it everywhere; editing it re-renders exactly the pages that |
| 94 | include it, because the render key follows includes. And because the *page* chooses its |
| 95 | layout, an individual page opts in with =#+TEMPLATE: post.html= or out with |
| 96 | =#+TEMPLATE: base.html=, without a rule deciding for a whole directory. |
| 97 | |
| 80 | 98 | * Variables |
| 81 | 99 | |
| 82 | 100 | ** body |
src/render.rs
+11 −2
| @@ -708,7 +708,12 @@ impl Renderer<'_> { |
| 708 | 708 | let order = self.order.clone(); |
| 709 | 709 | let inline_defs = self.inline_defs.clone(); |
| 710 | 710 | let block_defs = self.block_defs.clone(); |
| 711 | | out.push_str("<section class=\"footnotes\">\n<hr>\n<ol>\n"); |
| 711 | // Named, because a `<hr>` is a picture of a section break rather than a section: |
| 712 | // without the label this landmark is announced as "section" and the reader has to |
| 713 | // guess what they have reached. |
| 714 | out.push_str( |
| 715 | "<section class=\"footnotes\" aria-label=\"Footnotes\">\n<hr>\n<ol>\n", |
| 716 | ); |
| 712 | 717 | for (idx, label) in order.iter().enumerate() { |
| 713 | 718 | let n = idx + 1; |
| 714 | 719 | out.push_str(&format!("<li id=\"fn-{n}\">")); |
| @@ -719,8 +724,12 @@ impl Renderer<'_> { |
| 719 | 724 | self.render_element(el, out); |
| 720 | 725 | } |
| 721 | 726 | } |
| 727 | // The glyph is the whole visible link, so it is also the whole accessible |
| 728 | // name: a screen reader would otherwise announce "left arrow with hook", |
| 729 | // identically, once per note. |
| 722 | 730 | out.push_str(&format!( |
| 723 | | " <a class=\"footnote-back\" href=\"#fnr-{n}\">↩</a></li>\n" |
| 731 | " <a class=\"footnote-back\" href=\"#fnr-{n}\" \ |
| 732 | aria-label=\"Back to reference {n}\">↩</a></li>\n" |
| 724 | 733 | )); |
| 725 | 734 | } |
| 726 | 735 | out.push_str("</ol>\n</section>\n"); |
tests/constructs.rs
+17
| @@ -752,3 +752,20 @@ fn an_unexpanded_include_reports_itself() { |
| 752 | 752 | doc.diagnostics[0] |
| 753 | 753 | ); |
| 754 | 754 | } |
| 755 | |
| 756 | /// A back-link whose whole visible content is `↩` has that glyph as its whole accessible |
| 757 | /// name, so a screen reader announces "left arrow with hook" once per note and the reader |
| 758 | /// cannot tell which reference each one returns to. |
| 759 | #[test] |
| 760 | fn footnote_links_and_section_are_labelled() { |
| 761 | let html = html_of("Text[fn:1] and more[fn:2].\n\n[fn:1] First.\n\n[fn:2] Second.\n"); |
| 762 | assert!( |
| 763 | html.contains("<section class=\"footnotes\" aria-label=\"Footnotes\">"), |
| 764 | "the landmark is named:\n{html}" |
| 765 | ); |
| 766 | assert!( |
| 767 | html.contains("aria-label=\"Back to reference 1\"") |
| 768 | && html.contains("aria-label=\"Back to reference 2\""), |
| 769 | "each back-link says where it goes:\n{html}" |
| 770 | ); |
| 771 | } |
tests/snapshots/site__footnote_render.snap
+4 −4
| @@ -4,13 +4,13 @@ expression: "render_fragment(\"footnote.org\")" |
| 4 | 4 | --- |
| 5 | 5 | <p>Text with a reference.<sup class="footnote-ref"><a id="fnr-1" href="#fn-1">1</a></sup> And a second one.<sup class="footnote-ref"><a id="fnr-2" href="#fn-2">2</a></sup></p> |
| 6 | 6 | <p>An inline footnote.<sup class="footnote-ref"><a id="fnr-3" href="#fn-3">3</a></sup></p> |
| 7 | | <section class="footnotes"> |
| 7 | <section class="footnotes" aria-label="Footnotes"> |
| 8 | 8 | <hr> |
| 9 | 9 | <ol> |
| 10 | 10 | <li id="fn-1"><p>The first definition.</p> |
| 11 | | <a class="footnote-back" href="#fnr-1">↩</a></li> |
| 11 | <a class="footnote-back" href="#fnr-1" aria-label="Back to reference 1">↩</a></li> |
| 12 | 12 | <li id="fn-2"><p>The second definition, with <em>emphasis</em>.</p> |
| 13 | | <a class="footnote-back" href="#fnr-2">↩</a></li> |
| 14 | | <li id="fn-3">defined right here <a class="footnote-back" href="#fnr-3">↩</a></li> |
| 13 | <a class="footnote-back" href="#fnr-2" aria-label="Back to reference 2">↩</a></li> |
| 14 | <li id="fn-3">defined right here <a class="footnote-back" href="#fnr-3" aria-label="Back to reference 3">↩</a></li> |
| 15 | 15 | </ol> |
| 16 | 16 | </section> |
tests/snapshots/site__site_guide_html.snap
+2 −2
| @@ -40,11 +40,11 @@ expression: "page(&pages, \"guide.org\").html" |
| 40 | 40 | <tr><td>beta</td><td>20</td></tr> |
| 41 | 41 | </tbody> |
| 42 | 42 | </table> |
| 43 | | <section class="footnotes"> |
| 43 | <section class="footnotes" aria-label="Footnotes"> |
| 44 | 44 | <hr> |
| 45 | 45 | <ol> |
| 46 | 46 | <li id="fn-1"><p>Read the manual before you begin.</p> |
| 47 | | <a class="footnote-back" href="#fnr-1">↩</a></li> |
| 47 | <a class="footnote-back" href="#fnr-1" aria-label="Back to reference 1">↩</a></li> |
| 48 | 48 | </ol> |
| 49 | 49 | </section> |
| 50 | 50 | </main> |