krz/orgo

Lightning fast org-mode static site generator. fast go org-mode static-site-generator

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:
15Versions follow the compatibility promise in the README: config keys, template variables, 15Versions follow the compatibility promise in the README: config keys, template variables,
16CLI flags and URLs are the stable surface. 16CLI 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]]
677name = "org-ssg" 677name = "org-ssg"
678version = "0.19.0" 678version = "0.19.1"
679dependencies = [ 679dependencies = [
680 "anyhow", 680 "anyhow",
681 "blake3", 681 "blake3",
Cargo.toml +1 −1
@@ -1,6 +1,6 @@
1[package] 1[package]
2name = "org-ssg" 2name = "org-ssg"
3version = "0.19.0" 3version = "0.19.1"
4edition = "2021" 4edition = "2021"
5description = "Org-mode static site generator that renders the org element tree straight to HTML" 5description = "Org-mode static site generator that renders the org element tree straight to HTML"
6license = "MIT" 6license = "MIT"
docs/guide/04-templates.org +18
@@ -77,6 +77,24 @@ page.
77 77
78Subdirectories work, so ={% include "partials/header.html" %}= does what you expect. 78Subdirectories work, so ={% include "partials/header.html" %}= does what you expect.
79 79
80** Partials keep a snippet out of a layout
81
82A block of content that is not really layout — an invitation to reply, a licence line, a
83donation 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
93Emptying =reply.html= removes it everywhere; editing it re-renders exactly the pages that
94include it, because the render key follows includes. And because the *page* chooses its
95layout, 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}\">&#8617;</a></li>\n" 731 " <a class=\"footnote-back\" href=\"#fnr-{n}\" \
732 aria-label=\"Back to reference {n}\">&#8617;</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]
760fn 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">&#8617;</a></li> 11 <a class="footnote-back" href="#fnr-1" aria-label="Back to reference 1">&#8617;</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">&#8617;</a></li> 13 <a class="footnote-back" href="#fnr-2" aria-label="Back to reference 2">&#8617;</a></li>
14<li id="fn-3">defined right here <a class="footnote-back" href="#fnr-3">&#8617;</a></li> 14<li id="fn-3">defined right here <a class="footnote-back" href="#fnr-3" aria-label="Back to reference 3">&#8617;</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">&#8617;</a></li> 47 <a class="footnote-back" href="#fnr-1" aria-label="Back to reference 1">&#8617;</a></li>
48</ol> 48</ol>
49</section> 49</section>
50</main> 50</main>