Skip to content

Commit f2123f8

Browse files
show page title in table of contents
1 parent 2a87805 commit f2123f8

3 files changed

Lines changed: 116 additions & 0 deletions

File tree

src/css/custom.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,3 +149,10 @@ article a {
149149
.breadcrumbs__link {
150150
font-size: 0.8rem;
151151
}
152+
153+
/* Anchor that src/theme/DocItem/Content puts on the page title, so the "on this
154+
page" title entry has something to link to. Docusaurus only gives its own
155+
headings the offset that clears the sticky navbar, so set it here too. */
156+
#page-top {
157+
scroll-margin-top: calc(var(--ifm-navbar-height) + 1rem);
158+
}

src/theme/DocItem/Content/index.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import clsx from 'clsx';
2+
import {ThemeClassNames} from '@docusaurus/theme-common';
3+
import {useDoc} from '@docusaurus/plugin-content-docs/client';
4+
import Heading from '@theme/Heading';
5+
import MDXContent from '@theme/MDXContent';
6+
7+
/* Ejected from @docusaurus/theme-classic so the synthetic page title can carry
8+
an anchor. The table of contents is built from h2/h3 headings only, so the
9+
title is never in it; without an id here there is nothing for the "on this
10+
page" entry to link to. src/theme/TOC prepends that entry and targets this id. */
11+
export const PAGE_TOP_ID = 'page-top';
12+
13+
/* Docusaurus renders a "synthetic title" from front matter only when the page
14+
has not asked to hide it and the content does not already open with its own
15+
h1. src/theme/TOC repeats this test, so keep the two in step. */
16+
function useSyntheticTitle() {
17+
const {metadata, frontMatter, contentTitle} = useDoc();
18+
const shouldRender =
19+
!frontMatter.hide_title && typeof contentTitle === 'undefined';
20+
if (!shouldRender) {
21+
return null;
22+
}
23+
return metadata.title;
24+
}
25+
26+
export default function DocItemContent({children}) {
27+
const syntheticTitle = useSyntheticTitle();
28+
return (
29+
<div className={clsx(ThemeClassNames.docs.docMarkdown, 'markdown')}>
30+
{syntheticTitle && (
31+
<header id={PAGE_TOP_ID}>
32+
<Heading as="h1">{syntheticTitle}</Heading>
33+
</header>
34+
)}
35+
<MDXContent>{children}</MDXContent>
36+
</div>
37+
);
38+
}

src/theme/TOCItems/Tree/index.js

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import React from 'react';
2+
import Link from '@docusaurus/Link';
3+
import {useDoc} from '@docusaurus/plugin-content-docs/client';
4+
import {PAGE_TOP_ID} from '@theme/DocItem/Content';
5+
6+
/* Ejected from @docusaurus/theme-classic to put the page title at the top of
7+
the table of contents. Docusaurus builds the list from h2/h3 headings alone,
8+
so the title -- rendered as a synthetic h1 from front matter -- never appears.
9+
10+
Ejected rather than wrapped because the component recurses into itself, so a
11+
wrapper cannot reach inside the list it renders. Both the desktop sidebar and
12+
the mobile "on this page" dropdown render through here, so they stay in step. */
13+
14+
/* Docusaurus only renders a synthetic title when the page has not hidden it and
15+
the content does not already open with its own h1. src/theme/DocItem/Content
16+
makes the same test before adding the anchor; keep the two in step. */
17+
function useSyntheticTitle() {
18+
const {metadata, frontMatter, contentTitle} = useDoc();
19+
if (frontMatter.hide_title || typeof contentTitle !== 'undefined') {
20+
return null;
21+
}
22+
return metadata.title;
23+
}
24+
25+
/* A plain <a>, not a Docusaurus <Link>, on purpose. The build-time broken-anchor
26+
checker inspects <Link> only, and derives the valid anchors from the page's
27+
headings -- it cannot see an id added by a theme component, so a <Link> here
28+
reports a broken anchor on every page that has a title. */
29+
function PageTitleItem({linkClassName}) {
30+
const title = useSyntheticTitle();
31+
if (!title) {
32+
return null;
33+
}
34+
return (
35+
<li>
36+
<a className={linkClassName ?? undefined} href={`#${PAGE_TOP_ID}`}>
37+
{title}
38+
</a>
39+
</li>
40+
);
41+
}
42+
43+
function TOCItemTree({toc, className, linkClassName, isChild}) {
44+
if (!toc.length) {
45+
return null;
46+
}
47+
return (
48+
<ul className={isChild ? undefined : className}>
49+
{!isChild && <PageTitleItem linkClassName={linkClassName} />}
50+
{toc.map((heading) => (
51+
<li key={heading.id}>
52+
<Link
53+
to={`#${heading.id}`}
54+
className={linkClassName ?? undefined}
55+
// Developer provided the HTML, so assume it's safe.
56+
dangerouslySetInnerHTML={{__html: heading.value}}
57+
/>
58+
<TOCItemTree
59+
isChild
60+
toc={heading.children}
61+
className={className}
62+
linkClassName={linkClassName}
63+
/>
64+
</li>
65+
))}
66+
</ul>
67+
);
68+
}
69+
70+
// Memo only the tree root is enough
71+
export default React.memo(TOCItemTree);

0 commit comments

Comments
 (0)