Skip to content

Commit

Permalink
Autolink Markdown headings
Browse files Browse the repository at this point in the history
This commit will add anchor links to Markdown and MDX headings.

The first step is to generate heading IDs that can be used in the links.
Astro does this automatically for Markdown and MDX content.
Astro exports `{ rehypeHeadingIds } from "@astrojs/markdown-remark"`,
which can be imported into the Astro config file. In the `rehypePlugins`
array, `rehypeHeadingIds` must be specified first before other plugins
that use `rehypeHeadingIds`.
https://docs.astro.build/en/guides/markdown-content/

The next step is to use the heading IDs to create links. Astro supports
Rehype and Remark plugins, and the `rehype-autolink-headings` plugin can
be used to create heading links. This commit will install the plugin,
import it into the Astro config file, and add it to the `rehypePlugins`
array after `rehypeHeadingIds`.
https://github.com/rehypejs/rehype-autolink-headings

The final step is to structure and style the heading links. Heading
links can be separated from headings by displaying elements adjacent to
the headings, as GitHub does in READMEs and other Markdown content. Link
elements can contain text, such as an octothorpe (the "pound sign" `#`)
or emoji (🔗), or an icon font. SVG images can also be used in heading
link elements, but additional considerations are involved.

The SVG icon code itself needs to be added, either by pasting the SVG
string into a JavaScript or CSS file, by storing the SVG as an external
file and importing in a JavaScript or CSS file, or by building SVG nodes
programmatically with a library like https://github.com/syntax-tree/hast
(used in the Rehype/Remark/unifiedjs ecosystem projects). The approach
taken here is to store the SVG in an external file and import with CSS.
The link icon is from the same source as the other icons on the site,
Phosphor Icons (https://phosphoricons.com/, MIT license).

The icon then needs to be positioned in the DOM. GitHub approaches this
by placing the icon in the left margin. On small viewports like mobile
devices, the link icons are always visible. On larger viewports, the
link icons are hidden by default, then revealed when hovering over any
part of the heading or icon. A similar approach is taken here, with some
modifications to account for the pre-existing styles on this site. Size
units are similar to https://github.com/unifiedjs/unifiedjs.github.io.
For a more exact replication of GitHub's styling, see:
https://github.com/rehypejs/rehype-github
https://github.com/sindresorhus/github-markdown-css

Future work may add wrapper elements to improve heading link structure.
Wrapping heading elements in anchor elements, or conversely placing
anchor elements inside headings, can lead to accessibility limitations.
The parent-child element structure may not be clear to screen readers.
An alternative approach is to make the heading element and link element
siblings (at the same level in the HTML document) and then add a wrapper
element around both sibling elements. Again, this is what GitHub does.
https://amberwilson.co.uk/blog/are-your-anchor-links-accessible/
  • Loading branch information
br3ndonland committed Jul 23, 2024
1 parent 9704175 commit 70f8a3e
Show file tree
Hide file tree
Showing 5 changed files with 75 additions and 0 deletions.
21 changes: 21 additions & 0 deletions astro.config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,31 @@
import { defineConfig } from "astro/config"
import mdx from "@astrojs/mdx"
import sitemap from "@astrojs/sitemap"
import { rehypeHeadingIds } from "@astrojs/markdown-remark"
import type { Options as RehypeAutolinkOptions } from "rehype-autolink-headings"
import rehypeAutolinkHeadings from "rehype-autolink-headings"

const rehypeAutolinkOptions: RehypeAutolinkOptions = {
behavior: "prepend",
content: {
type: "element",
tagName: "span",
properties: {
className: ["anchor-icon"],
},
children: [],
},
headingProperties: { tabIndex: "-1", className: ["heading-element"] },
properties: { ariaLabel: "Link to self", className: ["anchor-link"] },
}

export default defineConfig({
integrations: [mdx(), sitemap()],
markdown: {
rehypePlugins: [
rehypeHeadingIds,
[rehypeAutolinkHeadings, rehypeAutolinkOptions],
],
shikiConfig: {
theme: "dracula",
},
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"@astrojs/mdx": "^3.1.2",
"@astrojs/sitemap": "^3.1.6",
"astro": "^4.11.3",
"rehype-autolink-headings": "^7.1.0",
"sharp": "^0.33.3",
"typescript": "^5.4.5"
},
Expand Down
22 changes: 22 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions public/images/link.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
30 changes: 30 additions & 0 deletions src/styles/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,23 @@ blockquote p {
}

/* Utilities */
.anchor-link {
display: inline-block;
font-size: var(--text-md);
margin-left: calc(-0.75 * (1em + 1ex));
margin-right: calc(-0.25 * (1em + 1ex));
width: calc(1 * (1em + 1ex));
}

.anchor-icon {
display: inline-block;
width: 16px;
height: 16px;
background-color: currentColor;
content: " ";
mask-image: url("/images/link.svg");
}

.d-none {
display: none;
}
Expand Down Expand Up @@ -319,6 +336,19 @@ blockquote p {
blockquote {
font-size: var(--text-md);
}
.anchor-link {
font-size: var(--text-lg);
}
.anchor-icon {
width: 20px;
height: 20px;
}
:is(h1, h2, h3, h4, h5, h6) .anchor-icon {
visibility: hidden;
}
:is(h1, h2, h3, h4, h5, h6):hover .anchor-icon {
visibility: visible;
}
.img-hero {
max-height: 40rem;
}
Expand Down

0 comments on commit 70f8a3e

Please sign in to comment.