mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-10 15:47:23 +02:00
feat(core): make broken link checker detect broken anchors - add onBrokenAnchors
config (#9528)
Co-authored-by: sebastienlorber <lorber.sebastien@gmail.com>
This commit is contained in:
parent
332a466893
commit
fd49301a45
52 changed files with 1220 additions and 519 deletions
|
@ -12,17 +12,17 @@ exports[`transformAsset plugin pathname protocol 1`] = `
|
|||
exports[`transformAsset plugin transform md links to <a /> 1`] = `
|
||||
"[asset](https://example.com/asset.pdf)
|
||||
|
||||
<a target="_blank" href={require("!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf").default} />
|
||||
<a target="_blank" data-noBrokenLinkCheck={true} href={require("!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf").default} />
|
||||
|
||||
<a target="_blank" href={require("!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf").default}>asset</a>
|
||||
<a target="_blank" data-noBrokenLinkCheck={true} href={require("!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf").default}>asset</a>
|
||||
|
||||
in paragraph <a target="_blank" href={require("!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf").default}>asset</a>
|
||||
in paragraph <a target="_blank" data-noBrokenLinkCheck={true} href={require("!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf").default}>asset</a>
|
||||
|
||||
<a target="_blank" href={require("!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset (2).pdf").default}>asset with URL encoded chars</a>
|
||||
<a target="_blank" data-noBrokenLinkCheck={true} href={require("!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset (2).pdf").default}>asset with URL encoded chars</a>
|
||||
|
||||
<a target="_blank" href={require("!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf").default + '#page=2'}>asset with hash</a>
|
||||
<a target="_blank" data-noBrokenLinkCheck={true} href={require("!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf").default + '#page=2'}>asset with hash</a>
|
||||
|
||||
<a target="_blank" href={require("!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf").default} title="Title">asset</a>
|
||||
<a target="_blank" data-noBrokenLinkCheck={true} href={require("!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf").default} title="Title">asset</a>
|
||||
|
||||
[page](noUrl.md)
|
||||
|
||||
|
@ -36,24 +36,24 @@ in paragraph <a target="_blank" href={require("!<PROJECT_ROOT>/node_modules/file
|
|||
|
||||
[assets](/github/!file-loader!/assets.pdf)
|
||||
|
||||
<a target="_blank" href={require("!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf").default}>asset</a>
|
||||
<a target="_blank" data-noBrokenLinkCheck={true} href={require("!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf").default}>asset</a>
|
||||
|
||||
<a target="_blank" href={require("!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static2/asset2.pdf").default}>asset2</a>
|
||||
<a target="_blank" data-noBrokenLinkCheck={true} href={require("!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static2/asset2.pdf").default}>asset2</a>
|
||||
|
||||
<a target="_blank" href={require("!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/staticAsset.pdf").default}>staticAsset.pdf</a>
|
||||
<a target="_blank" data-noBrokenLinkCheck={true} href={require("!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/staticAsset.pdf").default}>staticAsset.pdf</a>
|
||||
|
||||
<a target="_blank" href={require("!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/staticAsset.pdf").default}>@site/static/staticAsset.pdf</a>
|
||||
<a target="_blank" data-noBrokenLinkCheck={true} href={require("!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/staticAsset.pdf").default}>@site/static/staticAsset.pdf</a>
|
||||
|
||||
<a target="_blank" href={require("!<PROJECT_ROOT>/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</a>
|
||||
<a target="_blank" data-noBrokenLinkCheck={true} href={require("!<PROJECT_ROOT>/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</a>
|
||||
|
||||
<a target="_blank" href={require("!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/staticAsset.pdf").default}>Just staticAsset.pdf</a>, and <a target="_blank" href={require("!<PROJECT_ROOT>/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"'</a>, but also <a target="_blank" href={require("!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/staticAsset.pdf").default}>coded \`staticAsset 3.pdf\`</a>
|
||||
<a target="_blank" data-noBrokenLinkCheck={true} href={require("!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/staticAsset.pdf").default}>Just staticAsset.pdf</a>, and <a target="_blank" data-noBrokenLinkCheck={true} href={require("!<PROJECT_ROOT>/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"'</a>, but also <a target="_blank" data-noBrokenLinkCheck={true} href={require("!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/staticAsset.pdf").default}>coded \`staticAsset 3.pdf\`</a>
|
||||
|
||||
<a target="_blank" href={require("!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/staticAssetImage.png").default}><img alt="Clickable Docusaurus logo" src={require("!<PROJECT_ROOT>/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[contenthash].[ext]&fallback=<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js!./static/staticAssetImage.png").default} width="200" height="200" /></a>
|
||||
<a target="_blank" data-noBrokenLinkCheck={true} href={require("!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/staticAssetImage.png").default}><img alt="Clickable Docusaurus logo" src={require("!<PROJECT_ROOT>/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[contenthash].[ext]&fallback=<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js!./static/staticAssetImage.png").default} width="200" height="200" /></a>
|
||||
|
||||
<a target="_blank" href={require("!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf").default}><span style={{color: "red"}}>Stylized link to asset file</span></a>
|
||||
<a target="_blank" data-noBrokenLinkCheck={true} href={require("!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf").default}><span style={{color: "red"}}>Stylized link to asset file</span></a>
|
||||
|
||||
<a target="_blank" href={require("./data.raw!=!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./data.json").default}>JSON</a>
|
||||
<a target="_blank" data-noBrokenLinkCheck={true} href={require("./data.raw!=!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./data.json").default}>JSON</a>
|
||||
|
||||
<a target="_blank" href={require("./static/static-json.raw!=!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/static-json.json").default}>static JSON</a>
|
||||
<a target="_blank" data-noBrokenLinkCheck={true} href={require("./static/static-json.raw!=!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/static-json.json").default}>static JSON</a>
|
||||
"
|
||||
`;
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 <As {...props} id={undefined} />;
|
||||
}
|
||||
|
||||
brokenLinks.collectAnchor(id);
|
||||
|
||||
const anchorTitle = translate(
|
||||
{
|
||||
id: 'theme.common.headingLinkTitle',
|
||||
|
|
7
packages/docusaurus-types/src/config.d.ts
vendored
7
packages/docusaurus-types/src/config.d.ts
vendored
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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, '/');
|
||||
|
|
51
packages/docusaurus/src/client/BrokenLinksContext.tsx
Normal file
51
packages/docusaurus/src/client/BrokenLinksContext.tsx
Normal file
|
@ -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<string>();
|
||||
const allLinks = new Set<string>();
|
||||
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<BrokenLinks>({
|
||||
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 <Context.Provider value={brokenLinks}>{children}</Context.Provider>;
|
||||
}
|
|
@ -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<string>();
|
||||
return {
|
||||
collectLink: (link: string): void => {
|
||||
allLinks.add(link);
|
||||
},
|
||||
getCollectedLinks: (): string[] => [...allLinks],
|
||||
};
|
||||
};
|
||||
|
||||
const Context = React.createContext<LinksCollector>({
|
||||
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 <Context.Provider value={linksCollector}>{children}</Context.Provider>;
|
||||
}
|
|
@ -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<HTMLAnchorElement | null>(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 ? (
|
||||
|
|
13
packages/docusaurus/src/client/exports/useBrokenLinks.ts
Normal file
13
packages/docusaurus/src/client/exports/useBrokenLinks.ts
Normal file
|
@ -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();
|
||||
}
|
|
@ -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
|
||||
<Loadable.Capture report={(moduleName) => modules.add(moduleName)}>
|
||||
<HelmetProvider context={helmetContext}>
|
||||
<StaticRouter location={location} context={routerContext}>
|
||||
<LinksCollectorProvider linksCollector={linksCollector}>
|
||||
<BrokenLinksProvider brokenLinks={statefulBrokenLinks}>
|
||||
<App />
|
||||
</LinksCollectorProvider>
|
||||
</BrokenLinksProvider>
|
||||
</StaticRouter>
|
||||
</HelmetProvider>
|
||||
</Loadable.Capture>
|
||||
);
|
||||
|
||||
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();
|
||||
|
|
|
@ -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(
|
||||
|
|
6
packages/docusaurus/src/deps.d.ts
vendored
6
packages/docusaurus/src/deps.d.ts
vendored
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
"
|
||||
`;
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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<typeof handleBrokenLinks>[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: '/page1'},
|
||||
{path: '/nested/', routes: [{path: '/nested/page2'}]},
|
||||
],
|
||||
},
|
||||
{
|
||||
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',
|
||||
],
|
||||
'/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.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.
|
||||
|
||||
Exhaustive list of all broken links found:
|
||||
- Broken link on source page path = /page1:
|
||||
-> linking to /brokenLink
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
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('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.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('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)
|
||||
"
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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[]};
|
||||
|
||||
function getPageBrokenLinks({
|
||||
// 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 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,
|
||||
}),
|
||||
);
|
||||
|
||||
return _.pickBy(allBrokenLinks, (brokenLinks) => brokenLinks.length > 0);
|
||||
}
|
||||
|
||||
function getBrokenLinksErrorMessage(allBrokenLinks: {
|
||||
[location: string]: BrokenLink[];
|
||||
}): string | undefined {
|
||||
if (Object.keys(allBrokenLinks).length === 0) {
|
||||
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';
|
||||
|
||||
const anchorMessage =
|
||||
brokenLinks.length > 0
|
||||
? `- Broken ${type} on source page path = ${pagePath}:
|
||||
-> linking to ${brokenLinks
|
||||
.map(brokenLinkMessage)
|
||||
.join('\n -> linking to ')}`
|
||||
: '';
|
||||
|
||||
return `${anchorMessage}`;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
if (anchorBrokenLinks.length > 0) {
|
||||
brokenAnchors[pathname] = anchorBrokenLinks;
|
||||
}
|
||||
});
|
||||
|
||||
for (const file of filePathsToTry) {
|
||||
if (await isExistingFile(file)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
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<void> {
|
||||
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});
|
||||
}
|
||||
|
|
|
@ -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<DocusaurusConfig>({
|
|||
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),
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -16,6 +16,7 @@ exports[`getDocusaurusAliases returns appropriate webpack aliases 1`] = `
|
|||
"@docusaurus/renderRoutes": "<PROJECT_ROOT>/packages/docusaurus/src/client/exports/renderRoutes.ts",
|
||||
"@docusaurus/router": "<PROJECT_ROOT>/packages/docusaurus/src/client/exports/router.ts",
|
||||
"@docusaurus/useBaseUrl": "<PROJECT_ROOT>/packages/docusaurus/src/client/exports/useBaseUrl.ts",
|
||||
"@docusaurus/useBrokenLinks": "<PROJECT_ROOT>/packages/docusaurus/src/client/exports/useBrokenLinks.ts",
|
||||
"@docusaurus/useDocusaurusContext": "<PROJECT_ROOT>/packages/docusaurus/src/client/exports/useDocusaurusContext.ts",
|
||||
"@docusaurus/useGlobalData": "<PROJECT_ROOT>/packages/docusaurus/src/client/exports/useGlobalData.ts",
|
||||
"@docusaurus/useIsBrowser": "<PROJECT_ROOT>/packages/docusaurus/src/client/exports/useIsBrowser.ts",
|
||||
|
|
|
@ -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)
|
||||
```
|
||||
|
||||
<BrowserWindow>
|
||||
|
||||
- [`/pure-html`](/pure-html)
|
||||
- [`pathname:///pure-html`](pathname:///pure-html)
|
||||
|
||||
</BrowserWindow>
|
||||
|
||||
:::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"
|
||||
|
|
|
@ -188,7 +188,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
|
||||
|
||||
|
@ -196,13 +196,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}
|
||||
|
||||
|
|
|
@ -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` | <code>false \| string</code> | `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` | <a href="#SidebarGenerator"><code>SidebarGenerator</code></a> | _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` | <code>boolean \|</code> <a href="#PrefixParser"><code>PrefixParser</code></a> | _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` | <a href="#SidebarGenerator"><code>SidebarGenerator</code></a> | _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` | <code>boolean \|</code> <a href="#PrefixParser"><code>PrefixParser</code></a> | _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` | <code>string \| null</code> | 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` | <code>string \| null</code> | 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` | <code>string \| null</code> | 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 `<meta name="description" content="..."/>` and `<meta property="og:description" content="..."/>` in `<head>`, used by search engines. |
|
||||
|
|
|
@ -180,7 +180,7 @@ By default, the files are written in `website/i18n/<defaultLocale>/...`.
|
|||
|
||||
### `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 |
|
||||
| --- | --- | --- |
|
||||
|
|
|
@ -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>`](#link) component will collect links for you
|
||||
- the `@theme/Heading` (used for Markdown headings) will collect anchors
|
||||
|
||||
Use `useBrokenLinks()` if you implement your own `<Heading>` or `<Link>` 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 <h2 id={id}>Heading</h2>;
|
||||
}
|
||||
```
|
||||
|
||||
```js title="MyLink.js"
|
||||
import useBrokenLinks from '@docusaurus/useBrokenLinks';
|
||||
|
||||
export default function MyLink({targetLink, ...props}): JSX.Element {
|
||||
const brokenLinks = useBrokenLinks();
|
||||
|
||||
brokenLinks.collectLink(targetLink);
|
||||
|
||||
return <a href={targetLink}>Link</a>;
|
||||
}
|
||||
```
|
||||
|
||||
## Functions {#functions}
|
||||
|
||||
### `interpolate` {#interpolate-1}
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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)**.
|
||||
|
||||
:::
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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)
|
||||
```
|
||||
|
||||
<BrowserWindow>
|
||||
|
||||
- [`/pure-html`](/pure-html)
|
||||
- [`pathname:///pure-html`](pathname:///pure-html)
|
||||
|
||||
</BrowserWindow>
|
||||
|
||||
:::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"
|
||||
|
|
|
@ -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` | <code>false \| string</code> | `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` | <a href="#SidebarGenerator"><code>SidebarGenerator</code></a> | _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` | <code>boolean \|</code> <a href="#PrefixParser"><code>PrefixParser</code></a> | _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` | <a href="#SidebarGenerator"><code>SidebarGenerator</code></a> | _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` | <code>boolean \|</code> <a href="#PrefixParser"><code>PrefixParser</code></a> | _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` | <code>string \| null</code> | 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` | <code>string \| null</code> | 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 `<meta name="description" content="..."/>` and `<meta property="og:description" content="..."/>` in `<head>`, used by search engines. |
|
||||
|
|
|
@ -176,7 +176,7 @@ By default, the files are written in `website/i18n/<defaultLocale>/...`.
|
|||
|
||||
### `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 |
|
||||
| --- | --- | --- |
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -84,7 +84,7 @@ Since all doc files are parsed using MDX, anything that looks like HTML is actua
|
|||
<span style={{backgroundColor: 'red'}}>Foo</span>
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
|
|
|
@ -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)**.
|
||||
|
||||
:::
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
```
|
||||
|
||||
<BrowserWindow>
|
||||
|
||||
- [`/pure-html`](/pure-html)
|
||||
- [`pathname:///pure-html`](pathname:///pure-html)
|
||||
|
||||
</BrowserWindow>
|
||||
|
||||
:::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"
|
||||
|
|
|
@ -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` | <code>false \| string</code> | `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` | <a href="#SidebarGenerator"><code>SidebarGenerator</code></a> | _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` | <code>boolean \|</code> <a href="#PrefixParser"><code>PrefixParser</code></a> | _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` | <a href="#SidebarGenerator"><code>SidebarGenerator</code></a> | _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` | <code>boolean \|</code> <a href="#PrefixParser"><code>PrefixParser</code></a> | _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` | <code>string \| null</code> | 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` | <code>string \| null</code> | 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` | <code>string \| null</code> | 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 `<meta name="description" content="..."/>` and `<meta property="og:description" content="..."/>` in `<head>`, used by search engines. |
|
||||
|
|
|
@ -177,7 +177,7 @@ By default, the files are written in `website/i18n/<defaultLocale>/...`.
|
|||
|
||||
### `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 |
|
||||
| --- | --- | --- |
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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)**.
|
||||
|
||||
:::
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue