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:
@@ -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": {
|
||||
|
||||
@@ -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: "#" }],
|
||||
},
|
||||
},
|
||||
],
|
||||
];
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user