Skip to content

Commit 14e569f

Browse files
committed
Customize MDX links and headings
1 parent ec606ec commit 14e569f

File tree

4 files changed

+135
-7
lines changed

4 files changed

+135
-7
lines changed
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/**
2+
* @file MDX Heading components.
3+
* Copied from `nextra-theme-docs` and restyled.
4+
*/
5+
6+
import { clsx } from "clsx"
7+
import {
8+
useSetActiveAnchor,
9+
useIntersectionObserver,
10+
useSlugs,
11+
} from "nextra-theme-docs"
12+
13+
import { useEffect, useRef } from "react"
14+
15+
const headingClasses = {
16+
h1: "typography-h2 mt-2",
17+
h2: "typography-h3 mt-10",
18+
h3: "typography-body-lg mt-8",
19+
h4: "typography-body-md font-semibold mt-8",
20+
h5: "typography-label",
21+
h6: "typography-label",
22+
}
23+
24+
const createHeading = (
25+
Tag: `h${2 | 3 | 4 | 5 | 6}`,
26+
context: { index: number },
27+
) =>
28+
function Heading({
29+
children,
30+
id,
31+
className,
32+
...props
33+
}: React.ComponentPropsWithoutRef<"h2">): React.ReactElement {
34+
// Nextra tracks anchors in context
35+
const setActiveAnchor = useSetActiveAnchor()
36+
const slugs = useSlugs()
37+
const observer = useIntersectionObserver()
38+
const obRef = useRef<HTMLAnchorElement>(null)
39+
40+
useEffect(() => {
41+
const heading = obRef.current
42+
if (!id || !observer || !heading) return
43+
observer.observe(heading)
44+
slugs.set(heading, [id, (context.index += 1)])
45+
46+
return () => {
47+
observer.disconnect()
48+
slugs.delete(heading)
49+
setActiveAnchor(f => {
50+
const ret = { ...f }
51+
delete ret[id]
52+
return ret
53+
})
54+
}
55+
}, [id, slugs, observer, setActiveAnchor])
56+
57+
return (
58+
<Tag
59+
id={id}
60+
className={
61+
className === "sr-only"
62+
? // can be added by footnotes
63+
"sr-only"
64+
: clsx(headingClasses[Tag], "text-neu-900", className)
65+
}
66+
{...props}
67+
>
68+
{children}
69+
{id && (
70+
<a
71+
href={`#${id}`}
72+
className="nextra-focus subheading-anchor"
73+
aria-label="Permalink for this section"
74+
ref={obRef}
75+
/>
76+
)}
77+
</Tag>
78+
)
79+
}
80+
81+
export function getMdxHeadings() {
82+
const counter = ((globalThis as Record<string, any>).__headingsCounter ||= {
83+
index: 0,
84+
})
85+
86+
return {
87+
h1: (props: React.ComponentPropsWithoutRef<"h1">) => (
88+
<h1
89+
{...props}
90+
className={clsx(headingClasses.h1, "text-neu-900", props.className)}
91+
/>
92+
),
93+
h2: createHeading("h2", counter),
94+
h3: createHeading("h3", counter),
95+
h4: createHeading("h4", counter),
96+
h5: createHeading("h5", counter),
97+
h6: createHeading("h6", counter),
98+
}
99+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { getMdxHeadings } from "./get-mdx-headings"
2+
import { MdxLink } from "./mdx-link"
3+
4+
export const mdxComponents = {
5+
a: MdxLink,
6+
...getMdxHeadings(),
7+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { forwardRef } from "react"
2+
import { clsx } from "clsx"
3+
4+
import { Anchor } from "@/app/conf/_design-system/anchor"
5+
6+
export const MdxLink = forwardRef<
7+
HTMLAnchorElement,
8+
React.ComponentPropsWithoutRef<"a">
9+
>(function MdxLink(props, ref) {
10+
return (
11+
<Anchor
12+
{...props}
13+
ref={ref}
14+
// we remove `text-underline-position` from default Nextra link styles, because Neue Montreal font
15+
// has a different underline position than system fonts, and it looks bad in Safari.
16+
className={clsx(
17+
"typography-link text-neu-900 decoration-from-font underline-offset-2",
18+
props.className,
19+
)}
20+
href={props.href || ""}
21+
>
22+
{props.children}
23+
</Anchor>
24+
)
25+
})

theme.config.tsx

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@ import { useRouter } from "next/router"
55

66
import { GraphQLWordmarkLogo } from "./src/icons"
77
import { Footer } from "@/components/footer"
8+
import { mdxComponents } from "@/_design-system/mdx-components"
89

910
const graphQLLogo = (
1011
<GraphQLWordmarkLogo className="nextra-logo h-6" title="GraphQL" />
1112
)
1213

1314
export default {
1415
backgroundColor: {
15-
dark: "27,27,27",
16+
light: "251,251,249",
17+
dark: "13.7,14.7,10.8",
1618
},
1719
head: function useHead() {
1820
const { frontMatter, title: pageTitle } = useConfig()
@@ -90,10 +92,5 @@ export default {
9092
search: {
9193
placeholder: "Search…",
9294
},
93-
// TODO: @dimaMachina try to remove pnpm patch for `nextra` package later
94-
// components: {
95-
// // Override `next/image` imports from `nextra-theme-docs`
96-
// img: (props: any) =>
97-
// createElement(typeof props.src === "object" ? NextImage : "img", props),
98-
// },
95+
components: mdxComponents,
9996
} satisfies DocsThemeConfig

0 commit comments

Comments
 (0)