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 | Versions follow the compatibility promise in the README: config keys, template variables, |
15 | Versions follow the compatibility promise in the README: config keys, template variables, |
| 16 | CLI flags and URLs are the stable surface. |
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 | ## 0.19.0 |
25 | ## 0.19.0 |
| 19 | |
26 | |
| 20 | - **Full-content collections.** `include_content = true` gives a listing template each |
27 | - **Full-content collections.** `include_content = true` gives a listing template each |
Cargo.lock
+1 −1
| @@ -675,7 +675,7 @@ dependencies = [ |
| 675 | |
675 | |
| 676 | [[package]] |
676 | [[package]] |
| 677 | name = "org-ssg" |
677 | name = "org-ssg" |
| 678 | version = "0.19.0" |
678 | version = "0.19.1" |
| 679 | dependencies = [ |
679 | dependencies = [ |
| 680 | "anyhow", |
680 | "anyhow", |
| 681 | "blake3", |
681 | "blake3", |
Cargo.toml
+1 −1
| @@ -1,6 +1,6 @@ |
| 1 | [package] |
1 | [package] |
| 2 | name = "org-ssg" |
2 | name = "org-ssg" |
| 3 | version = "0.19.0" |
3 | version = "0.19.1" |
| 4 | edition = "2021" |
4 | edition = "2021" |
| 5 | description = "Org-mode static site generator that renders the org element tree straight to HTML" |
5 | description = "Org-mode static site generator that renders the org element tree straight to HTML" |
| 6 | license = "MIT" |
6 | license = "MIT" |
docs/guide/04-templates.org
+18
| @@ -77,6 +77,24 @@ page. |
| 77 | |
77 | |
| 78 | Subdirectories work, so ={% include "partials/header.html" %}= does what you expect. |
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 | * Variables |
98 | * Variables |
| 81 | |
99 | |
| 82 | ** body |
100 | ** body |
src/render.rs
+11 −2
| @@ -708,7 +708,12 @@ impl Renderer<'_> { |
| 708 | let order = self.order.clone(); |
708 | let order = self.order.clone(); |
| 709 | let inline_defs = self.inline_defs.clone(); |
709 | let inline_defs = self.inline_defs.clone(); |
| 710 | let block_defs = self.block_defs.clone(); |
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 | for (idx, label) in order.iter().enumerate() { |
717 | for (idx, label) in order.iter().enumerate() { |
| 713 | let n = idx + 1; |
718 | let n = idx + 1; |
| 714 | out.push_str(&format!("<li id=\"fn-{n}\">")); |
719 | out.push_str(&format!("<li id=\"fn-{n}\">")); |
| @@ -719,8 +724,12 @@ impl Renderer<'_> { |
| 719 | self.render_element(el, out); |
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 | out.push_str(&format!( |
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 | out.push_str("</ol>\n</section>\n"); |
735 | out.push_str("</ol>\n</section>\n"); |
tests/constructs.rs
+17
| @@ -752,3 +752,20 @@ fn an_unexpanded_include_reports_itself() { |
| 752 | doc.diagnostics[0] |
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 | <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> |
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 | <p>An inline footnote.<sup class="footnote-ref"><a id="fnr-3" href="#fn-3">3</a></sup></p> |
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 | <hr> |
8 | <hr> |
| 9 | <ol> |
9 | <ol> |
| 10 | <li id="fn-1"><p>The first definition.</p> |
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 | <li id="fn-2"><p>The second definition, with <em>emphasis</em>.</p> |
12 | <li id="fn-2"><p>The second definition, with <em>emphasis</em>.</p> |
| 13 | <a class="footnote-back" href="#fnr-2">↩</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">↩</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 | </ol> |
15 | </ol> |
| 16 | </section> |
16 | </section> |
tests/snapshots/site__site_guide_html.snap
+2 −2
| @@ -40,11 +40,11 @@ expression: "page(&pages, \"guide.org\").html" |
| 40 | <tr><td>beta</td><td>20</td></tr> |
40 | <tr><td>beta</td><td>20</td></tr> |
| 41 | </tbody> |
41 | </tbody> |
| 42 | </table> |
42 | </table> |
| 43 | <section class="footnotes"> |
43 | <section class="footnotes" aria-label="Footnotes"> |
| 44 | <hr> |
44 | <hr> |
| 45 | <ol> |
45 | <ol> |
| 46 | <li id="fn-1"><p>Read the manual before you begin.</p> |
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 | </ol> |
48 | </ol> |
| 49 | </section> |
49 | </section> |
| 50 | </main> |
50 | </main> |