fix(core): various broken anchor link fixes (#9732)

This commit is contained in:
Sébastien Lorber 2024-01-12 16:09:45 +01:00 committed by GitHub
parent d94adf6a6c
commit 4388267c26
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
63 changed files with 345 additions and 115 deletions

5
.eslintrc.js vendored
View file

@ -203,7 +203,10 @@ module.exports = {
})),
],
'no-template-curly-in-string': WARNING,
'no-unused-expressions': [WARNING, {allowTaggedTemplates: true}],
'no-unused-expressions': [
WARNING,
{allowTaggedTemplates: true, allowShortCircuit: true},
],
'no-useless-escape': WARNING,
'no-void': [ERROR, {allowAsStatement: true}],
'prefer-destructuring': WARNING,

View file

@ -262,8 +262,8 @@ declare module '@docusaurus/useRouteContext' {
declare module '@docusaurus/useBrokenLinks' {
export type BrokenLinks = {
collectLink: (link: string) => void;
collectAnchor: (anchor: string) => void;
collectLink: (link: string | undefined) => void;
collectAnchor: (anchor: string | undefined) => void;
};
export default function useBrokenLinks(): BrokenLinks;

View file

@ -867,6 +867,14 @@ declare module '@theme/MDXComponents/Ul' {
export default function MDXUl(props: Props): JSX.Element;
}
declare module '@theme/MDXComponents/Li' {
import type {ComponentProps} from 'react';
export interface Props extends ComponentProps<'li'> {}
export default function MDXLi(props: Props): JSX.Element;
}
declare module '@theme/MDXComponents/Img' {
import type {ComponentProps} from 'react';

View file

@ -0,0 +1,17 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React, {type ReactNode} from 'react';
import useBrokenLinks from '@docusaurus/useBrokenLinks';
import type {Props} from '@theme/MDXComponents/Li';
export default function MDXLi(props: Props): ReactNode | undefined {
// MDX Footnotes have ids such as <li id="user-content-fn-1-953011">
useBrokenLinks().collectAnchor(props.id);
return <li {...props} />;
}

View file

@ -13,6 +13,7 @@ import MDXPre from '@theme/MDXComponents/Pre';
import MDXDetails from '@theme/MDXComponents/Details';
import MDXHeading from '@theme/MDXComponents/Heading';
import MDXUl from '@theme/MDXComponents/Ul';
import MDXLi from '@theme/MDXComponents/Li';
import MDXImg from '@theme/MDXComponents/Img';
import Admonition from '@theme/Admonition';
import Mermaid from '@theme/Mermaid';
@ -27,6 +28,7 @@ const MDXComponents: MDXComponentsObject = {
a: MDXA,
pre: MDXPre,
ul: MDXUl,
li: MDXLi,
img: MDXImg,
h1: (props: ComponentProps<'h1'>) => <MDXHeading as="h1" {...props} />,
h2: (props: ComponentProps<'h2'>) => <MDXHeading as="h2" {...props} />,

View file

@ -89,6 +89,9 @@ function DropdownNavbarItemDesktop({
aria-haspopup="true"
aria-expanded={showDropdown}
role="button"
// # hash permits to make the <a> tag focusable in case no link target
// See https://github.com/facebook/docusaurus/pull/6003
// There's probably a better solution though...
href={props.to ? undefined : '#'}
className={clsx('navbar__link', className)}
{...props}

View file

@ -12,6 +12,7 @@ import React, {
type ReactElement,
} from 'react';
import clsx from 'clsx';
import useBrokenLinks from '@docusaurus/useBrokenLinks';
import useIsBrowser from '@docusaurus/useIsBrowser';
import {useCollapsible, Collapsible} from '../Collapsible';
import styles from './styles.module.css';
@ -47,6 +48,8 @@ export function Details({
children,
...props
}: DetailsProps): JSX.Element {
useBrokenLinks().collectAnchor(props.id);
const isBrowser = useIsBrowser();
const detailsRef = useRef<HTMLDetailsElement>(null);

View file

@ -301,6 +301,29 @@ describe('parseURLPath', () => {
});
});
it('parse anchor', () => {
expect(parseURLPath('#anchor')).toEqual({
pathname: '/',
search: undefined,
hash: 'anchor',
});
expect(parseURLPath('#anchor', '/page')).toEqual({
pathname: '/page',
search: undefined,
hash: 'anchor',
});
expect(parseURLPath('#')).toEqual({
pathname: '/',
search: undefined,
hash: '',
});
expect(parseURLPath('#', '/page')).toEqual({
pathname: '/page',
search: undefined,
hash: '',
});
});
it('parse hash', () => {
expect(parseURLPath('/page')).toEqual({
pathname: '/page',

View file

@ -18,11 +18,11 @@ export const createStatefulBrokenLinks = (): StatefulBrokenLinks => {
const allAnchors = new Set<string>();
const allLinks = new Set<string>();
return {
collectAnchor: (anchor: string): void => {
allAnchors.add(anchor);
collectAnchor: (anchor: string | undefined): void => {
typeof anchor !== 'undefined' && allAnchors.add(anchor);
},
collectLink: (link: string): void => {
allLinks.add(link);
collectLink: (link: string | undefined): void => {
typeof link !== 'undefined' && allLinks.add(link);
},
getCollectedAnchors: (): string[] => [...allAnchors],
getCollectedLinks: (): string[] => [...allLinks],

View file

@ -140,13 +140,20 @@ function Link(
};
}, [ioRef, targetLink, IOSupported, isInternal]);
// It is simple local anchor link targeting current page?
const isAnchorLink = targetLink?.startsWith('#') ?? false;
// Should we use a regular <a> tag instead of React-Router Link component?
const isRegularHtmlLink = !targetLink || !isInternal || isAnchorLink;
if (!isRegularHtmlLink && !noBrokenLinkCheck) {
if (!noBrokenLinkCheck && (isAnchorLink || !isRegularHtmlLink)) {
brokenLinks.collectLink(targetLink!);
}
if (props.id) {
brokenLinks.collectAnchor(props.id);
}
return isRegularHtmlLink ? (
// eslint-disable-next-line jsx-a11y/anchor-has-content, @docusaurus/no-html-links
<a

View file

@ -86,6 +86,71 @@ describe('handleBrokenLinks', () => {
});
});
it('accepts valid link with anchor reported with hash prefix', async () => {
await testBrokenLinks({
routes: [{path: '/page1'}, {path: '/page2'}],
collectedLinks: {
'/page1': {links: ['/page2#page2anchor'], anchors: []},
'/page2': {links: [], anchors: ['#page2anchor']},
},
});
});
it('accepts valid links and anchors, sparse arrays', async () => {
await testBrokenLinks({
routes: [{path: '/page1'}, {path: '/page2'}],
collectedLinks: {
'/page1': {
links: [
'/page1',
// @ts-expect-error: invalid type on purpose
undefined,
// @ts-expect-error: invalid type on purpose
null,
// @ts-expect-error: invalid type on purpose
42,
'/page2',
'/page2#page2anchor1',
'/page2#page2anchor2',
],
anchors: [],
},
'/page2': {
links: [],
anchors: [
'page2anchor1',
// @ts-expect-error: invalid type on purpose
undefined,
// @ts-expect-error: invalid type on purpose
null,
// @ts-expect-error: invalid type on purpose
42,
'page2anchor2',
],
},
},
});
});
it('accepts valid link and anchor to collected pages that are not in routes', async () => {
// This tests the edge-case of the 404 page:
// We don't have a {path: '404.html'} route
// But yet we collect links/anchors to it and allow linking to it
await testBrokenLinks({
routes: [],
collectedLinks: {
'/page 1': {
links: ['/page 2#anchor-page-2'],
anchors: ['anchor-page-1'],
},
'/page 2': {
links: ['/page 1#anchor-page-1', '/page%201#anchor-page-1'],
anchors: ['anchor-page-2'],
},
},
});
});
it('accepts valid link with querystring + anchor', async () => {
await testBrokenLinks({
routes: [{path: '/page1'}, {path: '/page2'}],
@ -132,10 +197,75 @@ describe('handleBrokenLinks', () => {
'/page%202',
'/page%202?age=42',
'/page%202?age=42#page2anchor',
'/some dir/page 3',
'/some dir/page 3#page3anchor',
'/some%20dir/page%203',
'/some%20dir/page%203#page3anchor',
'/some%20dir/page 3',
'/some dir/page%203',
'/some dir/page%203#page3anchor',
],
anchors: [],
},
'/page 2': {links: [], anchors: ['page2anchor']},
'/some dir/page 3': {links: [], anchors: ['page3anchor']},
},
});
});
it('accepts valid link with anchor with spaces and encoding', async () => {
await testBrokenLinks({
routes: [{path: '/page 1'}, {path: '/page 2'}],
collectedLinks: {
'/page 1': {
links: [
'/page 1#a b',
'#a b',
'#a%20b',
'#c d',
'#c%20d',
'/page 2#你好',
'/page%202#你好',
'/page 2#%E4%BD%A0%E5%A5%BD',
'/page%202#%E4%BD%A0%E5%A5%BD',
'/page 2#schrödingers-cat-principle',
'/page%202#schrödingers-cat-principle',
'/page 2#schr%C3%B6dingers-cat-principle',
'/page%202#schr%C3%B6dingers-cat-principle',
],
anchors: ['a b', 'c%20d'],
},
'/page 2': {
links: ['/page 1#a b', '/page%201#c d'],
anchors: ['你好', '#schr%C3%B6dingers-cat-principle'],
},
},
});
});
it('accepts valid link with empty anchor', async () => {
await testBrokenLinks({
routes: [{path: '/page 1'}, {path: '/page 2'}],
collectedLinks: {
'/page 1': {
links: [
'/page 1',
'/page 2',
'/page 1#',
'/page 2#',
'/page 1?age=42#',
'/page 2?age=42#',
'#',
'#',
'./page 1#',
'./page 2#',
],
anchors: [],
},
'/page 2': {links: [], anchors: []},
},
});
});
@ -225,28 +355,6 @@ describe('handleBrokenLinks', () => {
`);
});
it('rejects valid link with empty broken anchor', async () => {
await expect(() =>
testBrokenLinks({
routes: [{path: '/page1'}, {path: '/page2'}],
collectedLinks: {
'/page1': {links: ['/page2#'], anchors: []},
'/page2': {links: [], anchors: []},
},
}),
).rejects.toThrowErrorMatchingInlineSnapshot(`
"Docusaurus found broken anchors!
Please check the pages of your site in the list below, and make sure you don't reference any anchor that does not exist.
Note: it's possible to ignore broken anchors with the 'onBrokenAnchors' Docusaurus configuration, and let the build pass.
Exhaustive list of all broken anchors found:
- Broken anchor on source page path = /page1:
-> linking to /page2#
"
`);
});
it('rejects valid link with broken anchor + query-string', async () => {
await expect(() =>
testBrokenLinks({

View file

@ -38,15 +38,23 @@ function getBrokenLinksForPage({
pageAnchors: string[];
routes: RouteConfig[];
}): BrokenLink[] {
// console.log('routes:', routes);
const allCollectedPaths = new Set(Object.keys(collectedLinks));
function isPathBrokenLink(linkPath: URLPath) {
const matchedRoutes = [linkPath.pathname, decodeURI(linkPath.pathname)]
const pathnames = [linkPath.pathname, decodeURI(linkPath.pathname)];
const matchedRoutes = pathnames
// @ts-expect-error: React router types RouteConfig with an actual React
// component, but we load route components with string paths.
// We don't actually access component here, so it's fine.
.map((l) => matchRoutes(routes, l))
.flat();
return matchedRoutes.length === 0;
// The link path is broken if:
// - it doesn't match any route
// - it doesn't match any collected path
return (
matchedRoutes.length === 0 &&
!pathnames.some((p) => allCollectedPaths.has(p))
);
}
function isAnchorBrokenLink(linkPath: URLPath) {
@ -57,6 +65,13 @@ function getBrokenLinksForPage({
return false;
}
// Link has empty hash ("#", "/page#"...): we do not report it as broken
// Empty hashes are used for various weird reasons, by us and other users...
// See for example: https://github.com/facebook/docusaurus/pull/6003
if (hash === '') {
return false;
}
const targetPage =
collectedLinks[pathname] || collectedLinks[decodeURI(pathname)];
@ -68,7 +83,8 @@ function getBrokenLinksForPage({
// it's a broken anchor if the target page exists
// but the anchor does not exist on that page
return !targetPage.anchors.includes(hash);
const hashes = [hash, decodeURIComponent(hash)];
return !targetPage.anchors.some((anchor) => hashes.includes(anchor));
}
const brokenLinks = pageLinks.flatMap((link) => {
@ -117,15 +133,21 @@ function getBrokenLinks({
}): BrokenLinksMap {
const filteredRoutes = filterIntermediateRoutes(routes);
return _.mapValues(collectedLinks, (pageCollectedData, pagePath) =>
getBrokenLinksForPage({
collectedLinks,
pageLinks: pageCollectedData.links,
pageAnchors: pageCollectedData.anchors,
pagePath,
routes: filteredRoutes,
}),
);
return _.mapValues(collectedLinks, (pageCollectedData, pagePath) => {
try {
return getBrokenLinksForPage({
collectedLinks,
pageLinks: pageCollectedData.links,
pageAnchors: pageCollectedData.anchors,
pagePath,
routes: filteredRoutes,
});
} catch (e) {
throw new Error(`Unable to get broken links for page ${pagePath}.`, {
cause: e,
});
}
});
}
function brokenLinkMessage(brokenLink: BrokenLink): string {
@ -277,6 +299,21 @@ function reportBrokenLinks({
}
}
// Users might use the useBrokenLinks() API in weird unexpected ways
// JS users might call "collectLink(undefined)" for example
// TS users might call "collectAnchor('#hash')" with/without #
// We clean/normalize the collected data to avoid obscure errors being thrown
function normalizeCollectedLinks(
collectedLinks: CollectedLinks,
): CollectedLinks {
return _.mapValues(collectedLinks, (pageCollectedData) => ({
links: pageCollectedData.links.filter(_.isString),
anchors: pageCollectedData.anchors
.filter(_.isString)
.map((anchor) => (anchor.startsWith('#') ? anchor.slice(1) : anchor)),
}));
}
export async function handleBrokenLinks({
collectedLinks,
onBrokenLinks,
@ -291,6 +328,9 @@ export async function handleBrokenLinks({
if (onBrokenLinks === 'ignore' && onBrokenAnchors === 'ignore') {
return;
}
const brokenLinks = getBrokenLinks({routes, collectedLinks});
const brokenLinks = getBrokenLinks({
routes,
collectedLinks: normalizeCollectedLinks(collectedLinks),
});
reportBrokenLinks({brokenLinks, onBrokenLinks, onBrokenAnchors});
}

View file

@ -276,6 +276,15 @@ ${JSON.stringify(routeConfig)}`,
});
}
/**
* Old stuff
* As far as I understand, this is what permits to SSG the 404.html file
* This is rendered through the catch-all ComponentCreator("*") route
* Note CDNs only understand the 404.html file by convention
* The extension probably permits to avoid emitting "/404/index.html"
*/
const NotFoundRoutePath = '/404.html';
/**
* Routes are prepared into three temp files:
*
@ -296,7 +305,7 @@ export function loadRoutes(
routesConfig: '',
routesChunkNames: {},
registry: {},
routesPaths: [normalizeUrl([baseUrl, '404.html'])],
routesPaths: [normalizeUrl([baseUrl, NotFoundRoutePath])],
};
// `genRouteCode` would mutate `res`

View file

@ -66,6 +66,7 @@ datagit
Datagit's
dedup
devto
dingers
Dmitry
docsearch
Docsify
@ -401,3 +402,4 @@ yangshunz
Zhou
zoomable
zpao
ödingers

View file

@ -0,0 +1,9 @@
# Broken Anchors tests
import Link from '@docusaurus/Link';
<Link id="test-link-anchor">#test-link-anchor</Link>
[Markdown link to above anchor](#test-link-anchor)
[Markdown link to above anchor](#)

View file

@ -14,7 +14,7 @@ image: ./img/social-card.png
[DocSearch](https://docsearch.algolia.com/) is migrating to a new, more powerful system, which gives users their own Algolia application and new credentials.
Docusaurus site owners should upgrade their configuration with [their new credentials](#im-using-docusaurus-and-docsearch-can-i-migrate) **by February 1, 2022**, existing search indexes will be frozen and become read-only after this date.
Docusaurus site owners should upgrade their configuration with their new credentials **by February 1, 2022**, existing search indexes will be frozen and become read-only after this date.
<!--truncate-->
@ -92,7 +92,7 @@ And of course, **a lot more, for free**.
## FAQ
### I'm using Docusaurus and DocSearch, can I migrate?
### I'm using Docusaurus and DocSearch, can I migrate? {#im-using-docusaurus-and-docsearch-can-i-migrate}
At the time we are writing this, we are still at an early stage of the migration. We are doing small batches every week but will increase the load shortly, so please be patient and keep an eye out in your mailbox, you'll be contacted as soon as your Algolia app is ready!

View file

@ -33,7 +33,7 @@ website
`website/src/theme/Navbar.js` takes precedence whenever `@theme/Navbar` is imported. This behavior is called component swizzling. If you are familiar with Objective C where a function's implementation can be swapped during runtime, it's the exact same concept here with changing the target `@theme/Navbar` is pointing to!
We already talked about how the "userland theme" in `src/theme` can re-use a theme component through the [`@theme-original`](#wrapping) alias. One theme package can also wrap a component from another theme, by importing the component from the initial theme, using the `@theme-init` import.
We already talked about how the "userland theme" in `src/theme` can re-use a theme component through the [`@theme-original`](../swizzling.mdx#wrapping) alias. One theme package can also wrap a component from another theme, by importing the component from the initial theme, using the `@theme-init` import.
Here's an example of using this feature to enhance the default theme `CodeBlock` component with a `react-live` playground feature.

View file

@ -25,7 +25,7 @@ npm install --save @docusaurus/plugin-content-blog
If you use the preset `@docusaurus/preset-classic`, you don't need to install this plugin as a dependency.
You can configure this plugin through the [preset options](#ex-config-preset).
You can configure this plugin through the [preset options](../../using-plugins.mdx#docusauruspreset-classic).
:::

View file

@ -19,7 +19,7 @@ npm install --save @docusaurus/plugin-content-docs
If you use the preset `@docusaurus/preset-classic`, you don't need to install this plugin as a dependency.
You can configure this plugin through the preset options.
You can configure this plugin through the [preset options](../../using-plugins.mdx#docusauruspreset-classic).
:::

View file

@ -19,7 +19,7 @@ npm install --save @docusaurus/plugin-content-pages
If you use the preset `@docusaurus/preset-classic`, you don't need to install this plugin as a dependency.
You can configure this plugin through the preset options.
You can configure this plugin through the [preset options](../../using-plugins.mdx#docusauruspreset-classic).
:::

View file

@ -49,7 +49,7 @@ npm install --save @docusaurus/plugin-debug
If you use the preset `@docusaurus/preset-classic`, you don't need to install this plugin as a dependency.
You can configure this plugin through the preset options.
You can configure this plugin through the [preset options](../../using-plugins.mdx#docusauruspreset-classic).
:::

View file

@ -35,7 +35,7 @@ npm install --save @docusaurus/plugin-google-analytics
If you use the preset `@docusaurus/preset-classic`, you don't need to install this plugin as a dependency.
You can configure this plugin through the preset options.
You can configure this plugin through the [preset options](../../using-plugins.mdx#docusauruspreset-classic).
:::

View file

@ -31,7 +31,7 @@ npm install --save @docusaurus/plugin-google-gtag
If you use the preset `@docusaurus/preset-classic`, you don't need to install this plugin as a dependency.
You can configure this plugin through the preset options.
You can configure this plugin through the [preset options](../../using-plugins.mdx#docusauruspreset-classic).
:::

View file

@ -31,7 +31,7 @@ npm install --save @docusaurus/plugin-google-tag-manager
If you use the preset `@docusaurus/preset-classic`, you don't need to install this plugin as a dependency.
You can configure this plugin through the preset options.
You can configure this plugin through the [preset options](../../using-plugins.mdx#docusauruspreset-classic).
:::

View file

@ -25,7 +25,7 @@ npm install --save @docusaurus/plugin-sitemap
If you use the preset `@docusaurus/preset-classic`, you don't need to install this plugin as a dependency.
You can configure this plugin through the [preset options](#ex-config-preset).
You can configure this plugin through the [preset options](../../using-plugins.mdx#docusauruspreset-classic).
:::

View file

@ -627,24 +627,18 @@ Usage example:
```js title="MyHeading.js"
import useBrokenLinks from '@docusaurus/useBrokenLinks';
export default function MyHeading({id, ...props}): JSX.Element {
const brokenLinks = useBrokenLinks();
brokenLinks.collectAnchor(id);
return <h2 id={id}>Heading</h2>;
export default function MyHeading(props) {
useBrokenLinks().collectAnchor(props.id);
return <h2 {...props} />;
}
```
```js title="MyLink.js"
import useBrokenLinks from '@docusaurus/useBrokenLinks';
export default function MyLink({targetLink, ...props}): JSX.Element {
const brokenLinks = useBrokenLinks();
brokenLinks.collectLink(targetLink);
return <a href={targetLink}>Link</a>;
export default function MyLink(props) {
useBrokenLinks().collectLink(props.href);
return <a {...props} />;
}
```

View file

@ -61,11 +61,11 @@ export default {
};
```
If you use the doc shorthand or [autogenerated](#sidebar-item-autogenerated) sidebar, you would lose the ability to customize the sidebar label through item definition. You can, however, use the `sidebar_label` Markdown front matter within that doc, which has higher precedence over the `label` key in the sidebar item. Similarly, you can use `sidebar_custom_props` to declare custom metadata for a doc page.
If you use the doc shorthand or [autogenerated](autogenerated.mdx) sidebar, you would lose the ability to customize the sidebar label through item definition. You can, however, use the `sidebar_label` Markdown front matter within that doc, which has higher precedence over the `label` key in the sidebar item. Similarly, you can use `sidebar_custom_props` to declare custom metadata for a doc page.
:::note
A `doc` item sets an [implicit sidebar association](#sidebar-association). Don't assign the same doc to multiple sidebars: change the type to `ref` instead.
A `doc` item sets an [implicit sidebar association](./multiple-sidebars.mdx#sidebar-association). Don't assign the same doc to multiple sidebars: change the type to `ref` instead.
:::

View file

@ -24,7 +24,7 @@ The migration CLI migrates:
To use the migration CLI, follow these steps:
1. Before using the migration CLI, ensure that `/docs`, `/blog`, `/static`, `sidebars.json`, `siteConfig.js`, `package.json` follow the [structure](#) shown at the start of this page.
1. Before using the migration CLI, ensure that `/docs`, `/blog`, `/static`, `sidebars.json`, `siteConfig.js`, `package.json` follow the expected structure.
2. To migrate your v1 website, run the migration CLI with the appropriate filesystem paths:

View file

@ -13,6 +13,7 @@ import React, {
useRef,
useEffect,
} from 'react';
import useBrokenLinks from '@docusaurus/useBrokenLinks';
import {useHistory} from '@docusaurus/router';
import styles from './styles.module.css';
@ -41,6 +42,7 @@ function APITableRow(
const id = name ? `${name}-${entryName}` : entryName;
const anchor = `#${id}`;
const history = useHistory();
useBrokenLinks().collectAnchor(id);
return (
<tr
id={id}

View file

@ -33,7 +33,7 @@ website
`website/src/theme/Navbar.js` takes precedence whenever `@theme/Navbar` is imported. This behavior is called component swizzling. If you are familiar with Objective C where a function's implementation can be swapped during runtime, it's the exact same concept here with changing the target `@theme/Navbar` is pointing to!
We already talked about how the "userland theme" in `src/theme` can re-use a theme component through the [`@theme-original`](#wrapping) alias. One theme package can also wrap a component from another theme, by importing the component from the initial theme, using the `@theme-init` import.
We already talked about how the "userland theme" in `src/theme` can re-use a theme component through the [`@theme-original`](../swizzling.mdx#wrapping) alias. One theme package can also wrap a component from another theme, by importing the component from the initial theme, using the `@theme-init` import.
Here's an example of using this feature to enhance the default theme `CodeBlock` component with a `react-live` playground feature.

View file

@ -25,7 +25,7 @@ npm install --save @docusaurus/plugin-content-blog
If you use the preset `@docusaurus/preset-classic`, you don't need to install this plugin as a dependency.
You can configure this plugin through the [preset options](#ex-config-preset).
You can configure this plugin through the [preset options](../../using-plugins.mdx#docusauruspreset-classic).
:::

View file

@ -19,7 +19,7 @@ npm install --save @docusaurus/plugin-content-docs
If you use the preset `@docusaurus/preset-classic`, you don't need to install this plugin as a dependency.
You can configure this plugin through the preset options.
You can configure this plugin through the [preset options](../../using-plugins.mdx#docusauruspreset-classic).
:::

View file

@ -19,7 +19,7 @@ npm install --save @docusaurus/plugin-content-pages
If you use the preset `@docusaurus/preset-classic`, you don't need to install this plugin as a dependency.
You can configure this plugin through the preset options.
You can configure this plugin through the [preset options](../../using-plugins.mdx#docusauruspreset-classic).
:::

View file

@ -49,7 +49,7 @@ npm install --save @docusaurus/plugin-debug
If you use the preset `@docusaurus/preset-classic`, you don't need to install this plugin as a dependency.
You can configure this plugin through the preset options.
You can configure this plugin through the [preset options](../../using-plugins.mdx#docusauruspreset-classic).
:::

View file

@ -35,7 +35,7 @@ npm install --save @docusaurus/plugin-google-analytics
If you use the preset `@docusaurus/preset-classic`, you don't need to install this plugin as a dependency.
You can configure this plugin through the preset options.
You can configure this plugin through the [preset options](../../using-plugins.mdx#docusauruspreset-classic).
:::

View file

@ -31,7 +31,7 @@ npm install --save @docusaurus/plugin-google-gtag
If you use the preset `@docusaurus/preset-classic`, you don't need to install this plugin as a dependency.
You can configure this plugin through the preset options.
You can configure this plugin through the [preset options](../../using-plugins.mdx#docusauruspreset-classic).
:::

View file

@ -31,7 +31,7 @@ npm install --save @docusaurus/plugin-google-tag-manager
If you use the preset `@docusaurus/preset-classic`, you don't need to install this plugin as a dependency.
You can configure this plugin through the preset options.
You can configure this plugin through the [preset options](../../using-plugins.mdx#docusauruspreset-classic).
:::

View file

@ -25,7 +25,7 @@ npm install --save @docusaurus/plugin-sitemap
If you use the preset `@docusaurus/preset-classic`, you don't need to install this plugin as a dependency.
You can configure this plugin through the [preset options](#ex-config-preset).
You can configure this plugin through the [preset options](../../using-plugins.mdx#docusauruspreset-classic).
:::

View file

@ -61,11 +61,11 @@ module.exports = {
};
```
If you use the doc shorthand or [autogenerated](#sidebar-item-autogenerated) sidebar, you would lose the ability to customize the sidebar label through item definition. You can, however, use the `sidebar_label` Markdown front matter within that doc, which has higher precedence over the `label` key in the sidebar item. Similarly, you can use `sidebar_custom_props` to declare custom metadata for a doc page.
If you use the doc shorthand or [autogenerated](autogenerated.mdx) sidebar, you would lose the ability to customize the sidebar label through item definition. You can, however, use the `sidebar_label` Markdown front matter within that doc, which has higher precedence over the `label` key in the sidebar item. Similarly, you can use `sidebar_custom_props` to declare custom metadata for a doc page.
:::note
A `doc` item sets an [implicit sidebar association](#sidebar-association). Don't assign the same doc to multiple sidebars: change the type to `ref` instead.
A `doc` item sets an [implicit sidebar association](./multiple-sidebars.mdx#sidebar-association). Don't assign the same doc to multiple sidebars: change the type to `ref` instead.
:::

View file

@ -24,7 +24,7 @@ The migration CLI migrates:
To use the migration CLI, follow these steps:
1. Before using the migration CLI, ensure that `/docs`, `/blog`, `/static`, `sidebars.json`, `siteConfig.js`, `package.json` follow the [structure](#) shown at the start of this page.
1. Before using the migration CLI, ensure that `/docs`, `/blog`, `/static`, `sidebars.json`, `siteConfig.js`, `package.json` follow the expected structure.
2. To migrate your v1 website, run the migration CLI with the appropriate filesystem paths:

View file

@ -33,7 +33,7 @@ website
`website/src/theme/Navbar.js` takes precedence whenever `@theme/Navbar` is imported. This behavior is called component swizzling. If you are familiar with Objective C where a function's implementation can be swapped during runtime, it's the exact same concept here with changing the target `@theme/Navbar` is pointing to!
We already talked about how the "userland theme" in `src/theme` can re-use a theme component through the [`@theme-original`](#wrapping) alias. One theme package can also wrap a component from another theme, by importing the component from the initial theme, using the `@theme-init` import.
We already talked about how the "userland theme" in `src/theme` can re-use a theme component through the [`@theme-original`](../swizzling.mdx#wrapping) alias. One theme package can also wrap a component from another theme, by importing the component from the initial theme, using the `@theme-init` import.
Here's an example of using this feature to enhance the default theme `CodeBlock` component with a `react-live` playground feature.

View file

@ -25,7 +25,7 @@ npm install --save @docusaurus/plugin-content-blog
If you use the preset `@docusaurus/preset-classic`, you don't need to install this plugin as a dependency.
You can configure this plugin through the [preset options](#ex-config-preset).
You can configure this plugin through the [preset options](../../using-plugins.mdx#docusauruspreset-classic).
:::

View file

@ -19,7 +19,7 @@ npm install --save @docusaurus/plugin-content-docs
If you use the preset `@docusaurus/preset-classic`, you don't need to install this plugin as a dependency.
You can configure this plugin through the preset options.
You can configure this plugin through the [preset options](../../using-plugins.mdx#docusauruspreset-classic).
:::

View file

@ -19,7 +19,7 @@ npm install --save @docusaurus/plugin-content-pages
If you use the preset `@docusaurus/preset-classic`, you don't need to install this plugin as a dependency.
You can configure this plugin through the preset options.
You can configure this plugin through the [preset options](../../using-plugins.mdx#docusauruspreset-classic).
:::

View file

@ -49,7 +49,7 @@ npm install --save @docusaurus/plugin-debug
If you use the preset `@docusaurus/preset-classic`, you don't need to install this plugin as a dependency.
You can configure this plugin through the preset options.
You can configure this plugin through the [preset options](../../using-plugins.mdx#docusauruspreset-classic).
:::

View file

@ -35,7 +35,7 @@ npm install --save @docusaurus/plugin-google-analytics
If you use the preset `@docusaurus/preset-classic`, you don't need to install this plugin as a dependency.
You can configure this plugin through the preset options.
You can configure this plugin through the [preset options](../../using-plugins.mdx#docusauruspreset-classic).
:::

View file

@ -31,7 +31,7 @@ npm install --save @docusaurus/plugin-google-gtag
If you use the preset `@docusaurus/preset-classic`, you don't need to install this plugin as a dependency.
You can configure this plugin through the preset options.
You can configure this plugin through the [preset options](../../using-plugins.mdx#docusauruspreset-classic).
:::

View file

@ -31,7 +31,7 @@ npm install --save @docusaurus/plugin-google-tag-manager
If you use the preset `@docusaurus/preset-classic`, you don't need to install this plugin as a dependency.
You can configure this plugin through the preset options.
You can configure this plugin through the [preset options](../../using-plugins.mdx#docusauruspreset-classic).
:::

View file

@ -25,7 +25,7 @@ npm install --save @docusaurus/plugin-sitemap
If you use the preset `@docusaurus/preset-classic`, you don't need to install this plugin as a dependency.
You can configure this plugin through the [preset options](#ex-config-preset).
You can configure this plugin through the [preset options](../../using-plugins.mdx#docusauruspreset-classic).
:::

View file

@ -61,11 +61,11 @@ export default {
};
```
If you use the doc shorthand or [autogenerated](#sidebar-item-autogenerated) sidebar, you would lose the ability to customize the sidebar label through item definition. You can, however, use the `sidebar_label` Markdown front matter within that doc, which has higher precedence over the `label` key in the sidebar item. Similarly, you can use `sidebar_custom_props` to declare custom metadata for a doc page.
If you use the doc shorthand or [autogenerated](autogenerated.mdx) sidebar, you would lose the ability to customize the sidebar label through item definition. You can, however, use the `sidebar_label` Markdown front matter within that doc, which has higher precedence over the `label` key in the sidebar item. Similarly, you can use `sidebar_custom_props` to declare custom metadata for a doc page.
:::note
A `doc` item sets an [implicit sidebar association](#sidebar-association). Don't assign the same doc to multiple sidebars: change the type to `ref` instead.
A `doc` item sets an [implicit sidebar association](./multiple-sidebars.mdx#sidebar-association). Don't assign the same doc to multiple sidebars: change the type to `ref` instead.
:::

View file

@ -24,7 +24,7 @@ The migration CLI migrates:
To use the migration CLI, follow these steps:
1. Before using the migration CLI, ensure that `/docs`, `/blog`, `/static`, `sidebars.json`, `siteConfig.js`, `package.json` follow the [structure](#) shown at the start of this page.
1. Before using the migration CLI, ensure that `/docs`, `/blog`, `/static`, `sidebars.json`, `siteConfig.js`, `package.json` follow the expected structure.
2. To migrate your v1 website, run the migration CLI with the appropriate filesystem paths:

View file

@ -33,7 +33,7 @@ website
`website/src/theme/Navbar.js` takes precedence whenever `@theme/Navbar` is imported. This behavior is called component swizzling. If you are familiar with Objective C where a function's implementation can be swapped during runtime, it's the exact same concept here with changing the target `@theme/Navbar` is pointing to!
We already talked about how the "userland theme" in `src/theme` can re-use a theme component through the [`@theme-original`](#wrapping) alias. One theme package can also wrap a component from another theme, by importing the component from the initial theme, using the `@theme-init` import.
We already talked about how the "userland theme" in `src/theme` can re-use a theme component through the [`@theme-original`](../swizzling.mdx#wrapping) alias. One theme package can also wrap a component from another theme, by importing the component from the initial theme, using the `@theme-init` import.
Here's an example of using this feature to enhance the default theme `CodeBlock` component with a `react-live` playground feature.

View file

@ -25,7 +25,7 @@ npm install --save @docusaurus/plugin-content-blog
If you use the preset `@docusaurus/preset-classic`, you don't need to install this plugin as a dependency.
You can configure this plugin through the [preset options](#ex-config-preset).
You can configure this plugin through the [preset options](../../using-plugins.mdx#docusauruspreset-classic).
:::

View file

@ -19,7 +19,7 @@ npm install --save @docusaurus/plugin-content-docs
If you use the preset `@docusaurus/preset-classic`, you don't need to install this plugin as a dependency.
You can configure this plugin through the preset options.
You can configure this plugin through the [preset options](../../using-plugins.mdx#docusauruspreset-classic).
:::

View file

@ -19,7 +19,7 @@ npm install --save @docusaurus/plugin-content-pages
If you use the preset `@docusaurus/preset-classic`, you don't need to install this plugin as a dependency.
You can configure this plugin through the preset options.
You can configure this plugin through the [preset options](../../using-plugins.mdx#docusauruspreset-classic).
:::

View file

@ -49,7 +49,7 @@ npm install --save @docusaurus/plugin-debug
If you use the preset `@docusaurus/preset-classic`, you don't need to install this plugin as a dependency.
You can configure this plugin through the preset options.
You can configure this plugin through the [preset options](../../using-plugins.mdx#docusauruspreset-classic).
:::

View file

@ -35,7 +35,7 @@ npm install --save @docusaurus/plugin-google-analytics
If you use the preset `@docusaurus/preset-classic`, you don't need to install this plugin as a dependency.
You can configure this plugin through the preset options.
You can configure this plugin through the [preset options](../../using-plugins.mdx#docusauruspreset-classic).
:::

View file

@ -31,7 +31,7 @@ npm install --save @docusaurus/plugin-google-gtag
If you use the preset `@docusaurus/preset-classic`, you don't need to install this plugin as a dependency.
You can configure this plugin through the preset options.
You can configure this plugin through the [preset options](../../using-plugins.mdx#docusauruspreset-classic).
:::

View file

@ -31,7 +31,7 @@ npm install --save @docusaurus/plugin-google-tag-manager
If you use the preset `@docusaurus/preset-classic`, you don't need to install this plugin as a dependency.
You can configure this plugin through the preset options.
You can configure this plugin through the [preset options](../../using-plugins.mdx#docusauruspreset-classic).
:::

View file

@ -25,7 +25,7 @@ npm install --save @docusaurus/plugin-sitemap
If you use the preset `@docusaurus/preset-classic`, you don't need to install this plugin as a dependency.
You can configure this plugin through the [preset options](#ex-config-preset).
You can configure this plugin through the [preset options](../../using-plugins.mdx#docusauruspreset-classic).
:::

View file

@ -622,19 +622,19 @@ Usage example:
```js title="MyHeading.js"
import useBrokenLinks from '@docusaurus/useBrokenLinks';
export default function MyHeading({id, ...props}): JSX.Element {
const brokenLinks = useBrokenLinks();
brokenLinks.collectAnchor(id);
return <h2 id={id}>Heading</h2>;
export default function MyHeading(props) {
useBrokenLinks().collectAnchor(props.id);
return <h2 {...props} />;
}
```
```js title="MyLink.js"
import useBrokenLinks from '@docusaurus/useBrokenLinks';
export default function MyLink({targetLink, ...props}): JSX.Element {
const brokenLinks = useBrokenLinks();
brokenLinks.collectLink(targetLink);
return <a href={targetLink}>Link</a>;
export default function MyLink(props) {
useBrokenLinks().collectLink(props.href);
return <a {...props} />;
}
```

View file

@ -61,11 +61,11 @@ export default {
};
```
If you use the doc shorthand or [autogenerated](#sidebar-item-autogenerated) sidebar, you would lose the ability to customize the sidebar label through item definition. You can, however, use the `sidebar_label` Markdown front matter within that doc, which has higher precedence over the `label` key in the sidebar item. Similarly, you can use `sidebar_custom_props` to declare custom metadata for a doc page.
If you use the doc shorthand or [autogenerated](autogenerated.mdx) sidebar, you would lose the ability to customize the sidebar label through item definition. You can, however, use the `sidebar_label` Markdown front matter within that doc, which has higher precedence over the `label` key in the sidebar item. Similarly, you can use `sidebar_custom_props` to declare custom metadata for a doc page.
:::note
A `doc` item sets an [implicit sidebar association](#sidebar-association). Don't assign the same doc to multiple sidebars: change the type to `ref` instead.
A `doc` item sets an [implicit sidebar association](./multiple-sidebars.mdx#sidebar-association). Don't assign the same doc to multiple sidebars: change the type to `ref` instead.
:::

View file

@ -24,7 +24,7 @@ The migration CLI migrates:
To use the migration CLI, follow these steps:
1. Before using the migration CLI, ensure that `/docs`, `/blog`, `/static`, `sidebars.json`, `siteConfig.js`, `package.json` follow the [structure](#) shown at the start of this page.
1. Before using the migration CLI, ensure that `/docs`, `/blog`, `/static`, `sidebars.json`, `siteConfig.js`, `package.json` follow the expected structure.
2. To migrate your v1 website, run the migration CLI with the appropriate filesystem paths: