feat(website): add anchor links to docs headings

Hover a heading to reveal a # link on the left that updates the URL
hash without layout shift, matching GitHub README behavior.
This commit is contained in:
Mohamed Boudra
2026-05-01 21:11:14 +07:00
parent f2a23039cb
commit 65b814c7dc
6 changed files with 151 additions and 2 deletions
+2
View File
@@ -20,6 +20,8 @@
"react": "^19.1.4",
"react-dom": "^19.1.4",
"react-markdown": "^10.1.0",
"rehype-autolink-headings": "^7.1.0",
"rehype-slug": "^6.0.0",
"wrangler": "^4.59.1"
},
"devDependencies": {
+20
View File
@@ -0,0 +1,20 @@
import rehypeAutolinkHeadings from "rehype-autolink-headings";
import rehypeSlug from "rehype-slug";
import type { PluggableList } from "unified";
export const docsRehypePlugins: PluggableList = [
rehypeSlug,
[
rehypeAutolinkHeadings,
{
behavior: "prepend",
properties: { className: "heading-anchor", ariaLabel: "Link to this section" },
content: {
type: "element",
tagName: "span",
properties: { className: "heading-anchor-icon", ariaHidden: "true" },
children: [{ type: "text", value: "#" }],
},
},
],
];
+2 -1
View File
@@ -1,6 +1,7 @@
import { createFileRoute } from "@tanstack/react-router";
import ReactMarkdown from "react-markdown";
import { getDoc } from "~/docs";
import { docsRehypePlugins } from "~/docs-rehype";
import { pageMeta } from "~/meta";
export const Route = createFileRoute("/docs/$")({
@@ -24,5 +25,5 @@ function DocsPage() {
return <p className="text-muted-foreground">Doc not found.</p>;
}
return <ReactMarkdown>{doc.content}</ReactMarkdown>;
return <ReactMarkdown rehypePlugins={docsRehypePlugins}>{doc.content}</ReactMarkdown>;
}
+2 -1
View File
@@ -1,6 +1,7 @@
import { createFileRoute } from "@tanstack/react-router";
import ReactMarkdown from "react-markdown";
import { getDoc } from "~/docs";
import { docsRehypePlugins } from "~/docs-rehype";
import { pageMeta } from "~/meta";
export const Route = createFileRoute("/docs/")({
@@ -17,5 +18,5 @@ export const Route = createFileRoute("/docs/")({
function DocsIndex() {
const doc = getDoc("");
if (!doc) return <p className="text-muted-foreground">Doc not found.</p>;
return <ReactMarkdown>{doc.content}</ReactMarkdown>;
return <ReactMarkdown rehypePlugins={docsRehypePlugins}>{doc.content}</ReactMarkdown>;
}
+43
View File
@@ -94,6 +94,16 @@
margin-top: 0;
}
.docs-prose h1,
.docs-prose h2,
.docs-prose h3,
.docs-prose h4,
.docs-prose h5,
.docs-prose h6 {
position: relative;
scroll-margin-top: 1.5rem;
}
.docs-prose h1 {
color: var(--color-foreground);
font-size: 1.875rem;
@@ -121,6 +131,39 @@
margin-bottom: 0.75rem;
}
.docs-prose .heading-anchor {
position: absolute;
right: 100%;
top: 0;
bottom: 0;
display: inline-flex;
align-items: center;
padding-right: 0.375rem;
text-decoration: none;
color: rgba(255, 255, 255, 0.4);
opacity: 0;
transition: opacity 0.15s ease;
}
.docs-prose .heading-anchor:hover {
color: var(--color-foreground);
}
.docs-prose h1:hover .heading-anchor,
.docs-prose h2:hover .heading-anchor,
.docs-prose h3:hover .heading-anchor,
.docs-prose h4:hover .heading-anchor,
.docs-prose h5:hover .heading-anchor,
.docs-prose h6:hover .heading-anchor,
.docs-prose .heading-anchor:focus {
opacity: 1;
}
.docs-prose .heading-anchor-icon {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-weight: 400;
}
.docs-prose p {
margin-top: 1rem;
margin-bottom: 1rem;