diff --git a/eleventy.config.js b/eleventy.config.js index 84f904e..3014625 100644 --- a/eleventy.config.js +++ b/eleventy.config.js @@ -9,6 +9,7 @@ import markdownItAnchor from "markdown-it-anchor"; import markdownItFootnote from "markdown-it-footnote"; import syntaxHighlight from "@11ty/eleventy-plugin-syntaxhighlight"; import { minify } from "html-minifier-terser"; +import posthtml from "posthtml"; import { minify as minifyJS } from "terser"; import registerUnfurlShortcode, { getCachedCard, prefetchUrl } from "./lib/unfurl-shortcode.js"; import matter from "gray-matter"; @@ -489,6 +490,110 @@ export default function (eleventyConfig) { return result; }); + // Sidenotes — convert markdown-it-footnote output into margin sidenotes. + // Wide screens (xl+): sidenotes float left. Narrow: footnote section at bottom. + eleventyConfig.addTransform("sidenotes", async function (content, outputPath) { + // Fast bail-outs + if (typeof outputPath !== "string" || !outputPath.endsWith(".html")) return content; + if (!content.includes('class="footnote-ref"')) return content; + const isPostPage = ["/articles/", "/notes/", "/bookmarks/", "/photos/", "/replies/", "/reposts/", "/likes/", "/pages/"] + .some(p => outputPath.includes(p)); + if (!isPostPage) return content; + + const result = await posthtml([ + (tree) => { + // 1. Build map: fnId → inline HTML (backref stripped,
wrappers stripped)
+ const fnMap = {};
+ tree.walk(node => {
+ if (
+ node.tag === "li" &&
+ node.attrs?.class?.includes("footnote-item") &&
+ node.attrs?.id
+ ) {
+ const fnId = node.attrs.id;
+ // Collect children, skip
+ const children = (node.content || []).flatMap(child => {
+ if (child.tag === "p") {
+ // Strip outer , keep inner content (excluding backref )
+ return (child.content || []).filter(c =>
+ !(c.tag === "a" && c.attrs?.class?.includes("footnote-backref"))
+ );
+ }
+ return [child];
+ });
+ fnMap[fnId] = children;
+ }
+ return node;
+ });
+
+ // 2. Track whether any sidenotes were injected
+ let hasSidenotes = false;
+
+ // 3. Replace each with sidenote-host + aside
+ tree.walk(node => {
+ if (node.tag === "sup" && node.attrs?.class?.includes("footnote-ref")) {
+ // Find the child to get href and id
+ const anchor = (node.content || []).find(c => c.tag === "a");
+ if (!anchor) return node;
+
+ const href = anchor.attrs?.href || ""; // e.g. "#fn1"
+ const fnId = href.replace(/^#/, ""); // e.g. "fn1"
+ const refId = anchor.attrs?.id || ""; // e.g. "fnref1"
+
+ // Extract numeric label from anchor text e.g. "[1]" → "1"
+ const rawLabel = (anchor.content || []).find(c => typeof c === "string") || "";
+ const label = rawLabel.replace(/[\[\]]/g, "").trim();
+
+ const noteContent = fnMap[fnId] || [];
+ hasSidenotes = true;
+
+ return {
+ tag: "span",
+ attrs: { class: "sidenote-host" },
+ content: [
+ {
+ tag: "span",
+ attrs: { class: "footnote-ref-num", id: refId },
+ content: [label],
+ },
+ {
+ tag: "aside",
+ attrs: {
+ class: "sidenote",
+ "aria-label": `Sidenote ${label}`,
+ },
+ content: [
+ {
+ tag: "span",
+ attrs: { class: "sidenote-number" },
+ content: [label],
+ },
+ " ",
+ ...noteContent,
+ ],
+ },
+ ],
+ };
+ }
+ return node;
+ });
+
+ // 4. Add has-sidenotes class to