From 4e2f579e71585ea55ccd3a4b6f902e13318dd29e Mon Sep 17 00:00:00 2001 From: Ricardo Date: Tue, 17 Feb 2026 19:09:09 +0100 Subject: [PATCH] feat: add heading anchors for direct linking to article sections Uses markdown-it-anchor to generate linkable IDs on h2-h4 headings. Headings become clickable links with a subtle # indicator on hover. Includes scroll-margin-top so anchored headings don't hide behind the sticky header. --- css/tailwind.css | 25 +++++++++++++++++++++++++ eleventy.config.js | 6 ++++++ package-lock.json | 36 ++++++++++++++++++++++++++++++++++++ package.json | 1 + 4 files changed, 68 insertions(+) diff --git a/css/tailwind.css b/css/tailwind.css index bb1528b..6b4d758 100644 --- a/css/tailwind.css +++ b/css/tailwind.css @@ -468,6 +468,31 @@ scroll-margin-top: 80px; /* Prevent header overlap when scrolling to anchors */ } + /* Heading anchors — generated by markdown-it-anchor */ + .prose h2[id], + .prose h3[id], + .prose h4[id] { + scroll-margin-top: 80px; + } + .prose :is(h2, h3, h4) > a.header-anchor { + color: inherit; + text-decoration: none; + } + .prose :is(h2, h3, h4) > a.header-anchor:hover { + text-decoration: underline; + text-decoration-color: var(--tw-prose-links, currentColor); + text-underline-offset: 4px; + } + .prose :is(h2, h3, h4) > a.header-anchor::after { + content: " #"; + opacity: 0; + font-weight: normal; + transition: opacity 0.15s; + } + .prose :is(h2, h3, h4):hover > a.header-anchor::after { + opacity: 0.4; + } + .post-list li { content-visibility: auto; contain-intrinsic-size: auto 200px; diff --git a/eleventy.config.js b/eleventy.config.js index bbe41b2..d3919b1 100644 --- a/eleventy.config.js +++ b/eleventy.config.js @@ -4,6 +4,7 @@ import embedEverything from "eleventy-plugin-embed-everything"; import { eleventyImageTransformPlugin } from "@11ty/eleventy-img"; import sitemap from "@quasibit/eleventy-plugin-sitemap"; import markdownIt from "markdown-it"; +import markdownItAnchor from "markdown-it-anchor"; import syntaxHighlight from "@11ty/eleventy-plugin-syntaxhighlight"; import { minify } from "html-minifier-terser"; import { createHash } from "crypto"; @@ -48,6 +49,11 @@ export default function (eleventyConfig) { linkify: true, // Auto-convert URLs to clickable links typographer: true, }); + md.use(markdownItAnchor, { + permalink: markdownItAnchor.permalink.headerLink(), + slugify: (s) => s.toLowerCase().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, ""), + level: [2, 3, 4], + }); eleventyConfig.setLibrary("md", md); // Syntax highlighting for fenced code blocks (```lang) diff --git a/package-lock.json b/package-lock.json index a786c65..f41c894 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "eleventy-plugin-embed-everything": "^1.21.0", "html-minifier-terser": "^7.0.0", "markdown-it": "^14.0.0", + "markdown-it-anchor": "^9.2.0", "pagefind": "^1.3.0", "rss-parser": "^3.13.0" }, @@ -1053,6 +1054,31 @@ "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "license": "MIT", + "peer": true + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "license": "MIT", + "peer": true + }, "node_modules/@types/node": { "version": "14.18.63", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", @@ -2812,6 +2838,16 @@ "markdown-it": "bin/markdown-it.mjs" } }, + "node_modules/markdown-it-anchor": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-9.2.0.tgz", + "integrity": "sha512-sa2ErMQ6kKOA4l31gLGYliFQrMKkqSO0ZJgGhDHKijPf0pNFM9vghjAh3gn26pS4JDRs7Iwa9S36gxm3vgZTzg==", + "license": "Unlicense", + "peerDependencies": { + "@types/markdown-it": "*", + "markdown-it": "*" + } + }, "node_modules/markdown-it/node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", diff --git a/package.json b/package.json index 7fe874a..1380505 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "eleventy-plugin-embed-everything": "^1.21.0", "html-minifier-terser": "^7.0.0", "markdown-it": "^14.0.0", + "markdown-it-anchor": "^9.2.0", "pagefind": "^1.3.0", "rss-parser": "^3.13.0" },