Skip to content

Commit bd6e5a4

Browse files
committed
feat: add Broken Links plugin to the docs site
1 parent dc5eb9c commit bd6e5a4

13 files changed

Lines changed: 198 additions & 6 deletions

File tree

‎README.md‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ It documents BinaryNoir's open-source VitePress plugins, Vite plugins and comman
1515
| [`@binarynoir/vitepress-downloads`](https://github.com/binarynoir/vitepress-downloads) | Downloadable files from a `downloads` folder |
1616
| [`markdown-it-glossary`](https://github.com/binarynoir/markdown-it-glossary) | Hover definitions from `docs/reference/glossary.md` |
1717
| [`@binarynoir/vitepress-html-escape`](https://github.com/binarynoir/vitepress-html-escape) | Stray tags like `Array<string>` and `{{ }}` render as text; hints underline them in `docs:dev` |
18+
| [`@binarynoir/vitepress-broken-links`](https://github.com/binarynoir/vitepress-broken-links) | Dead links render as text with a fix-it popover instead of failing the build |
1819
| [`@binarynoir/vite-plugin-optimize-images`](https://github.com/binarynoir/vite-plugin-optimize-images) | Re-compresses images in the build output |
1920
| [`@binarynoir/vite-plugin-image-fallback`](https://github.com/binarynoir/vite-plugin-image-fallback) | Placeholder for missing images (one is missing on purpose) |
2021

@@ -36,10 +37,12 @@ Pushing to `main` runs [`.github/workflows/deploy.yml`](.github/workflows/deploy
3637

3738
The repository's Pages source must be set to **GitHub Actions** (Settings → Pages).
3839

39-
## A note on the missing image
40+
## A note on the missing image and dead links
4041

4142
`docs/plugins/image-fallback.md` references an image that does not exist, on purpose, to demonstrate the image-fallback plugin. The build logs a warning for it. That is expected.
4243

44+
Likewise, `docs/plugins/broken-links.md` links to two pages that do not exist, to demonstrate the broken-links plugin. The build logs a warning for each one, and that is expected too.
45+
4346
## License
4447

4548
Code is [MIT](LICENSE) licensed. The BinaryNoir name, logos and site content are the property of John Smith III / BinaryNoir and are not covered by that license; see [docs/legal.md](docs/legal.md).

‎docs/.vitepress/config.mts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { optimizeImagesPlugin } from '@binarynoir/vite-plugin-optimize-images';
1111
import { downloadsMarkdown, downloadsVitePlugin } from '@binarynoir/vitepress-downloads';
1212
import { downloadsSrcExclude } from '@binarynoir/vitepress-downloads/vitepress';
1313
import { markdownHtmlEscape } from '@binarynoir/vitepress-html-escape';
14+
import { markdownBrokenLinks } from '@binarynoir/vitepress-broken-links';
1415

1516
// import.meta.dirname, not __dirname: VitePress configs are ESM.
1617
const docsRoot = path.resolve(import.meta.dirname, '..');
@@ -113,6 +114,9 @@ export default defineConfig({
113114
// render as text instead of breaking the Vue compile. It runs after every other rule.
114115
// In `vitepress dev`, hints underline each fix and explain it; `vitepress build` leaves them out.
115116
md.use(markdownHtmlEscape, { hints: process.env.NODE_ENV !== 'production' });
117+
// @binarynoir/vitepress-broken-links: a link to a page that doesn't exist renders as text
118+
// with a popover explaining it, and a warning in the log, instead of failing the build.
119+
md.use(markdownBrokenLinks);
116120
},
117121
},
118122

‎docs/.vitepress/theme/index.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { registerMarkdownTags } from '@binarynoir/vitepress-markdown-tags/theme'
44
import '@binarynoir/vitepress-markdown-tags/style.css';
55
import { enableGlossaryTooltips } from 'markdown-it-glossary/client';
66
import { enableHtmlEscapeHints } from '@binarynoir/vitepress-html-escape/client';
7+
import { enableBrokenLinkHints } from '@binarynoir/vitepress-broken-links/client';
78
import './custom.css';
89

910
export default {
@@ -15,5 +16,7 @@ export default {
1516
enableGlossaryTooltips();
1617
// Wavy underlines and fix-it popovers on pages that turn on HTML Escape hints.
1718
enableHtmlEscapeHints();
19+
// Wavy underlines and popovers on links to pages that don't exist.
20+
enableBrokenLinkHints();
1821
},
1922
} satisfies Theme;

‎docs/guide/how-this-site-works.md‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ title: How this site works
66

77
[[readingTime]]
88

9-
This site is plain VitePress 2 plus nine plugins. There is no hand-written navigation anywhere: add a Markdown file in the right folder and it shows up in the sidebar, the navbar, search and the sitemap.
9+
This site is plain VitePress 2 plus ten plugins. There is no hand-written navigation anywhere: add a Markdown file in the right folder and it shows up in the sidebar, the navbar, search and the sitemap.
1010

1111
## Folder layout
1212

@@ -40,6 +40,7 @@ docs/
4040
| Dotted-underline hovers | [Glossary Tooltips](/plugins/glossary/) | `markdown.config` |
4141
| File download links | [Downloads](/plugins/downloads) | `markdown.config` and `vite.plugins` |
4242
| Stray `<tags>` shown as text | [HTML Escape](/plugins/html-escape/) | `markdown.config`, plus the theme |
43+
| Dead links shown as text | [Broken Links](/plugins/broken-links) | `markdown.config`, plus the theme |
4344
| Smaller images | [Optimize Images](/plugins/optimize-images) | `vite.plugins` |
4445
| Placeholder for a missing image | [Image Fallback](/plugins/image-fallback) | `vite.plugins` |
4546

@@ -49,14 +50,14 @@ docs/
4950

5051
## The theme
5152

52-
The theme registers the badge component and loads its stylesheet, and turns on the two popovers: glossary definitions, and the HTML Escape hints that explain broken markup:
53+
The theme registers the badge component and loads its stylesheet, and turns on the three popovers: glossary definitions, the HTML Escape hints that explain broken markup, and the Broken Links hints that explain dead links:
5354

5455
<<< @/.vitepress/theme/index.ts{ts}
5556

5657
## Adding a page
5758

5859
1. Create `docs/<section>/my-page.md` with a `# Heading`.
5960
2. Optionally, add a line for it to that section's `.sidebar` file to choose its position and title. Pages without a line are listed alphabetically where the `...` line is, or at the end if there is none.
60-
3. Run `npm run docs:dev`. The page is already in the sidebar. Anything [HTML Escape](/plugins/html-escape/) had to fix, such as an `Array<T>` outside backticks, has a wavy underline there: hover it to see what is wrong and how to fix it. The deployed site leaves the underlines out.
61+
3. Run `npm run docs:dev`. The page is already in the sidebar. Anything [HTML Escape](/plugins/html-escape/) had to fix, such as an `Array<T>` outside backticks, has a wavy underline there: hover it to see what is wrong and how to fix it. The deployed site leaves the underlines out. A link to a page that doesn't exist yet doesn't stop the build either: [Broken Links](/plugins/broken-links) shows it as text, with a red underline that explains it, in both `docs:dev` and the deployed site.
6162

6263
To add a whole new section, create a folder with an `index.md`. It becomes a navbar entry and its own sidebar automatically.

‎docs/index.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,15 @@ features:
7474
details: A stray Array&lt;string&gt; or {{ }} in your Markdown shows up as text instead of breaking the build.
7575
link: /plugins/html-escape/
7676
linkText: See the HTML Escape plugin
77+
- icon:
78+
light: /icons/link-light.svg
79+
dark: /icons/link-dark.svg
80+
width: 28
81+
height: 28
82+
title: Broken Links
83+
details: A link to a page that doesn't exist shows up as text with a fix-it hint instead of failing the build.
84+
link: /plugins/broken-links
85+
linkText: See the Broken Links plugin
7786
- icon:
7887
light: /icons/image-light.svg
7988
dark: /icons/image-dark.svg
@@ -110,6 +119,7 @@ npm install --save-dev \
110119
@binarynoir/vitepress-reading-time-tag \
111120
@binarynoir/vitepress-downloads \
112121
@binarynoir/vitepress-html-escape \
122+
@binarynoir/vitepress-broken-links \
113123
@binarynoir/vite-plugin-optimize-images \
114124
@binarynoir/vite-plugin-image-fallback \
115125
markdown-it-glossary

‎docs/plugins/.sidebar‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@ reading-time-tag.md:Reading Time Tag
88
downloads.md:Downloads
99
glossary:Glossary Tooltips
1010
html-escape:HTML Escape
11+
broken-links.md:Broken Links
1112
optimize-images.md:Optimize Images
1213
image-fallback.md:Image Fallback

‎docs/plugins/broken-links.md‎

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
---
2+
title: Broken Links
3+
---
4+
5+
# Broken Links ((tag|VitePress plugin|blue)) ((tag|markdown-it|orange))
6+
7+
[[readingTime]]
8+
9+
[![GitHub repo](https://img.shields.io/badge/GitHub-binarynoir%2Fvitepress--broken--links-181717?logo=github)](https://github.com/binarynoir/vitepress-broken-links) [![npm version](https://img.shields.io/npm/v/@binarynoir/vitepress-broken-links)](https://www.npmjs.com/package/@binarynoir/vitepress-broken-links) [![CI](https://github.com/binarynoir/vitepress-broken-links/actions/workflows/ci.yml/badge.svg)](https://github.com/binarynoir/vitepress-broken-links/actions/workflows/ci.yml) [![License](https://img.shields.io/github/license/binarynoir/vitepress-broken-links)](https://github.com/binarynoir/vitepress-broken-links/blob/main/LICENSE)
10+
11+
`@binarynoir/vitepress-broken-links` stops one dead link from taking down your whole build. VitePress checks every internal link against your pages, and a link to a page that doesn't exist normally stops the build with `1 dead link(s) found`. With this plugin the link is shown as text instead, with a wavy underline and a popover that says what is wrong and which line of which file it is on. Links that work are left exactly as they are.
12+
13+
It is built for docs sites with many contributors, where a mistyped or outdated link on one page shouldn't block deploying all the others.
14+
15+
## See it live
16+
17+
The second and third links below point at pages that don't exist, on purpose. Without the plugin this page would fail the whole site build. With it, the build finishes and logs a warning for each one, and the links render like this. Hover one, or click or tap it, to see the popover:
18+
19+
```md
20+
Start with [the overview](/plugins/), then read [the setup guide](/guide/setup)
21+
and [part two of this page](./broken-links-part-2.md).
22+
```
23+
24+
::: info On the page
25+
Start with [the overview](/plugins/), then read [the setup guide](/guide/setup)
26+
and [part two of this page](./broken-links-part-2.md).
27+
:::
28+
29+
The build log names each one, with its file and line:
30+
31+
```txt
32+
(!) Broken link /guide/setup in plugins/broken-links.md:25 is shown as text instead of a link.
33+
```
34+
35+
That warning is the plugin working as designed.
36+
37+
## Install
38+
39+
```sh
40+
npm install --save-dev @binarynoir/vitepress-broken-links
41+
```
42+
43+
## Use
44+
45+
Register it in VitePress's `markdown.config` hook, the same place as any other markdown-it plugin:
46+
47+
```ts
48+
// .vitepress/config.mts
49+
import { defineConfig } from 'vitepress';
50+
import { markdownBrokenLinks } from '@binarynoir/vitepress-broken-links';
51+
52+
export default defineConfig({
53+
markdown: {
54+
config(md) {
55+
md.use(markdownBrokenLinks);
56+
},
57+
},
58+
});
59+
```
60+
61+
Or wrap the config with `withBrokenLinks` from `@binarynoir/vitepress-broken-links/vitepress`. It keeps any `markdown.config` you already have, including one set by another `withX()` wrapper.
62+
63+
```ts
64+
import { withBrokenLinks } from '@binarynoir/vitepress-broken-links/vitepress';
65+
66+
export default withBrokenLinks(
67+
defineConfig({
68+
// ...your config
69+
}),
70+
);
71+
```
72+
73+
Then turn on the underline and popovers in your theme:
74+
75+
```ts
76+
// .vitepress/theme/index.ts
77+
import DefaultTheme from 'vitepress/theme';
78+
import { enableBrokenLinkHints } from '@binarynoir/vitepress-broken-links/client';
79+
80+
export default {
81+
extends: DefaultTheme,
82+
enhanceApp() {
83+
enableBrokenLinkHints();
84+
},
85+
};
86+
```
87+
88+
The popovers work the same way as [HTML Escape's hints](/plugins/html-escape/#hints), and the two run side by side on this site.
89+
90+
## What counts as dead
91+
92+
Exactly what VitePress itself would report. The plugin reads your running VitePress config, so it uses the same page list, `rewrites`, HTML files in `public/`, and `ignoreDeadLinks` entries. Left alone are:
93+
94+
- links to pages that exist, relative or absolute, with or without `.md`
95+
- external links such as `https://…`, and `#anchors` on the same page
96+
- links to files such as `.pdf` or `.zip`
97+
- anything matched by your `ignoreDeadLinks` setting
98+
99+
Links to `localhost` are dead to VitePress too, since they only work on your own machine. They get their own message, which points to `ignoreDeadLinks: 'localhostLinks'` if you mean to keep them.
100+
101+
## Why not `ignoreDeadLinks`?
102+
103+
VitePress's built-in [`ignoreDeadLinks`](https://vitepress.dev/reference/site-config#ignoredeadlinks) option also keeps dead links from failing the build, but it does that by skipping the check, not by handling the link:
104+
105+
- **Readers still get a 404.** An ignored link is published as a normal link. It looks like it works until someone clicks it. This plugin removes the link, so it can't send anyone to a missing page, and the popover tells them why.
106+
- **Authors aren't told.** `ignoreDeadLinks: true` turns the check off for the whole site, so dead links no longer show up anywhere in the build output. This plugin still logs each one with its file and line, and marks it on the page.
107+
- **You'd have to know the links in advance.** The string, RegExp and function forms only skip links you have already listed. They suit known exceptions, but not a typo someone adds next week.
108+
- **It's all or nothing per site.** With this plugin, a page that must never ship a dead link can opt out in its frontmatter, and VitePress's check applies to that page again.
109+
110+
The two work together. Links matched by `ignoreDeadLinks` stay links, so use it for the exceptions you mean to keep, and let this plugin handle the mistakes. If you want a dead link to block a release, for example as a CI gate on pull requests, leave this plugin out of that build.
111+
112+
## Options
113+
114+
`markdownBrokenLinks(md, options)` and `withBrokenLinks(config, options)` take:
115+
116+
| Option | Default | Description |
117+
| ------- | ------- | --------------------------------------------------------------------------------------------- |
118+
| `hints` | `true` | Attach the explanation, file and line to each broken link. `false` shows the text plainly. |
119+
| `warn` | `true` | Log a warning for each broken link while rendering, in the same place VitePress reports them. |
120+
121+
`enableBrokenLinkHints(options)` takes:
122+
123+
| Option | Default | Description |
124+
| -------------- | ------- | ------------------------------------------------------------------------------- |
125+
| `hover` | `true` | Show the popover on mouse hover. `false` for click and tap only. |
126+
| `hoverDelay` | `300` | Milliseconds the mouse rests on a hint before the popover shows. |
127+
| `injectStyles` | `true` | Inject the default styles. `false` lets you style `.broken-link-hint` yourself. |
128+
129+
The colors follow the VitePress theme and can be changed with the `--broken-link-hint-error` CSS variable.
130+
131+
## Per-page settings
132+
133+
The `brokenLinks` frontmatter key overrides the options for one page:
134+
135+
| Frontmatter | What it does |
136+
| ------------------------------- | --------------------------------------------------------------------------- |
137+
| `brokenLinks: { hints: false }` | Shows broken links on the page as plain text, with no underline or popover. |
138+
| `brokenLinks: { warn: false }` | Stops logging warnings for the page. |
139+
| `brokenLinks: false` | Hands the page back to VitePress, so a dead link on it fails the build. |
140+
141+
There is no live page for `brokenLinks: false`: a dead link on it would stop this site from building, which is the point.
142+
143+
## Limits
144+
145+
- **Only Markdown links are checked.** A raw `<a href>` in HTML isn't checked by VitePress either, so it is left alone.
146+
- **A missing heading isn't a dead link.** `./page#nope` on a page that exists stays a link, as it does in VitePress.
147+
- **It needs VitePress.** The page list comes from the running VitePress process, so under plain markdown-it the plugin does nothing.
148+
149+
[Source and full README on GitHub](https://github.com/binarynoir/vitepress-broken-links)

‎docs/plugins/index.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ title: Plugins
66

77
[[readingTime]]
88

9-
Nine small packages that make a documentation site easier to build and nicer to read. Each one does a single job and they share conventions. All nine are running on the site you are looking at.
9+
Ten small packages that make a documentation site easier to build and nicer to read. Each one does a single job and they share conventions. All ten are running on the site you are looking at.
1010

1111
| Plugin | Works with | What it does |
1212
| --------------------------------------------- | ---------------------------- | ------------------------------------------------------------ |
@@ -17,6 +17,7 @@ Nine small packages that make a documentation site easier to build and nicer to
1717
| [Downloads](/plugins/downloads) | VitePress | Files in a `downloads` folder become linkable downloads. |
1818
| [Glossary Tooltips](/plugins/glossary/) | VitePress, markdown-it | Hover definitions from one glossary file. |
1919
| [HTML Escape](/plugins/html-escape/) | VitePress 2, markdown-it | Stray `<tags>` and braces render as text, with fix-it hints. |
20+
| [Broken Links](/plugins/broken-links) | VitePress 2 | Dead links render as text, with fix-it hints. |
2021
| [Optimize Images](/plugins/optimize-images) | Vite | Re-compresses images in the build output. |
2122
| [Image Fallback](/plugins/image-fallback) | Vite | Placeholder for missing images instead of a failed build. |
2223

@@ -30,14 +31,15 @@ npm install --save-dev \
3031
@binarynoir/vitepress-reading-time-tag \
3132
@binarynoir/vitepress-downloads \
3233
@binarynoir/vitepress-html-escape \
34+
@binarynoir/vitepress-broken-links \
3335
@binarynoir/vite-plugin-optimize-images \
3436
@binarynoir/vite-plugin-image-fallback \
3537
markdown-it-glossary
3638
```
3739

3840
The VitePress plugins target VitePress 2 (currently an alpha release line). Node 22 or newer is required.
3941

40-
## One config, all nine
42+
## One config, all ten
4143

4244
This is this site's real `config.mts`, included straight from the repository, so it can't drift from what is deployed. [How this site works](/guide/how-this-site-works) walks through it.
4345

‎docs/public/icons/link-dark.svg‎

Lines changed: 1 addition & 0 deletions
Loading

‎docs/public/icons/link-light.svg‎

Lines changed: 1 addition & 0 deletions
Loading

0 commit comments

Comments
 (0)