krz/orgo
Lightning fast org-mode static site generator.
clone: git clone https://gitbay.org/krz/orgo.git
2ff694787076f19a71fb0378186033fa937fb278
verified · cmc
author: Christian Cleberg <hello@cleberg.net> · 2026-08-11T21:12:36Z
CHANGELOG.md | 7 +++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- docs/guide/04-templates.org | 18 ++++++++++++++++++ src/render.rs | 13 +++++++++++-- tests/constructs.rs | 17 +++++++++++++++++ tests/snapshots/site__footnote_render.snap | 8 ++++---- tests/snapshots/site__site_guide_html.snap | 4 ++-- 8 files changed, 61 insertions(+), 10 deletions(-) @@ -15,6 +15,13 @@ Two conventions worth knowing before reading: Versions follow the compatibility promise in the README: config keys, template variables, CLI flags and URLs are the stable surface. +## 0.19.1 + +- Footnote back-links carry `aria-label="Back to reference N"`, and the notes section is + labelled. A link whose only visible content is `↩` has that glyph as its whole + accessible name, so a screen reader announced "left arrow with hook" once per note with + no way to tell them apart. + ## 0.19.0 - **Full-content collections.** `include_content = true` gives a listing template each @@ -675,7 +675,7 @@ dependencies = [ [[package]] name = "org-ssg" -version = "0.19.0" +version = "0.19.1" dependencies = [ "anyhow", "blake3", @@ -1,6 +1,6 @@ [package] name = "org-ssg" -version = "0.19.0" +version = "0.19.1" edition = "2021" description = "Org-mode static site generator that renders the org element tree straight to HTML" license = "MIT" @@ -77,6 +77,24 @@ page. Subdirectories work, so ={% include "partials/header.html" %}= does what you expect. +** Partials keep a snippet out of a layout + +A block of content that is not really layout — an invitation to reply, a licence line, a +donation ask — is better as its own file than as a line inside =base.html=: + +#+BEGIN_SRC html +{% extends "base.html" %} +{% block content %} +{{ body | safe }} +{% include "reply.html" %} +{% endblock %} +#+END_SRC + +Emptying =reply.html= removes it everywhere; editing it re-renders exactly the pages that +include it, because the render key follows includes. And because the *page* chooses its +layout, an individual page opts in with =#+TEMPLATE: post.html= or out with +=#+TEMPLATE: base.html=, without a rule deciding for a whole directory. + * Variables ** body @@ -708,7 +708,12 @@ impl Renderer<'_> { let order = self.order.clone(); let inline_defs = self.inline_defs.clone(); let block_defs = self.block_defs.clone(); - out.push_str("<section class=\"footnotes\">\n<hr>\n<ol>\n"); + // Named, because a `<hr>` is a picture of a section break rather than a section: + // without the label this landmark is announced as "section" and the reader has to + // guess what they have reached. + out.push_str( + "<section class=\"footnotes\" aria-label=\"Footnotes\">\n<hr>\n<ol>\n", + ); for (idx, label) in order.iter().enumerate() { let n = idx + 1; out.push_str(&format!("<li id=\"fn-{n}\">")); @@ -719,8 +724,12 @@ impl Renderer<'_> { self.render_element(el, out); } } + // The glyph is the whole visible link, so it is also the whole accessible + // name: a screen reader would otherwise announce "left arrow with hook", + // identically, once per note. out.push_str(&format!( - " <a class=\"footnote-back\" href=\"#fnr-{n}\">↩</a></li>\n" + " <a class=\"footnote-back\" href=\"#fnr-{n}\" \ + aria-label=\"Back to reference {n}\">↩</a></li>\n" )); } out.push_str("</ol>\n</section>\n"); @@ -752,3 +752,20 @@ fn an_unexpanded_include_reports_itself() { doc.diagnostics[0] ); } + +/// A back-link whose whole visible content is `↩` has that glyph as its whole accessible +/// name, so a screen reader announces "left arrow with hook" once per note and the reader +/// cannot tell which reference each one returns to. +#[test] +fn footnote_links_and_section_are_labelled() { + let html = html_of("Text[fn:1] and more[fn:2].\n\n[fn:1] First.\n\n[fn:2] Second.\n"); + assert!( + html.contains("<section class=\"footnotes\" aria-label=\"Footnotes\">"), + "the landmark is named:\n{html}" + ); + assert!( + html.contains("aria-label=\"Back to reference 1\"") + && html.contains("aria-label=\"Back to reference 2\""), + "each back-link says where it goes:\n{html}" + ); +} @@ -4,13 +4,13 @@ expression: "render_fragment(\"footnote.org\")" --- <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> <p>An inline footnote.<sup class="footnote-ref"><a id="fnr-3" href="#fn-3">3</a></sup></p> -<section class="footnotes"> +<section class="footnotes" aria-label="Footnotes"> <hr> <ol> <li id="fn-1"><p>The first definition.</p> - <a class="footnote-back" href="#fnr-1">↩</a></li> + <a class="footnote-back" href="#fnr-1" aria-label="Back to reference 1">↩</a></li> <li id="fn-2"><p>The second definition, with <em>emphasis</em>.</p> - <a class="footnote-back" href="#fnr-2">↩</a></li> -<li id="fn-3">defined right here <a class="footnote-back" href="#fnr-3">↩</a></li> + <a class="footnote-back" href="#fnr-2" aria-label="Back to reference 2">↩</a></li> +<li id="fn-3">defined right here <a class="footnote-back" href="#fnr-3" aria-label="Back to reference 3">↩</a></li> </ol> </section> @@ -40,11 +40,11 @@ expression: "page(&pages, \"guide.org\").html" <tr><td>beta</td><td>20</td></tr> </tbody> </table> -<section class="footnotes"> +<section class="footnotes" aria-label="Footnotes"> <hr> <ol> <li id="fn-1"><p>Read the manual before you begin.</p> - <a class="footnote-back" href="#fnr-1">↩</a></li> + <a class="footnote-back" href="#fnr-1" aria-label="Back to reference 1">↩</a></li> </ol> </section> </main>