Sometimes it is useful to create a link to a specific section of an article.
So it is common that headings of articles have an id which can be used to create an anchor link.
<h1 id="hello-world">Hello World</h1>
If we want to create a link which scrolls directly to that heading,
we can append the following anchor to the url of the article #hello-world
.
So much for the theory, but how can we automate this?
Generating ids
Generating ids for each heading is easy with MDX.
We can use the rehype-slug plugin, which does the whole work for us.
# pnpm
pnpm add -D rehype-slug
# yarn
yarn add -D rehype-slug
# npm
npm install --save-dev rehype-slug
After the installation we have to tell MDX that it should use the plugin.
The example below shows a contentlayer config,
but it should work with every MDX setup.
import rehypeSlug from "rehype-slug";
export default makeSource({
contentDirPath: "content/posts",
documentTypes: [Post],
mdx: {
rehypePlugins: [rehypeSlug],
remarkPlugins: [],
},
});
That's it.
Now every heading in our articles should have an id and we could create links for them.
But how do we create those links?
Do we need to look up the id of the heading from the source code?
This sounds too complicated, there must be an easier way.
Self link
The simplest way to make it easier to generate links to our headings,
is to turn the headings into links which point to themselves.
Doing so, we can click them and copy the url from the browser address bar afterwards.
We could use rehype-autolink-headings for this.
But if we want to do some styling with TailwindCSS, we have to wrap the heading manually.
To wrap our headings into links,
we need to override the default MDX heading components.
const Markdown = ({ code }: Props) => {
const MDXComponent = useMDXComponent(code);
return (
<section className="prose">
<MDXComponent
components={{
h1: heading("h1"),
h2: heading("h2"),
h3: heading("h3"),
h4: heading("h4"),
h5: heading("h5"),
h6: heading("h6"),
}}
/>
</section>
);
};
We use a simple factory function to create a component for each heading:
import { Hash } from "lucide-react";
import { ReactNode } from "react";
type HeadingProps = {
id?: string;
children?: ReactNode;
};
const heading = (As: "h1", `h2`, `h3`, `h4`, `h5`, `h6`) => {
const Heading = ({ id, children }: HeadingProps) => (
<a href={`#${id}`} className="group relative no-underline">
<Hash className="absolute -left-6 hidden group-hover:block p-1 pink-cyan-500 h-full" />
<As id={id}>{children}</As>
</a>
);
Heading.displayName = As;
return Heading;
};
The heading component renders an a
tag,
with an href
pointing to the id
which was generated by the rehype-slug and passed as a prop to our component.
On the a
tag we use the group class
which allows us to apply styling to children if the parent is hovered.
Also we use the relative
class, because we want to position an icon absolute
to the left of the heading.
Inside of the a
tag we use a Hash
icon from the lucide-react package.
The icon is positioned absolute with -1.5rem to the left of its normal position (absolute -left-6
).
The icon is hidden by default, but gets displayed as block if the group is hovered (hidden group-hover:block
).
The last child of the a
tag is the heading itself which is one of h1
, h2
, h3
, h4
, h5
or h6
depending on the parameter with which the factory function was called.
The heading renders the id
where the href
of the a
tag points to and the children
which is the text of the heading.
And that's it, we can now hover over our MDX headings and should see a hash to the left.
If we click a heading, we should see the id appended to the url in the browser address bar.
Top comments (0)