Skip to content

Commit 6f9b9b4

Browse files
Validate in-page anchor links in deadLinkChecker and fix broken anchors
1 parent 6b0a30d commit 6f9b9b4

12 files changed

Lines changed: 79 additions & 31 deletions

File tree

‎scripts/deadLinkChecker.js‎

Lines changed: 61 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -47,29 +47,41 @@ function getMarkdownFiles() {
4747
path.posix.join(baseDir, '**/*.md'),
4848
path.posix.join(baseDir, '**/*.mdx'),
4949
];
50-
return globby.sync(patterns);
50+
return globby.sync(patterns).map((file) => path.normalize(file));
5151
}
5252

5353
function extractAnchorsFromContent(content) {
5454
const anchors = new Set();
5555

5656
// MDX-style heading IDs: {/*anchor-id*/}
57-
const mdxPattern = /\{\/\*([a-zA-Z0-9-_]+)\*\/\}/g;
57+
const mdxPattern = /\{\/\*([^\*]+)\*\/\}/g;
5858
let match;
5959
while ((match = mdxPattern.exec(content)) !== null) {
60-
anchors.add(match[1].toLowerCase());
60+
anchors.add(match[1].trim().toLowerCase());
6161
}
6262

63-
// HTML id attributes
64-
const htmlIdPattern = /\sid=["']([a-zA-Z0-9-_]+)["']/g;
63+
// HTML or JSX id attributes
64+
const htmlIdPattern = /\sid=["']([^"']+)["']/g;
6565
while ((match = htmlIdPattern.exec(content)) !== null) {
66-
anchors.add(match[1].toLowerCase());
66+
anchors.add(match[1].trim().toLowerCase());
67+
}
68+
69+
// Component titleId attributes (e.g. <Recipes titleId="examples-connecting" />)
70+
const titleIdPattern = /\stitleId=["']([^"']+)["']/g;
71+
while ((match = titleIdPattern.exec(content)) !== null) {
72+
anchors.add(match[1].trim().toLowerCase());
73+
}
74+
75+
// TeamMember permalink attributes (e.g. <TeamMember permalink="dan-abramov" />)
76+
const permalinkPattern = /\spermalink=["']([^"']+)["']/g;
77+
while ((match = permalinkPattern.exec(content)) !== null) {
78+
anchors.add(match[1].trim().toLowerCase());
6779
}
6880

6981
// Markdown heading with explicit ID: ## Heading {#anchor-id}
70-
const markdownHeadingPattern = /^#+\s+.*\{#([a-zA-Z0-9-_]+)\}/gm;
82+
const markdownHeadingPattern = /^#+\s+.*\{#([^}]+)\}/gm;
7183
while ((match = markdownHeadingPattern.exec(content)) !== null) {
72-
anchors.add(match[1].toLowerCase());
84+
anchors.add(match[1].trim().toLowerCase());
7385
}
7486

7587
return anchors;
@@ -92,7 +104,11 @@ function extractLinksFromContent(content) {
92104

93105
while ((match = linkPattern.exec(content)) !== null) {
94106
const [, linkText, linkUrl] = match;
95-
if (linkUrl.startsWith('/') && !linkUrl.startsWith('//')) {
107+
const isInternalLink =
108+
(linkUrl.startsWith('/') && !linkUrl.startsWith('//')) ||
109+
linkUrl.startsWith('#');
110+
111+
if (isInternalLink) {
96112
const lines = content.substring(0, match.index).split('\n');
97113
const line = lines.length;
98114
const lastLineStart =
@@ -160,7 +176,39 @@ async function findTargetFile(urlPath) {
160176
return null;
161177
}
162178

163-
async function validateLink(link) {
179+
async function validateLink(link, currentFilePath) {
180+
if (link.url.startsWith('#')) {
181+
if (link.url === '#' || link.url.length === 1) {
182+
return {valid: false, reason: 'Empty anchor link'};
183+
}
184+
const rawAnchor = link.url.substring(1);
185+
let anchorId;
186+
try {
187+
anchorId = decodeURIComponent(rawAnchor).toLowerCase();
188+
} catch {
189+
anchorId = rawAnchor.toLowerCase();
190+
}
191+
192+
switch (anchorId) {
193+
case 'challenges':
194+
case 'recap': {
195+
return {valid: true};
196+
}
197+
}
198+
199+
const fileAnchors = anchorMap.get(path.normalize(currentFilePath));
200+
if (!fileAnchors || !fileAnchors.has(anchorId)) {
201+
return {
202+
valid: false,
203+
reason: `Anchor #${rawAnchor} not found in ${path.relative(
204+
CONTENT_DIR,
205+
currentFilePath
206+
)}`,
207+
};
208+
}
209+
return {valid: true};
210+
}
211+
164212
const urlAnchorPattern = /#([a-zA-Z0-9-_]+)$/;
165213
const anchorMatch = link.url.match(urlAnchorPattern);
166214
const urlWithoutAnchor = link.url.replace(urlAnchorPattern, '');
@@ -182,7 +230,7 @@ async function validateLink(link) {
182230
...link,
183231
url: redirectDestination + (anchorMatch ? anchorMatch[0] : ''),
184232
};
185-
return validateLink(redirectedLink);
233+
return validateLink(redirectedLink, currentFilePath);
186234
}
187235

188236
// Check if it's an error code link
@@ -244,7 +292,7 @@ async function validateLink(link) {
244292
}
245293
}
246294

247-
const fileAnchors = anchorMap.get(targetFile);
295+
const fileAnchors = anchorMap.get(path.normalize(targetFile));
248296

249297
if (!fileAnchors || !fileAnchors.has(anchorId)) {
250298
return {
@@ -266,7 +314,7 @@ async function processFile(filePath) {
266314
const deadLinks = [];
267315

268316
for (const link of links) {
269-
const result = await validateLink(link);
317+
const result = await validateLink(link, filePath);
270318
if (!result.valid) {
271319
deadLinks.push({
272320
file: path.relative(process.cwd(), filePath),

‎src/content/reference/react-dom/components/common.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,7 @@ An event handler type for the [CSS animation](https://developer.mozilla.org/en-U
356356

357357
---
358358

359-
### `ClipboardEvent` handler function {/*clipboadevent-handler*/}
359+
### `ClipboardEvent` handler function {/*clipboardevent-handler*/}
360360

361361
An event handler type for the [Clipboard API](https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API) events.
362362

@@ -368,7 +368,7 @@ An event handler type for the [Clipboard API](https://developer.mozilla.org/en-U
368368
/>
369369
```
370370

371-
#### Parameters {/*clipboadevent-handler-parameters*/}
371+
#### Parameters {/*clipboardevent-handler-parameters*/}
372372

373373
* `e`: A [React event object](#react-event-object) with these extra [`ClipboardEvent`](https://developer.mozilla.org/en-US/docs/Web/API/ClipboardEvent) properties:
374374

‎src/content/reference/react-dom/components/textarea.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ These `<textarea>` props are relevant both for uncontrolled and controlled text
5252
* [`form`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea#form): A string. Specifies the `id` of the `<form>` this input belongs to. If omitted, it's the closest parent form.
5353
* [`maxLength`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea#maxlength): A number. Specifies the maximum length of text.
5454
* [`minLength`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea#minlength): A number. Specifies the minimum length of text.
55-
* [`name`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#name): A string. Specifies the name for this input that's [submitted with the form.](#reading-the-textarea-value-when-submitting-a-form)
55+
* [`name`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#name): A string. Specifies the name for this input that's [submitted with the form.](#reading-the-text-area-value-when-submitting-a-form)
5656
* `onChange`: An [`Event` handler](/reference/react-dom/components/common#event-handler) function. Required for [controlled text areas.](#controlling-a-text-area-with-a-state-variable) Fires immediately when the input's value is changed by the user (for example, it fires on every keystroke). Behaves like the browser [`input` event.](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/input_event)
5757
* `onChangeCapture`: A version of `onChange` that fires in the [capture phase.](/learn/responding-to-events#capture-phase-events)
5858
* [`onInput`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/input_event): An [`Event` handler](/reference/react-dom/components/common#event-handler) function. Fires immediately when the value is changed by the user. For historical reasons, in React it is idiomatic to use `onChange` instead which works similarly.

‎src/content/reference/react-dom/server/resume.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ async function handler(request, writable) {
4747
* `postponedState`: The opaque `postpone` object returned from a [prerender API](/reference/react-dom/static/index), loaded from wherever you stored it (e.g. redis, a file, or S3).
4848
* **optional** `options`: An object with streaming options.
4949
* **optional** `nonce`: A [`nonce`](http://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#nonce) string to allow scripts for [`script-src` Content-Security-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src).
50-
* **optional** `signal`: An [abort signal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) that lets you [abort server rendering](#aborting-server-rendering) and render the rest on the client.
50+
* **optional** `signal`: An [abort signal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) that lets you [abort server rendering](/reference/react-dom/server/renderToReadableStream#aborting-server-rendering) and render the rest on the client.
5151
* **optional** `onBrowserBailout`: A callback React calls when it recovers from [`browser()`](/reference/react-dom/browser) by leaving a Suspense fallback for the browser to replace. It receives an `Error` describing the browser-only render and an `errorInfo` object containing the `componentStack`. If a reason was passed to `browser`, it is available as `error.cause`. By default, React does nothing. [See how to report browser-only rendering.](/reference/react-dom/browser#reporting-browser-only-rendering-on-the-server)
5252
* **optional** `onError`: A callback that fires whenever there is a server error, whether [recoverable](/reference/react-dom/server/renderToReadableStream#recovering-from-errors-outside-the-shell) or [not.](/reference/react-dom/server/renderToReadableStream#recovering-from-errors-inside-the-shell) By default, this only calls `console.error`. If you override it to [log crash reports,](/reference/react-dom/server/renderToReadableStream#logging-crashes-on-the-server) make sure that you still call `console.error`.
5353

‎src/content/reference/react-dom/server/resumeToPipeableStream.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,16 +53,16 @@ async function handler(request, response) {
5353
* **optional** `onAllReady`: A callback that fires when all rendering is complete, including both the shell and all additional content. You can call `pipe` here instead of in `onShellReady` for crawlers and static generation. The stream will contain the final HTML.
5454
* **optional** `onBrowserBailout`: A callback React calls when it recovers from [`browser()`](/reference/react-dom/browser) by leaving a Suspense fallback for the browser to replace. It receives an `Error` describing the browser-only render and an `errorInfo` object containing the `componentStack`. If a reason was passed to `browser`, it is available as `error.cause`. By default, React does nothing. [See how to report browser-only rendering.](/reference/react-dom/browser#reporting-browser-only-rendering-on-the-server)
5555
* **optional** `onError`: A callback that fires whenever there is a server error, whether [recoverable](/reference/react-dom/server/renderToReadableStream#recovering-from-errors-outside-the-shell) or [not.](/reference/react-dom/server/renderToReadableStream#recovering-from-errors-inside-the-shell) By default, this only calls `console.error`. If you override it to [log crash reports,](/reference/react-dom/server/renderToReadableStream#logging-crashes-on-the-server) make sure that you still call `console.error`.
56-
* **optional** `onShellReady`: A callback that fires right after the [shell](#specifying-what-goes-into-the-shell) has finished. You can call `pipe` here to start streaming. React will [stream the additional content](#streaming-more-content-as-it-loads) after the shell along with the inline `<script>` tags that replace the HTML loading fallbacks with the content.
57-
* **optional** `onShellError`: A callback that fires if there was an error rendering the shell. It receives the error as an argument. No bytes were emitted from the stream yet, and neither `onShellReady` nor `onAllReady` will get called, so you can [output a fallback HTML shell](#recovering-from-errors-inside-the-shell) or use the prelude.
56+
* **optional** `onShellReady`: A callback that fires right after the [shell](/reference/react-dom/server/renderToPipeableStream#specifying-what-goes-into-the-shell) has finished. You can call `pipe` here to start streaming. React will [stream the additional content](/reference/react-dom/server/renderToPipeableStream#streaming-more-content-as-it-loads) after the shell along with the inline `<script>` tags that replace the HTML loading fallbacks with the content.
57+
* **optional** `onShellError`: A callback that fires if there was an error rendering the shell. It receives the error as an argument. No bytes were emitted from the stream yet, and neither `onShellReady` nor `onAllReady` will get called, so you can [output a fallback HTML shell](/reference/react-dom/server/renderToPipeableStream#recovering-from-errors-inside-the-shell) or use the prelude.
5858
5959
6060
#### Returns {/*returns*/}
6161
6262
`resumeToPipeableStream` returns an object with two methods:
6363
6464
* `pipe` outputs the HTML into the provided [Writable Node.js Stream.](https://nodejs.org/api/stream.html#writable-streams) Call `pipe` in `onShellReady` if you want to enable streaming, or in `onAllReady` for crawlers and static generation.
65-
* `abort` lets you [abort server rendering](#aborting-server-rendering) and render the rest on the client.
65+
* `abort` lets you [abort server rendering](/reference/react-dom/server/renderToPipeableStream#aborting-server-rendering) and render the rest on the client.
6666
6767
#### Caveats {/*caveats*/}
6868

‎src/content/reference/react-dom/static/resumeAndPrerender.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,9 @@ On the client, call [`hydrateRoot`](/reference/react-dom/client/hydrateRoot) to
5252
* `reactNode`: The React node you called `prerender` (or a previous `resumeAndPrerender`) with. For example, a JSX element like `<App />`. It is expected to represent the entire document, so the `App` component should render the `<html>` tag.
5353
* `postponedState`: The opaque `postpone` object returned from a [prerender API](/reference/react-dom/static/index), loaded from wherever you stored it (e.g. redis, a file, or S3).
5454
* **optional** `options`: An object with streaming options.
55-
* **optional** `signal`: An [abort signal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) that lets you [abort server rendering](#aborting-server-rendering) and render the rest on the client.
55+
* **optional** `signal`: An [abort signal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) that lets you [abort server rendering](/reference/react-dom/server/renderToReadableStream#aborting-server-rendering) and render the rest on the client.
5656
* **optional** `onBrowserBailout`: A callback React calls when it recovers from [`browser()`](/reference/react-dom/browser) by leaving a Suspense fallback for the browser to replace. It receives an `Error` describing the browser-only render and an `errorInfo` object containing the `componentStack`. If a reason was passed to `browser`, it is available as `error.cause`. By default, React does nothing. [See how to report browser-only rendering.](/reference/react-dom/browser#reporting-browser-only-rendering-on-the-server)
57-
* **optional** `onError`: A callback that fires whenever there is a server error, whether [recoverable](#recovering-from-errors-outside-the-shell) or [not.](#recovering-from-errors-inside-the-shell) By default, this only calls `console.error`. If you override it to [log crash reports,](#logging-crashes-on-the-server) make sure that you still call `console.error`.
57+
* **optional** `onError`: A callback that fires whenever there is a server error, whether [recoverable](/reference/react-dom/server/renderToReadableStream#recovering-from-errors-outside-the-shell) or [not.](/reference/react-dom/server/renderToReadableStream#recovering-from-errors-inside-the-shell) By default, this only calls `console.error`. If you override it to [log crash reports,](/reference/react-dom/server/renderToReadableStream#logging-crashes-on-the-server) make sure that you still call `console.error`.
5858
5959
#### Returns {/*returns*/}
6060

‎src/content/reference/react-dom/static/resumeAndPrerenderToNodeStream.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,9 @@ On the client, call [`hydrateRoot`](/reference/react-dom/client/hydrateRoot) to
4848
* `reactNode`: The React node you called `prerender` (or a previous `resumeAndPrerenderToNodeStream`) with. For example, a JSX element like `<App />`. It is expected to represent the entire document, so the `App` component should render the `<html>` tag.
4949
* `postponedState`: The opaque `postpone` object returned from a [prerender API](/reference/react-dom/static/index), loaded from wherever you stored it (e.g. redis, a file, or S3).
5050
* **optional** `options`: An object with streaming options.
51-
* **optional** `signal`: An [abort signal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) that lets you [abort server rendering](#aborting-server-rendering) and render the rest on the client.
51+
* **optional** `signal`: An [abort signal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) that lets you [abort server rendering](/reference/react-dom/server/renderToPipeableStream#aborting-server-rendering) and render the rest on the client.
5252
* **optional** `onBrowserBailout`: A callback React calls when it recovers from [`browser()`](/reference/react-dom/browser) by leaving a Suspense fallback for the browser to replace. It receives an `Error` describing the browser-only render and an `errorInfo` object containing the `componentStack`. If a reason was passed to `browser`, it is available as `error.cause`. By default, React does nothing. [See how to report browser-only rendering.](/reference/react-dom/browser#reporting-browser-only-rendering-on-the-server)
53-
* **optional** `onError`: A callback that fires whenever there is a server error, whether [recoverable](#recovering-from-errors-outside-the-shell) or [not.](#recovering-from-errors-inside-the-shell) By default, this only calls `console.error`. If you override it to [log crash reports,](#logging-crashes-on-the-server) make sure that you still call `console.error`.
53+
* **optional** `onError`: A callback that fires whenever there is a server error, whether [recoverable](/reference/react-dom/server/renderToPipeableStream#recovering-from-errors-outside-the-shell) or [not.](/reference/react-dom/server/renderToPipeableStream#recovering-from-errors-inside-the-shell) By default, this only calls `console.error`. If you override it to [log crash reports,](/reference/react-dom/server/renderToPipeableStream#logging-crashes-on-the-server) make sure that you still call `console.error`.
5454
5555
#### Returns {/*returns*/}
5656

‎src/content/reference/react/Component.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -856,7 +856,7 @@ There is no direct equivalent to `UNSAFE_componentWillUpdate` in function compon
856856

857857
### `static contextType` {/*static-contexttype*/}
858858

859-
If you want to read [`this.context`](#context-instance-field) from your class component, you must specify which context it needs to read. The context you specify as the `static contextType` must be a value previously created by [`createContext`.](/reference/react/createContext)
859+
If you want to read [`this.context`](#context) from your class component, you must specify which context it needs to read. The context you specify as the `static contextType` must be a value previously created by [`createContext`.](/reference/react/createContext)
860860

861861
```js {2}
862862
class Button extends Component {

‎src/content/reference/react/ViewTransition.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ React automatically decides the type of View Transition animation to trigger:
8686

8787
By default, `<ViewTransition>` animates with a smooth cross-fade (the browser default view transition).
8888

89-
You can customize the animation by providing a [View Transition Class](#view-transition-class) to the `<ViewTransition>` component for each kind of trigger (see [Styling View Transitions](#styling-view-transitions)), or by using [ViewTransition Events](#view-transition-events) to control the animation with JavaScript using the [Web Animations API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API).
89+
You can customize the animation by providing a [View Transition Class](#view-transition-class) to the `<ViewTransition>` component for each kind of trigger (see [Styling View Transitions](#styling-view-transitions)), or by using [ViewTransition Events](#view-transition-event) to control the animation with JavaScript using the [Web Animations API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API).
9090

9191
<Note>
9292

0 commit comments

Comments
 (0)