diff --git a/css/tailwind.css b/css/tailwind.css index 1bbafb7..1f027d7 100644 --- a/css/tailwind.css +++ b/css/tailwind.css @@ -1155,3 +1155,65 @@ body[data-indiekit-auth="true"] .share-post-btn:hover { background-color: rgb(142 192 124 / 0.1); } } + +/* ── Sidenotes ───────────────────────────────────────────────────────────── */ +/* Narrow: hide sidenotes; footnote
is visible by default */ + +.sidenote { + display: none; +} + +.sidenote-host { + display: inline; +} + +.footnote-ref-num { + font-size: 0.75em; + vertical-align: super; + line-height: 0; + font-weight: 600; + color: #458588; /* accent-400 */ + font-variant-numeric: tabular-nums; +} + +/* Wide (xl+): left gutter + floating sidenotes */ +@media (min-width: 1280px) { + /* Cancel the overflow:clip BFC so the float can escape the content box + into the padding gutter. The !important overrides Tailwind prose resets. */ + article.has-sidenotes .e-content { + overflow-x: visible !important; + padding-left: 210px !important; + } + + article.has-sidenotes .sidenote { + display: block; + float: left; + clear: left; + width: 190px; + margin-left: -210px; /* pull into the 210px padding gutter */ + margin-top: 0.15em; /* subtle optical alignment with line */ + font-size: 0.8rem; + font-style: italic; + line-height: 1.5; + color: #7c6f64; /* surface-500 */ + } + + .dark article.has-sidenotes .sidenote { + color: #a89984; /* surface-400 */ + } + + article.has-sidenotes .sidenote-number { + display: block; + font-size: 0.7rem; + font-family: monospace; + font-style: normal; + color: #458588; /* accent-400 */ + margin-bottom: 2px; + } + + /* Hide the footnote fallback section on wide screens */ + article.has-sidenotes .footnotes-sep, + article.has-sidenotes section.footnotes { + display: none; + } +} diff --git a/eleventy.config.js b/eleventy.config.js index 84f904e..86f79a9 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)\/[^/]+\/index\.html$/.test(outputPath); + 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" or "[1:1]" → "1" + const rawLabel = (anchor.content || []).find(c => typeof c === "string") || ""; + const label = rawLabel.replace(/[\[\]]/g, "").replace(/:.*$/, "").trim(); + + const noteContent = fnMap[fnId] || []; + if (!noteContent.length) return node; // Skip orphan refs with no definition + 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

if any sidenotes were injected + if (hasSidenotes) { + tree.walk(node => { + if (node.tag === "article") { + const existing = node.attrs?.class || ""; + node.attrs = { ...node.attrs, class: (existing + " has-sidenotes").trim() }; + } + return node; + }); + } + }, + ]).process(content, { sync: false }); + + return result.html; + }); + // HTML minification — only during initial build, skip during watch rebuilds eleventyConfig.addTransform("htmlmin", async function (content, outputPath) { if (outputPath && outputPath.endsWith(".html") && process.env.ELEVENTY_RUN_MODE === "build") { diff --git a/package-lock.json b/package-lock.json index 73e31c7..fed4176 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,6 +44,7 @@ "autoprefixer": "^10.4.0", "postcss": "^8.4.0", "postcss-cli": "^11.0.0", + "posthtml": "^0.16.7", "tailwindcss": "^3.4.0" } }, diff --git a/package.json b/package.json index 991e831..5d2903b 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "autoprefixer": "^10.4.0", "postcss": "^8.4.0", "postcss-cli": "^11.0.0", + "posthtml": "^0.16.7", "tailwindcss": "^3.4.0" } }