diff --git a/packages/docusaurus-mdx-loader/src/remark/transformLinks/__tests__/__snapshots__/index.test.ts.snap b/packages/docusaurus-mdx-loader/src/remark/transformLinks/__tests__/__snapshots__/index.test.ts.snap
index 06f8fcbcabe7..a0186db7b10b 100644
--- a/packages/docusaurus-mdx-loader/src/remark/transformLinks/__tests__/__snapshots__/index.test.ts.snap
+++ b/packages/docusaurus-mdx-loader/src/remark/transformLinks/__tests__/__snapshots__/index.test.ts.snap
@@ -12,17 +12,17 @@ exports[`transformAsset plugin pathname protocol 1`] = `
exports[`transformAsset plugin transform md links to 1`] = `
"[asset](https://example.com/asset.pdf)
-/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf").default} />
+/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf").default} />
-/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf").default}>asset
+/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf").default}>asset
-in paragraph /node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf").default}>asset
+in paragraph /node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf").default}>asset
-/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset (2).pdf").default}>asset with URL encoded chars
+/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset (2).pdf").default}>asset with URL encoded chars
-/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf").default + '#page=2'}>asset with hash
+/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf").default + '#page=2'}>asset with hash
-/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf").default} title="Title">asset
+/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf").default} title="Title">asset
[page](noUrl.md)
@@ -36,24 +36,24 @@ in paragraph /node_modules/file
[assets](/github/!file-loader!/assets.pdf)
-/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf").default}>asset
+/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf").default}>asset
-/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static2/asset2.pdf").default}>asset2
+/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static2/asset2.pdf").default}>asset2
-/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/staticAsset.pdf").default}>staticAsset.pdf
+/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/staticAsset.pdf").default}>staticAsset.pdf
-/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/staticAsset.pdf").default}>@site/static/staticAsset.pdf
+/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/staticAsset.pdf").default}>@site/static/staticAsset.pdf
-/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/staticAsset.pdf").default + '#page=2'} title="Title">@site/static/staticAsset.pdf
+/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/staticAsset.pdf").default + '#page=2'} title="Title">@site/static/staticAsset.pdf
-/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/staticAsset.pdf").default}>Just staticAsset.pdf, and /node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/staticAsset.pdf").default}>**awesome** staticAsset 2.pdf 'It is really "AWESOME"', but also /node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/staticAsset.pdf").default}>coded \`staticAsset 3.pdf\`
+/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/staticAsset.pdf").default}>Just staticAsset.pdf, and /node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/staticAsset.pdf").default}>**awesome** staticAsset 2.pdf 'It is really "AWESOME"', but also /node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/staticAsset.pdf").default}>coded \`staticAsset 3.pdf\`
-/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/staticAssetImage.png").default}>/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[contenthash].[ext]&fallback=/node_modules/file-loader/dist/cjs.js!./static/staticAssetImage.png").default} width="200" height="200" />
+/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/staticAssetImage.png").default}>/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[contenthash].[ext]&fallback=/node_modules/file-loader/dist/cjs.js!./static/staticAssetImage.png").default} width="200" height="200" />
-/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf").default}>Stylized link to asset file
+/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf").default}>Stylized link to asset file
-/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./data.json").default}>JSON
+/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./data.json").default}>JSON
-/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/static-json.json").default}>static JSON
+/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/static-json.json").default}>static JSON
"
`;
diff --git a/packages/docusaurus-mdx-loader/src/remark/transformLinks/index.ts b/packages/docusaurus-mdx-loader/src/remark/transformLinks/index.ts
index 252289f97452..179bcadb770d 100644
--- a/packages/docusaurus-mdx-loader/src/remark/transformLinks/index.ts
+++ b/packages/docusaurus-mdx-loader/src/remark/transformLinks/index.ts
@@ -73,6 +73,34 @@ async function toAssetRequireNode(
value: '_blank',
});
+ // Assets are not routes, and are required by Webpack already
+ // They should not trigger the broken link checker
+ attributes.push({
+ type: 'mdxJsxAttribute',
+ name: 'data-noBrokenLinkCheck',
+ value: {
+ type: 'mdxJsxAttributeValueExpression',
+ value: 'true',
+ data: {
+ estree: {
+ type: 'Program',
+ body: [
+ {
+ type: 'ExpressionStatement',
+ expression: {
+ type: 'Literal',
+ value: true,
+ raw: 'true',
+ },
+ },
+ ],
+ sourceType: 'module',
+ comments: [],
+ },
+ },
+ },
+ });
+
attributes.push({
type: 'mdxJsxAttribute',
name: 'href',
diff --git a/packages/docusaurus-module-type-aliases/src/index.d.ts b/packages/docusaurus-module-type-aliases/src/index.d.ts
index 1f0e7679420a..5cb44f06e402 100644
--- a/packages/docusaurus-module-type-aliases/src/index.d.ts
+++ b/packages/docusaurus-module-type-aliases/src/index.d.ts
@@ -260,6 +260,15 @@ declare module '@docusaurus/useRouteContext' {
export default function useRouteContext(): PluginRouteContext;
}
+declare module '@docusaurus/useBrokenLinks' {
+ export type BrokenLinks = {
+ collectLink: (link: string) => void;
+ collectAnchor: (anchor: string) => void;
+ };
+
+ export default function useBrokenLinks(): BrokenLinks;
+}
+
declare module '@docusaurus/useIsBrowser' {
export default function useIsBrowser(): boolean;
}
diff --git a/packages/docusaurus-theme-classic/src/theme/Heading/index.tsx b/packages/docusaurus-theme-classic/src/theme/Heading/index.tsx
index 9ca1bc3f0db9..b17cd12ec3b1 100644
--- a/packages/docusaurus-theme-classic/src/theme/Heading/index.tsx
+++ b/packages/docusaurus-theme-classic/src/theme/Heading/index.tsx
@@ -10,11 +10,13 @@ import clsx from 'clsx';
import {translate} from '@docusaurus/Translate';
import {useThemeConfig} from '@docusaurus/theme-common';
import Link from '@docusaurus/Link';
+import useBrokenLinks from '@docusaurus/useBrokenLinks';
import type {Props} from '@theme/Heading';
import styles from './styles.module.css';
export default function Heading({as: As, id, ...props}: Props): JSX.Element {
+ const brokenLinks = useBrokenLinks();
const {
navbar: {hideOnScroll},
} = useThemeConfig();
@@ -23,6 +25,8 @@ export default function Heading({as: As, id, ...props}: Props): JSX.Element {
return ;
}
+ brokenLinks.collectAnchor(id);
+
const anchorTitle = translate(
{
id: 'theme.common.headingLinkTitle',
diff --git a/packages/docusaurus-types/src/config.d.ts b/packages/docusaurus-types/src/config.d.ts
index 2efea9280b69..47bfde898d44 100644
--- a/packages/docusaurus-types/src/config.d.ts
+++ b/packages/docusaurus-types/src/config.d.ts
@@ -175,6 +175,13 @@ export type DocusaurusConfig = {
* @default "throw"
*/
onBrokenLinks: ReportingSeverity;
+ /**
+ * The behavior of Docusaurus when it detects any broken link.
+ *
+ * @see https://docusaurus.io/docs/api/docusaurus-config#onBrokenAnchors
+ * @default "warn"
+ */
+ onBrokenAnchors: ReportingSeverity;
/**
* The behavior of Docusaurus when it detects any broken markdown link.
*
diff --git a/packages/docusaurus-utils/src/__tests__/urlUtils.test.ts b/packages/docusaurus-utils/src/__tests__/urlUtils.test.ts
index 30625e400554..301a91ae3224 100644
--- a/packages/docusaurus-utils/src/__tests__/urlUtils.test.ts
+++ b/packages/docusaurus-utils/src/__tests__/urlUtils.test.ts
@@ -18,6 +18,8 @@ import {
buildSshUrl,
buildHttpsUrl,
hasSSHProtocol,
+ parseURLPath,
+ serializeURLPath,
} from '../urlUtils';
describe('normalizeUrl', () => {
@@ -232,6 +234,137 @@ describe('removeTrailingSlash', () => {
});
});
+describe('parseURLPath', () => {
+ it('parse and resolve pathname', () => {
+ expect(parseURLPath('')).toEqual({
+ pathname: '/',
+ search: undefined,
+ hash: undefined,
+ });
+ expect(parseURLPath('/')).toEqual({
+ pathname: '/',
+ search: undefined,
+ hash: undefined,
+ });
+ expect(parseURLPath('/page')).toEqual({
+ pathname: '/page',
+ search: undefined,
+ hash: undefined,
+ });
+ expect(parseURLPath('/dir1/page')).toEqual({
+ pathname: '/dir1/page',
+ search: undefined,
+ hash: undefined,
+ });
+ expect(parseURLPath('/dir1/dir2/./../page')).toEqual({
+ pathname: '/dir1/page',
+ search: undefined,
+ hash: undefined,
+ });
+ expect(parseURLPath('/dir1/dir2/../..')).toEqual({
+ pathname: '/',
+ search: undefined,
+ hash: undefined,
+ });
+ expect(parseURLPath('/dir1/dir2/../../..')).toEqual({
+ pathname: '/',
+ search: undefined,
+ hash: undefined,
+ });
+ expect(parseURLPath('./dir1/dir2./../page', '/dir3/dir4/page2')).toEqual({
+ pathname: '/dir3/dir4/dir1/page',
+ search: undefined,
+ hash: undefined,
+ });
+ });
+
+ it('parse query string', () => {
+ expect(parseURLPath('/page')).toEqual({
+ pathname: '/page',
+ search: undefined,
+ hash: undefined,
+ });
+ expect(parseURLPath('/page?')).toEqual({
+ pathname: '/page',
+ search: '',
+ hash: undefined,
+ });
+ expect(parseURLPath('/page?test')).toEqual({
+ pathname: '/page',
+ search: 'test',
+ hash: undefined,
+ });
+ expect(parseURLPath('/page?age=42&great=true')).toEqual({
+ pathname: '/page',
+ search: 'age=42&great=true',
+ hash: undefined,
+ });
+ });
+
+ it('parse hash', () => {
+ expect(parseURLPath('/page')).toEqual({
+ pathname: '/page',
+ search: undefined,
+ hash: undefined,
+ });
+ expect(parseURLPath('/page#')).toEqual({
+ pathname: '/page',
+ search: undefined,
+ hash: '',
+ });
+ expect(parseURLPath('/page#anchor')).toEqual({
+ pathname: '/page',
+ search: undefined,
+ hash: 'anchor',
+ });
+ });
+
+ it('parse fancy real-world edge cases', () => {
+ expect(parseURLPath('/page?#')).toEqual({
+ pathname: '/page',
+ search: '',
+ hash: '',
+ });
+ expect(
+ parseURLPath('dir1/dir2/../page?age=42#anchor', '/dir3/page2'),
+ ).toEqual({
+ pathname: '/dir3/dir1/page',
+ search: 'age=42',
+ hash: 'anchor',
+ });
+ });
+});
+
+describe('serializeURLPath', () => {
+ function test(input: string, base?: string, expectedOutput?: string) {
+ expect(serializeURLPath(parseURLPath(input, base))).toEqual(
+ expectedOutput ?? input,
+ );
+ }
+
+ it('works for already resolved paths', () => {
+ test('/');
+ test('/dir1/page');
+ test('/dir1/page?');
+ test('/dir1/page#');
+ test('/dir1/page?#');
+ test('/dir1/page?age=42#anchor');
+ });
+
+ it('works for relative paths', () => {
+ test('', undefined, '/');
+ test('', '/dir1/dir2/page2', '/dir1/dir2/page2');
+ test('page', '/dir1/dir2/page2', '/dir1/dir2/page');
+ test('../page', '/dir1/dir2/page2', '/dir1/page');
+ test('/dir1/dir2/../page', undefined, '/dir1/page');
+ test(
+ '/dir1/dir2/../page?age=42#anchor',
+ undefined,
+ '/dir1/page?age=42#anchor',
+ );
+ });
+});
+
describe('resolvePathname', () => {
it('works', () => {
// These tests are directly copied from https://github.com/mjackson/resolve-pathname/blob/master/modules/__tests__/resolvePathname-test.js
diff --git a/packages/docusaurus-utils/src/index.ts b/packages/docusaurus-utils/src/index.ts
index 5b374898bf62..6db01244d006 100644
--- a/packages/docusaurus-utils/src/index.ts
+++ b/packages/docusaurus-utils/src/index.ts
@@ -48,6 +48,8 @@ export {
encodePath,
isValidPathname,
resolvePathname,
+ parseURLPath,
+ serializeURLPath,
addLeadingSlash,
addTrailingSlash,
removeTrailingSlash,
@@ -55,6 +57,7 @@ export {
buildHttpsUrl,
buildSshUrl,
} from './urlUtils';
+export type {URLPath} from './urlUtils';
export {
type Tag,
type TagsListItem,
diff --git a/packages/docusaurus-utils/src/urlUtils.ts b/packages/docusaurus-utils/src/urlUtils.ts
index bb901a291d06..8a7af4aa4b6d 100644
--- a/packages/docusaurus-utils/src/urlUtils.ts
+++ b/packages/docusaurus-utils/src/urlUtils.ts
@@ -165,14 +165,73 @@ export function isValidPathname(str: string): boolean {
}
}
+export type URLPath = {pathname: string; search?: string; hash?: string};
+
+// Let's name the concept of (pathname + search + hash) as URLPath
+// See also https://twitter.com/kettanaito/status/1741768992866308120
+// Note: this function also resolves relative pathnames while parsing!
+export function parseURLPath(urlPath: string, fromPath?: string): URLPath {
+ function parseURL(url: string, base?: string | URL): URL {
+ try {
+ // A possible alternative? https://github.com/unjs/ufo#url
+ return new URL(url, base ?? 'https://example.com');
+ } catch (e) {
+ throw new Error(
+ `Can't parse URL ${url}${base ? ` with base ${base}` : ''}`,
+ {cause: e},
+ );
+ }
+ }
+
+ const base = fromPath ? parseURL(fromPath) : undefined;
+ const url = parseURL(urlPath, base);
+
+ const {pathname} = url;
+
+ // Fixes annoying url.search behavior
+ // "" => undefined
+ // "?" => ""
+ // "?param => "param"
+ const search = url.search
+ ? url.search.slice(1)
+ : urlPath.includes('?')
+ ? ''
+ : undefined;
+
+ // Fixes annoying url.hash behavior
+ // "" => undefined
+ // "#" => ""
+ // "?param => "param"
+ const hash = url.hash
+ ? url.hash.slice(1)
+ : urlPath.includes('#')
+ ? ''
+ : undefined;
+
+ return {
+ pathname,
+ search,
+ hash,
+ };
+}
+
+export function serializeURLPath(urlPath: URLPath): string {
+ const search = urlPath.search === undefined ? '' : `?${urlPath.search}`;
+ const hash = urlPath.hash === undefined ? '' : `#${urlPath.hash}`;
+ return `${urlPath.pathname}${search}${hash}`;
+}
+
/**
* Resolve pathnames and fail-fast if resolution fails. Uses standard URL
* semantics (provided by `resolve-pathname` which is used internally by React
* router)
*/
export function resolvePathname(to: string, from?: string): string {
+ // TODO do we really need resolve-pathname lib anymore?
+ // possible alternative: decodeURI(parseURLPath(to, from).pathname);
return resolvePathnameUnsafe(to, from);
}
+
/** Appends a leading slash to `str`, if one doesn't exist. */
export function addLeadingSlash(str: string): string {
return addPrefix(str, '/');
diff --git a/packages/docusaurus/src/client/BrokenLinksContext.tsx b/packages/docusaurus/src/client/BrokenLinksContext.tsx
new file mode 100644
index 000000000000..e04e8ab14731
--- /dev/null
+++ b/packages/docusaurus/src/client/BrokenLinksContext.tsx
@@ -0,0 +1,51 @@
+/**
+ * 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, useContext} from 'react';
+import type {BrokenLinks} from '@docusaurus/useBrokenLinks';
+
+export type StatefulBrokenLinks = BrokenLinks & {
+ getCollectedLinks: () => string[];
+ getCollectedAnchors: () => string[];
+};
+
+export const createStatefulBrokenLinks = (): StatefulBrokenLinks => {
+ // Set to dedup, as it's not useful to collect multiple times the same value
+ const allAnchors = new Set();
+ const allLinks = new Set();
+ return {
+ collectAnchor: (anchor: string): void => {
+ allAnchors.add(anchor);
+ },
+ collectLink: (link: string): void => {
+ allLinks.add(link);
+ },
+ getCollectedAnchors: (): string[] => [...allAnchors],
+ getCollectedLinks: (): string[] => [...allLinks],
+ };
+};
+
+const Context = React.createContext({
+ collectAnchor: () => {
+ // No-op for client
+ },
+ collectLink: () => {
+ // No-op for client
+ },
+});
+
+export const useBrokenLinksContext = (): BrokenLinks => useContext(Context);
+
+export function BrokenLinksProvider({
+ children,
+ brokenLinks,
+}: {
+ children: ReactNode;
+ brokenLinks: BrokenLinks;
+}): JSX.Element {
+ return {children};
+}
diff --git a/packages/docusaurus/src/client/LinksCollector.tsx b/packages/docusaurus/src/client/LinksCollector.tsx
deleted file mode 100644
index d0fb33b9ec03..000000000000
--- a/packages/docusaurus/src/client/LinksCollector.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-/**
- * 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, useContext} from 'react';
-
-type LinksCollector = {
- collectLink: (link: string) => void;
-};
-
-type StatefulLinksCollector = LinksCollector & {
- getCollectedLinks: () => string[];
-};
-
-export const createStatefulLinksCollector = (): StatefulLinksCollector => {
- // Set to dedup, as it's not useful to collect multiple times the same link
- const allLinks = new Set();
- return {
- collectLink: (link: string): void => {
- allLinks.add(link);
- },
- getCollectedLinks: (): string[] => [...allLinks],
- };
-};
-
-const Context = React.createContext({
- collectLink: () => {
- // No-op for client. We only use the broken links checker server-side.
- },
-});
-
-export const useLinksCollector = (): LinksCollector => useContext(Context);
-
-export function LinksCollectorProvider({
- children,
- linksCollector,
-}: {
- children: ReactNode;
- linksCollector: LinksCollector;
-}): JSX.Element {
- return {children};
-}
diff --git a/packages/docusaurus/src/client/exports/Link.tsx b/packages/docusaurus/src/client/exports/Link.tsx
index 4a7453dfef8f..8b886c8e7073 100644
--- a/packages/docusaurus/src/client/exports/Link.tsx
+++ b/packages/docusaurus/src/client/exports/Link.tsx
@@ -16,7 +16,7 @@ import {applyTrailingSlash} from '@docusaurus/utils-common';
import useDocusaurusContext from './useDocusaurusContext';
import isInternalUrl from './isInternalUrl';
import ExecutionEnvironment from './ExecutionEnvironment';
-import {useLinksCollector} from '../LinksCollector';
+import useBrokenLinks from './useBrokenLinks';
import {useBaseUrlUtils} from './useBaseUrl';
import type {Props} from '@docusaurus/Link';
@@ -44,7 +44,7 @@ function Link(
siteConfig: {trailingSlash, baseUrl},
} = useDocusaurusContext();
const {withBaseUrl} = useBaseUrlUtils();
- const linksCollector = useLinksCollector();
+ const brokenLinks = useBrokenLinks();
const innerRef = useRef(null);
useImperativeHandle(forwardedRef, () => innerRef.current!);
@@ -144,7 +144,7 @@ function Link(
const isRegularHtmlLink = !targetLink || !isInternal || isAnchorLink;
if (!isRegularHtmlLink && !noBrokenLinkCheck) {
- linksCollector.collectLink(targetLink!);
+ brokenLinks.collectLink(targetLink!);
}
return isRegularHtmlLink ? (
diff --git a/packages/docusaurus/src/client/exports/useBrokenLinks.ts b/packages/docusaurus/src/client/exports/useBrokenLinks.ts
new file mode 100644
index 000000000000..979aa399cdb8
--- /dev/null
+++ b/packages/docusaurus/src/client/exports/useBrokenLinks.ts
@@ -0,0 +1,13 @@
+/**
+ * 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 {useBrokenLinksContext} from '../BrokenLinksContext';
+import type {BrokenLinks} from '@docusaurus/useBrokenLinks';
+
+export default function useBrokenLinks(): BrokenLinks {
+ return useBrokenLinksContext();
+}
diff --git a/packages/docusaurus/src/client/serverEntry.tsx b/packages/docusaurus/src/client/serverEntry.tsx
index 2d67558926e7..c01c4779e904 100644
--- a/packages/docusaurus/src/client/serverEntry.tsx
+++ b/packages/docusaurus/src/client/serverEntry.tsx
@@ -20,9 +20,9 @@ import {renderStaticApp} from './serverRenderer';
import preload from './preload';
import App from './App';
import {
- createStatefulLinksCollector,
- LinksCollectorProvider,
-} from './LinksCollector';
+ createStatefulBrokenLinks,
+ BrokenLinksProvider,
+} from './BrokenLinksContext';
import type {Locals} from '@slorber/static-site-generator-webpack-plugin';
const getCompiledSSRTemplate = _.memoize((template: string) =>
@@ -96,23 +96,27 @@ async function doRender(locals: Locals & {path: string}) {
const routerContext = {};
const helmetContext = {};
- const linksCollector = createStatefulLinksCollector();
+ const statefulBrokenLinks = createStatefulBrokenLinks();
const app = (
// @ts-expect-error: we are migrating away from react-loadable anyways
modules.add(moduleName)}>
-
+
-
+
);
const appHtml = await renderStaticApp(app);
- onLinksCollected(location, linksCollector.getCollectedLinks());
+ onLinksCollected({
+ staticPagePath: location,
+ anchors: statefulBrokenLinks.getCollectedAnchors(),
+ links: statefulBrokenLinks.getCollectedLinks(),
+ });
const {helmet} = helmetContext as FilledContext;
const htmlAttributes = helmet.htmlAttributes.toString();
diff --git a/packages/docusaurus/src/commands/build.ts b/packages/docusaurus/src/commands/build.ts
index 3b20912150ec..c0a38164092a 100644
--- a/packages/docusaurus/src/commands/build.ts
+++ b/packages/docusaurus/src/commands/build.ts
@@ -152,8 +152,8 @@ async function buildLocale({
generatedFilesDir,
plugins,
siteConfig: {
- baseUrl,
onBrokenLinks,
+ onBrokenAnchors,
staticDirectories: staticDirectoriesOption,
},
routes,
@@ -180,13 +180,15 @@ async function buildLocale({
},
);
- const allCollectedLinks: {[location: string]: string[]} = {};
+ const collectedLinks: {
+ [pathname: string]: {links: string[]; anchors: string[]};
+ } = {};
const headTags: {[location: string]: HelmetServerState} = {};
let serverConfig: Configuration = await createServerConfig({
props,
- onLinksCollected: (staticPagePath, links) => {
- allCollectedLinks[staticPagePath] = links;
+ onLinksCollected: ({staticPagePath, links, anchors}) => {
+ collectedLinks[staticPagePath] = {links, anchors};
},
onHeadTagsCollected: (staticPagePath, tags) => {
headTags[staticPagePath] = tags;
@@ -288,11 +290,10 @@ async function buildLocale({
);
await handleBrokenLinks({
- allCollectedLinks,
+ collectedLinks,
routes,
onBrokenLinks,
- outDir,
- baseUrl,
+ onBrokenAnchors,
});
logger.success`Generated static files in path=${path.relative(
diff --git a/packages/docusaurus/src/deps.d.ts b/packages/docusaurus/src/deps.d.ts
index 49bca18d06f9..199f39900970 100644
--- a/packages/docusaurus/src/deps.d.ts
+++ b/packages/docusaurus/src/deps.d.ts
@@ -42,7 +42,11 @@ declare module '@slorber/static-site-generator-webpack-plugin' {
headTags: string;
preBodyTags: string;
postBodyTags: string;
- onLinksCollected: (staticPagePath: string, links: string[]) => void;
+ onLinksCollected: (params: {
+ staticPagePath: string;
+ links: string[];
+ anchors: string[];
+ }) => void;
onHeadTagsCollected: (
staticPagePath: string,
tags: HelmetServerState,
diff --git a/packages/docusaurus/src/server/__tests__/__snapshots__/brokenLinks.test.ts.snap b/packages/docusaurus/src/server/__tests__/__snapshots__/brokenLinks.test.ts.snap
deleted file mode 100644
index 8aa3a3e837dc..000000000000
--- a/packages/docusaurus/src/server/__tests__/__snapshots__/brokenLinks.test.ts.snap
+++ /dev/null
@@ -1,86 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`handleBrokenLinks reports all broken links 1`] = `
-"Docusaurus found broken links!
-
-Please check the pages of your site in the list below, and make sure you don't reference any path that does not exist.
-Note: it's possible to ignore broken links with the 'onBrokenLinks' Docusaurus configuration, and let the build pass.
-
-Exhaustive list of all broken links found:
-
-- On source page path = /docs/good doc with space:
- -> linking to ./some%20other%20non-existent%20doc1 (resolved as: /docs/some%20other%20non-existent%20doc1)
- -> linking to ./break%2F..%2F..%2Fout2 (resolved as: /docs/break%2F..%2F..%2Fout2)
-
-- On source page path = /docs/goodDoc:
- -> linking to ../anotherGoodDoc#reported-because-of-bad-relative-path1 (resolved as: /anotherGoodDoc)
- -> linking to ./docThatDoesNotExist2 (resolved as: /docs/docThatDoesNotExist2)
- -> linking to ./badRelativeLink3 (resolved as: /docs/badRelativeLink3)
- -> linking to ../badRelativeLink4 (resolved as: /badRelativeLink4)
-
-- On source page path = /community:
- -> linking to /someNonExistentDoc1
- -> linking to /badLink2
- -> linking to ./badLink3 (resolved as: /badLink3)
-
-- On source page path = /page1:
- -> linking to /link1
- -> linking to /emptyFolder
-
-- On source page path = /page2:
- -> linking to /docs/link2
- -> linking to /emptyFolder/
- -> linking to /hey/link3
-"
-`;
-
-exports[`handleBrokenLinks reports frequent broken links 1`] = `
-"Docusaurus found broken links!
-
-Please check the pages of your site in the list below, and make sure you don't reference any path that does not exist.
-Note: it's possible to ignore broken links with the 'onBrokenLinks' Docusaurus configuration, and let the build pass.
-
-It looks like some of the broken links we found appear in many pages of your site.
-Maybe those broken links appear on all pages through your site layout?
-We recommend that you check your theme configuration for such links (particularly, theme navbar and footer).
-Frequent broken links are linking to:
-- /frequent
-- ./maybe-not
-
-Exhaustive list of all broken links found:
-
-- On source page path = /docs/good doc with space:
- -> linking to ./some%20other%20non-existent%20doc1 (resolved as: /docs/some%20other%20non-existent%20doc1)
- -> linking to ./break%2F..%2F..%2Fout2 (resolved as: /docs/break%2F..%2F..%2Fout2)
- -> linking to /frequent
- -> linking to ./maybe-not (resolved as: /docs/maybe-not)
-
-- On source page path = /docs/goodDoc:
- -> linking to ../anotherGoodDoc#reported-because-of-bad-relative-path1 (resolved as: /anotherGoodDoc)
- -> linking to ./docThatDoesNotExist2 (resolved as: /docs/docThatDoesNotExist2)
- -> linking to ./badRelativeLink3 (resolved as: /docs/badRelativeLink3)
- -> linking to ../badRelativeLink4 (resolved as: /badRelativeLink4)
- -> linking to /frequent
- -> linking to ./maybe-not (resolved as: /docs/maybe-not)
-
-- On source page path = /community:
- -> linking to /someNonExistentDoc1
- -> linking to /badLink2
- -> linking to ./badLink3 (resolved as: /badLink3)
- -> linking to /frequent
- -> linking to ./maybe-not (resolved as: /maybe-not)
-
-- On source page path = /page1:
- -> linking to /link1
- -> linking to /emptyFolder
- -> linking to /frequent
- -> linking to ./maybe-not (resolved as: /maybe-not)
-
-- On source page path = /page2:
- -> linking to /docs/link2
- -> linking to /emptyFolder/
- -> linking to /hey/link3
- -> linking to /frequent
- -> linking to ./maybe-not (resolved as: /maybe-not)
-"
-`;
diff --git a/packages/docusaurus/src/server/__tests__/__snapshots__/config.test.ts.snap b/packages/docusaurus/src/server/__tests__/__snapshots__/config.test.ts.snap
index 629e5b02fca2..c10c4833901e 100644
--- a/packages/docusaurus/src/server/__tests__/__snapshots__/config.test.ts.snap
+++ b/packages/docusaurus/src/server/__tests__/__snapshots__/config.test.ts.snap
@@ -29,6 +29,7 @@ exports[`loadSiteConfig website with .cjs siteConfig 1`] = `
"remarkRehypeOptions": undefined,
},
"noIndex": false,
+ "onBrokenAnchors": "warn",
"onBrokenLinks": "throw",
"onBrokenMarkdownLinks": "warn",
"onDuplicateRoutes": "warn",
@@ -79,6 +80,7 @@ exports[`loadSiteConfig website with ts + js config 1`] = `
"remarkRehypeOptions": undefined,
},
"noIndex": false,
+ "onBrokenAnchors": "warn",
"onBrokenLinks": "throw",
"onBrokenMarkdownLinks": "warn",
"onDuplicateRoutes": "warn",
@@ -129,6 +131,7 @@ exports[`loadSiteConfig website with valid JS CJS config 1`] = `
"remarkRehypeOptions": undefined,
},
"noIndex": false,
+ "onBrokenAnchors": "warn",
"onBrokenLinks": "throw",
"onBrokenMarkdownLinks": "warn",
"onDuplicateRoutes": "warn",
@@ -179,6 +182,7 @@ exports[`loadSiteConfig website with valid JS ESM config 1`] = `
"remarkRehypeOptions": undefined,
},
"noIndex": false,
+ "onBrokenAnchors": "warn",
"onBrokenLinks": "throw",
"onBrokenMarkdownLinks": "warn",
"onDuplicateRoutes": "warn",
@@ -229,6 +233,7 @@ exports[`loadSiteConfig website with valid TypeScript CJS config 1`] = `
"remarkRehypeOptions": undefined,
},
"noIndex": false,
+ "onBrokenAnchors": "warn",
"onBrokenLinks": "throw",
"onBrokenMarkdownLinks": "warn",
"onDuplicateRoutes": "warn",
@@ -279,6 +284,7 @@ exports[`loadSiteConfig website with valid TypeScript ESM config 1`] = `
"remarkRehypeOptions": undefined,
},
"noIndex": false,
+ "onBrokenAnchors": "warn",
"onBrokenLinks": "throw",
"onBrokenMarkdownLinks": "warn",
"onDuplicateRoutes": "warn",
@@ -329,6 +335,7 @@ exports[`loadSiteConfig website with valid async config 1`] = `
"remarkRehypeOptions": undefined,
},
"noIndex": false,
+ "onBrokenAnchors": "warn",
"onBrokenLinks": "throw",
"onBrokenMarkdownLinks": "warn",
"onDuplicateRoutes": "warn",
@@ -381,6 +388,7 @@ exports[`loadSiteConfig website with valid async config creator function 1`] = `
"remarkRehypeOptions": undefined,
},
"noIndex": false,
+ "onBrokenAnchors": "warn",
"onBrokenLinks": "throw",
"onBrokenMarkdownLinks": "warn",
"onDuplicateRoutes": "warn",
@@ -433,6 +441,7 @@ exports[`loadSiteConfig website with valid config creator function 1`] = `
"remarkRehypeOptions": undefined,
},
"noIndex": false,
+ "onBrokenAnchors": "warn",
"onBrokenLinks": "throw",
"onBrokenMarkdownLinks": "warn",
"onDuplicateRoutes": "warn",
@@ -488,6 +497,7 @@ exports[`loadSiteConfig website with valid siteConfig 1`] = `
"remarkRehypeOptions": undefined,
},
"noIndex": false,
+ "onBrokenAnchors": "warn",
"onBrokenLinks": "throw",
"onBrokenMarkdownLinks": "warn",
"onDuplicateRoutes": "warn",
diff --git a/packages/docusaurus/src/server/__tests__/__snapshots__/index.test.ts.snap b/packages/docusaurus/src/server/__tests__/__snapshots__/index.test.ts.snap
index d3b6b670cc27..cba9622267ad 100644
--- a/packages/docusaurus/src/server/__tests__/__snapshots__/index.test.ts.snap
+++ b/packages/docusaurus/src/server/__tests__/__snapshots__/index.test.ts.snap
@@ -103,6 +103,7 @@ exports[`load loads props for site with custom i18n path 1`] = `
"remarkRehypeOptions": undefined,
},
"noIndex": false,
+ "onBrokenAnchors": "warn",
"onBrokenLinks": "throw",
"onBrokenMarkdownLinks": "warn",
"onDuplicateRoutes": "warn",
diff --git a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts
index 40e76ed45b7a..158af9165af7 100644
--- a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts
+++ b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts
@@ -6,190 +6,608 @@
*/
import {jest} from '@jest/globals';
-import path from 'path';
-import _ from 'lodash';
import {handleBrokenLinks} from '../brokenLinks';
import type {RouteConfig} from '@docusaurus/types';
+type Params = Parameters[0];
+
+// We don't need all the routes attributes for our tests
+type SimpleRoute = {path: string; routes?: SimpleRoute[]};
+
+// Conveniently apply defaults to function under test
+async function testBrokenLinks(params: {
+ collectedLinks?: Params['collectedLinks'];
+ onBrokenLinks?: Params['onBrokenLinks'];
+ onBrokenAnchors?: Params['onBrokenAnchors'];
+ routes?: SimpleRoute[];
+}) {
+ await handleBrokenLinks({
+ collectedLinks: {},
+ onBrokenLinks: 'throw',
+ onBrokenAnchors: 'throw',
+ ...params,
+ // Unsafe but convenient for tests
+ routes: (params.routes ?? []) as RouteConfig[],
+ });
+}
+
describe('handleBrokenLinks', () => {
- const routes: RouteConfig[] = [
- {
- path: '/community',
- component: '',
- },
- {
- path: '/docs',
- component: '',
+ it('accepts valid link', async () => {
+ await testBrokenLinks({
+ routes: [{path: '/page1'}, {path: '/page2'}],
+ collectedLinks: {
+ '/page1': {links: ['/page2'], anchors: []},
+ '/page2': {links: [], anchors: []},
+ },
+ });
+ });
+
+ it('accepts valid link to uncollected page', async () => {
+ await testBrokenLinks({
+ routes: [{path: '/page1'}, {path: '/page2'}],
+ collectedLinks: {
+ '/page1': {links: ['/page2'], anchors: []},
+ // /page2 is absent on purpose: it doesn't contain any link/anchor
+ },
+ });
+ });
+
+ it('accepts valid link to nested route', async () => {
+ await testBrokenLinks({
routes: [
- {path: '/docs/goodDoc', component: ''},
- {path: '/docs/anotherGoodDoc', component: ''},
- {path: '/docs/good doc with space', component: ''},
- {path: '/docs/another good doc with space', component: ''},
- {path: '/docs/weird%20but%20good', component: ''},
- ],
- },
- {
- path: '*',
- component: '',
- },
- ];
-
- const link1 = '/link1';
- const link2 = '/docs/link2';
- const link3 = '/hey/link3';
-
- const linkToJavadoc1 = '/javadoc';
- const linkToJavadoc2 = '/javadoc/';
- const linkToJavadoc3 = '/javadoc/index.html';
- const linkToJavadoc4 = '/javadoc/index.html#foo';
-
- const linkToZipFile = '/files/file.zip';
- const linkToHtmlFile1 = '/files/hey.html';
- const linkToHtmlFile2 = '/files/hey';
-
- const linkToEmptyFolder1 = '/emptyFolder';
- const linkToEmptyFolder2 = '/emptyFolder/';
- const allCollectedLinks = {
- '/docs/good doc with space': [
- // Good - valid file with spaces in name
- './another%20good%20doc%20with%20space',
- // Good - valid file with percent-20 in its name
- './weird%20but%20good',
- // Bad - non-existent file with spaces in name
- './some%20other%20non-existent%20doc1',
- // Evil - trying to use ../../ but '/' won't get decoded
- // cSpell:ignore Fout
- './break%2F..%2F..%2Fout2',
- ],
- '/docs/goodDoc': [
- // Good links
- './anotherGoodDoc#someHash',
- '/docs/anotherGoodDoc?someQueryString=true#someHash',
- '../docs/anotherGoodDoc?someQueryString=true',
- '../docs/anotherGoodDoc#someHash',
- // Bad links
- '../anotherGoodDoc#reported-because-of-bad-relative-path1',
- './docThatDoesNotExist2',
- './badRelativeLink3',
- '../badRelativeLink4',
- ],
- '/community': [
- // Good links
- '/docs/goodDoc',
- '/docs/anotherGoodDoc#someHash',
- './docs/goodDoc#someHash',
- './docs/anotherGoodDoc',
- // Bad links
- '/someNonExistentDoc1',
- '/badLink2',
- './badLink3',
- ],
- '/page1': [
- link1,
- linkToHtmlFile1,
- linkToJavadoc1,
- linkToHtmlFile2,
- linkToJavadoc3,
- linkToJavadoc4,
- linkToEmptyFolder1, // Not filtered!
- ],
- '/page2': [
- link2,
- linkToEmptyFolder2, // Not filtered!
- linkToJavadoc2,
- link3,
- linkToJavadoc3,
- linkToZipFile,
- ],
- };
-
- const outDir = path.resolve(__dirname, '__fixtures__/brokenLinks/outDir');
-
- it('do not report anything for correct paths', async () => {
- const consoleMock = jest
- .spyOn(console, 'warn')
- .mockImplementation(() => {});
- const allCollectedCorrectLinks = {
- '/docs/good doc with space': [
- './another%20good%20doc%20with%20space',
- './weird%20but%20good',
- ],
- '/docs/goodDoc': [
- './anotherGoodDoc#someHash',
- '/docs/anotherGoodDoc?someQueryString=true#someHash',
- '../docs/anotherGoodDoc?someQueryString=true',
- '../docs/anotherGoodDoc#someHash',
- ],
- '/community': [
- '/docs/goodDoc',
- '/docs/anotherGoodDoc#someHash',
- './docs/goodDoc#someHash',
- './docs/anotherGoodDoc',
+ {path: '/page1'},
+ {path: '/nested/', routes: [{path: '/nested/page2'}]},
],
- '/page1': [
- linkToHtmlFile1,
- linkToJavadoc1,
- linkToHtmlFile2,
- linkToJavadoc3,
- linkToJavadoc4,
- ],
- };
- await handleBrokenLinks({
- allCollectedLinks: allCollectedCorrectLinks,
- onBrokenLinks: 'warn',
- routes,
- baseUrl: '/',
- outDir,
+ collectedLinks: {
+ '/page1': {links: ['/nested/page2'], anchors: []},
+ },
});
- expect(consoleMock).toHaveBeenCalledTimes(0);
});
- it('reports all broken links', async () => {
+ it('accepts valid relative link', async () => {
+ await testBrokenLinks({
+ routes: [{path: '/dir/page1'}, {path: '/dir/page2'}],
+ collectedLinks: {
+ '/dir/page1': {
+ links: ['./page2', '../dir/page2', '/dir/page2'],
+ anchors: [],
+ },
+ },
+ });
+ });
+
+ it('accepts valid link with anchor', async () => {
+ await testBrokenLinks({
+ routes: [{path: '/page1'}, {path: '/page2'}],
+ collectedLinks: {
+ '/page1': {links: ['/page2#page2anchor'], anchors: []},
+ '/page2': {links: [], anchors: ['page2anchor']},
+ },
+ });
+ });
+
+ it('accepts valid link with querystring + anchor', async () => {
+ await testBrokenLinks({
+ routes: [{path: '/page1'}, {path: '/page2'}],
+ collectedLinks: {
+ '/page1': {
+ links: ['/page2?age=42&theme=dark#page2anchor'],
+ anchors: [],
+ },
+ '/page2': {links: [], anchors: ['page2anchor']},
+ },
+ });
+ });
+
+ it('accepts valid link to self', async () => {
+ await testBrokenLinks({
+ routes: [{path: '/page1'}],
+ collectedLinks: {
+ '/page1': {
+ links: [
+ '/page1',
+ './page1',
+ '',
+ '/page1#anchor1',
+ '#anchor1',
+ '/page1?age=42#anchor1',
+ '?age=42#anchor1',
+ ],
+ anchors: ['anchor1'],
+ },
+ },
+ });
+ });
+
+ it('accepts valid link with spaces and encoding', async () => {
+ await testBrokenLinks({
+ routes: [{path: '/page 1'}, {path: '/page 2'}],
+ collectedLinks: {
+ '/page 1': {
+ links: [
+ '/page 1',
+ '/page%201',
+ '/page%201?age=42',
+ '/page 2',
+ '/page%202',
+ '/page%202?age=42',
+ '/page%202?age=42#page2anchor',
+ ],
+ anchors: [],
+ },
+ '/page 2': {links: [], anchors: ['page2anchor']},
+ },
+ });
+ });
+
+ it('rejects broken link', async () => {
await expect(() =>
- handleBrokenLinks({
- allCollectedLinks,
- onBrokenLinks: 'throw',
- routes,
- baseUrl: '/',
- outDir,
+ testBrokenLinks({
+ routes: [{path: '/page1'}, {path: '/page2'}],
+ collectedLinks: {
+ '/page1': {links: ['/brokenLink'], anchors: []},
+ },
+ }),
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`
+ "Docusaurus found broken links!
+
+ Please check the pages of your site in the list below, and make sure you don't reference any path that does not exist.
+ Note: it's possible to ignore broken links with the 'onBrokenLinks' Docusaurus configuration, and let the build pass.
+
+ Exhaustive list of all broken links found:
+ - Broken link on source page path = /page1:
+ -> linking to /brokenLink
+ "
+ `);
+ });
+
+ it('rejects broken link with anchor', async () => {
+ await expect(() =>
+ testBrokenLinks({
+ routes: [{path: '/page1'}, {path: '/page2'}],
+ collectedLinks: {
+ '/page1': {links: ['/brokenLink#anchor'], anchors: []},
+ },
+ }),
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`
+ "Docusaurus found broken links!
+
+ Please check the pages of your site in the list below, and make sure you don't reference any path that does not exist.
+ Note: it's possible to ignore broken links with the 'onBrokenLinks' Docusaurus configuration, and let the build pass.
+
+ Exhaustive list of all broken links found:
+ - Broken link on source page path = /page1:
+ -> linking to /brokenLink#anchor
+ "
+ `);
+ });
+
+ it('rejects broken link with querystring + anchor', async () => {
+ await expect(() =>
+ testBrokenLinks({
+ routes: [{path: '/page1'}, {path: '/page2'}],
+ collectedLinks: {
+ '/page1': {links: ['/brokenLink?age=42#anchor'], anchors: []},
+ },
+ }),
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`
+ "Docusaurus found broken links!
+
+ Please check the pages of your site in the list below, and make sure you don't reference any path that does not exist.
+ Note: it's possible to ignore broken links with the 'onBrokenLinks' Docusaurus configuration, and let the build pass.
+
+ Exhaustive list of all broken links found:
+ - Broken link on source page path = /page1:
+ -> linking to /brokenLink?age=42#anchor
+ "
+ `);
+ });
+
+ it('rejects valid link with broken anchor', async () => {
+ await expect(() =>
+ testBrokenLinks({
+ routes: [{path: '/page1'}, {path: '/page2'}],
+ collectedLinks: {
+ '/page1': {links: ['/page2#brokenAnchor'], 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#brokenAnchor
+ "
+ `);
+ });
+
+ 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({
+ routes: [{path: '/page1'}, {path: '/page2'}],
+ collectedLinks: {
+ '/page1': {
+ links: ['/page2?age=42&theme=dark#brokenAnchor'],
+ 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?age=42&theme=dark#brokenAnchor
+ "
+ `);
+ });
+
+ it('rejects valid link with broken anchor to self', async () => {
+ await expect(() =>
+ testBrokenLinks({
+ routes: [{path: '/page1'}],
+ collectedLinks: {
+ '/page1': {
+ links: [
+ '/page1',
+ '',
+ '#goodAnchor',
+ '/page1#goodAnchor',
+ '/page1?age=42#goodAnchor',
+ '#badAnchor1',
+ '/page1#badAnchor2',
+ '/page1?age=42#badAnchor3',
+ ],
+
+ anchors: ['goodAnchor'],
+ },
+ },
+ }),
+ ).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 #badAnchor1 (resolved as: /page1#badAnchor1)
+ -> linking to /page1#badAnchor2
+ -> linking to /page1?age=42#badAnchor3
+ "
+ `);
+ });
+
+ it('rejects valid link with broken anchor to uncollected page', async () => {
+ await expect(() =>
+ testBrokenLinks({
+ routes: [{path: '/page1'}, {path: '/page2'}],
+ collectedLinks: {
+ '/page1': {links: ['/page2#brokenAnchor'], anchors: []},
+ // /page2 is absent on purpose: it doesn't contain any link/anchor
+ },
}),
- ).rejects.toThrowErrorMatchingSnapshot();
+ ).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#brokenAnchor
+ "
+ `);
+ });
+
+ it('rejects broken anchor with query-string to uncollected page', async () => {
+ await expect(() =>
+ testBrokenLinks({
+ routes: [{path: '/page1'}, {path: '/page2'}],
+ collectedLinks: {
+ '/page1': {
+ links: ['/page2?age=42&theme=dark#brokenAnchor'],
+ anchors: [],
+ },
+ // /page2 is absent on purpose: it doesn't contain any link/anchor
+ },
+ }),
+ ).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?age=42&theme=dark#brokenAnchor
+ "
+ `);
});
- it('no-op for ignore', async () => {
- // In any case, _.mapValues will always be called, unless handleBrokenLinks
- // has already bailed
- const lodashMock = jest.spyOn(_, 'mapValues');
- await handleBrokenLinks({
- allCollectedLinks,
+ it('can ignore broken links', async () => {
+ await testBrokenLinks({
onBrokenLinks: 'ignore',
- routes,
- baseUrl: '/',
- outDir,
+ routes: [{path: '/page1'}],
+ collectedLinks: {
+ '/page1': {
+ links: ['/page2'],
+ anchors: [],
+ },
+ },
});
- expect(lodashMock).toHaveBeenCalledTimes(0);
- lodashMock.mockRestore();
- });
-
- it('reports frequent broken links', async () => {
- Object.values(allCollectedLinks).forEach((links) =>
- links.push(
- '/frequent',
- // This is in the gray area of what should be reported. Relative paths
- // may be resolved to different slugs on different locations. But if
- // this comes from a layout link, it should be reported anyways
- './maybe-not',
- ),
+ });
+
+ it('can ignore broken anchors', async () => {
+ await testBrokenLinks({
+ onBrokenAnchors: 'ignore',
+ routes: [{path: '/page1'}],
+ collectedLinks: {
+ '/page1': {
+ links: ['/page1#brokenAnchor'],
+ anchors: [],
+ },
+ },
+ });
+ });
+
+ it('can ignore broken anchors but report broken link', async () => {
+ await expect(() =>
+ testBrokenLinks({
+ onBrokenAnchors: 'ignore',
+ routes: [{path: '/page1'}],
+ collectedLinks: {
+ '/page1': {
+ links: ['/page1#brokenAnchor', '/page2'],
+ anchors: [],
+ },
+ },
+ }),
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`
+ "Docusaurus found broken links!
+
+ Please check the pages of your site in the list below, and make sure you don't reference any path that does not exist.
+ Note: it's possible to ignore broken links with the 'onBrokenLinks' Docusaurus configuration, and let the build pass.
+
+ Exhaustive list of all broken links found:
+ - Broken link on source page path = /page1:
+ -> linking to /page2
+ "
+ `);
+ });
+
+ it('can ignore broken link but report broken anchors', async () => {
+ await expect(() =>
+ testBrokenLinks({
+ onBrokenLinks: 'ignore',
+ routes: [{path: '/page1'}],
+ collectedLinks: {
+ '/page1': {
+ links: [
+ '/page2',
+ '/page1#brokenAnchor1',
+ '/page1#brokenAnchor2',
+ '#brokenAnchor3',
+ ],
+
+ 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 /page1#brokenAnchor1
+ -> linking to /page1#brokenAnchor2
+ -> linking to #brokenAnchor3 (resolved as: /page1#brokenAnchor3)
+ "
+ `);
+ });
+
+ it('can warn for broken links', async () => {
+ const warnMock = jest.spyOn(console, 'warn');
+
+ await testBrokenLinks({
+ onBrokenLinks: 'warn',
+ routes: [{path: '/page1'}],
+ collectedLinks: {
+ '/page1': {
+ links: ['/page2'],
+ anchors: [],
+ },
+ },
+ });
+
+ expect(warnMock).toHaveBeenCalledTimes(1);
+ expect(warnMock.mock.calls).toMatchInlineSnapshot(`
+ [
+ [
+ "[WARNING] Docusaurus found broken links!
+
+ Please check the pages of your site in the list below, and make sure you don't reference any path that does not exist.
+ Note: it's possible to ignore broken links with the 'onBrokenLinks' Docusaurus configuration, and let the build pass.
+
+ Exhaustive list of all broken links found:
+ - Broken link on source page path = /page1:
+ -> linking to /page2
+ ",
+ ],
+ ]
+ `);
+ warnMock.mockRestore();
+ });
+
+ it('can warn for broken anchors', async () => {
+ const warnMock = jest.spyOn(console, 'warn');
+
+ await testBrokenLinks({
+ onBrokenAnchors: 'warn',
+ routes: [{path: '/page1'}],
+ collectedLinks: {
+ '/page1': {
+ links: ['/page1#brokenAnchor'],
+ anchors: [],
+ },
+ },
+ });
+
+ expect(warnMock).toHaveBeenCalledTimes(1);
+ expect(warnMock.mock.calls).toMatchInlineSnapshot(`
+ [
+ [
+ "[WARNING] 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 /page1#brokenAnchor
+ ",
+ ],
+ ]
+ `);
+ warnMock.mockRestore();
+ });
+
+ it('can warn for both broken links and anchors', async () => {
+ const warnMock = jest.spyOn(console, 'warn');
+
+ await testBrokenLinks({
+ onBrokenLinks: 'warn',
+ onBrokenAnchors: 'warn',
+ routes: [{path: '/page1'}],
+ collectedLinks: {
+ '/page1': {
+ links: ['/page1#brokenAnchor', '/page2'],
+ anchors: [],
+ },
+ },
+ });
+
+ expect(warnMock).toHaveBeenCalledTimes(2);
+ expect(warnMock.mock.calls).toMatchInlineSnapshot(`
+ [
+ [
+ "[WARNING] Docusaurus found broken links!
+
+ Please check the pages of your site in the list below, and make sure you don't reference any path that does not exist.
+ Note: it's possible to ignore broken links with the 'onBrokenLinks' Docusaurus configuration, and let the build pass.
+
+ Exhaustive list of all broken links found:
+ - Broken link on source page path = /page1:
+ -> linking to /page2
+ ",
+ ],
+ [
+ "[WARNING] 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 /page1#brokenAnchor
+ ",
+ ],
+ ]
+ `);
+ warnMock.mockRestore();
+ });
+
+ it('reports frequent broken links differently', async () => {
+ const pagePaths = [
+ '/page1',
+ '/page2',
+ '/dir/page3',
+ '/dir/page4',
+ '/dir/page5',
+ ];
+
+ const routes: SimpleRoute[] = pagePaths.map((pagePath) => ({
+ path: pagePath,
+ }));
+
+ const collectedLinks: Params['collectedLinks'] = Object.fromEntries(
+ pagePaths.map((pagePath) => [
+ pagePath,
+ {
+ links: ['/frequentBrokenLink', './relativeFrequentBrokenLink'],
+ anchors: [],
+ },
+ ]),
);
await expect(() =>
- handleBrokenLinks({
- allCollectedLinks,
- onBrokenLinks: 'throw',
+ testBrokenLinks({
routes,
- baseUrl: '/',
- outDir,
+ collectedLinks,
}),
- ).rejects.toThrowErrorMatchingSnapshot();
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`
+ "Docusaurus found broken links!
+
+ Please check the pages of your site in the list below, and make sure you don't reference any path that does not exist.
+ Note: it's possible to ignore broken links with the 'onBrokenLinks' Docusaurus configuration, and let the build pass.
+
+ It looks like some of the broken links we found appear in many pages of your site.
+ Maybe those broken links appear on all pages through your site layout?
+ We recommend that you check your theme configuration for such links (particularly, theme navbar and footer).
+ Frequent broken links are linking to:
+ - /frequentBrokenLink
+ - ./relativeFrequentBrokenLink
+
+ Exhaustive list of all broken links found:
+ - Broken link on source page path = /page1:
+ -> linking to /frequentBrokenLink
+ -> linking to ./relativeFrequentBrokenLink (resolved as: /relativeFrequentBrokenLink)
+ - Broken link on source page path = /page2:
+ -> linking to /frequentBrokenLink
+ -> linking to ./relativeFrequentBrokenLink (resolved as: /relativeFrequentBrokenLink)
+ - Broken link on source page path = /dir/page3:
+ -> linking to /frequentBrokenLink
+ -> linking to ./relativeFrequentBrokenLink (resolved as: /dir/relativeFrequentBrokenLink)
+ - Broken link on source page path = /dir/page4:
+ -> linking to /frequentBrokenLink
+ -> linking to ./relativeFrequentBrokenLink (resolved as: /dir/relativeFrequentBrokenLink)
+ - Broken link on source page path = /dir/page5:
+ -> linking to /frequentBrokenLink
+ -> linking to ./relativeFrequentBrokenLink (resolved as: /dir/relativeFrequentBrokenLink)
+ "
+ `);
});
});
diff --git a/packages/docusaurus/src/server/brokenLinks.ts b/packages/docusaurus/src/server/brokenLinks.ts
index f443a4659cf9..ccbaadcd3ffb 100644
--- a/packages/docusaurus/src/server/brokenLinks.ts
+++ b/packages/docusaurus/src/server/brokenLinks.ts
@@ -5,45 +5,42 @@
* LICENSE file in the root directory of this source tree.
*/
-import fs from 'fs-extra';
-import path from 'path';
import _ from 'lodash';
import logger from '@docusaurus/logger';
-import combinePromises from 'combine-promises';
import {matchRoutes} from 'react-router-config';
-import {removePrefix, removeSuffix, resolvePathname} from '@docusaurus/utils';
+import {parseURLPath, serializeURLPath, type URLPath} from '@docusaurus/utils';
import {getAllFinalRoutes} from './utils';
import type {RouteConfig, ReportingSeverity} from '@docusaurus/types';
type BrokenLink = {
link: string;
resolvedLink: string;
+ anchor: boolean;
};
-// matchRoutes does not support qs/anchors, so we remove it!
-function onlyPathname(link: string) {
- return link.split('#')[0]!.split('?')[0]!;
-}
+type BrokenLinksMap = {[pathname: string]: BrokenLink[]};
+
+// The linking data that has been collected on Docusaurus pages during SSG
+// {rendered page pathname => links and anchors collected on that page}
+type CollectedLinks = {
+ [pathname: string]: {links: string[]; anchors: string[]};
+};
-function getPageBrokenLinks({
+function getBrokenLinksForPage({
+ collectedLinks,
pagePath,
pageLinks,
routes,
}: {
+ collectedLinks: CollectedLinks;
pagePath: string;
pageLinks: string[];
+ pageAnchors: string[];
routes: RouteConfig[];
}): BrokenLink[] {
- // ReactRouter is able to support links like ./../somePath but `matchRoutes`
- // does not do this resolution internally. We must resolve the links before
- // using `matchRoutes`. `resolvePathname` is used internally by React Router
- function resolveLink(link: string) {
- const resolvedLink = resolvePathname(onlyPathname(link), pagePath);
- return {link, resolvedLink};
- }
-
- function isBrokenLink(link: string) {
- const matchedRoutes = [link, decodeURI(link)]
+ // console.log('routes:', routes);
+ function isPathBrokenLink(linkPath: URLPath) {
+ const matchedRoutes = [linkPath.pathname, decodeURI(linkPath.pathname)]
// @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.
@@ -52,7 +49,52 @@ function getPageBrokenLinks({
return matchedRoutes.length === 0;
}
- return pageLinks.map(resolveLink).filter((l) => isBrokenLink(l.resolvedLink));
+ function isAnchorBrokenLink(linkPath: URLPath) {
+ const {pathname, hash} = linkPath;
+
+ // Link has no hash: it can't be a broken anchor link
+ if (hash === undefined) {
+ return false;
+ }
+
+ const targetPage =
+ collectedLinks[pathname] || collectedLinks[decodeURI(pathname)];
+
+ // link with anchor to a page that does not exist (or did not collect any
+ // link/anchor) is considered as a broken anchor
+ if (!targetPage) {
+ return true;
+ }
+
+ // 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 brokenLinks = pageLinks.flatMap((link) => {
+ const linkPath = parseURLPath(link, pagePath);
+ if (isPathBrokenLink(linkPath)) {
+ return [
+ {
+ link,
+ resolvedLink: serializeURLPath(linkPath),
+ anchor: false,
+ },
+ ];
+ }
+ if (isAnchorBrokenLink(linkPath)) {
+ return [
+ {
+ link,
+ resolvedLink: serializeURLPath(linkPath),
+ anchor: true,
+ },
+ ];
+ }
+ return [];
+ });
+
+ return brokenLinks;
}
/**
@@ -66,45 +108,76 @@ function filterIntermediateRoutes(routesInput: RouteConfig[]): RouteConfig[] {
return getAllFinalRoutes(routesWithout404);
}
-function getAllBrokenLinks({
- allCollectedLinks,
+function getBrokenLinks({
+ collectedLinks,
routes,
}: {
- allCollectedLinks: {[location: string]: string[]};
+ collectedLinks: CollectedLinks;
routes: RouteConfig[];
-}): {[location: string]: BrokenLink[]} {
+}): BrokenLinksMap {
const filteredRoutes = filterIntermediateRoutes(routes);
- const allBrokenLinks = _.mapValues(allCollectedLinks, (pageLinks, pagePath) =>
- getPageBrokenLinks({pageLinks, pagePath, routes: filteredRoutes}),
+ return _.mapValues(collectedLinks, (pageCollectedData, pagePath) =>
+ getBrokenLinksForPage({
+ collectedLinks,
+ pageLinks: pageCollectedData.links,
+ pageAnchors: pageCollectedData.anchors,
+ pagePath,
+ routes: filteredRoutes,
+ }),
);
+}
+
+function brokenLinkMessage(brokenLink: BrokenLink): string {
+ const showResolvedLink = brokenLink.link !== brokenLink.resolvedLink;
+ return `${brokenLink.link}${
+ showResolvedLink ? ` (resolved as: ${brokenLink.resolvedLink})` : ''
+ }`;
+}
+
+function createBrokenLinksMessage(
+ pagePath: string,
+ brokenLinks: BrokenLink[],
+): string {
+ const type = brokenLinks[0]?.anchor === true ? 'anchor' : 'link';
- return _.pickBy(allBrokenLinks, (brokenLinks) => brokenLinks.length > 0);
+ const anchorMessage =
+ brokenLinks.length > 0
+ ? `- Broken ${type} on source page path = ${pagePath}:
+ -> linking to ${brokenLinks
+ .map(brokenLinkMessage)
+ .join('\n -> linking to ')}`
+ : '';
+
+ return `${anchorMessage}`;
}
-function getBrokenLinksErrorMessage(allBrokenLinks: {
- [location: string]: BrokenLink[];
-}): string | undefined {
- if (Object.keys(allBrokenLinks).length === 0) {
+function createBrokenAnchorsMessage(
+ brokenAnchors: BrokenLinksMap,
+): string | undefined {
+ if (Object.keys(brokenAnchors).length === 0) {
return undefined;
}
- function brokenLinkMessage(brokenLink: BrokenLink): string {
- const showResolvedLink = brokenLink.link !== brokenLink.resolvedLink;
- return `${brokenLink.link}${
- showResolvedLink ? ` (resolved as: ${brokenLink.resolvedLink})` : ''
- }`;
- }
+ return `Docusaurus found broken anchors!
- function pageBrokenLinksMessage(
- pagePath: string,
- brokenLinks: BrokenLink[],
- ): string {
- return `
-- On source page path = ${pagePath}:
- -> linking to ${brokenLinks
- .map(brokenLinkMessage)
- .join('\n -> linking to ')}`;
+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:
+${Object.entries(brokenAnchors)
+ .map(([pagePath, brokenLinks]) =>
+ createBrokenLinksMessage(pagePath, brokenLinks),
+ )
+ .join('\n')}
+`;
+}
+
+function createBrokenPathsMessage(
+ brokenPathsMap: BrokenLinksMap,
+): string | undefined {
+ if (Object.keys(brokenPathsMap).length === 0) {
+ return undefined;
}
/**
@@ -113,7 +186,7 @@ function getBrokenLinksErrorMessage(allBrokenLinks: {
* this out. See https://github.com/facebook/docusaurus/issues/3567#issuecomment-706973805
*/
function getLayoutBrokenLinksHelpMessage() {
- const flatList = Object.entries(allBrokenLinks).flatMap(
+ const flatList = Object.entries(brokenPathsMap).flatMap(
([pagePage, brokenLinks]) =>
brokenLinks.map((brokenLink) => ({pagePage, brokenLink})),
);
@@ -146,102 +219,78 @@ Please check the pages of your site in the list below, and make sure you don't r
Note: it's possible to ignore broken links with the 'onBrokenLinks' Docusaurus configuration, and let the build pass.${getLayoutBrokenLinksHelpMessage()}
Exhaustive list of all broken links found:
-${Object.entries(allBrokenLinks)
- .map(([pagePath, brokenLinks]) =>
- pageBrokenLinksMessage(pagePath, brokenLinks),
+${Object.entries(brokenPathsMap)
+ .map(([pagePath, brokenPaths]) =>
+ createBrokenLinksMessage(pagePath, brokenPaths),
)
.join('\n')}
`;
}
-async function isExistingFile(filePath: string) {
- try {
- return (await fs.stat(filePath)).isFile();
- } catch {
- return false;
- }
-}
+function splitBrokenLinks(brokenLinks: BrokenLinksMap): {
+ brokenPaths: BrokenLinksMap;
+ brokenAnchors: BrokenLinksMap;
+} {
+ const brokenPaths: BrokenLinksMap = {};
+ const brokenAnchors: BrokenLinksMap = {};
-// If a file actually exist on the file system, we know the link is valid
-// even if docusaurus does not know about this file, so we don't report it
-async function filterExistingFileLinks({
- baseUrl,
- outDir,
- allCollectedLinks,
-}: {
- baseUrl: string;
- outDir: string;
- allCollectedLinks: {[location: string]: string[]};
-}): Promise<{[location: string]: string[]}> {
- async function linkFileExists(link: string) {
- // /baseUrl/javadoc/ -> /outDir/javadoc
- const baseFilePath = onlyPathname(
- removeSuffix(`${outDir}/${removePrefix(link, baseUrl)}`, '/'),
+ Object.entries(brokenLinks).forEach(([pathname, pageBrokenLinks]) => {
+ const [anchorBrokenLinks, pathBrokenLinks] = _.partition(
+ pageBrokenLinks,
+ (link) => link.anchor,
);
- // -> /outDir/javadoc
- // -> /outDir/javadoc.html
- // -> /outDir/javadoc/index.html
- const filePathsToTry: string[] = [baseFilePath];
- if (!path.extname(baseFilePath)) {
- filePathsToTry.push(
- `${baseFilePath}.html`,
- path.join(baseFilePath, 'index.html'),
- );
+ if (pathBrokenLinks.length > 0) {
+ brokenPaths[pathname] = pathBrokenLinks;
}
-
- for (const file of filePathsToTry) {
- if (await isExistingFile(file)) {
- return true;
- }
+ if (anchorBrokenLinks.length > 0) {
+ brokenAnchors[pathname] = anchorBrokenLinks;
}
- return false;
+ });
+
+ return {brokenPaths, brokenAnchors};
+}
+
+function reportBrokenLinks({
+ brokenLinks,
+ onBrokenLinks,
+ onBrokenAnchors,
+}: {
+ brokenLinks: BrokenLinksMap;
+ onBrokenLinks: ReportingSeverity;
+ onBrokenAnchors: ReportingSeverity;
+}) {
+ // We need to split the broken links reporting in 2 for better granularity
+ // This is because we need to report broken path/anchors independently
+ // For v3.x retro-compatibility, we can't throw by default for broken anchors
+ // TODO Docusaurus v4: make onBrokenAnchors throw by default?
+ const {brokenPaths, brokenAnchors} = splitBrokenLinks(brokenLinks);
+
+ const pathErrorMessage = createBrokenPathsMessage(brokenPaths);
+ if (pathErrorMessage) {
+ logger.report(onBrokenLinks)(pathErrorMessage);
}
- return combinePromises(
- _.mapValues(allCollectedLinks, async (links) =>
- (
- await Promise.all(
- links.map(async (link) => ((await linkFileExists(link)) ? '' : link)),
- )
- ).filter(Boolean),
- ),
- );
+ const anchorErrorMessage = createBrokenAnchorsMessage(brokenAnchors);
+ if (anchorErrorMessage) {
+ logger.report(onBrokenAnchors)(anchorErrorMessage);
+ }
}
export async function handleBrokenLinks({
- allCollectedLinks,
+ collectedLinks,
onBrokenLinks,
+ onBrokenAnchors,
routes,
- baseUrl,
- outDir,
}: {
- allCollectedLinks: {[location: string]: string[]};
+ collectedLinks: CollectedLinks;
onBrokenLinks: ReportingSeverity;
+ onBrokenAnchors: ReportingSeverity;
routes: RouteConfig[];
- baseUrl: string;
- outDir: string;
}): Promise {
- if (onBrokenLinks === 'ignore') {
+ if (onBrokenLinks === 'ignore' && onBrokenAnchors === 'ignore') {
return;
}
-
- // If we link to a file like /myFile.zip, and the file actually exist for the
- // file system. It is not a broken link, it may simply be a link to an
- // existing static file...
- const allCollectedLinksFiltered = await filterExistingFileLinks({
- allCollectedLinks,
- baseUrl,
- outDir,
- });
-
- const allBrokenLinks = getAllBrokenLinks({
- allCollectedLinks: allCollectedLinksFiltered,
- routes,
- });
-
- const errorMessage = getBrokenLinksErrorMessage(allBrokenLinks);
- if (errorMessage) {
- logger.report(onBrokenLinks)(errorMessage);
- }
+ const brokenLinks = getBrokenLinks({routes, collectedLinks});
+ reportBrokenLinks({brokenLinks, onBrokenLinks, onBrokenAnchors});
}
diff --git a/packages/docusaurus/src/server/configValidation.ts b/packages/docusaurus/src/server/configValidation.ts
index c020a09cae9f..eafabc3cedad 100644
--- a/packages/docusaurus/src/server/configValidation.ts
+++ b/packages/docusaurus/src/server/configValidation.ts
@@ -46,6 +46,7 @@ export const DEFAULT_CONFIG: Pick<
DocusaurusConfig,
| 'i18n'
| 'onBrokenLinks'
+ | 'onBrokenAnchors'
| 'onBrokenMarkdownLinks'
| 'onDuplicateRoutes'
| 'plugins'
@@ -66,6 +67,7 @@ export const DEFAULT_CONFIG: Pick<
> = {
i18n: DEFAULT_I18N_CONFIG,
onBrokenLinks: 'throw',
+ onBrokenAnchors: 'warn', // TODO Docusaurus v4: change to throw
onBrokenMarkdownLinks: 'warn',
onDuplicateRoutes: 'warn',
plugins: [],
@@ -211,6 +213,9 @@ export const ConfigSchema = Joi.object({
onBrokenLinks: Joi.string()
.equal('ignore', 'log', 'warn', 'throw')
.default(DEFAULT_CONFIG.onBrokenLinks),
+ onBrokenAnchors: Joi.string()
+ .equal('ignore', 'log', 'warn', 'throw')
+ .default(DEFAULT_CONFIG.onBrokenAnchors),
onBrokenMarkdownLinks: Joi.string()
.equal('ignore', 'log', 'warn', 'throw')
.default(DEFAULT_CONFIG.onBrokenMarkdownLinks),
diff --git a/packages/docusaurus/src/webpack/__tests__/__snapshots__/base.test.ts.snap b/packages/docusaurus/src/webpack/__tests__/__snapshots__/base.test.ts.snap
index 9567720d587f..7299deaf977a 100644
--- a/packages/docusaurus/src/webpack/__tests__/__snapshots__/base.test.ts.snap
+++ b/packages/docusaurus/src/webpack/__tests__/__snapshots__/base.test.ts.snap
@@ -16,6 +16,7 @@ exports[`base webpack config creates webpack aliases 1`] = `
"@docusaurus/renderRoutes": "../../../../client/exports/renderRoutes.ts",
"@docusaurus/router": "../../../../client/exports/router.ts",
"@docusaurus/useBaseUrl": "../../../../client/exports/useBaseUrl.ts",
+ "@docusaurus/useBrokenLinks": "../../../../client/exports/useBrokenLinks.ts",
"@docusaurus/useDocusaurusContext": "../../../../client/exports/useDocusaurusContext.ts",
"@docusaurus/useGlobalData": "../../../../client/exports/useGlobalData.ts",
"@docusaurus/useIsBrowser": "../../../../client/exports/useIsBrowser.ts",
diff --git a/packages/docusaurus/src/webpack/aliases/__tests__/__snapshots__/index.test.ts.snap b/packages/docusaurus/src/webpack/aliases/__tests__/__snapshots__/index.test.ts.snap
index c9738c847d56..46390d21c92d 100644
--- a/packages/docusaurus/src/webpack/aliases/__tests__/__snapshots__/index.test.ts.snap
+++ b/packages/docusaurus/src/webpack/aliases/__tests__/__snapshots__/index.test.ts.snap
@@ -16,6 +16,7 @@ exports[`getDocusaurusAliases returns appropriate webpack aliases 1`] = `
"@docusaurus/renderRoutes": "/packages/docusaurus/src/client/exports/renderRoutes.ts",
"@docusaurus/router": "/packages/docusaurus/src/client/exports/router.ts",
"@docusaurus/useBaseUrl": "/packages/docusaurus/src/client/exports/useBaseUrl.ts",
+ "@docusaurus/useBrokenLinks": "/packages/docusaurus/src/client/exports/useBrokenLinks.ts",
"@docusaurus/useDocusaurusContext": "/packages/docusaurus/src/client/exports/useDocusaurusContext.ts",
"@docusaurus/useGlobalData": "/packages/docusaurus/src/client/exports/useGlobalData.ts",
"@docusaurus/useIsBrowser": "/packages/docusaurus/src/client/exports/useIsBrowser.ts",
diff --git a/website/docs/advanced/routing.mdx b/website/docs/advanced/routing.mdx
index 5bf943072ba6..e9e96d4892c7 100644
--- a/website/docs/advanced/routing.mdx
+++ b/website/docs/advanced/routing.mdx
@@ -265,26 +265,18 @@ export function PageRoute() {
Docusaurus builds a [single-page application](https://developer.mozilla.org/en-US/docs/Glossary/SPA), where route transitions are done through the `history.push()` method of React router. This operation is done on the client side. However, the prerequisite for a route transition to happen this way is that the target URL is known to our router. Otherwise, the router catches this path and displays a 404 page instead.
-If you put some HTML pages under the `static` folder, they will be copied to the build output and therefore become accessible as part of your website, yet it's not part of the Docusaurus route system. We provide a `pathname://` protocol that allows you to redirect to another part of your domain in a non-SPA fashion, as if this route is an external link. Try the following two links:
+If you put some HTML pages under the `static` folder, they will be copied to the build output and therefore become accessible as part of your website, yet it's not part of the Docusaurus route system. We provide a `pathname://` protocol that allows you to redirect to another part of your domain in a non-SPA fashion, as if this route is an external link.
```md
-- [/pure-html](/pure-html)
- [pathname:///pure-html](pathname:///pure-html)
```
-- [`/pure-html`](/pure-html)
- [`pathname:///pure-html`](pathname:///pure-html)
-:::tip
-
-The first link will **not** trigger a "broken links detected" check during the production build, because the respective file actually exists. Nevertheless, when you click on the link, a "page not found" will be displayed until you refresh.
-
-:::
-
The `pathname://` protocol is useful for referencing any content in the static folder. For example, Docusaurus would convert [all Markdown static assets to require() calls](../guides/markdown-features/markdown-features-assets.mdx#static-assets). You can use `pathname://` to keep it a regular link instead of being hashed by Webpack.
```md title="my-doc.md"
diff --git a/website/docs/api/docusaurus.config.js.mdx b/website/docs/api/docusaurus.config.js.mdx
index 47afd62fde98..ee481288f706 100644
--- a/website/docs/api/docusaurus.config.js.mdx
+++ b/website/docs/api/docusaurus.config.js.mdx
@@ -190,7 +190,7 @@ export default {
The behavior of Docusaurus when it detects any broken link.
-By default, it throws an error, to ensure you never ship any broken link, but you can lower this security if needed.
+By default, it throws an error, to ensure you never ship any broken link.
:::note
@@ -198,13 +198,21 @@ The broken links detection is only available for a production build (`docusaurus
:::
+### `onBrokenAnchors` {#onBrokenAnchors}
+
+- Type: `'ignore' | 'log' | 'warn' | 'throw'`
+
+The behavior of Docusaurus when it detects any broken anchor declared with the `Heading` component of Docusaurus.
+
+By default, it prints a warning, to let you know about your broken anchors.
+
### `onBrokenMarkdownLinks` {#onBrokenMarkdownLinks}
- Type: `'ignore' | 'log' | 'warn' | 'throw'`
The behavior of Docusaurus when it detects any broken Markdown link.
-By default, it prints a warning, to let you know about your broken Markdown link, but you can change this security if needed.
+By default, it prints a warning, to let you know about your broken Markdown link.
### `onDuplicateRoutes` {#onDuplicateRoutes}
diff --git a/website/docs/api/plugins/plugin-content-docs.mdx b/website/docs/api/plugins/plugin-content-docs.mdx
index d9238f65bb36..754a56d293d9 100644
--- a/website/docs/api/plugins/plugin-content-docs.mdx
+++ b/website/docs/api/plugins/plugin-content-docs.mdx
@@ -42,10 +42,10 @@ Accepted fields:
| `include` | `string[]` | `['**/*.{md,mdx}']` | Array of glob patterns matching Markdown files to be built, relative to the content path. |
| `exclude` | `string[]` | _See example configuration_ | Array of glob patterns matching Markdown files to be excluded. Serves as refinement based on the `include` option. |
| `sidebarPath` | false \| string | `undefined` | Path to sidebar configuration. Use `false` to disable sidebars, or `undefined` to create a fully autogenerated sidebar. |
-| `sidebarCollapsible` | `boolean` | `true` | Whether sidebar categories are collapsible by default. See also [Collapsible categories](/docs/sidebar#collapsible-categories) |
-| `sidebarCollapsed` | `boolean` | `true` | Whether sidebar categories are collapsed by default. See also [Expanded categories by default](/docs/sidebar#expanded-categories-by-default) |
-| `sidebarItemsGenerator` | SidebarGenerator | _Omitted_ | Function used to replace the sidebar items of type `'autogenerated'` with real sidebar items (docs, categories, links...). See also [Customize the sidebar items generator](/docs/sidebar#customize-the-sidebar-items-generator) |
-| `numberPrefixParser` | boolean \|PrefixParser | _Omitted_ | Custom parsing logic to extract number prefixes from file names. Use `false` to disable this behavior and leave the docs untouched, and `true` to use the default parser. See also [Using number prefixes](/docs/sidebar#using-number-prefixes) |
+| `sidebarCollapsible` | `boolean` | `true` | Whether sidebar categories are collapsible by default. See also [Collapsible categories](/docs/sidebar/items#collapsible-categories) |
+| `sidebarCollapsed` | `boolean` | `true` | Whether sidebar categories are collapsed by default. See also [Expanded categories by default](/docs/sidebar/items#expanded-categories-by-default) |
+| `sidebarItemsGenerator` | SidebarGenerator | _Omitted_ | Function used to replace the sidebar items of type `'autogenerated'` with real sidebar items (docs, categories, links...). See also [Customize the sidebar items generator](/docs/sidebar/autogenerated#customize-the-sidebar-items-generator) |
+| `numberPrefixParser` | boolean \|PrefixParser | _Omitted_ | Custom parsing logic to extract number prefixes from file names. Use `false` to disable this behavior and leave the docs untouched, and `true` to use the default parser. See also [Using number prefixes](/docs/sidebar/autogenerated#using-number-prefixes) |
| `docsRootComponent` | `string` | `'@theme/DocsRoot'` | Parent component of all the docs plugin pages (including all versions). Stays mounted when navigation between docs pages and versions. |
| `docVersionRootComponent` | `string` | `'@theme/DocVersionLayout'` | Parent component of all docs pages of an individual version (doc pages with sidebars, tags pages). Stays mounted when navigation between pages of that specific version. |
| `docRootComponent` | `string` | `'@theme/DocPage'` | Parent component of all doc pages with sidebars (regular docs pages, category generated index pages). Stays mounted when navigation between such pages. |
@@ -275,7 +275,7 @@ Accepted fields:
| `title` | `string` | Markdown title or `id` | The text title of your document. Used for the page metadata and as a fallback value in multiple places (sidebar, next/previous buttons...). Automatically added at the top of your doc if it does not contain any Markdown title. |
| `pagination_label` | `string` | `sidebar_label` or `title` | The text used in the document next/previous buttons for this document. |
| `sidebar_label` | `string` | `title` | The text shown in the document sidebar for this document. |
-| `sidebar_position` | `number` | Default ordering | Controls the position of a doc inside the generated sidebar slice when using `autogenerated` sidebar items. See also [Autogenerated sidebar metadata](/docs/sidebar#autogenerated-sidebar-metadata). |
+| `sidebar_position` | `number` | Default ordering | Controls the position of a doc inside the generated sidebar slice when using `autogenerated` sidebar items. See also [Autogenerated sidebar metadata](/docs/sidebar/autogenerated#autogenerated-sidebar-metadata). |
| `sidebar_class_name` | `string` | `undefined` | Gives the corresponding sidebar label a special class name when using autogenerated sidebars. |
| `sidebar_custom_props` | `object` | `undefined` | Assign [custom props](../../guides/docs/sidebar/index.mdx#passing-custom-props) to the sidebar item referencing this doc |
| `displayed_sidebar` | `string` | `undefined` | Force the display of a given sidebar when browsing the current document. Read the [multiple sidebars guide](../../guides/docs/sidebar/multiple-sidebars.mdx) for details. |
@@ -285,7 +285,7 @@ Accepted fields:
| `toc_max_heading_level` | `number` | `3` | The max heading level shown in the table of contents. Must be between 2 and 6. |
| `pagination_next` | string \| null | Next doc in the sidebar | The ID of the documentation you want the "Next" pagination to link to. Use `null` to disable showing "Next" for this page. |
| `pagination_prev` | string \| null | Previous doc in the sidebar | The ID of the documentation you want the "Previous" pagination to link to. Use `null` to disable showing "Previous" for this page. |
-| `parse_number_prefixes` | `boolean` | `numberPrefixParser` plugin option | Whether number prefix parsing is disabled on this doc. See also [Using number prefixes](/docs/sidebar#using-number-prefixes). |
+| `parse_number_prefixes` | `boolean` | `numberPrefixParser` plugin option | Whether number prefix parsing is disabled on this doc. See also [Using number prefixes](/docs/sidebar/autogenerated#using-number-prefixes). |
| `custom_edit_url` | string \| null | Computed using the `editUrl` plugin option | The URL for editing this document. Use `null` to disable showing "Edit this page" for this page. |
| `keywords` | `string[]` | `undefined` | Keywords meta tag for the document page, for search engines. |
| `description` | `string` | The first line of Markdown content | The description of your document, which will become the `` and `` in ``, used by search engines. |
diff --git a/website/docs/cli.mdx b/website/docs/cli.mdx
index 0b38c01776f7..bb1f32c91d68 100644
--- a/website/docs/cli.mdx
+++ b/website/docs/cli.mdx
@@ -177,7 +177,7 @@ By default, the files are written in `website/i18n//...`.
### `docusaurus write-heading-ids [siteDir] [files]` {#docusaurus-write-heading-ids-sitedir}
-Add [explicit heading IDs](./guides/markdown-features/markdown-features-toc.mdx#explicit-ids) to the Markdown documents of your site.
+Add [explicit heading IDs](./guides/markdown-features/markdown-features-toc.mdx#heading-ids) to the Markdown documents of your site.
| Name | Default | Description |
| --- | --- | --- |
diff --git a/website/docs/docusaurus-core.mdx b/website/docs/docusaurus-core.mdx
index e96d9ccf5068..4249f47bec24 100644
--- a/website/docs/docusaurus-core.mdx
+++ b/website/docs/docusaurus-core.mdx
@@ -605,6 +605,49 @@ const MyComponent = () => {
};
```
+### `useBrokenLinks` {#useBrokenLinks}
+
+React hook to access the Docusaurus broken link checker APIs, exposing a way for a Docusaurus pages to report and collect their links and anchors.
+
+:::warning
+
+This is an **advanced** API that **most Docusaurus users don't need to use directly**.
+
+It is already **built-in** in existing high-level components:
+
+- the [``](#link) component will collect links for you
+- the `@theme/Heading` (used for Markdown headings) will collect anchors
+
+Use `useBrokenLinks()` if you implement your own `` or `` component.
+
+:::
+
+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
Heading
;
+}
+```
+
+```js title="MyLink.js"
+import useBrokenLinks from '@docusaurus/useBrokenLinks';
+
+export default function MyLink({targetLink, ...props}): JSX.Element {
+ const brokenLinks = useBrokenLinks();
+
+ brokenLinks.collectLink(targetLink);
+
+ return Link;
+}
+```
+
## Functions {#functions}
### `interpolate` {#interpolate-1}
diff --git a/website/docs/guides/docs/sidebar/autogenerated.mdx b/website/docs/guides/docs/sidebar/autogenerated.mdx
index 000e1e4cbdcf..7e3bfcf0a005 100644
--- a/website/docs/guides/docs/sidebar/autogenerated.mdx
+++ b/website/docs/guides/docs/sidebar/autogenerated.mdx
@@ -371,7 +371,7 @@ customProps:
:::info
-If the `link` is explicitly specified, Docusaurus will not apply any [default conventions](items.mdx#category-index-convention).
+If the `link` is explicitly specified, Docusaurus will not apply any [default conventions](#category-index-convention).
The doc links can be specified relatively, e.g. if the category is generated with the `guides` directory, `"link": {"type": "doc", "id": "intro"}` will be resolved to the ID `guides/intro`, only falling back to `intro` if a doc with the former ID doesn't exist.
diff --git a/website/docs/guides/docs/versioning.mdx b/website/docs/guides/docs/versioning.mdx
index b473d69a1268..1fa34fb1c5f4 100644
--- a/website/docs/guides/docs/versioning.mdx
+++ b/website/docs/guides/docs/versioning.mdx
@@ -106,7 +106,7 @@ npm run docusaurus docs:version 1.1.0
When tagging a new version, the document versioning mechanism will:
- Copy the full `docs/` folder contents into a new `versioned_docs/version-[versionName]/` folder.
-- Create a versioned sidebars file based from your current [sidebar](docs-introduction.mdx#sidebar) configuration (if it exists) - saved as `versioned_sidebars/version-[versionName]-sidebars.json`.
+- Create a versioned sidebars file based from your current [sidebar](./sidebar/index.mdx) configuration (if it exists) - saved as `versioned_sidebars/version-[versionName]-sidebars.json`.
- Append the new version number to `versions.json`.
### Creating new docs {#creating-new-docs}
diff --git a/website/docs/i18n/i18n-tutorial.mdx b/website/docs/i18n/i18n-tutorial.mdx
index b74896547cd7..a88e2f0a388b 100644
--- a/website/docs/i18n/i18n-tutorial.mdx
+++ b/website/docs/i18n/i18n-tutorial.mdx
@@ -445,7 +445,7 @@ Generated IDs are not always a good fit for localized sites, as it requires you
+ [link](#bonjour-le-monde)
```
-For localized sites, it is recommended to use **[explicit heading IDs](../guides/markdown-features/markdown-features-toc.mdx#explicit-ids)**.
+For localized sites, it is recommended to use **[explicit heading IDs](../guides/markdown-features/markdown-features-toc.mdx#heading-ids)**.
:::
diff --git a/website/docs/seo.mdx b/website/docs/seo.mdx
index 147bf99657c0..031ab1ddf340 100644
--- a/website/docs/seo.mdx
+++ b/website/docs/seo.mdx
@@ -211,7 +211,7 @@ For example, [`/examples/noIndex`](/examples/noIndex) is not included in the [Do
## Human readable links {#human-readable-links}
-Docusaurus uses your file names as links, but you can always change that using slugs, see this [tutorial](./guides/docs/docs-introduction.mdx#document-id) for more details.
+Docusaurus uses your file names as links, but you can always change that using slugs, see this [tutorial](./guides/docs/docs-create-doc.mdx#document-id) for more details.
## Structured content {#structured-content}
diff --git a/website/docs/using-plugins.mdx b/website/docs/using-plugins.mdx
index 28e25e491541..92d86097d717 100644
--- a/website/docs/using-plugins.mdx
+++ b/website/docs/using-plugins.mdx
@@ -114,7 +114,7 @@ At most one plugin instance can be the "default plugin instance", by omitting th
## Using themes {#using-themes}
-Themes are loaded in the exact same way as plugins. From the consumer perspective, the `themes` and `plugins` entries are interchangeable when installing and configuring a plugin. The only nuance is that themes are loaded after plugins, and it's possible for [a theme to override a plugin's default theme components](./swizzling.mdx#theme-aliases).
+Themes are loaded in the exact same way as plugins. From the consumer perspective, the `themes` and `plugins` entries are interchangeable when installing and configuring a plugin. The only nuance is that themes are loaded after plugins, and it's possible for [a theme to override a plugin's default theme components](./advanced/client.mdx#theme-aliases).
:::tip
diff --git a/website/docusaurus.config.ts b/website/docusaurus.config.ts
index d9d91ae715c7..dbdc49dd2675 100644
--- a/website/docusaurus.config.ts
+++ b/website/docusaurus.config.ts
@@ -206,7 +206,11 @@ export default async function createConfigAsync() {
},
},
onBrokenLinks:
- isBuildFast ||
+ isVersioningDisabled ||
+ process.env.DOCUSAURUS_CURRENT_LOCALE !== defaultLocale
+ ? 'warn'
+ : 'throw',
+ onBrokenAnchors:
isVersioningDisabled ||
process.env.DOCUSAURUS_CURRENT_LOCALE !== defaultLocale
? 'warn'
diff --git a/website/versioned_docs/version-2.x/advanced/routing.mdx b/website/versioned_docs/version-2.x/advanced/routing.mdx
index e6513637e379..9e3551fdd37e 100644
--- a/website/versioned_docs/version-2.x/advanced/routing.mdx
+++ b/website/versioned_docs/version-2.x/advanced/routing.mdx
@@ -265,26 +265,18 @@ export function PageRoute() {
Docusaurus builds a [single-page application](https://developer.mozilla.org/en-US/docs/Glossary/SPA), where route transitions are done through the `history.push()` method of React router. This operation is done on the client side. However, the prerequisite for a route transition to happen this way is that the target URL is known to our router. Otherwise, the router catches this path and displays a 404 page instead.
-If you put some HTML pages under the `static` folder, they will be copied to the build output and therefore become accessible as part of your website, yet it's not part of the Docusaurus route system. We provide a `pathname://` protocol that allows you to redirect to another part of your domain in a non-SPA fashion, as if this route is an external link. Try the following two links:
+If you put some HTML pages under the `static` folder, they will be copied to the build output and therefore become accessible as part of your website, yet it's not part of the Docusaurus route system. We provide a `pathname://` protocol that allows you to redirect to another part of your domain in a non-SPA fashion, as if this route is an external link.
```md
-- [/pure-html](/pure-html)
- [pathname:///pure-html](pathname:///pure-html)
```
-- [`/pure-html`](/pure-html)
- [`pathname:///pure-html`](pathname:///pure-html)
-:::tip
-
-The first link will **not** trigger a "broken links detected" check during the production build, because the respective file actually exists. Nevertheless, when you click on the link, a "page not found" will be displayed until you refresh.
-
-:::
-
The `pathname://` protocol is useful for referencing any content in the static folder. For example, Docusaurus would convert [all Markdown static assets to require() calls](../guides/markdown-features/markdown-features-assets.mdx#static-assets). You can use `pathname://` to keep it a regular link instead of being hashed by Webpack.
```md title="my-doc.md"
diff --git a/website/versioned_docs/version-2.x/api/plugins/plugin-content-docs.mdx b/website/versioned_docs/version-2.x/api/plugins/plugin-content-docs.mdx
index e8343d9742cc..cd98d4e99e1c 100644
--- a/website/versioned_docs/version-2.x/api/plugins/plugin-content-docs.mdx
+++ b/website/versioned_docs/version-2.x/api/plugins/plugin-content-docs.mdx
@@ -42,10 +42,10 @@ Accepted fields:
| `include` | `string[]` | `['**/*.{md,mdx}']` | Array of glob patterns matching Markdown files to be built, relative to the content path. |
| `exclude` | `string[]` | _See example configuration_ | Array of glob patterns matching Markdown files to be excluded. Serves as refinement based on the `include` option. |
| `sidebarPath` | false \| string | `undefined` | Path to sidebar configuration. Use `false` to disable sidebars, or `undefined` to create a fully autogenerated sidebar. |
-| `sidebarCollapsible` | `boolean` | `true` | Whether sidebar categories are collapsible by default. See also [Collapsible categories](/docs/sidebar#collapsible-categories) |
-| `sidebarCollapsed` | `boolean` | `true` | Whether sidebar categories are collapsed by default. See also [Expanded categories by default](/docs/sidebar#expanded-categories-by-default) |
-| `sidebarItemsGenerator` | SidebarGenerator | _Omitted_ | Function used to replace the sidebar items of type `'autogenerated'` with real sidebar items (docs, categories, links...). See also [Customize the sidebar items generator](/docs/sidebar#customize-the-sidebar-items-generator) |
-| `numberPrefixParser` | boolean \|PrefixParser | _Omitted_ | Custom parsing logic to extract number prefixes from file names. Use `false` to disable this behavior and leave the docs untouched, and `true` to use the default parser. See also [Using number prefixes](/docs/sidebar#using-number-prefixes) |
+| `sidebarCollapsible` | `boolean` | `true` | Whether sidebar categories are collapsible by default. See also [Collapsible categories](/docs/sidebar/items#collapsible-categories) |
+| `sidebarCollapsed` | `boolean` | `true` | Whether sidebar categories are collapsed by default. See also [Expanded categories by default](/docs/sidebar/items#expanded-categories-by-default) |
+| `sidebarItemsGenerator` | SidebarGenerator | _Omitted_ | Function used to replace the sidebar items of type `'autogenerated'` with real sidebar items (docs, categories, links...). See also [Customize the sidebar items generator](/docs/sidebar/autogenerated#customize-the-sidebar-items-generator) |
+| `numberPrefixParser` | boolean \|PrefixParser | _Omitted_ | Custom parsing logic to extract number prefixes from file names. Use `false` to disable this behavior and leave the docs untouched, and `true` to use the default parser. See also [Using number prefixes](/docs/sidebar/autogenerated#using-number-prefixes) |
| `docLayoutComponent` | `string` | `'@theme/DocPage'` | Root layout component of each doc page. Provides the version data context, and is not unmounted when switching docs. |
| `docItemComponent` | `string` | `'@theme/DocItem'` | Main doc container, with TOC, pagination, etc. |
| `docTagsListComponent` | `string` | `'@theme/DocTagsListPage'` | Root component of the tags list page |
@@ -273,7 +273,7 @@ Accepted fields:
| `title` | `string` | Markdown title or `id` | The text title of your document. Used for the page metadata and as a fallback value in multiple places (sidebar, next/previous buttons...). Automatically added at the top of your doc if it does not contain any Markdown title. |
| `pagination_label` | `string` | `sidebar_label` or `title` | The text used in the document next/previous buttons for this document. |
| `sidebar_label` | `string` | `title` | The text shown in the document sidebar for this document. |
-| `sidebar_position` | `number` | Default ordering | Controls the position of a doc inside the generated sidebar slice when using `autogenerated` sidebar items. See also [Autogenerated sidebar metadata](/docs/sidebar#autogenerated-sidebar-metadata). |
+| `sidebar_position` | `number` | Default ordering | Controls the position of a doc inside the generated sidebar slice when using `autogenerated` sidebar items. See also [Autogenerated sidebar metadata](/docs/sidebar/autogenerated#autogenerated-sidebar-metadata). |
| `sidebar_class_name` | `string` | `undefined` | Gives the corresponding sidebar label a special class name when using autogenerated sidebars. |
| `sidebar_custom_props` | `object` | `undefined` | Assign [custom props](../../guides/docs/sidebar/index.mdx#passing-custom-props) to the sidebar item referencing this doc |
| `displayed_sidebar` | `string` | `undefined` | Force the display of a given sidebar when browsing the current document. Read the [multiple sidebars guide](../../guides/docs/sidebar/multiple-sidebars.mdx) for details. |
@@ -283,7 +283,7 @@ Accepted fields:
| `toc_max_heading_level` | `number` | `3` | The max heading level shown in the table of contents. Must be between 2 and 6. |
| `pagination_next` | string \| null | Next doc in the sidebar | The ID of the documentation you want the "Next" pagination to link to. Use `null` to disable showing "Next" for this page. |
| `pagination_prev` | string \| null | Previous doc in the sidebar | The ID of the documentation you want the "Previous" pagination to link to. Use `null` to disable showing "Previous" for this page. |
-| `parse_number_prefixes` | `boolean` | `numberPrefixParser` plugin option | Whether number prefix parsing is disabled on this doc. See also [Using number prefixes](/docs/sidebar#using-number-prefixes). |
+| `parse_number_prefixes` | `boolean` | `numberPrefixParser` plugin option | Whether number prefix parsing is disabled on this doc. See also [Using number prefixes](/docs/sidebar/autogenerated#using-number-prefixes). |
| `custom_edit_url` | `string` | Computed using the `editUrl` plugin option | The URL for editing this document. |
| `keywords` | `string[]` | `undefined` | Keywords meta tag for the document page, for search engines. |
| `description` | `string` | The first line of Markdown content | The description of your document, which will become the `` and `` in ``, used by search engines. |
diff --git a/website/versioned_docs/version-2.x/cli.mdx b/website/versioned_docs/version-2.x/cli.mdx
index aaa652a48d5a..551b560aef84 100644
--- a/website/versioned_docs/version-2.x/cli.mdx
+++ b/website/versioned_docs/version-2.x/cli.mdx
@@ -176,7 +176,7 @@ By default, the files are written in `website/i18n//...`.
### `docusaurus write-heading-ids [siteDir] [files]` {#docusaurus-write-heading-ids-sitedir}
-Add [explicit heading IDs](./guides/markdown-features/markdown-features-toc.mdx#explicit-ids) to the Markdown documents of your site.
+Add [explicit heading IDs](./guides/markdown-features/markdown-features-toc.mdx#heading-ids) to the Markdown documents of your site.
| Name | Default | Description |
| --- | --- | --- |
diff --git a/website/versioned_docs/version-2.x/deployment.mdx b/website/versioned_docs/version-2.x/deployment.mdx
index 0c49003366ba..c45e7fc6c10d 100644
--- a/website/versioned_docs/version-2.x/deployment.mdx
+++ b/website/versioned_docs/version-2.x/deployment.mdx
@@ -57,7 +57,7 @@ Use [slorber/trailing-slash-guide](https://github.com/slorber/trailing-slash-gui
## Using environment variables {#using-environment-variables}
-Putting potentially sensitive information in the environment is common practice. However, in a typical Docusaurus website, the `docusaurus.config.js` file is the only interface to the Node.js environment (see [our architecture overview](advanced/architecture.mdx)), while everything else—MDX pages, React components... are client side and do not have direct access to the `process` global. In this case, you can consider using [`customFields`](api/docusaurus.config.js.mdx#customFields) to pass environment variables to the client side.
+Putting potentially sensitive information in the environment is common practice. However, in a typical Docusaurus website, the `docusaurus.config.js` file is the only interface to the Node.js environment (see [our architecture overview](advanced/architecture.mdx)), while everything else—MDX pages, React components... are client side and do not have direct access to the `process` global. In this case, you can consider using [`customFields`](api/docusaurus.config.js.mdx#customfields) to pass environment variables to the client side.
```js title="docusaurus.config.js"
// If you are using dotenv (https://www.npmjs.com/package/dotenv)
diff --git a/website/versioned_docs/version-2.x/guides/docs/sidebar/autogenerated.mdx b/website/versioned_docs/version-2.x/guides/docs/sidebar/autogenerated.mdx
index 4ac6fa6e620f..6d9a074f19d6 100644
--- a/website/versioned_docs/version-2.x/guides/docs/sidebar/autogenerated.mdx
+++ b/website/versioned_docs/version-2.x/guides/docs/sidebar/autogenerated.mdx
@@ -371,7 +371,7 @@ customProps:
:::info
-If the `link` is explicitly specified, Docusaurus will not apply any [default conventions](items.mdx#category-index-convention).
+If the `link` is explicitly specified, Docusaurus will not apply any [default conventions](#category-index-convention).
The doc links can be specified relatively, e.g. if the category is generated with the `guides` directory, `"link": {"type": "doc", "id": "intro"}` will be resolved to the ID `guides/intro`, only falling back to `intro` if a doc with the former ID doesn't exist.
diff --git a/website/versioned_docs/version-2.x/guides/docs/versioning.mdx b/website/versioned_docs/version-2.x/guides/docs/versioning.mdx
index 2b5657f48d81..60e41494d745 100644
--- a/website/versioned_docs/version-2.x/guides/docs/versioning.mdx
+++ b/website/versioned_docs/version-2.x/guides/docs/versioning.mdx
@@ -106,7 +106,7 @@ npm run docusaurus docs:version 1.1.0
When tagging a new version, the document versioning mechanism will:
- Copy the full `docs/` folder contents into a new `versioned_docs/version-[versionName]/` folder.
-- Create a versioned sidebars file based from your current [sidebar](docs-introduction.mdx#sidebar) configuration (if it exists) - saved as `versioned_sidebars/version-[versionName]-sidebars.json`.
+- Create a versioned sidebars file based from your current [sidebar](./sidebar/index.mdx) configuration (if it exists) - saved as `versioned_sidebars/version-[versionName]-sidebars.json`.
- Append the new version number to `versions.json`.
### Creating new docs {#creating-new-docs}
diff --git a/website/versioned_docs/version-2.x/guides/markdown-features/markdown-features-react.mdx b/website/versioned_docs/version-2.x/guides/markdown-features/markdown-features-react.mdx
index ac2c4a2ebc94..606ddf5e2451 100644
--- a/website/versioned_docs/version-2.x/guides/markdown-features/markdown-features-react.mdx
+++ b/website/versioned_docs/version-2.x/guides/markdown-features/markdown-features-react.mdx
@@ -84,7 +84,7 @@ Since all doc files are parsed using MDX, anything that looks like HTML is actua
Foo
```
-This behavior is different from Docusaurus 1. See also [Migrating from v1 to v2](../../migration/migration-manual.mdx#convert-style-attributes-to-style-objects-in-mdx).
+This behavior is different from Docusaurus 1. See also [Migrating from v1 to v2](../../migration/migration-manual.mdx).
In addition, MDX is not [100% compatible with CommonMark](https://github.com/facebook/docusaurus/issues/3018). Use the **[MDX playground](https://mdx-git-renovate-babel-monorepo-mdx.vercel.app/playground)** to ensure that your syntax is valid MDX.
diff --git a/website/versioned_docs/version-2.x/i18n/i18n-tutorial.mdx b/website/versioned_docs/version-2.x/i18n/i18n-tutorial.mdx
index de7c22845af1..6c80b02dcc00 100644
--- a/website/versioned_docs/version-2.x/i18n/i18n-tutorial.mdx
+++ b/website/versioned_docs/version-2.x/i18n/i18n-tutorial.mdx
@@ -437,7 +437,7 @@ Generated IDs are not always a good fit for localized sites, as it requires you
+ [link](#bonjour-le-monde)
```
-For localized sites, it is recommended to use **[explicit heading IDs](../guides/markdown-features/markdown-features-toc.mdx#explicit-ids)**.
+For localized sites, it is recommended to use **[explicit heading IDs](../guides/markdown-features/markdown-features-toc.mdx#heading-ids)**.
:::
diff --git a/website/versioned_docs/version-2.x/seo.mdx b/website/versioned_docs/version-2.x/seo.mdx
index ea09f87d4ae8..a8af9c30c75a 100644
--- a/website/versioned_docs/version-2.x/seo.mdx
+++ b/website/versioned_docs/version-2.x/seo.mdx
@@ -152,7 +152,7 @@ For example, [`/examples/noIndex`](/examples/noIndex) is not included in the [Do
## Human readable links {#human-readable-links}
-Docusaurus uses your file names as links, but you can always change that using slugs, see this [tutorial](./guides/docs/docs-introduction.mdx#document-id) for more details.
+Docusaurus uses your file names as links, but you can always change that using slugs, see this [tutorial](./guides/docs/docs-create-doc.mdx#document-id) for more details.
## Structured content {#structured-content}
diff --git a/website/versioned_docs/version-2.x/using-plugins.mdx b/website/versioned_docs/version-2.x/using-plugins.mdx
index c40672830ee6..8d7accf505b5 100644
--- a/website/versioned_docs/version-2.x/using-plugins.mdx
+++ b/website/versioned_docs/version-2.x/using-plugins.mdx
@@ -114,7 +114,7 @@ At most one plugin instance can be the "default plugin instance", by omitting th
## Using themes {#using-themes}
-Themes are loaded in the exact same way as plugins. From the consumer perspective, the `themes` and `plugins` entries are interchangeable when installing and configuring a plugin. The only nuance is that themes are loaded after plugins, and it's possible for [a theme to override a plugin's default theme components](./swizzling.mdx#theme-aliases).
+Themes are loaded in the exact same way as plugins. From the consumer perspective, the `themes` and `plugins` entries are interchangeable when installing and configuring a plugin. The only nuance is that themes are loaded after plugins, and it's possible for [a theme to override a plugin's default theme components](./advanced/client.mdx#theme-aliases).
:::tip
diff --git a/website/versioned_docs/version-3.0.0/advanced/routing.mdx b/website/versioned_docs/version-3.0.0/advanced/routing.mdx
index 5bf943072ba6..e9e96d4892c7 100644
--- a/website/versioned_docs/version-3.0.0/advanced/routing.mdx
+++ b/website/versioned_docs/version-3.0.0/advanced/routing.mdx
@@ -265,26 +265,18 @@ export function PageRoute() {
Docusaurus builds a [single-page application](https://developer.mozilla.org/en-US/docs/Glossary/SPA), where route transitions are done through the `history.push()` method of React router. This operation is done on the client side. However, the prerequisite for a route transition to happen this way is that the target URL is known to our router. Otherwise, the router catches this path and displays a 404 page instead.
-If you put some HTML pages under the `static` folder, they will be copied to the build output and therefore become accessible as part of your website, yet it's not part of the Docusaurus route system. We provide a `pathname://` protocol that allows you to redirect to another part of your domain in a non-SPA fashion, as if this route is an external link. Try the following two links:
+If you put some HTML pages under the `static` folder, they will be copied to the build output and therefore become accessible as part of your website, yet it's not part of the Docusaurus route system. We provide a `pathname://` protocol that allows you to redirect to another part of your domain in a non-SPA fashion, as if this route is an external link.
```md
-- [/pure-html](/pure-html)
- [pathname:///pure-html](pathname:///pure-html)
```
-- [`/pure-html`](/pure-html)
- [`pathname:///pure-html`](pathname:///pure-html)
-:::tip
-
-The first link will **not** trigger a "broken links detected" check during the production build, because the respective file actually exists. Nevertheless, when you click on the link, a "page not found" will be displayed until you refresh.
-
-:::
-
The `pathname://` protocol is useful for referencing any content in the static folder. For example, Docusaurus would convert [all Markdown static assets to require() calls](../guides/markdown-features/markdown-features-assets.mdx#static-assets). You can use `pathname://` to keep it a regular link instead of being hashed by Webpack.
```md title="my-doc.md"
diff --git a/website/versioned_docs/version-3.0.0/api/plugins/plugin-content-docs.mdx b/website/versioned_docs/version-3.0.0/api/plugins/plugin-content-docs.mdx
index d9238f65bb36..754a56d293d9 100644
--- a/website/versioned_docs/version-3.0.0/api/plugins/plugin-content-docs.mdx
+++ b/website/versioned_docs/version-3.0.0/api/plugins/plugin-content-docs.mdx
@@ -42,10 +42,10 @@ Accepted fields:
| `include` | `string[]` | `['**/*.{md,mdx}']` | Array of glob patterns matching Markdown files to be built, relative to the content path. |
| `exclude` | `string[]` | _See example configuration_ | Array of glob patterns matching Markdown files to be excluded. Serves as refinement based on the `include` option. |
| `sidebarPath` | false \| string | `undefined` | Path to sidebar configuration. Use `false` to disable sidebars, or `undefined` to create a fully autogenerated sidebar. |
-| `sidebarCollapsible` | `boolean` | `true` | Whether sidebar categories are collapsible by default. See also [Collapsible categories](/docs/sidebar#collapsible-categories) |
-| `sidebarCollapsed` | `boolean` | `true` | Whether sidebar categories are collapsed by default. See also [Expanded categories by default](/docs/sidebar#expanded-categories-by-default) |
-| `sidebarItemsGenerator` | SidebarGenerator | _Omitted_ | Function used to replace the sidebar items of type `'autogenerated'` with real sidebar items (docs, categories, links...). See also [Customize the sidebar items generator](/docs/sidebar#customize-the-sidebar-items-generator) |
-| `numberPrefixParser` | boolean \|PrefixParser | _Omitted_ | Custom parsing logic to extract number prefixes from file names. Use `false` to disable this behavior and leave the docs untouched, and `true` to use the default parser. See also [Using number prefixes](/docs/sidebar#using-number-prefixes) |
+| `sidebarCollapsible` | `boolean` | `true` | Whether sidebar categories are collapsible by default. See also [Collapsible categories](/docs/sidebar/items#collapsible-categories) |
+| `sidebarCollapsed` | `boolean` | `true` | Whether sidebar categories are collapsed by default. See also [Expanded categories by default](/docs/sidebar/items#expanded-categories-by-default) |
+| `sidebarItemsGenerator` | SidebarGenerator | _Omitted_ | Function used to replace the sidebar items of type `'autogenerated'` with real sidebar items (docs, categories, links...). See also [Customize the sidebar items generator](/docs/sidebar/autogenerated#customize-the-sidebar-items-generator) |
+| `numberPrefixParser` | boolean \|PrefixParser | _Omitted_ | Custom parsing logic to extract number prefixes from file names. Use `false` to disable this behavior and leave the docs untouched, and `true` to use the default parser. See also [Using number prefixes](/docs/sidebar/autogenerated#using-number-prefixes) |
| `docsRootComponent` | `string` | `'@theme/DocsRoot'` | Parent component of all the docs plugin pages (including all versions). Stays mounted when navigation between docs pages and versions. |
| `docVersionRootComponent` | `string` | `'@theme/DocVersionLayout'` | Parent component of all docs pages of an individual version (doc pages with sidebars, tags pages). Stays mounted when navigation between pages of that specific version. |
| `docRootComponent` | `string` | `'@theme/DocPage'` | Parent component of all doc pages with sidebars (regular docs pages, category generated index pages). Stays mounted when navigation between such pages. |
@@ -275,7 +275,7 @@ Accepted fields:
| `title` | `string` | Markdown title or `id` | The text title of your document. Used for the page metadata and as a fallback value in multiple places (sidebar, next/previous buttons...). Automatically added at the top of your doc if it does not contain any Markdown title. |
| `pagination_label` | `string` | `sidebar_label` or `title` | The text used in the document next/previous buttons for this document. |
| `sidebar_label` | `string` | `title` | The text shown in the document sidebar for this document. |
-| `sidebar_position` | `number` | Default ordering | Controls the position of a doc inside the generated sidebar slice when using `autogenerated` sidebar items. See also [Autogenerated sidebar metadata](/docs/sidebar#autogenerated-sidebar-metadata). |
+| `sidebar_position` | `number` | Default ordering | Controls the position of a doc inside the generated sidebar slice when using `autogenerated` sidebar items. See also [Autogenerated sidebar metadata](/docs/sidebar/autogenerated#autogenerated-sidebar-metadata). |
| `sidebar_class_name` | `string` | `undefined` | Gives the corresponding sidebar label a special class name when using autogenerated sidebars. |
| `sidebar_custom_props` | `object` | `undefined` | Assign [custom props](../../guides/docs/sidebar/index.mdx#passing-custom-props) to the sidebar item referencing this doc |
| `displayed_sidebar` | `string` | `undefined` | Force the display of a given sidebar when browsing the current document. Read the [multiple sidebars guide](../../guides/docs/sidebar/multiple-sidebars.mdx) for details. |
@@ -285,7 +285,7 @@ Accepted fields:
| `toc_max_heading_level` | `number` | `3` | The max heading level shown in the table of contents. Must be between 2 and 6. |
| `pagination_next` | string \| null | Next doc in the sidebar | The ID of the documentation you want the "Next" pagination to link to. Use `null` to disable showing "Next" for this page. |
| `pagination_prev` | string \| null | Previous doc in the sidebar | The ID of the documentation you want the "Previous" pagination to link to. Use `null` to disable showing "Previous" for this page. |
-| `parse_number_prefixes` | `boolean` | `numberPrefixParser` plugin option | Whether number prefix parsing is disabled on this doc. See also [Using number prefixes](/docs/sidebar#using-number-prefixes). |
+| `parse_number_prefixes` | `boolean` | `numberPrefixParser` plugin option | Whether number prefix parsing is disabled on this doc. See also [Using number prefixes](/docs/sidebar/autogenerated#using-number-prefixes). |
| `custom_edit_url` | string \| null | Computed using the `editUrl` plugin option | The URL for editing this document. Use `null` to disable showing "Edit this page" for this page. |
| `keywords` | `string[]` | `undefined` | Keywords meta tag for the document page, for search engines. |
| `description` | `string` | The first line of Markdown content | The description of your document, which will become the `` and `` in ``, used by search engines. |
diff --git a/website/versioned_docs/version-3.0.0/cli.mdx b/website/versioned_docs/version-3.0.0/cli.mdx
index 0b38c01776f7..bb1f32c91d68 100644
--- a/website/versioned_docs/version-3.0.0/cli.mdx
+++ b/website/versioned_docs/version-3.0.0/cli.mdx
@@ -177,7 +177,7 @@ By default, the files are written in `website/i18n//...`.
### `docusaurus write-heading-ids [siteDir] [files]` {#docusaurus-write-heading-ids-sitedir}
-Add [explicit heading IDs](./guides/markdown-features/markdown-features-toc.mdx#explicit-ids) to the Markdown documents of your site.
+Add [explicit heading IDs](./guides/markdown-features/markdown-features-toc.mdx#heading-ids) to the Markdown documents of your site.
| Name | Default | Description |
| --- | --- | --- |
diff --git a/website/versioned_docs/version-3.0.0/guides/docs/sidebar/autogenerated.mdx b/website/versioned_docs/version-3.0.0/guides/docs/sidebar/autogenerated.mdx
index 000e1e4cbdcf..7e3bfcf0a005 100644
--- a/website/versioned_docs/version-3.0.0/guides/docs/sidebar/autogenerated.mdx
+++ b/website/versioned_docs/version-3.0.0/guides/docs/sidebar/autogenerated.mdx
@@ -371,7 +371,7 @@ customProps:
:::info
-If the `link` is explicitly specified, Docusaurus will not apply any [default conventions](items.mdx#category-index-convention).
+If the `link` is explicitly specified, Docusaurus will not apply any [default conventions](#category-index-convention).
The doc links can be specified relatively, e.g. if the category is generated with the `guides` directory, `"link": {"type": "doc", "id": "intro"}` will be resolved to the ID `guides/intro`, only falling back to `intro` if a doc with the former ID doesn't exist.
diff --git a/website/versioned_docs/version-3.0.0/guides/docs/versioning.mdx b/website/versioned_docs/version-3.0.0/guides/docs/versioning.mdx
index b473d69a1268..1fa34fb1c5f4 100644
--- a/website/versioned_docs/version-3.0.0/guides/docs/versioning.mdx
+++ b/website/versioned_docs/version-3.0.0/guides/docs/versioning.mdx
@@ -106,7 +106,7 @@ npm run docusaurus docs:version 1.1.0
When tagging a new version, the document versioning mechanism will:
- Copy the full `docs/` folder contents into a new `versioned_docs/version-[versionName]/` folder.
-- Create a versioned sidebars file based from your current [sidebar](docs-introduction.mdx#sidebar) configuration (if it exists) - saved as `versioned_sidebars/version-[versionName]-sidebars.json`.
+- Create a versioned sidebars file based from your current [sidebar](./sidebar/index.mdx) configuration (if it exists) - saved as `versioned_sidebars/version-[versionName]-sidebars.json`.
- Append the new version number to `versions.json`.
### Creating new docs {#creating-new-docs}
diff --git a/website/versioned_docs/version-3.0.0/i18n/i18n-tutorial.mdx b/website/versioned_docs/version-3.0.0/i18n/i18n-tutorial.mdx
index b74896547cd7..a88e2f0a388b 100644
--- a/website/versioned_docs/version-3.0.0/i18n/i18n-tutorial.mdx
+++ b/website/versioned_docs/version-3.0.0/i18n/i18n-tutorial.mdx
@@ -445,7 +445,7 @@ Generated IDs are not always a good fit for localized sites, as it requires you
+ [link](#bonjour-le-monde)
```
-For localized sites, it is recommended to use **[explicit heading IDs](../guides/markdown-features/markdown-features-toc.mdx#explicit-ids)**.
+For localized sites, it is recommended to use **[explicit heading IDs](../guides/markdown-features/markdown-features-toc.mdx#heading-ids)**.
:::
diff --git a/website/versioned_docs/version-3.0.0/seo.mdx b/website/versioned_docs/version-3.0.0/seo.mdx
index 147bf99657c0..031ab1ddf340 100644
--- a/website/versioned_docs/version-3.0.0/seo.mdx
+++ b/website/versioned_docs/version-3.0.0/seo.mdx
@@ -211,7 +211,7 @@ For example, [`/examples/noIndex`](/examples/noIndex) is not included in the [Do
## Human readable links {#human-readable-links}
-Docusaurus uses your file names as links, but you can always change that using slugs, see this [tutorial](./guides/docs/docs-introduction.mdx#document-id) for more details.
+Docusaurus uses your file names as links, but you can always change that using slugs, see this [tutorial](./guides/docs/docs-create-doc.mdx#document-id) for more details.
## Structured content {#structured-content}
diff --git a/website/versioned_docs/version-3.0.0/using-plugins.mdx b/website/versioned_docs/version-3.0.0/using-plugins.mdx
index 28e25e491541..92d86097d717 100644
--- a/website/versioned_docs/version-3.0.0/using-plugins.mdx
+++ b/website/versioned_docs/version-3.0.0/using-plugins.mdx
@@ -114,7 +114,7 @@ At most one plugin instance can be the "default plugin instance", by omitting th
## Using themes {#using-themes}
-Themes are loaded in the exact same way as plugins. From the consumer perspective, the `themes` and `plugins` entries are interchangeable when installing and configuring a plugin. The only nuance is that themes are loaded after plugins, and it's possible for [a theme to override a plugin's default theme components](./swizzling.mdx#theme-aliases).
+Themes are loaded in the exact same way as plugins. From the consumer perspective, the `themes` and `plugins` entries are interchangeable when installing and configuring a plugin. The only nuance is that themes are loaded after plugins, and it's possible for [a theme to override a plugin's default theme components](./advanced/client.mdx#theme-aliases).
:::tip