From 37edf2604895fd7c87636eb163e743ed56585949 Mon Sep 17 00:00:00 2001 From: ozakione <29860391+OzakIOne@users.noreply.github.com> Date: Fri, 10 Nov 2023 11:02:04 +0100 Subject: [PATCH 01/69] wip: broken anchors warning --- .../src/index.d.ts | 15 +++++++ .../src/theme/Heading/index.tsx | 11 +++++ .../src/client/AnchorsCollector.tsx | 44 +++++++++++++++++++ .../src/client/exports/useAnchor.ts | 19 ++++++++ .../docusaurus/src/client/serverEntry.tsx | 36 ++++++++++++++- 5 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 packages/docusaurus/src/client/AnchorsCollector.tsx create mode 100644 packages/docusaurus/src/client/exports/useAnchor.ts diff --git a/packages/docusaurus-module-type-aliases/src/index.d.ts b/packages/docusaurus-module-type-aliases/src/index.d.ts index 09c3f2fb3d59..0c44d0382bb9 100644 --- a/packages/docusaurus-module-type-aliases/src/index.d.ts +++ b/packages/docusaurus-module-type-aliases/src/index.d.ts @@ -260,6 +260,21 @@ declare module '@docusaurus/useRouteContext' { export default function useRouteContext(): PluginRouteContext; } +declare module '@docusaurus/useAnchor' { + export type AnchorsCollector = { + collectAnchor: (link: string) => void; + }; + + export type StatefulAnchorsCollector = AnchorsCollector & { + getCollectedAnchors: () => string[]; + }; + + export default function useAnchor(): [ + AnchorsCollector, + () => StatefulAnchorsCollector, + ]; +} + declare module '@docusaurus/useIsBrowser' { export default function useIsBrowser(): boolean; } diff --git a/packages/docusaurus-theme-classic/src/theme/Heading/index.tsx b/packages/docusaurus-theme-classic/src/theme/Heading/index.tsx index 9ca1bc3f0db9..38e42543abc6 100644 --- a/packages/docusaurus-theme-classic/src/theme/Heading/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/Heading/index.tsx @@ -10,11 +10,13 @@ import clsx from 'clsx'; import {translate} from '@docusaurus/Translate'; import {useThemeConfig} from '@docusaurus/theme-common'; import Link from '@docusaurus/Link'; +import useAnchor from '@docusaurus/useAnchor'; import type {Props} from '@theme/Heading'; import styles from './styles.module.css'; export default function Heading({as: As, id, ...props}: Props): JSX.Element { + const [anchorsCollector, createAnchorList] = useAnchor(); const { navbar: {hideOnScroll}, } = useThemeConfig(); @@ -23,6 +25,15 @@ export default function Heading({as: As, id, ...props}: Props): JSX.Element { return ; } + const list = createAnchorList(); + + anchorsCollector.collectAnchor(id); + // console.log('Heading id:'); + // console.log(id); + list.collectAnchor(id); + // console.log('Heading anchor list:'); + // console.log(list.getCollectedAnchors()); + const anchorTitle = translate( { id: 'theme.common.headingLinkTitle', diff --git a/packages/docusaurus/src/client/AnchorsCollector.tsx b/packages/docusaurus/src/client/AnchorsCollector.tsx new file mode 100644 index 000000000000..77928a8e24d2 --- /dev/null +++ b/packages/docusaurus/src/client/AnchorsCollector.tsx @@ -0,0 +1,44 @@ +/** + * 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 {AnchorsCollector} from '@docusaurus/useAnchor'; + +type StatefulAnchorsCollector = AnchorsCollector & { + getCollectedAnchors: () => string[]; +}; + +export const createStatefulAnchorsCollector = (): StatefulAnchorsCollector => { + // Set to dedup, as it's not useful to collect multiple times the same link + const allAnchors = new Set(); + return { + collectAnchor: (link: string): void => { + allAnchors.add(link); + }, + getCollectedAnchors: (): string[] => [...allAnchors], + }; +}; + +const Context = React.createContext({ + collectAnchor: () => { + // No-op for client. We only use the broken links checker server-side. + }, +}); + +export const useAnchorsCollector = (): AnchorsCollector => useContext(Context); + +export function AnchorsCollectorProvider({ + children, + anchorsCollector, +}: { + children: ReactNode; + anchorsCollector: AnchorsCollector; +}): JSX.Element { + return ( + {children} + ); +} diff --git a/packages/docusaurus/src/client/exports/useAnchor.ts b/packages/docusaurus/src/client/exports/useAnchor.ts new file mode 100644 index 000000000000..caa26f26e395 --- /dev/null +++ b/packages/docusaurus/src/client/exports/useAnchor.ts @@ -0,0 +1,19 @@ +/** + * 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 { + useAnchorsCollector, + createStatefulAnchorsCollector, +} from '../AnchorsCollector'; +import type {AnchorsCollector} from '@docusaurus/useAnchor'; + +export default function useAnchor(): [ + AnchorsCollector, + () => AnchorsCollector, +] { + return [useAnchorsCollector(), createStatefulAnchorsCollector]; +} diff --git a/packages/docusaurus/src/client/serverEntry.tsx b/packages/docusaurus/src/client/serverEntry.tsx index 2d67558926e7..48c562189301 100644 --- a/packages/docusaurus/src/client/serverEntry.tsx +++ b/packages/docusaurus/src/client/serverEntry.tsx @@ -19,6 +19,10 @@ import {minify} from 'html-minifier-terser'; import {renderStaticApp} from './serverRenderer'; import preload from './preload'; import App from './App'; +import { + AnchorsCollectorProvider, + createStatefulAnchorsCollector, +} from './AnchorsCollector'; import { createStatefulLinksCollector, LinksCollectorProvider, @@ -98,13 +102,17 @@ async function doRender(locals: Locals & {path: string}) { const linksCollector = createStatefulLinksCollector(); + const anchorsCollector = createStatefulAnchorsCollector(); + const app = ( // @ts-expect-error: we are migrating away from react-loadable anyways modules.add(moduleName)}> - + + + @@ -114,6 +122,32 @@ async function doRender(locals: Locals & {path: string}) { const appHtml = await renderStaticApp(app); onLinksCollected(location, linksCollector.getCollectedLinks()); + // console.log('Collected anchors'); + // console.log(anchorsCollector.getCollectedAnchors()); + // fs.writeFile( + // 'anchors.json', + // JSON.stringify(anchorsCollector.getCollectedAnchors()), + // (err) => { + // if (err) { + // throw err; + // } + // console.log('Saved!'); + // }, + // ); + + // console.log('Collected links'); + // console.log(linksCollector.getCollectedLinks()); + // fs.writeFile( + // 'links.json', + // JSON.stringify(linksCollector.getCollectedLinks()), + // (err) => { + // if (err) { + // throw err; + // } + // console.log('Saved!'); + // }, + // ); + const {helmet} = helmetContext as FilledContext; const htmlAttributes = helmet.htmlAttributes.toString(); const bodyAttributes = helmet.bodyAttributes.toString(); From 7c83b68abdb4bda77a4ba8e2cacbd2f6d8d744ce Mon Sep 17 00:00:00 2001 From: ozakione <29860391+OzakIOne@users.noreply.github.com> Date: Fri, 10 Nov 2023 11:35:00 +0100 Subject: [PATCH 02/69] wip: refactor --- .../docusaurus/src/client/serverEntry.tsx | 6 +- packages/docusaurus/src/commands/build.ts | 8 +- packages/docusaurus/src/deps.d.ts | 7 +- packages/docusaurus/src/server/brokenLinks.ts | 107 ++++++------------ 4 files changed, 53 insertions(+), 75 deletions(-) diff --git a/packages/docusaurus/src/client/serverEntry.tsx b/packages/docusaurus/src/client/serverEntry.tsx index 48c562189301..1d2c60099c03 100644 --- a/packages/docusaurus/src/client/serverEntry.tsx +++ b/packages/docusaurus/src/client/serverEntry.tsx @@ -120,7 +120,11 @@ async function doRender(locals: Locals & {path: string}) { ); const appHtml = await renderStaticApp(app); - onLinksCollected(location, linksCollector.getCollectedLinks()); + onLinksCollected( + location, + linksCollector.getCollectedLinks(), + anchorsCollector.getCollectedAnchors(), + ); // console.log('Collected anchors'); // console.log(anchorsCollector.getCollectedAnchors()); diff --git a/packages/docusaurus/src/commands/build.ts b/packages/docusaurus/src/commands/build.ts index 3b20912150ec..8fce0b0c642f 100644 --- a/packages/docusaurus/src/commands/build.ts +++ b/packages/docusaurus/src/commands/build.ts @@ -180,13 +180,15 @@ async function buildLocale({ }, ); - const allCollectedLinks: {[location: string]: string[]} = {}; + const allCollectedLinks: { + [location: 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) => { + allCollectedLinks[staticPagePath] = {links, anchors}; }, onHeadTagsCollected: (staticPagePath, tags) => { headTags[staticPagePath] = tags; diff --git a/packages/docusaurus/src/deps.d.ts b/packages/docusaurus/src/deps.d.ts index 49bca18d06f9..915dfae1670f 100644 --- a/packages/docusaurus/src/deps.d.ts +++ b/packages/docusaurus/src/deps.d.ts @@ -42,7 +42,12 @@ declare module '@slorber/static-site-generator-webpack-plugin' { headTags: string; preBodyTags: string; postBodyTags: string; - onLinksCollected: (staticPagePath: string, links: string[]) => void; + // TODO transform arguments into object + onLinksCollected: ( + staticPagePath: string, + links: string[], + anchors: string[], + ) => void; onHeadTagsCollected: ( staticPagePath: string, tags: HelmetServerState, diff --git a/packages/docusaurus/src/server/brokenLinks.ts b/packages/docusaurus/src/server/brokenLinks.ts index f443a4659cf9..7e923d3fc6dd 100644 --- a/packages/docusaurus/src/server/brokenLinks.ts +++ b/packages/docusaurus/src/server/brokenLinks.ts @@ -5,13 +5,13 @@ * LICENSE file in the root directory of this source tree. */ -import fs from 'fs-extra'; -import path from 'path'; +// TODO Remove this +/* eslint-disable @typescript-eslint/no-unused-vars */ + 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 {resolvePathname} from '@docusaurus/utils'; import {getAllFinalRoutes} from './utils'; import type {RouteConfig, ReportingSeverity} from '@docusaurus/types'; @@ -70,13 +70,19 @@ function getAllBrokenLinks({ allCollectedLinks, routes, }: { - allCollectedLinks: {[location: string]: string[]}; + allCollectedLinks: {[location: string]: {links: string[]; anchors: string[]}}; routes: RouteConfig[]; }): {[location: string]: BrokenLink[]} { const filteredRoutes = filterIntermediateRoutes(routes); - const allBrokenLinks = _.mapValues(allCollectedLinks, (pageLinks, pagePath) => - getPageBrokenLinks({pageLinks, pagePath, routes: filteredRoutes}), + const allBrokenLinks = _.mapValues( + allCollectedLinks, + (pageCollectedData, pagePath) => + getPageBrokenLinks({ + pageLinks: pageCollectedData.links, + pagePath, + routes: filteredRoutes, + }), ); return _.pickBy(allBrokenLinks, (brokenLinks) => brokenLinks.length > 0); @@ -154,69 +160,25 @@ ${Object.entries(allBrokenLinks) `; } -async function isExistingFile(filePath: string) { - try { - return (await fs.stat(filePath)).isFile(); - } catch { - return false; - } -} - -// 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, -}: { +export async function handleBrokenLinks(params: { + allCollectedLinks: {[location: string]: {links: string[]; anchors: string[]}}; + onBrokenLinks: ReportingSeverity; + routes: RouteConfig[]; 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)}`, '/'), - ); - - // -> /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'), - ); - } - - for (const file of filePathsToTry) { - if (await isExistingFile(file)) { - return true; - } - } - return false; - } - - return combinePromises( - _.mapValues(allCollectedLinks, async (links) => - ( - await Promise.all( - links.map(async (link) => ((await linkFileExists(link)) ? '' : link)), - ) - ).filter(Boolean), - ), - ); +}): Promise { + await handlePathBrokenLinks(params); + await handleAnchorBrokenLinks(params); } -export async function handleBrokenLinks({ +async function handlePathBrokenLinks({ allCollectedLinks, onBrokenLinks, routes, baseUrl, outDir, }: { - allCollectedLinks: {[location: string]: string[]}; + allCollectedLinks: {[location: string]: {links: string[]; anchors: string[]}}; onBrokenLinks: ReportingSeverity; routes: RouteConfig[]; baseUrl: string; @@ -226,17 +188,8 @@ export async function handleBrokenLinks({ 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, + allCollectedLinks, routes, }); @@ -245,3 +198,17 @@ export async function handleBrokenLinks({ logger.report(onBrokenLinks)(errorMessage); } } + +async function handleAnchorBrokenLinks({ + allCollectedLinks, + onBrokenLinks, + routes, + baseUrl, + outDir, +}: { + allCollectedLinks: {[location: string]: {links: string[]; anchors: string[]}}; + onBrokenLinks: ReportingSeverity; + routes: RouteConfig[]; + baseUrl: string; + outDir: string; +}): Promise {} From 76a3a55eff048b7c4d3446ecc29149ce544a9bee Mon Sep 17 00:00:00 2001 From: ozakione <29860391+OzakIOne@users.noreply.github.com> Date: Fri, 10 Nov 2023 11:44:59 +0100 Subject: [PATCH 03/69] wip: refactor --- packages/docusaurus/src/server/brokenLinks.ts | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/packages/docusaurus/src/server/brokenLinks.ts b/packages/docusaurus/src/server/brokenLinks.ts index 7e923d3fc6dd..7605bb43168a 100644 --- a/packages/docusaurus/src/server/brokenLinks.ts +++ b/packages/docusaurus/src/server/brokenLinks.ts @@ -18,6 +18,7 @@ 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! @@ -39,7 +40,8 @@ function getPageBrokenLinks({ // using `matchRoutes`. `resolvePathname` is used internally by React Router function resolveLink(link: string) { const resolvedLink = resolvePathname(onlyPathname(link), pagePath); - return {link, resolvedLink}; + // TODO change anchor value + return {link, resolvedLink, anchor: false}; } function isBrokenLink(link: string) { @@ -168,7 +170,6 @@ export async function handleBrokenLinks(params: { outDir: string; }): Promise { await handlePathBrokenLinks(params); - await handleAnchorBrokenLinks(params); } async function handlePathBrokenLinks({ @@ -198,17 +199,3 @@ async function handlePathBrokenLinks({ logger.report(onBrokenLinks)(errorMessage); } } - -async function handleAnchorBrokenLinks({ - allCollectedLinks, - onBrokenLinks, - routes, - baseUrl, - outDir, -}: { - allCollectedLinks: {[location: string]: {links: string[]; anchors: string[]}}; - onBrokenLinks: ReportingSeverity; - routes: RouteConfig[]; - baseUrl: string; - outDir: string; -}): Promise {} From 2ec40922952d119b2399654a7e0a95d003d2060d Mon Sep 17 00:00:00 2001 From: ozakione <29860391+OzakIOne@users.noreply.github.com> Date: Mon, 20 Nov 2023 13:04:23 +0100 Subject: [PATCH 04/69] wip: refactor --- .../src/index.d.ts | 1 + .../src/theme/Heading/index.tsx | 7 +- packages/docusaurus-types/src/config.d.ts | 7 + .../docusaurus/src/client/serverEntry.tsx | 26 --- packages/docusaurus/src/commands/build.ts | 2 + .../src/server/__tests__/brokenLinks.test.ts | 165 +++++++++++------- packages/docusaurus/src/server/brokenLinks.ts | 59 ++++++- .../docusaurus/src/server/configValidation.ts | 5 + 8 files changed, 167 insertions(+), 105 deletions(-) diff --git a/packages/docusaurus-module-type-aliases/src/index.d.ts b/packages/docusaurus-module-type-aliases/src/index.d.ts index 0c44d0382bb9..64a4c3e8b012 100644 --- a/packages/docusaurus-module-type-aliases/src/index.d.ts +++ b/packages/docusaurus-module-type-aliases/src/index.d.ts @@ -269,6 +269,7 @@ declare module '@docusaurus/useAnchor' { getCollectedAnchors: () => string[]; }; + // useAnchorCollector export default function useAnchor(): [ AnchorsCollector, () => StatefulAnchorsCollector, diff --git a/packages/docusaurus-theme-classic/src/theme/Heading/index.tsx b/packages/docusaurus-theme-classic/src/theme/Heading/index.tsx index 38e42543abc6..fb2e6d1f69c9 100644 --- a/packages/docusaurus-theme-classic/src/theme/Heading/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/Heading/index.tsx @@ -27,12 +27,11 @@ export default function Heading({as: As, id, ...props}: Props): JSX.Element { const list = createAnchorList(); + // ! should not be called 2 times, not a problem because we use + // Set but still must be removed anchorsCollector.collectAnchor(id); - // console.log('Heading id:'); - // console.log(id); + list.collectAnchor(id); - // console.log('Heading anchor list:'); - // console.log(list.getCollectedAnchors()); const anchorTitle = translate( { diff --git a/packages/docusaurus-types/src/config.d.ts b/packages/docusaurus-types/src/config.d.ts index 3a7bb99ae743..e95c6768f10b 100644 --- a/packages/docusaurus-types/src/config.d.ts +++ b/packages/docusaurus-types/src/config.d.ts @@ -143,6 +143,13 @@ export type DocusaurusConfig = { * @default "throw" */ onBrokenLinks: ReportingSeverity; + /** + * The behavior of Docusaurus when it detects any broken link. + * + * @see // TODO + * @default "warn" + */ + onBrokenAnchors: ReportingSeverity; /** * The behavior of Docusaurus when it detects any broken markdown link. * diff --git a/packages/docusaurus/src/client/serverEntry.tsx b/packages/docusaurus/src/client/serverEntry.tsx index 1d2c60099c03..1778f464fbe6 100644 --- a/packages/docusaurus/src/client/serverEntry.tsx +++ b/packages/docusaurus/src/client/serverEntry.tsx @@ -126,32 +126,6 @@ async function doRender(locals: Locals & {path: string}) { anchorsCollector.getCollectedAnchors(), ); - // console.log('Collected anchors'); - // console.log(anchorsCollector.getCollectedAnchors()); - // fs.writeFile( - // 'anchors.json', - // JSON.stringify(anchorsCollector.getCollectedAnchors()), - // (err) => { - // if (err) { - // throw err; - // } - // console.log('Saved!'); - // }, - // ); - - // console.log('Collected links'); - // console.log(linksCollector.getCollectedLinks()); - // fs.writeFile( - // 'links.json', - // JSON.stringify(linksCollector.getCollectedLinks()), - // (err) => { - // if (err) { - // throw err; - // } - // console.log('Saved!'); - // }, - // ); - const {helmet} = helmetContext as FilledContext; const htmlAttributes = helmet.htmlAttributes.toString(); const bodyAttributes = helmet.bodyAttributes.toString(); diff --git a/packages/docusaurus/src/commands/build.ts b/packages/docusaurus/src/commands/build.ts index 8fce0b0c642f..e2fc00197a6a 100644 --- a/packages/docusaurus/src/commands/build.ts +++ b/packages/docusaurus/src/commands/build.ts @@ -154,6 +154,7 @@ async function buildLocale({ siteConfig: { baseUrl, onBrokenLinks, + onBrokenAnchors, staticDirectories: staticDirectoriesOption, }, routes, @@ -293,6 +294,7 @@ async function buildLocale({ allCollectedLinks, routes, onBrokenLinks, + onBrokenAnchors, outDir, baseUrl, }); diff --git a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts index 40e76ed45b7a..af8cbbfe6e18 100644 --- a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts +++ b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts @@ -50,93 +50,121 @@ describe('handleBrokenLinks', () => { 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': [ + '/docs/good doc with space': { + links: [ + // 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': [ + anchors: [], + }, + '/docs/goodDoc': { + links: [ + // 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': [ + anchors: [], + }, + '/community': { + links: [ + // Good links '/docs/goodDoc', '/docs/anotherGoodDoc#someHash', './docs/goodDoc#someHash', './docs/anotherGoodDoc', + // Bad links + '/someNonExistentDoc1', + '/badLink2', + './badLink3', ], - '/page1': [ + anchors: [], + }, + '/page1': { + links: [ + link1, linkToHtmlFile1, linkToJavadoc1, linkToHtmlFile2, linkToJavadoc3, linkToJavadoc4, + linkToEmptyFolder1, // Not filtered! + ], + anchors: [], + }, + '/page2': { + links: [ + link2, + linkToEmptyFolder2, // Not filtered! + linkToJavadoc2, + link3, + linkToJavadoc3, + linkToZipFile, ], + anchors: [], + }, + }; + + 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': { + links: [ + './another%20good%20doc%20with%20space', + './weird%20but%20good', + ], + anchors: [], + }, + '/docs/goodDoc': { + links: [ + './anotherGoodDoc#someHash', + '/docs/anotherGoodDoc?someQueryString=true#someHash', + '../docs/anotherGoodDoc?someQueryString=true', + '../docs/anotherGoodDoc#someHash', + ], + anchors: ['someHash'], + }, + '/community': { + links: [ + '/docs/goodDoc', + '/docs/anotherGoodDoc#someHash', + './docs/goodDoc#someHash', + './docs/anotherGoodDoc', + ], + anchors: [], + }, + '/page1': { + links: [ + linkToHtmlFile1, + linkToJavadoc1, + linkToHtmlFile2, + linkToJavadoc3, + linkToJavadoc4, + ], + anchors: [], + }, }; await handleBrokenLinks({ allCollectedLinks: allCollectedCorrectLinks, onBrokenLinks: 'warn', + onBrokenAnchors: 'throw', routes, baseUrl: '/', outDir, @@ -149,6 +177,7 @@ describe('handleBrokenLinks', () => { handleBrokenLinks({ allCollectedLinks, onBrokenLinks: 'throw', + onBrokenAnchors: 'throw', routes, baseUrl: '/', outDir, @@ -163,6 +192,7 @@ describe('handleBrokenLinks', () => { await handleBrokenLinks({ allCollectedLinks, onBrokenLinks: 'ignore', + onBrokenAnchors: 'throw', routes, baseUrl: '/', outDir, @@ -172,20 +202,21 @@ describe('handleBrokenLinks', () => { }); it('reports frequent broken links', async () => { - Object.values(allCollectedLinks).forEach((links) => + 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', - ), - ); + ); + }); await expect(() => handleBrokenLinks({ allCollectedLinks, onBrokenLinks: 'throw', + onBrokenAnchors: 'throw', routes, baseUrl: '/', outDir, diff --git a/packages/docusaurus/src/server/brokenLinks.ts b/packages/docusaurus/src/server/brokenLinks.ts index 7605bb43168a..4745c517f516 100644 --- a/packages/docusaurus/src/server/brokenLinks.ts +++ b/packages/docusaurus/src/server/brokenLinks.ts @@ -29,10 +29,12 @@ function onlyPathname(link: string) { function getPageBrokenLinks({ pagePath, pageLinks, + pageAnchors, routes, }: { pagePath: string; pageLinks: string[]; + pageAnchors: string[]; routes: RouteConfig[]; }): BrokenLink[] { // ReactRouter is able to support links like ./../somePath but `matchRoutes` @@ -40,11 +42,10 @@ function getPageBrokenLinks({ // using `matchRoutes`. `resolvePathname` is used internally by React Router function resolveLink(link: string) { const resolvedLink = resolvePathname(onlyPathname(link), pagePath); - // TODO change anchor value - return {link, resolvedLink, anchor: false}; + return resolvedLink; } - function isBrokenLink(link: string) { + function isPathBrokenLink(link: string) { const matchedRoutes = [link, decodeURI(link)] // @ts-expect-error: React router types RouteConfig with an actual React // component, but we load route components with string paths. @@ -54,7 +55,25 @@ function getPageBrokenLinks({ return matchedRoutes.length === 0; } - return pageLinks.map(resolveLink).filter((l) => isBrokenLink(l.resolvedLink)); + function isAnchorBrokenLink(link: string) { + console.log('link', link); + console.log('pageAnchors', pageAnchors); + const urlHash = link.split('#')[1] ?? ''; + + return !pageAnchors.includes(urlHash); + } + + const brokenLinks = pageLinks.flatMap((pageLink) => { + const resolvedLink = resolveLink(pageLink); + if (isPathBrokenLink(resolvedLink)) { + return [{link: pageLink, resolvedLink, anchor: false}]; + } + if (isAnchorBrokenLink(pageLink)) { + return [{link: pageLink, resolvedLink, anchor: true}]; + } + return []; + }); + return brokenLinks; } /** @@ -82,6 +101,7 @@ function getAllBrokenLinks({ (pageCollectedData, pagePath) => getPageBrokenLinks({ pageLinks: pageCollectedData.links, + pageAnchors: pageCollectedData.anchors, pagePath, routes: filteredRoutes, }), @@ -108,11 +128,28 @@ function getBrokenLinksErrorMessage(allBrokenLinks: { pagePath: string, brokenLinks: BrokenLink[], ): string { - return ` -- On source page path = ${pagePath}: - -> linking to ${brokenLinks + const [pathBrokenLinks, anchorBrokenLinks] = _.partition( + brokenLinks, + 'anchor', + ); + + const pathMessage = + pathBrokenLinks.length > 0 + ? `- On source page path = ${pagePath}: + -> linking to ${pathBrokenLinks + .map(brokenLinkMessage) + .join('\n -> linking to ')}` + : ''; + + const anchorMessage = + anchorBrokenLinks.length > 0 + ? `- Anchor On source page path = ${pagePath}: + -> linking to ${anchorBrokenLinks .map(brokenLinkMessage) - .join('\n -> linking to ')}`; + .join('\n -> linking to ')}` + : ''; + + return `${pathMessage}${anchorMessage}`; } /** @@ -165,6 +202,7 @@ ${Object.entries(allBrokenLinks) export async function handleBrokenLinks(params: { allCollectedLinks: {[location: string]: {links: string[]; anchors: string[]}}; onBrokenLinks: ReportingSeverity; + onBrokenAnchors: ReportingSeverity; routes: RouteConfig[]; baseUrl: string; outDir: string; @@ -175,12 +213,14 @@ export async function handleBrokenLinks(params: { async function handlePathBrokenLinks({ allCollectedLinks, onBrokenLinks, + onBrokenAnchors, routes, baseUrl, outDir, }: { allCollectedLinks: {[location: string]: {links: string[]; anchors: string[]}}; onBrokenLinks: ReportingSeverity; + onBrokenAnchors: ReportingSeverity; routes: RouteConfig[]; baseUrl: string; outDir: string; @@ -188,6 +228,9 @@ async function handlePathBrokenLinks({ if (onBrokenLinks === 'ignore') { return; } + if (onBrokenAnchors === 'ignore') { + return; + } const allBrokenLinks = getAllBrokenLinks({ allCollectedLinks, diff --git a/packages/docusaurus/src/server/configValidation.ts b/packages/docusaurus/src/server/configValidation.ts index 3f9de2ce6807..99ade2ffa59c 100644 --- a/packages/docusaurus/src/server/configValidation.ts +++ b/packages/docusaurus/src/server/configValidation.ts @@ -28,6 +28,7 @@ export const DEFAULT_CONFIG: Pick< DocusaurusConfig, | 'i18n' | 'onBrokenLinks' + | 'onBrokenAnchors' | 'onBrokenMarkdownLinks' | 'onDuplicateRoutes' | 'plugins' @@ -48,6 +49,7 @@ export const DEFAULT_CONFIG: Pick< > = { i18n: DEFAULT_I18N_CONFIG, onBrokenLinks: 'throw', + onBrokenAnchors: 'warn', onBrokenMarkdownLinks: 'warn', onDuplicateRoutes: 'warn', plugins: [], @@ -202,6 +204,9 @@ export const ConfigSchema = Joi.object({ onBrokenLinks: Joi.string() .equal('ignore', 'log', 'warn', 'throw') .default(DEFAULT_CONFIG.onBrokenLinks), + onBrokenAnchors: Joi.string() + .equal('ignore', 'log', 'warn', 'throw') + .default(DEFAULT_CONFIG.onBrokenAnchors), onBrokenMarkdownLinks: Joi.string() .equal('ignore', 'log', 'warn', 'throw') .default(DEFAULT_CONFIG.onBrokenMarkdownLinks), From f60242a3059840ed7ac3e6eec43e624a3ae781fb Mon Sep 17 00:00:00 2001 From: ozakione <29860391+OzakIOne@users.noreply.github.com> Date: Tue, 21 Nov 2023 14:29:31 +0100 Subject: [PATCH 05/69] wip: refactor --- .../__snapshots__/brokenLinks.test.ts.snap | 48 ++++++++++++------- .../src/server/__tests__/brokenLinks.test.ts | 24 +++++----- packages/docusaurus/src/server/brokenLinks.ts | 29 +++++++---- 3 files changed, 63 insertions(+), 38 deletions(-) diff --git a/packages/docusaurus/src/server/__tests__/__snapshots__/brokenLinks.test.ts.snap b/packages/docusaurus/src/server/__tests__/__snapshots__/brokenLinks.test.ts.snap index 8aa3a3e837dc..2843ed0f7aab 100644 --- a/packages/docusaurus/src/server/__tests__/__snapshots__/brokenLinks.test.ts.snap +++ b/packages/docusaurus/src/server/__tests__/__snapshots__/brokenLinks.test.ts.snap @@ -1,44 +1,52 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`handleBrokenLinks reports all broken links 1`] = ` -"Docusaurus found broken links! +"Docusaurus found broken links / anchors! 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. +Note: it's possible to ignore broken links with the 'onBrokenLinks' or 'onBrokenAnchors' Docusaurus configuration, and let the build pass. Exhaustive list of all broken links found: - -- On source page path = /docs/good doc with space: +- Broken link 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: +- Broken link 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: +- Broken link on source page path = /community: -> linking to /someNonExistentDoc1 -> linking to /badLink2 -> linking to ./badLink3 (resolved as: /badLink3) -- On source page path = /page1: +- Broken link on source page path = /page1: -> linking to /link1 + -> linking to /files/hey.html + -> linking to /javadoc + -> linking to /files/hey + -> linking to /javadoc/index.html + -> linking to /javadoc/index.html#foo (resolved as: /javadoc/index.html) -> linking to /emptyFolder -- On source page path = /page2: +- Broken link on source page path = /page2: -> linking to /docs/link2 -> linking to /emptyFolder/ + -> linking to /javadoc/ -> linking to /hey/link3 + -> linking to /javadoc/index.html + -> linking to /files/file.zip + " `; exports[`handleBrokenLinks reports frequent broken links 1`] = ` -"Docusaurus found broken links! +"Docusaurus found broken links / anchors! 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. +Note: it's possible to ignore broken links with the 'onBrokenLinks' or 'onBrokenAnchors' 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? @@ -48,14 +56,13 @@ Frequent broken links are linking to: - ./maybe-not Exhaustive list of all broken links found: - -- On source page path = /docs/good doc with space: +- Broken link 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: +- Broken link 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) @@ -63,24 +70,33 @@ Exhaustive list of all broken links found: -> linking to /frequent -> linking to ./maybe-not (resolved as: /docs/maybe-not) -- On source page path = /community: +- Broken link 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: +- Broken link on source page path = /page1: -> linking to /link1 + -> linking to /files/hey.html + -> linking to /javadoc + -> linking to /files/hey + -> linking to /javadoc/index.html + -> linking to /javadoc/index.html#foo (resolved as: /javadoc/index.html) -> linking to /emptyFolder -> linking to /frequent -> linking to ./maybe-not (resolved as: /maybe-not) -- On source page path = /page2: +- Broken link on source page path = /page2: -> linking to /docs/link2 -> linking to /emptyFolder/ + -> linking to /javadoc/ -> linking to /hey/link3 + -> linking to /javadoc/index.html + -> linking to /files/file.zip -> linking to /frequent -> linking to ./maybe-not (resolved as: /maybe-not) + " `; diff --git a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts index af8cbbfe6e18..18c375584d4f 100644 --- a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts +++ b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts @@ -150,21 +150,21 @@ describe('handleBrokenLinks', () => { ], anchors: [], }, - '/page1': { - links: [ - linkToHtmlFile1, - linkToJavadoc1, - linkToHtmlFile2, - linkToJavadoc3, - linkToJavadoc4, - ], - anchors: [], - }, + // '/page1': { + // links: [ + // linkToHtmlFile1, + // linkToJavadoc1, + // linkToHtmlFile2, + // linkToJavadoc3, + // linkToJavadoc4, + // ], + // anchors: [], + // }, }; await handleBrokenLinks({ allCollectedLinks: allCollectedCorrectLinks, onBrokenLinks: 'warn', - onBrokenAnchors: 'throw', + onBrokenAnchors: 'warn', routes, baseUrl: '/', outDir, @@ -192,7 +192,7 @@ describe('handleBrokenLinks', () => { await handleBrokenLinks({ allCollectedLinks, onBrokenLinks: 'ignore', - onBrokenAnchors: 'throw', + onBrokenAnchors: 'ignore', routes, baseUrl: '/', outDir, diff --git a/packages/docusaurus/src/server/brokenLinks.ts b/packages/docusaurus/src/server/brokenLinks.ts index 4745c517f516..48da5e0d13d5 100644 --- a/packages/docusaurus/src/server/brokenLinks.ts +++ b/packages/docusaurus/src/server/brokenLinks.ts @@ -56,11 +56,20 @@ function getPageBrokenLinks({ } function isAnchorBrokenLink(link: string) { - console.log('link', link); - console.log('pageAnchors', pageAnchors); - const urlHash = link.split('#')[1] ?? ''; + const urlHash = link.split('#')[1]; - return !pageAnchors.includes(urlHash); + if (urlHash === undefined || pageAnchors.length === 0) { + return false; + } + + const brokenAnchors = pageAnchors.flatMap((anchor) => { + if (anchor === urlHash) { + return []; + } + return [anchor]; + }); + + return brokenAnchors.length > 0; } const brokenLinks = pageLinks.flatMap((pageLink) => { @@ -128,14 +137,14 @@ function getBrokenLinksErrorMessage(allBrokenLinks: { pagePath: string, brokenLinks: BrokenLink[], ): string { - const [pathBrokenLinks, anchorBrokenLinks] = _.partition( + const [anchorBrokenLinks, pathBrokenLinks] = _.partition( brokenLinks, 'anchor', ); const pathMessage = pathBrokenLinks.length > 0 - ? `- On source page path = ${pagePath}: + ? `- Broken link on source page path = ${pagePath}: -> linking to ${pathBrokenLinks .map(brokenLinkMessage) .join('\n -> linking to ')}` @@ -143,13 +152,13 @@ function getBrokenLinksErrorMessage(allBrokenLinks: { const anchorMessage = anchorBrokenLinks.length > 0 - ? `- Anchor On source page path = ${pagePath}: + ? `- Broken anchor on source page path = ${pagePath}: -> linking to ${anchorBrokenLinks .map(brokenLinkMessage) .join('\n -> linking to ')}` : ''; - return `${pathMessage}${anchorMessage}`; + return `${pathMessage}\n${anchorMessage}`; } /** @@ -185,10 +194,10 @@ We recommend that you check your theme configuration for such links (particularl Frequent broken links are linking to:${frequentLinks}`; } - return `Docusaurus found broken links! + return `Docusaurus found broken links / anchors! 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.${getLayoutBrokenLinksHelpMessage()} +Note: it's possible to ignore broken links with the 'onBrokenLinks' or 'onBrokenAnchors' Docusaurus configuration, and let the build pass.${getLayoutBrokenLinksHelpMessage()} Exhaustive list of all broken links found: ${Object.entries(allBrokenLinks) From ca33fb84f23b6a6f9355a43e7294bee272989a95 Mon Sep 17 00:00:00 2001 From: ozakione <29860391+OzakIOne@users.noreply.github.com> Date: Thu, 23 Nov 2023 14:41:21 +0100 Subject: [PATCH 06/69] wip: refactor --- .../src/server/__tests__/brokenLinks.test.ts | 8 ++- packages/docusaurus/src/server/brokenLinks.ts | 64 ++++++++++++++----- 2 files changed, 53 insertions(+), 19 deletions(-) diff --git a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts index 18c375584d4f..ccb7af040e9f 100644 --- a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts +++ b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts @@ -134,21 +134,23 @@ describe('handleBrokenLinks', () => { }, '/docs/goodDoc': { links: [ + '#someHash', + '/community#anchorFromGoodDoc', './anotherGoodDoc#someHash', '/docs/anotherGoodDoc?someQueryString=true#someHash', '../docs/anotherGoodDoc?someQueryString=true', '../docs/anotherGoodDoc#someHash', ], - anchors: ['someHash'], + anchors: ['someHash'], // anchors here are anchors of the page itself (/docs/goodDoc) not the links in it }, '/community': { links: [ '/docs/goodDoc', - '/docs/anotherGoodDoc#someHash', + '/docs/anotherGoodDoc#someHash', // anchor here is an anchor of an other page, it should be checked against the anchors of the other page './docs/goodDoc#someHash', './docs/anotherGoodDoc', ], - anchors: [], + anchors: ['anchorFromGoodDoc'], }, // '/page1': { // links: [ diff --git a/packages/docusaurus/src/server/brokenLinks.ts b/packages/docusaurus/src/server/brokenLinks.ts index 48da5e0d13d5..f9fb71198a0a 100644 --- a/packages/docusaurus/src/server/brokenLinks.ts +++ b/packages/docusaurus/src/server/brokenLinks.ts @@ -21,11 +21,46 @@ type BrokenLink = { anchor: boolean; }; +type CollectedLinks = { + [location: string]: {links: string[]; anchors: string[]}; +}; + // matchRoutes does not support qs/anchors, so we remove it! function onlyPathname(link: string) { return link.split('#')[0]!.split('?')[0]!; } +function checkAnchorsInOtherRoutes(allCollectedCorrectLinks: CollectedLinks): { + [location: string]: BrokenLink[]; +} { + const brokenLinksByLocation: {[location: string]: BrokenLink[]} = {}; + + Object.entries(allCollectedCorrectLinks).forEach(([key, value]) => { + const brokenLinks = value.links.flatMap((link) => { + const [route, anchor] = link.split('#'); + if (route !== '' && anchor !== undefined) { + const targetRoute = allCollectedCorrectLinks[route!]; + if (targetRoute && !targetRoute.anchors.includes(anchor)) { + return [ + { + link: `${route}#${anchor}`, + resolvedLink: route!, + anchor: true, + }, + ]; + } + } + return []; + }); + + if (brokenLinks.length > 0) { + brokenLinksByLocation[key] = brokenLinks; + } + }); + + return brokenLinksByLocation; +} + function getPageBrokenLinks({ pagePath, pageLinks, @@ -56,18 +91,14 @@ function getPageBrokenLinks({ } function isAnchorBrokenLink(link: string) { - const urlHash = link.split('#')[1]; + const [urlPath, urlHash] = link.split('#'); - if (urlHash === undefined || pageAnchors.length === 0) { + // ignore anchors that are not on the current page + if (urlHash === undefined || pageAnchors.length === 0 || urlPath !== '') { return false; } - const brokenAnchors = pageAnchors.flatMap((anchor) => { - if (anchor === urlHash) { - return []; - } - return [anchor]; - }); + const brokenAnchors = pageAnchors.filter((anchor) => anchor !== urlHash); return brokenAnchors.length > 0; } @@ -100,7 +131,7 @@ function getAllBrokenLinks({ allCollectedLinks, routes, }: { - allCollectedLinks: {[location: string]: {links: string[]; anchors: string[]}}; + allCollectedLinks: CollectedLinks; routes: RouteConfig[]; }): {[location: string]: BrokenLink[]} { const filteredRoutes = filterIntermediateRoutes(routes); @@ -116,7 +147,11 @@ function getAllBrokenLinks({ }), ); - return _.pickBy(allBrokenLinks, (brokenLinks) => brokenLinks.length > 0); + const allBrokenAnchors = checkAnchorsInOtherRoutes(allCollectedLinks); + + const brokenCollection = _.merge(allBrokenLinks, allBrokenAnchors); + + return _.pickBy(brokenCollection, (brokenLinks) => brokenLinks.length > 0); } function getBrokenLinksErrorMessage(allBrokenLinks: { @@ -209,7 +244,7 @@ ${Object.entries(allBrokenLinks) } export async function handleBrokenLinks(params: { - allCollectedLinks: {[location: string]: {links: string[]; anchors: string[]}}; + allCollectedLinks: CollectedLinks; onBrokenLinks: ReportingSeverity; onBrokenAnchors: ReportingSeverity; routes: RouteConfig[]; @@ -227,17 +262,14 @@ async function handlePathBrokenLinks({ baseUrl, outDir, }: { - allCollectedLinks: {[location: string]: {links: string[]; anchors: string[]}}; + allCollectedLinks: CollectedLinks; onBrokenLinks: ReportingSeverity; onBrokenAnchors: ReportingSeverity; routes: RouteConfig[]; baseUrl: string; outDir: string; }): Promise { - if (onBrokenLinks === 'ignore') { - return; - } - if (onBrokenAnchors === 'ignore') { + if (onBrokenLinks === 'ignore' || onBrokenAnchors === 'ignore') { return; } From 6846cb82428cfcd2166ef6f046bc7ef57eda431b Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Thu, 7 Dec 2023 16:04:35 +0100 Subject: [PATCH 07/69] create broken links public API --- .../src/index.d.ts | 17 ++----- .../src/client/AnchorsCollector.tsx | 44 ---------------- .../src/client/BrokenLinksContext.tsx | 51 +++++++++++++++++++ .../docusaurus/src/client/LinksCollector.tsx | 45 ---------------- .../docusaurus/src/client/exports/Link.tsx | 6 +-- .../src/client/exports/useAnchor.ts | 19 ------- .../src/client/exports/useBrokenLinks.ts | 13 +++++ .../docusaurus/src/client/serverEntry.tsx | 32 +++++------- packages/docusaurus/src/commands/build.ts | 2 +- packages/docusaurus/src/deps.d.ts | 10 ++-- 10 files changed, 90 insertions(+), 149 deletions(-) delete mode 100644 packages/docusaurus/src/client/AnchorsCollector.tsx create mode 100644 packages/docusaurus/src/client/BrokenLinksContext.tsx delete mode 100644 packages/docusaurus/src/client/LinksCollector.tsx delete mode 100644 packages/docusaurus/src/client/exports/useAnchor.ts create mode 100644 packages/docusaurus/src/client/exports/useBrokenLinks.ts diff --git a/packages/docusaurus-module-type-aliases/src/index.d.ts b/packages/docusaurus-module-type-aliases/src/index.d.ts index 64a4c3e8b012..2b28aa4c51b3 100644 --- a/packages/docusaurus-module-type-aliases/src/index.d.ts +++ b/packages/docusaurus-module-type-aliases/src/index.d.ts @@ -260,20 +260,13 @@ declare module '@docusaurus/useRouteContext' { export default function useRouteContext(): PluginRouteContext; } -declare module '@docusaurus/useAnchor' { - export type AnchorsCollector = { - collectAnchor: (link: string) => void; +declare module '@docusaurus/useBrokenLinks' { + export type BrokenLinks = { + collectLink: (link: string) => void; + collectAnchor: (anchor: string) => void; }; - export type StatefulAnchorsCollector = AnchorsCollector & { - getCollectedAnchors: () => string[]; - }; - - // useAnchorCollector - export default function useAnchor(): [ - AnchorsCollector, - () => StatefulAnchorsCollector, - ]; + export default function useBrokenLinks(): BrokenLinks; } declare module '@docusaurus/useIsBrowser' { diff --git a/packages/docusaurus/src/client/AnchorsCollector.tsx b/packages/docusaurus/src/client/AnchorsCollector.tsx deleted file mode 100644 index 77928a8e24d2..000000000000 --- a/packages/docusaurus/src/client/AnchorsCollector.tsx +++ /dev/null @@ -1,44 +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'; -import type {AnchorsCollector} from '@docusaurus/useAnchor'; - -type StatefulAnchorsCollector = AnchorsCollector & { - getCollectedAnchors: () => string[]; -}; - -export const createStatefulAnchorsCollector = (): StatefulAnchorsCollector => { - // Set to dedup, as it's not useful to collect multiple times the same link - const allAnchors = new Set(); - return { - collectAnchor: (link: string): void => { - allAnchors.add(link); - }, - getCollectedAnchors: (): string[] => [...allAnchors], - }; -}; - -const Context = React.createContext({ - collectAnchor: () => { - // No-op for client. We only use the broken links checker server-side. - }, -}); - -export const useAnchorsCollector = (): AnchorsCollector => useContext(Context); - -export function AnchorsCollectorProvider({ - children, - anchorsCollector, -}: { - children: ReactNode; - anchorsCollector: AnchorsCollector; -}): JSX.Element { - return ( - {children} - ); -} diff --git a/packages/docusaurus/src/client/BrokenLinksContext.tsx b/packages/docusaurus/src/client/BrokenLinksContext.tsx new file mode 100644 index 000000000000..e04e8ab14731 --- /dev/null +++ b/packages/docusaurus/src/client/BrokenLinksContext.tsx @@ -0,0 +1,51 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React, {type ReactNode, useContext} from 'react'; +import type {BrokenLinks} from '@docusaurus/useBrokenLinks'; + +export type StatefulBrokenLinks = BrokenLinks & { + getCollectedLinks: () => string[]; + getCollectedAnchors: () => string[]; +}; + +export const createStatefulBrokenLinks = (): StatefulBrokenLinks => { + // Set to dedup, as it's not useful to collect multiple times the same value + const allAnchors = new Set(); + const allLinks = new Set(); + return { + collectAnchor: (anchor: string): void => { + allAnchors.add(anchor); + }, + collectLink: (link: string): void => { + allLinks.add(link); + }, + getCollectedAnchors: (): string[] => [...allAnchors], + getCollectedLinks: (): string[] => [...allLinks], + }; +}; + +const Context = React.createContext({ + collectAnchor: () => { + // No-op for client + }, + collectLink: () => { + // No-op for client + }, +}); + +export const useBrokenLinksContext = (): BrokenLinks => useContext(Context); + +export function BrokenLinksProvider({ + children, + brokenLinks, +}: { + children: ReactNode; + brokenLinks: BrokenLinks; +}): JSX.Element { + return {children}; +} diff --git a/packages/docusaurus/src/client/LinksCollector.tsx b/packages/docusaurus/src/client/LinksCollector.tsx deleted file mode 100644 index d0fb33b9ec03..000000000000 --- a/packages/docusaurus/src/client/LinksCollector.tsx +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import React, {type ReactNode, useContext} from 'react'; - -type LinksCollector = { - collectLink: (link: string) => void; -}; - -type StatefulLinksCollector = LinksCollector & { - getCollectedLinks: () => string[]; -}; - -export const createStatefulLinksCollector = (): StatefulLinksCollector => { - // Set to dedup, as it's not useful to collect multiple times the same link - const allLinks = new Set(); - return { - collectLink: (link: string): void => { - allLinks.add(link); - }, - getCollectedLinks: (): string[] => [...allLinks], - }; -}; - -const Context = React.createContext({ - collectLink: () => { - // No-op for client. We only use the broken links checker server-side. - }, -}); - -export const useLinksCollector = (): LinksCollector => useContext(Context); - -export function LinksCollectorProvider({ - children, - linksCollector, -}: { - children: ReactNode; - linksCollector: LinksCollector; -}): JSX.Element { - return {children}; -} diff --git a/packages/docusaurus/src/client/exports/Link.tsx b/packages/docusaurus/src/client/exports/Link.tsx index 4a7453dfef8f..8b886c8e7073 100644 --- a/packages/docusaurus/src/client/exports/Link.tsx +++ b/packages/docusaurus/src/client/exports/Link.tsx @@ -16,7 +16,7 @@ import {applyTrailingSlash} from '@docusaurus/utils-common'; import useDocusaurusContext from './useDocusaurusContext'; import isInternalUrl from './isInternalUrl'; import ExecutionEnvironment from './ExecutionEnvironment'; -import {useLinksCollector} from '../LinksCollector'; +import useBrokenLinks from './useBrokenLinks'; import {useBaseUrlUtils} from './useBaseUrl'; import type {Props} from '@docusaurus/Link'; @@ -44,7 +44,7 @@ function Link( siteConfig: {trailingSlash, baseUrl}, } = useDocusaurusContext(); const {withBaseUrl} = useBaseUrlUtils(); - const linksCollector = useLinksCollector(); + const brokenLinks = useBrokenLinks(); const innerRef = useRef(null); useImperativeHandle(forwardedRef, () => innerRef.current!); @@ -144,7 +144,7 @@ function Link( const isRegularHtmlLink = !targetLink || !isInternal || isAnchorLink; if (!isRegularHtmlLink && !noBrokenLinkCheck) { - linksCollector.collectLink(targetLink!); + brokenLinks.collectLink(targetLink!); } return isRegularHtmlLink ? ( diff --git a/packages/docusaurus/src/client/exports/useAnchor.ts b/packages/docusaurus/src/client/exports/useAnchor.ts deleted file mode 100644 index caa26f26e395..000000000000 --- a/packages/docusaurus/src/client/exports/useAnchor.ts +++ /dev/null @@ -1,19 +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 { - useAnchorsCollector, - createStatefulAnchorsCollector, -} from '../AnchorsCollector'; -import type {AnchorsCollector} from '@docusaurus/useAnchor'; - -export default function useAnchor(): [ - AnchorsCollector, - () => AnchorsCollector, -] { - return [useAnchorsCollector(), createStatefulAnchorsCollector]; -} diff --git a/packages/docusaurus/src/client/exports/useBrokenLinks.ts b/packages/docusaurus/src/client/exports/useBrokenLinks.ts new file mode 100644 index 000000000000..979aa399cdb8 --- /dev/null +++ b/packages/docusaurus/src/client/exports/useBrokenLinks.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import {useBrokenLinksContext} from '../BrokenLinksContext'; +import type {BrokenLinks} from '@docusaurus/useBrokenLinks'; + +export default function useBrokenLinks(): BrokenLinks { + return useBrokenLinksContext(); +} diff --git a/packages/docusaurus/src/client/serverEntry.tsx b/packages/docusaurus/src/client/serverEntry.tsx index 1778f464fbe6..c01c4779e904 100644 --- a/packages/docusaurus/src/client/serverEntry.tsx +++ b/packages/docusaurus/src/client/serverEntry.tsx @@ -20,13 +20,9 @@ import {renderStaticApp} from './serverRenderer'; import preload from './preload'; import App from './App'; import { - AnchorsCollectorProvider, - createStatefulAnchorsCollector, -} from './AnchorsCollector'; -import { - createStatefulLinksCollector, - LinksCollectorProvider, -} from './LinksCollector'; + createStatefulBrokenLinks, + BrokenLinksProvider, +} from './BrokenLinksContext'; import type {Locals} from '@slorber/static-site-generator-webpack-plugin'; const getCompiledSSRTemplate = _.memoize((template: string) => @@ -100,31 +96,27 @@ async function doRender(locals: Locals & {path: string}) { const routerContext = {}; const helmetContext = {}; - const linksCollector = createStatefulLinksCollector(); - - const anchorsCollector = createStatefulAnchorsCollector(); + const statefulBrokenLinks = createStatefulBrokenLinks(); const app = ( // @ts-expect-error: we are migrating away from react-loadable anyways modules.add(moduleName)}> - - - - - + + + ); const appHtml = await renderStaticApp(app); - onLinksCollected( - location, - linksCollector.getCollectedLinks(), - anchorsCollector.getCollectedAnchors(), - ); + onLinksCollected({ + staticPagePath: location, + anchors: statefulBrokenLinks.getCollectedAnchors(), + links: statefulBrokenLinks.getCollectedLinks(), + }); const {helmet} = helmetContext as FilledContext; const htmlAttributes = helmet.htmlAttributes.toString(); diff --git a/packages/docusaurus/src/commands/build.ts b/packages/docusaurus/src/commands/build.ts index e2fc00197a6a..264ccb780277 100644 --- a/packages/docusaurus/src/commands/build.ts +++ b/packages/docusaurus/src/commands/build.ts @@ -188,7 +188,7 @@ async function buildLocale({ let serverConfig: Configuration = await createServerConfig({ props, - onLinksCollected: (staticPagePath, links, anchors) => { + onLinksCollected: ({staticPagePath, links, anchors}) => { allCollectedLinks[staticPagePath] = {links, anchors}; }, onHeadTagsCollected: (staticPagePath, tags) => { diff --git a/packages/docusaurus/src/deps.d.ts b/packages/docusaurus/src/deps.d.ts index 915dfae1670f..07fa6671c11e 100644 --- a/packages/docusaurus/src/deps.d.ts +++ b/packages/docusaurus/src/deps.d.ts @@ -43,11 +43,11 @@ declare module '@slorber/static-site-generator-webpack-plugin' { preBodyTags: string; postBodyTags: string; // TODO transform arguments into object - onLinksCollected: ( - staticPagePath: string, - links: string[], - anchors: string[], - ) => void; + onLinksCollected: (params: { + staticPagePath: string; + links: string[]; + anchors: string[]; + }) => void; onHeadTagsCollected: ( staticPagePath: string, tags: HelmetServerState, From 233e420ccdd85ee6e6505e00e9479e14b583d929 Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Thu, 7 Dec 2023 16:11:32 +0100 Subject: [PATCH 08/69] update snapshots --- .../server/__tests__/__snapshots__/config.test.ts.snap | 10 ++++++++++ .../server/__tests__/__snapshots__/index.test.ts.snap | 1 + .../webpack/__tests__/__snapshots__/base.test.ts.snap | 1 + .../aliases/__tests__/__snapshots__/index.test.ts.snap | 1 + 4 files changed, 13 insertions(+) diff --git a/packages/docusaurus/src/server/__tests__/__snapshots__/config.test.ts.snap b/packages/docusaurus/src/server/__tests__/__snapshots__/config.test.ts.snap index 2ed2b796fd01..7642770e6ce5 100644 --- a/packages/docusaurus/src/server/__tests__/__snapshots__/config.test.ts.snap +++ b/packages/docusaurus/src/server/__tests__/__snapshots__/config.test.ts.snap @@ -27,6 +27,7 @@ exports[`loadSiteConfig website with .cjs siteConfig 1`] = ` "preprocessor": undefined, }, "noIndex": false, + "onBrokenAnchors": "warn", "onBrokenLinks": "throw", "onBrokenMarkdownLinks": "warn", "onDuplicateRoutes": "warn", @@ -75,6 +76,7 @@ exports[`loadSiteConfig website with ts + js config 1`] = ` "preprocessor": undefined, }, "noIndex": false, + "onBrokenAnchors": "warn", "onBrokenLinks": "throw", "onBrokenMarkdownLinks": "warn", "onDuplicateRoutes": "warn", @@ -123,6 +125,7 @@ exports[`loadSiteConfig website with valid JS CJS config 1`] = ` "preprocessor": undefined, }, "noIndex": false, + "onBrokenAnchors": "warn", "onBrokenLinks": "throw", "onBrokenMarkdownLinks": "warn", "onDuplicateRoutes": "warn", @@ -171,6 +174,7 @@ exports[`loadSiteConfig website with valid JS ESM config 1`] = ` "preprocessor": undefined, }, "noIndex": false, + "onBrokenAnchors": "warn", "onBrokenLinks": "throw", "onBrokenMarkdownLinks": "warn", "onDuplicateRoutes": "warn", @@ -219,6 +223,7 @@ exports[`loadSiteConfig website with valid TypeScript CJS config 1`] = ` "preprocessor": undefined, }, "noIndex": false, + "onBrokenAnchors": "warn", "onBrokenLinks": "throw", "onBrokenMarkdownLinks": "warn", "onDuplicateRoutes": "warn", @@ -267,6 +272,7 @@ exports[`loadSiteConfig website with valid TypeScript ESM config 1`] = ` "preprocessor": undefined, }, "noIndex": false, + "onBrokenAnchors": "warn", "onBrokenLinks": "throw", "onBrokenMarkdownLinks": "warn", "onDuplicateRoutes": "warn", @@ -315,6 +321,7 @@ exports[`loadSiteConfig website with valid async config 1`] = ` "preprocessor": undefined, }, "noIndex": false, + "onBrokenAnchors": "warn", "onBrokenLinks": "throw", "onBrokenMarkdownLinks": "warn", "onDuplicateRoutes": "warn", @@ -365,6 +372,7 @@ exports[`loadSiteConfig website with valid async config creator function 1`] = ` "preprocessor": undefined, }, "noIndex": false, + "onBrokenAnchors": "warn", "onBrokenLinks": "throw", "onBrokenMarkdownLinks": "warn", "onDuplicateRoutes": "warn", @@ -415,6 +423,7 @@ exports[`loadSiteConfig website with valid config creator function 1`] = ` "preprocessor": undefined, }, "noIndex": false, + "onBrokenAnchors": "warn", "onBrokenLinks": "throw", "onBrokenMarkdownLinks": "warn", "onDuplicateRoutes": "warn", @@ -468,6 +477,7 @@ exports[`loadSiteConfig website with valid siteConfig 1`] = ` "preprocessor": undefined, }, "noIndex": false, + "onBrokenAnchors": "warn", "onBrokenLinks": "throw", "onBrokenMarkdownLinks": "warn", "onDuplicateRoutes": "warn", diff --git a/packages/docusaurus/src/server/__tests__/__snapshots__/index.test.ts.snap b/packages/docusaurus/src/server/__tests__/__snapshots__/index.test.ts.snap index 45b94b869406..8320a6ec5fd9 100644 --- a/packages/docusaurus/src/server/__tests__/__snapshots__/index.test.ts.snap +++ b/packages/docusaurus/src/server/__tests__/__snapshots__/index.test.ts.snap @@ -101,6 +101,7 @@ exports[`load loads props for site with custom i18n path 1`] = ` "preprocessor": undefined, }, "noIndex": false, + "onBrokenAnchors": "warn", "onBrokenLinks": "throw", "onBrokenMarkdownLinks": "warn", "onDuplicateRoutes": "warn", diff --git a/packages/docusaurus/src/webpack/__tests__/__snapshots__/base.test.ts.snap b/packages/docusaurus/src/webpack/__tests__/__snapshots__/base.test.ts.snap index 9567720d587f..7299deaf977a 100644 --- a/packages/docusaurus/src/webpack/__tests__/__snapshots__/base.test.ts.snap +++ b/packages/docusaurus/src/webpack/__tests__/__snapshots__/base.test.ts.snap @@ -16,6 +16,7 @@ exports[`base webpack config creates webpack aliases 1`] = ` "@docusaurus/renderRoutes": "../../../../client/exports/renderRoutes.ts", "@docusaurus/router": "../../../../client/exports/router.ts", "@docusaurus/useBaseUrl": "../../../../client/exports/useBaseUrl.ts", + "@docusaurus/useBrokenLinks": "../../../../client/exports/useBrokenLinks.ts", "@docusaurus/useDocusaurusContext": "../../../../client/exports/useDocusaurusContext.ts", "@docusaurus/useGlobalData": "../../../../client/exports/useGlobalData.ts", "@docusaurus/useIsBrowser": "../../../../client/exports/useIsBrowser.ts", diff --git a/packages/docusaurus/src/webpack/aliases/__tests__/__snapshots__/index.test.ts.snap b/packages/docusaurus/src/webpack/aliases/__tests__/__snapshots__/index.test.ts.snap index c9738c847d56..46390d21c92d 100644 --- a/packages/docusaurus/src/webpack/aliases/__tests__/__snapshots__/index.test.ts.snap +++ b/packages/docusaurus/src/webpack/aliases/__tests__/__snapshots__/index.test.ts.snap @@ -16,6 +16,7 @@ exports[`getDocusaurusAliases returns appropriate webpack aliases 1`] = ` "@docusaurus/renderRoutes": "/packages/docusaurus/src/client/exports/renderRoutes.ts", "@docusaurus/router": "/packages/docusaurus/src/client/exports/router.ts", "@docusaurus/useBaseUrl": "/packages/docusaurus/src/client/exports/useBaseUrl.ts", + "@docusaurus/useBrokenLinks": "/packages/docusaurus/src/client/exports/useBrokenLinks.ts", "@docusaurus/useDocusaurusContext": "/packages/docusaurus/src/client/exports/useDocusaurusContext.ts", "@docusaurus/useGlobalData": "/packages/docusaurus/src/client/exports/useGlobalData.ts", "@docusaurus/useIsBrowser": "/packages/docusaurus/src/client/exports/useIsBrowser.ts", From 0c2380a66ea1746c9e23f4c8ee29dfa6c0d30aa7 Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Thu, 7 Dec 2023 16:29:10 +0100 Subject: [PATCH 09/69] fix broken links api usage --- .../src/theme/Heading/index.tsx | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/docusaurus-theme-classic/src/theme/Heading/index.tsx b/packages/docusaurus-theme-classic/src/theme/Heading/index.tsx index fb2e6d1f69c9..b17cd12ec3b1 100644 --- a/packages/docusaurus-theme-classic/src/theme/Heading/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/Heading/index.tsx @@ -10,13 +10,13 @@ import clsx from 'clsx'; import {translate} from '@docusaurus/Translate'; import {useThemeConfig} from '@docusaurus/theme-common'; import Link from '@docusaurus/Link'; -import useAnchor from '@docusaurus/useAnchor'; +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 [anchorsCollector, createAnchorList] = useAnchor(); + const brokenLinks = useBrokenLinks(); const { navbar: {hideOnScroll}, } = useThemeConfig(); @@ -25,13 +25,7 @@ export default function Heading({as: As, id, ...props}: Props): JSX.Element { return ; } - const list = createAnchorList(); - - // ! should not be called 2 times, not a problem because we use - // Set but still must be removed - anchorsCollector.collectAnchor(id); - - list.collectAnchor(id); + brokenLinks.collectAnchor(id); const anchorTitle = translate( { From 135bb14b1d174259e6e9051deacc90e1d45ee12b Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Thu, 7 Dec 2023 16:29:23 +0100 Subject: [PATCH 10/69] add todo for onBrokenAnchors --- packages/docusaurus/src/server/configValidation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/docusaurus/src/server/configValidation.ts b/packages/docusaurus/src/server/configValidation.ts index 99ade2ffa59c..f5b14a7be177 100644 --- a/packages/docusaurus/src/server/configValidation.ts +++ b/packages/docusaurus/src/server/configValidation.ts @@ -49,7 +49,7 @@ export const DEFAULT_CONFIG: Pick< > = { i18n: DEFAULT_I18N_CONFIG, onBrokenLinks: 'throw', - onBrokenAnchors: 'warn', + onBrokenAnchors: 'warn', // TODO Docusaurus v4: change to throw onBrokenMarkdownLinks: 'warn', onDuplicateRoutes: 'warn', plugins: [], From 3412aea0d088cf93adf94cc042b1f4d51a084b2c Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Thu, 7 Dec 2023 16:37:25 +0100 Subject: [PATCH 11/69] Add anchor broken links pseudo code --- packages/docusaurus/src/server/brokenLinks.ts | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/packages/docusaurus/src/server/brokenLinks.ts b/packages/docusaurus/src/server/brokenLinks.ts index f9fb71198a0a..2ce88420cbf5 100644 --- a/packages/docusaurus/src/server/brokenLinks.ts +++ b/packages/docusaurus/src/server/brokenLinks.ts @@ -154,7 +154,13 @@ function getAllBrokenLinks({ return _.pickBy(brokenCollection, (brokenLinks) => brokenLinks.length > 0); } -function getBrokenLinksErrorMessage(allBrokenLinks: { +function getAnchorBrokenLinksErrorMessage(allBrokenLinks: { + [location: string]: BrokenLink[]; +}): string | undefined { + return undefined; // TODO +} + +function getPathBrokenLinksErrorMessage(allBrokenLinks: { [location: string]: BrokenLink[]; }): string | undefined { if (Object.keys(allBrokenLinks).length === 0) { @@ -185,6 +191,7 @@ function getBrokenLinksErrorMessage(allBrokenLinks: { .join('\n -> linking to ')}` : ''; + // TODO move to getAnchorBrokenLinksErrorMessage const anchorMessage = anchorBrokenLinks.length > 0 ? `- Broken anchor on source page path = ${pagePath}: @@ -243,18 +250,7 @@ ${Object.entries(allBrokenLinks) `; } -export async function handleBrokenLinks(params: { - allCollectedLinks: CollectedLinks; - onBrokenLinks: ReportingSeverity; - onBrokenAnchors: ReportingSeverity; - routes: RouteConfig[]; - baseUrl: string; - outDir: string; -}): Promise { - await handlePathBrokenLinks(params); -} - -async function handlePathBrokenLinks({ +export async function handleBrokenLinks({ allCollectedLinks, onBrokenLinks, onBrokenAnchors, @@ -269,7 +265,7 @@ async function handlePathBrokenLinks({ baseUrl: string; outDir: string; }): Promise { - if (onBrokenLinks === 'ignore' || onBrokenAnchors === 'ignore') { + if (onBrokenLinks === 'ignore' && onBrokenAnchors === 'ignore') { return; } @@ -278,8 +274,13 @@ async function handlePathBrokenLinks({ routes, }); - const errorMessage = getBrokenLinksErrorMessage(allBrokenLinks); - if (errorMessage) { - logger.report(onBrokenLinks)(errorMessage); + const pathErrorMessage = getPathBrokenLinksErrorMessage(allBrokenLinks); + if (pathErrorMessage) { + logger.report(onBrokenLinks)(pathErrorMessage); + } + + const anchorErrorMessage = getAnchorBrokenLinksErrorMessage(allBrokenLinks); + if (anchorErrorMessage) { + logger.report(onBrokenAnchors)(anchorErrorMessage); } } From 93fa9204cbd56a805675d2bb52372f2462345098 Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Thu, 7 Dec 2023 16:40:15 +0100 Subject: [PATCH 12/69] remove todo --- packages/docusaurus/src/deps.d.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/docusaurus/src/deps.d.ts b/packages/docusaurus/src/deps.d.ts index 07fa6671c11e..199f39900970 100644 --- a/packages/docusaurus/src/deps.d.ts +++ b/packages/docusaurus/src/deps.d.ts @@ -42,7 +42,6 @@ declare module '@slorber/static-site-generator-webpack-plugin' { headTags: string; preBodyTags: string; postBodyTags: string; - // TODO transform arguments into object onLinksCollected: (params: { staticPagePath: string; links: string[]; From 901fccf29e562091063e59ac858e799537e89b6f Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Thu, 7 Dec 2023 16:54:33 +0100 Subject: [PATCH 13/69] fix website onBrokenLinks/onBrokenAnchors --- website/docusaurus.config.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/website/docusaurus.config.ts b/website/docusaurus.config.ts index b3dca14db3ba..101e56c78c1e 100644 --- a/website/docusaurus.config.ts +++ b/website/docusaurus.config.ts @@ -195,7 +195,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' From 5cfc3fafc8d192e893ed64fd6bdd3d121e30f5ae Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Thu, 7 Dec 2023 17:13:45 +0100 Subject: [PATCH 14/69] markdown assets links should not trigger the broken link checker --- .../__snapshots__/index.test.ts.snap | 32 +++++++++---------- .../src/remark/transformLinks/index.ts | 28 ++++++++++++++++ 2 files changed, 44 insertions(+), 16 deletions(-) diff --git a/packages/docusaurus-mdx-loader/src/remark/transformLinks/__tests__/__snapshots__/index.test.ts.snap b/packages/docusaurus-mdx-loader/src/remark/transformLinks/__tests__/__snapshots__/index.test.ts.snap index 06f8fcbcabe7..a0186db7b10b 100644 --- a/packages/docusaurus-mdx-loader/src/remark/transformLinks/__tests__/__snapshots__/index.test.ts.snap +++ b/packages/docusaurus-mdx-loader/src/remark/transformLinks/__tests__/__snapshots__/index.test.ts.snap @@ -12,17 +12,17 @@ exports[`transformAsset plugin pathname protocol 1`] = ` exports[`transformAsset plugin transform md links to 1`] = ` "[asset](https://example.com/asset.pdf) -/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf").default} /> +/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf").default} /> -/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf").default}>asset +/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf").default}>asset -in paragraph /node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf").default}>asset +in paragraph /node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf").default}>asset -/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset (2).pdf").default}>asset with URL encoded chars +/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset (2).pdf").default}>asset with URL encoded chars -/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf").default + '#page=2'}>asset with hash +/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf").default + '#page=2'}>asset with hash -/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf").default} title="Title">asset +/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf").default} title="Title">asset [page](noUrl.md) @@ -36,24 +36,24 @@ in paragraph /node_modules/file [assets](/github/!file-loader!/assets.pdf) -/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf").default}>asset +/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf").default}>asset -/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static2/asset2.pdf").default}>asset2 +/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static2/asset2.pdf").default}>asset2 -/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/staticAsset.pdf").default}>staticAsset.pdf +/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/staticAsset.pdf").default}>staticAsset.pdf -/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/staticAsset.pdf").default}>@site/static/staticAsset.pdf +/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/staticAsset.pdf").default}>@site/static/staticAsset.pdf -/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/staticAsset.pdf").default + '#page=2'} title="Title">@site/static/staticAsset.pdf +/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/staticAsset.pdf").default + '#page=2'} title="Title">@site/static/staticAsset.pdf -/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/staticAsset.pdf").default}>Just staticAsset.pdf, and /node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/staticAsset.pdf").default}>**awesome** staticAsset 2.pdf 'It is really "AWESOME"', but also /node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/staticAsset.pdf").default}>coded \`staticAsset 3.pdf\` +/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/staticAsset.pdf").default}>Just staticAsset.pdf, and /node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/staticAsset.pdf").default}>**awesome** staticAsset 2.pdf 'It is really "AWESOME"', but also /node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/staticAsset.pdf").default}>coded \`staticAsset 3.pdf\` -/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/staticAssetImage.png").default}>Clickable Docusaurus logo/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[contenthash].[ext]&fallback=/node_modules/file-loader/dist/cjs.js!./static/staticAssetImage.png").default} width="200" height="200" /> +/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/staticAssetImage.png").default}>Clickable Docusaurus logo/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[contenthash].[ext]&fallback=/node_modules/file-loader/dist/cjs.js!./static/staticAssetImage.png").default} width="200" height="200" /> -/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf").default}>Stylized link to asset file +/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf").default}>Stylized link to asset file -/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./data.json").default}>JSON +/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./data.json").default}>JSON -/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/static-json.json").default}>static JSON +/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/static-json.json").default}>static JSON " `; diff --git a/packages/docusaurus-mdx-loader/src/remark/transformLinks/index.ts b/packages/docusaurus-mdx-loader/src/remark/transformLinks/index.ts index 252289f97452..179bcadb770d 100644 --- a/packages/docusaurus-mdx-loader/src/remark/transformLinks/index.ts +++ b/packages/docusaurus-mdx-loader/src/remark/transformLinks/index.ts @@ -73,6 +73,34 @@ async function toAssetRequireNode( value: '_blank', }); + // Assets are not routes, and are required by Webpack already + // They should not trigger the broken link checker + attributes.push({ + type: 'mdxJsxAttribute', + name: 'data-noBrokenLinkCheck', + value: { + type: 'mdxJsxAttributeValueExpression', + value: 'true', + data: { + estree: { + type: 'Program', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'Literal', + value: true, + raw: 'true', + }, + }, + ], + sourceType: 'module', + comments: [], + }, + }, + }, + }); + attributes.push({ type: 'mdxJsxAttribute', name: 'href', From 408eb2669b7df1b8eaaa2faea4b48c9fcc623e37 Mon Sep 17 00:00:00 2001 From: ozakione <29860391+OzakIOne@users.noreply.github.com> Date: Mon, 18 Dec 2023 14:07:50 +0100 Subject: [PATCH 15/69] wip: split logging --- packages/docusaurus/src/commands/build.ts | 5 +- .../__snapshots__/brokenLinks.test.ts.snap | 18 +--- .../src/server/__tests__/brokenLinks.test.ts | 11 --- packages/docusaurus/src/server/brokenLinks.ts | 92 +++++++++---------- 4 files changed, 51 insertions(+), 75 deletions(-) diff --git a/packages/docusaurus/src/commands/build.ts b/packages/docusaurus/src/commands/build.ts index 264ccb780277..660b64d43f33 100644 --- a/packages/docusaurus/src/commands/build.ts +++ b/packages/docusaurus/src/commands/build.ts @@ -110,7 +110,7 @@ export async function build( ...i18n.locales.filter((locale) => locale !== i18n.defaultLocale), ]; - const results = await mapAsyncSequential(orderedLocales, (locale) => { + const results = await mapAsyncSequential(orderedLocales, (locale: any) => { const isLastLocale = orderedLocales.indexOf(locale) === orderedLocales.length - 1; return tryToBuildLocale({locale, isLastLocale}); @@ -152,7 +152,6 @@ async function buildLocale({ generatedFilesDir, plugins, siteConfig: { - baseUrl, onBrokenLinks, onBrokenAnchors, staticDirectories: staticDirectoriesOption, @@ -295,8 +294,6 @@ async function buildLocale({ routes, onBrokenLinks, onBrokenAnchors, - outDir, - baseUrl, }); logger.success`Generated static files in path=${path.relative( diff --git a/packages/docusaurus/src/server/__tests__/__snapshots__/brokenLinks.test.ts.snap b/packages/docusaurus/src/server/__tests__/__snapshots__/brokenLinks.test.ts.snap index 2843ed0f7aab..e51aaa01c897 100644 --- a/packages/docusaurus/src/server/__tests__/__snapshots__/brokenLinks.test.ts.snap +++ b/packages/docusaurus/src/server/__tests__/__snapshots__/brokenLinks.test.ts.snap @@ -1,27 +1,24 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`handleBrokenLinks reports all broken links 1`] = ` -"Docusaurus found broken links / anchors! +"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' or 'onBrokenAnchors' Docusaurus configuration, and let the build pass. +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 = /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) - - Broken link 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) - - Broken link on source page path = /community: -> linking to /someNonExistentDoc1 -> linking to /badLink2 -> linking to ./badLink3 (resolved as: /badLink3) - - Broken link on source page path = /page1: -> linking to /link1 -> linking to /files/hey.html @@ -30,7 +27,6 @@ Exhaustive list of all broken links found: -> linking to /javadoc/index.html -> linking to /javadoc/index.html#foo (resolved as: /javadoc/index.html) -> linking to /emptyFolder - - Broken link on source page path = /page2: -> linking to /docs/link2 -> linking to /emptyFolder/ @@ -38,15 +34,14 @@ Exhaustive list of all broken links found: -> linking to /hey/link3 -> linking to /javadoc/index.html -> linking to /files/file.zip - " `; exports[`handleBrokenLinks reports frequent broken links 1`] = ` -"Docusaurus found broken links / anchors! +"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' or 'onBrokenAnchors' Docusaurus configuration, and let the build pass. +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? @@ -61,7 +56,6 @@ Exhaustive list of all broken links found: -> 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) - - Broken link 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) @@ -69,14 +63,12 @@ Exhaustive list of all broken links found: -> linking to ../badRelativeLink4 (resolved as: /badRelativeLink4) -> linking to /frequent -> linking to ./maybe-not (resolved as: /docs/maybe-not) - - Broken link 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) - - Broken link on source page path = /page1: -> linking to /link1 -> linking to /files/hey.html @@ -87,7 +79,6 @@ Exhaustive list of all broken links found: -> linking to /emptyFolder -> linking to /frequent -> linking to ./maybe-not (resolved as: /maybe-not) - - Broken link on source page path = /page2: -> linking to /docs/link2 -> linking to /emptyFolder/ @@ -97,6 +88,5 @@ Exhaustive list of all broken links found: -> linking to /files/file.zip -> linking to /frequent -> linking to ./maybe-not (resolved as: /maybe-not) - " `; diff --git a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts index ccb7af040e9f..2f9928e2760f 100644 --- a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts +++ b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts @@ -6,7 +6,6 @@ */ import {jest} from '@jest/globals'; -import path from 'path'; import _ from 'lodash'; import {handleBrokenLinks} from '../brokenLinks'; import type {RouteConfig} from '@docusaurus/types'; @@ -118,8 +117,6 @@ describe('handleBrokenLinks', () => { }, }; - const outDir = path.resolve(__dirname, '__fixtures__/brokenLinks/outDir'); - it('do not report anything for correct paths', async () => { const consoleMock = jest .spyOn(console, 'warn') @@ -168,8 +165,6 @@ describe('handleBrokenLinks', () => { onBrokenLinks: 'warn', onBrokenAnchors: 'warn', routes, - baseUrl: '/', - outDir, }); expect(consoleMock).toHaveBeenCalledTimes(0); }); @@ -181,8 +176,6 @@ describe('handleBrokenLinks', () => { onBrokenLinks: 'throw', onBrokenAnchors: 'throw', routes, - baseUrl: '/', - outDir, }), ).rejects.toThrowErrorMatchingSnapshot(); }); @@ -196,8 +189,6 @@ describe('handleBrokenLinks', () => { onBrokenLinks: 'ignore', onBrokenAnchors: 'ignore', routes, - baseUrl: '/', - outDir, }); expect(lodashMock).toHaveBeenCalledTimes(0); lodashMock.mockRestore(); @@ -220,8 +211,6 @@ describe('handleBrokenLinks', () => { onBrokenLinks: 'throw', onBrokenAnchors: 'throw', routes, - baseUrl: '/', - outDir, }), ).rejects.toThrowErrorMatchingSnapshot(); }); diff --git a/packages/docusaurus/src/server/brokenLinks.ts b/packages/docusaurus/src/server/brokenLinks.ts index 2ce88420cbf5..5d5212c885bb 100644 --- a/packages/docusaurus/src/server/brokenLinks.ts +++ b/packages/docusaurus/src/server/brokenLinks.ts @@ -5,9 +5,6 @@ * LICENSE file in the root directory of this source tree. */ -// TODO Remove this -/* eslint-disable @typescript-eslint/no-unused-vars */ - import _ from 'lodash'; import logger from '@docusaurus/logger'; import {matchRoutes} from 'react-router-config'; @@ -154,53 +151,60 @@ function getAllBrokenLinks({ return _.pickBy(brokenCollection, (brokenLinks) => brokenLinks.length > 0); } -function getAnchorBrokenLinksErrorMessage(allBrokenLinks: { - [location: string]: BrokenLink[]; -}): string | undefined { - return undefined; // TODO +function brokenLinkMessage(brokenLink: BrokenLink): string { + const showResolvedLink = brokenLink.link !== brokenLink.resolvedLink; + return `${brokenLink.link}${ + showResolvedLink ? ` (resolved as: ${brokenLink.resolvedLink})` : '' + }`; } -function getPathBrokenLinksErrorMessage(allBrokenLinks: { +function generalBrokenLinksMessage( + type: 'link' | 'anchor', + pagePath: string, + brokenLinks: BrokenLink[], +): string { + const anchorBrokenLinks = + type === 'anchor' + ? brokenLinks.filter((link) => link.anchor) + : brokenLinks.filter((link) => !link.anchor); + + const anchorMessage = + anchorBrokenLinks.length > 0 + ? `- Broken ${type} on source page path = ${pagePath}: + -> linking to ${anchorBrokenLinks + .map(brokenLinkMessage) + .join('\n -> linking to ')}` + : ''; + + return `${anchorMessage}`; +} + +function getAnchorBrokenLinksErrorMessage(allBrokenLinks: { [location: string]: BrokenLink[]; }): string | undefined { if (Object.keys(allBrokenLinks).length === 0) { return undefined; } - function brokenLinkMessage(brokenLink: BrokenLink): string { - const showResolvedLink = brokenLink.link !== brokenLink.resolvedLink; - return `${brokenLink.link}${ - showResolvedLink ? ` (resolved as: ${brokenLink.resolvedLink})` : '' - }`; - } - - function pageBrokenLinksMessage( - pagePath: string, - brokenLinks: BrokenLink[], - ): string { - const [anchorBrokenLinks, pathBrokenLinks] = _.partition( - brokenLinks, - 'anchor', - ); + return `Docusaurus found broken anchors! - const pathMessage = - pathBrokenLinks.length > 0 - ? `- Broken link on source page path = ${pagePath}: - -> linking to ${pathBrokenLinks - .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 path that does not exist. +Note: it's possible to ignore broken anchors with the 'onBrokenAnchors' Docusaurus configuration, and let the build pass. - // TODO move to getAnchorBrokenLinksErrorMessage - const anchorMessage = - anchorBrokenLinks.length > 0 - ? `- Broken anchor on source page path = ${pagePath}: - -> linking to ${anchorBrokenLinks - .map(brokenLinkMessage) - .join('\n -> linking to ')}` - : ''; +Exhaustive list of all broken anchors found: +${Object.entries(allBrokenLinks) + .map(([pagePath, brokenLinks]) => + generalBrokenLinksMessage('anchor', pagePath, brokenLinks), + ) + .join('\n')} +`; +} - return `${pathMessage}\n${anchorMessage}`; +function getPathBrokenLinksErrorMessage(allBrokenLinks: { + [location: string]: BrokenLink[]; +}): string | undefined { + if (Object.keys(allBrokenLinks).length === 0) { + return undefined; } /** @@ -236,15 +240,15 @@ We recommend that you check your theme configuration for such links (particularl Frequent broken links are linking to:${frequentLinks}`; } - return `Docusaurus found broken links / anchors! + return `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' or 'onBrokenAnchors' Docusaurus configuration, and let the build pass.${getLayoutBrokenLinksHelpMessage()} +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), + generalBrokenLinksMessage('link', pagePath, brokenLinks), ) .join('\n')} `; @@ -255,15 +259,11 @@ export async function handleBrokenLinks({ onBrokenLinks, onBrokenAnchors, routes, - baseUrl, - outDir, }: { allCollectedLinks: CollectedLinks; onBrokenLinks: ReportingSeverity; onBrokenAnchors: ReportingSeverity; routes: RouteConfig[]; - baseUrl: string; - outDir: string; }): Promise { if (onBrokenLinks === 'ignore' && onBrokenAnchors === 'ignore') { return; From 06858f2d76fbc7bbfc887e3a3e865bcb1f58d23e Mon Sep 17 00:00:00 2001 From: ozakione <29860391+OzakIOne@users.noreply.github.com> Date: Mon, 18 Dec 2023 14:08:14 +0100 Subject: [PATCH 16/69] docs: remove deprecated tips --- website/docs/advanced/routing.mdx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/website/docs/advanced/routing.mdx b/website/docs/advanced/routing.mdx index 5bf943072ba6..cd498f58eca6 100644 --- a/website/docs/advanced/routing.mdx +++ b/website/docs/advanced/routing.mdx @@ -265,7 +265,7 @@ 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) @@ -279,12 +279,6 @@ If you put some HTML pages under the `static` folder, they will be copied to the -:::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" From 2df3f28b6d1ce7d5ff15feea507902f42db62d49 Mon Sep 17 00:00:00 2001 From: ozakione <29860391+OzakIOne@users.noreply.github.com> Date: Thu, 21 Dec 2023 23:30:08 +0100 Subject: [PATCH 17/69] fix: doc links & bug --- packages/docusaurus/src/server/brokenLinks.ts | 15 ++++++++------- website/docs/advanced/routing.mdx | 2 -- website/docs/api/plugins/plugin-content-docs.mdx | 12 ++++++------ website/docs/cli.mdx | 2 +- .../docs/guides/docs/sidebar/autogenerated.mdx | 2 +- website/docs/guides/docs/versioning.mdx | 2 +- website/docs/i18n/i18n-tutorial.mdx | 2 +- website/docs/seo.mdx | 2 +- website/docs/using-plugins.mdx | 2 +- .../version-2.x/advanced/routing.mdx | 10 +--------- .../version-3.0.1/advanced/routing.mdx | 10 +--------- 11 files changed, 22 insertions(+), 39 deletions(-) diff --git a/packages/docusaurus/src/server/brokenLinks.ts b/packages/docusaurus/src/server/brokenLinks.ts index 5d5212c885bb..f9f2f867f62f 100644 --- a/packages/docusaurus/src/server/brokenLinks.ts +++ b/packages/docusaurus/src/server/brokenLinks.ts @@ -130,7 +130,7 @@ function getAllBrokenLinks({ }: { allCollectedLinks: CollectedLinks; routes: RouteConfig[]; -}): {[location: string]: BrokenLink[]} { +}): [{[location: string]: BrokenLink[]}, {[location: string]: BrokenLink[]}] { const filteredRoutes = filterIntermediateRoutes(routes); const allBrokenLinks = _.mapValues( @@ -143,12 +143,13 @@ function getAllBrokenLinks({ routes: filteredRoutes, }), ); + const filteredLinks = Object.fromEntries( + Object.entries(allBrokenLinks).filter(([, value]) => value.length > 0), + ); const allBrokenAnchors = checkAnchorsInOtherRoutes(allCollectedLinks); - const brokenCollection = _.merge(allBrokenLinks, allBrokenAnchors); - - return _.pickBy(brokenCollection, (brokenLinks) => brokenLinks.length > 0); + return [filteredLinks, allBrokenAnchors]; } function brokenLinkMessage(brokenLink: BrokenLink): string { @@ -269,17 +270,17 @@ export async function handleBrokenLinks({ return; } - const allBrokenLinks = getAllBrokenLinks({ + const [brokenLinks, brokenAnchors] = getAllBrokenLinks({ allCollectedLinks, routes, }); - const pathErrorMessage = getPathBrokenLinksErrorMessage(allBrokenLinks); + const pathErrorMessage = getPathBrokenLinksErrorMessage(brokenLinks); if (pathErrorMessage) { logger.report(onBrokenLinks)(pathErrorMessage); } - const anchorErrorMessage = getAnchorBrokenLinksErrorMessage(allBrokenLinks); + const anchorErrorMessage = getAnchorBrokenLinksErrorMessage(brokenAnchors); if (anchorErrorMessage) { logger.report(onBrokenAnchors)(anchorErrorMessage); } diff --git a/website/docs/advanced/routing.mdx b/website/docs/advanced/routing.mdx index cd498f58eca6..e9e96d4892c7 100644 --- a/website/docs/advanced/routing.mdx +++ b/website/docs/advanced/routing.mdx @@ -268,13 +268,11 @@ Docusaurus builds a [single-page application](https://developer.mozilla.org/en-U If you put some HTML pages under the `static` folder, they will be copied to the build output and therefore become accessible as part of your website, yet it's not part of the Docusaurus route system. We provide a `pathname://` protocol that allows you to redirect to another part of your domain in a non-SPA fashion, as if this route is an external link. ```md -- [/pure-html](/pure-html) - [pathname:///pure-html](pathname:///pure-html) ``` -- [`/pure-html`](/pure-html) - [`pathname:///pure-html`](pathname:///pure-html) diff --git a/website/docs/api/plugins/plugin-content-docs.mdx b/website/docs/api/plugins/plugin-content-docs.mdx index 3613e11fe4a9..916e3a3caa56 100644 --- a/website/docs/api/plugins/plugin-content-docs.mdx +++ b/website/docs/api/plugins/plugin-content-docs.mdx @@ -42,10 +42,10 @@ Accepted fields: | `include` | `string[]` | `['**/*.{md,mdx}']` | Array of glob patterns matching Markdown files to be built, relative to the content path. | | `exclude` | `string[]` | _See example configuration_ | Array of glob patterns matching Markdown files to be excluded. Serves as refinement based on the `include` option. | | `sidebarPath` | false \| string | `undefined` | Path to sidebar configuration. Use `false` to disable sidebars, or `undefined` to create a fully autogenerated sidebar. | -| `sidebarCollapsible` | `boolean` | `true` | Whether sidebar categories are collapsible by default. See also [Collapsible categories](/docs/sidebar#collapsible-categories) | -| `sidebarCollapsed` | `boolean` | `true` | Whether sidebar categories are collapsed by default. See also [Expanded categories by default](/docs/sidebar#expanded-categories-by-default) | -| `sidebarItemsGenerator` | SidebarGenerator | _Omitted_ | Function used to replace the sidebar items of type `'autogenerated'` with real sidebar items (docs, categories, links...). See also [Customize the sidebar items generator](/docs/sidebar#customize-the-sidebar-items-generator) | -| `numberPrefixParser` | boolean \| PrefixParser | _Omitted_ | Custom parsing logic to extract number prefixes from file names. Use `false` to disable this behavior and leave the docs untouched, and `true` to use the default parser. See also [Using number prefixes](/docs/sidebar#using-number-prefixes) | +| `sidebarCollapsible` | `boolean` | `true` | Whether sidebar categories are collapsible by default. See also [Collapsible categories](/docs/sidebar/items#collapsible-categories) | +| `sidebarCollapsed` | `boolean` | `true` | Whether sidebar categories are collapsed by default. See also [Expanded categories by default](/docs/sidebar/items#expanded-categories-by-default) | +| `sidebarItemsGenerator` | SidebarGenerator | _Omitted_ | Function used to replace the sidebar items of type `'autogenerated'` with real sidebar items (docs, categories, links...). See also [Customize the sidebar items generator](/docs/sidebar/autogenerated#customize-the-sidebar-items-generator) | +| `numberPrefixParser` | boolean \| PrefixParser | _Omitted_ | Custom parsing logic to extract number prefixes from file names. Use `false` to disable this behavior and leave the docs untouched, and `true` to use the default parser. See also [Using number prefixes](/docs/sidebar/autogenerated#using-number-prefixes) | | `docsRootComponent` | `string` | `'@theme/DocsRoot'` | Parent component of all the docs plugin pages (including all versions). Stays mounted when navigation between docs pages and versions. | | `docVersionRootComponent` | `string` | `'@theme/DocVersionLayout'` | Parent component of all docs pages of an individual version (doc pages with sidebars, tags pages). Stays mounted when navigation between pages of that specific version. | | `docRootComponent` | `string` | `'@theme/DocPage'` | Parent component of all doc pages with sidebars (regular docs pages, category generated index pages). Stays mounted when navigation between such pages. | @@ -275,7 +275,7 @@ Accepted fields: | `title` | `string` | Markdown title or `id` | The text title of your document. Used for the page metadata and as a fallback value in multiple places (sidebar, next/previous buttons...). Automatically added at the top of your doc if it does not contain any Markdown title. | | `pagination_label` | `string` | `sidebar_label` or `title` | The text used in the document next/previous buttons for this document. | | `sidebar_label` | `string` | `title` | The text shown in the document sidebar for this document. | -| `sidebar_position` | `number` | Default ordering | Controls the position of a doc inside the generated sidebar slice when using `autogenerated` sidebar items. See also [Autogenerated sidebar metadata](/docs/sidebar#autogenerated-sidebar-metadata). | +| `sidebar_position` | `number` | Default ordering | Controls the position of a doc inside the generated sidebar slice when using `autogenerated` sidebar items. See also [Autogenerated sidebar metadata](/docs/sidebar/autogenerated#autogenerated-sidebar-metadata). | | `sidebar_class_name` | `string` | `undefined` | Gives the corresponding sidebar label a special class name when using autogenerated sidebars. | | `sidebar_custom_props` | `object` | `undefined` | Assign [custom props](../../guides/docs/sidebar/index.mdx#passing-custom-props) to the sidebar item referencing this doc | | `displayed_sidebar` | `string` | `undefined` | Force the display of a given sidebar when browsing the current document. Read the [multiple sidebars guide](../../guides/docs/sidebar/multiple-sidebars.mdx) for details. | @@ -285,7 +285,7 @@ Accepted fields: | `toc_max_heading_level` | `number` | `3` | The max heading level shown in the table of contents. Must be between 2 and 6. | | `pagination_next` | string \| null | Next doc in the sidebar | The ID of the documentation you want the "Next" pagination to link to. Use `null` to disable showing "Next" for this page. | | `pagination_prev` | string \| null | Previous doc in the sidebar | The ID of the documentation you want the "Previous" pagination to link to. Use `null` to disable showing "Previous" for this page. | -| `parse_number_prefixes` | `boolean` | `numberPrefixParser` plugin option | Whether number prefix parsing is disabled on this doc. See also [Using number prefixes](/docs/sidebar#using-number-prefixes). | +| `parse_number_prefixes` | `boolean` | `numberPrefixParser` plugin option | Whether number prefix parsing is disabled on this doc. See also [Using number prefixes](/docs/sidebar/autogenerated#using-number-prefixes). | | `custom_edit_url` | string \| null | Computed using the `editUrl` plugin option | The URL for editing this document. Use `null` to disable showing "Edit this page" for this page. | | `keywords` | `string[]` | `undefined` | Keywords meta tag for the document page, for search engines. | | `description` | `string` | The first line of Markdown content | The description of your document, which will become the `` and `` in ``, used by search engines. | diff --git a/website/docs/cli.mdx b/website/docs/cli.mdx index 0b38c01776f7..bb1f32c91d68 100644 --- a/website/docs/cli.mdx +++ b/website/docs/cli.mdx @@ -177,7 +177,7 @@ By default, the files are written in `website/i18n//...`. ### `docusaurus write-heading-ids [siteDir] [files]` {#docusaurus-write-heading-ids-sitedir} -Add [explicit heading IDs](./guides/markdown-features/markdown-features-toc.mdx#explicit-ids) to the Markdown documents of your site. +Add [explicit heading IDs](./guides/markdown-features/markdown-features-toc.mdx#heading-ids) to the Markdown documents of your site. | Name | Default | Description | | --- | --- | --- | diff --git a/website/docs/guides/docs/sidebar/autogenerated.mdx b/website/docs/guides/docs/sidebar/autogenerated.mdx index 000e1e4cbdcf..7e3bfcf0a005 100644 --- a/website/docs/guides/docs/sidebar/autogenerated.mdx +++ b/website/docs/guides/docs/sidebar/autogenerated.mdx @@ -371,7 +371,7 @@ customProps: :::info -If the `link` is explicitly specified, Docusaurus will not apply any [default conventions](items.mdx#category-index-convention). +If the `link` is explicitly specified, Docusaurus will not apply any [default conventions](#category-index-convention). The doc links can be specified relatively, e.g. if the category is generated with the `guides` directory, `"link": {"type": "doc", "id": "intro"}` will be resolved to the ID `guides/intro`, only falling back to `intro` if a doc with the former ID doesn't exist. diff --git a/website/docs/guides/docs/versioning.mdx b/website/docs/guides/docs/versioning.mdx index 527a71facf02..3bb307c0fcf8 100644 --- a/website/docs/guides/docs/versioning.mdx +++ b/website/docs/guides/docs/versioning.mdx @@ -106,7 +106,7 @@ npm run docusaurus docs:version 1.1.0 When tagging a new version, the document versioning mechanism will: - Copy the full `docs/` folder contents into a new `versioned_docs/version-[versionName]/` folder. -- Create a versioned sidebars file based from your current [sidebar](docs-introduction.mdx#sidebar) configuration (if it exists) - saved as `versioned_sidebars/version-[versionName]-sidebars.json`. +- Create a versionedfrom your current [s sidebars file based idebar](./sidebar#sidebar) configuration (if it exists) - saved as `versioned_sidebars/version-[versionName]-sidebars.json`. - Append the new version number to `versions.json`. ### Creating new docs {#creating-new-docs} diff --git a/website/docs/i18n/i18n-tutorial.mdx b/website/docs/i18n/i18n-tutorial.mdx index b74896547cd7..a88e2f0a388b 100644 --- a/website/docs/i18n/i18n-tutorial.mdx +++ b/website/docs/i18n/i18n-tutorial.mdx @@ -445,7 +445,7 @@ Generated IDs are not always a good fit for localized sites, as it requires you + [link](#bonjour-le-monde) ``` -For localized sites, it is recommended to use **[explicit heading IDs](../guides/markdown-features/markdown-features-toc.mdx#explicit-ids)**. +For localized sites, it is recommended to use **[explicit heading IDs](../guides/markdown-features/markdown-features-toc.mdx#heading-ids)**. ::: diff --git a/website/docs/seo.mdx b/website/docs/seo.mdx index 147bf99657c0..b200ef35dd4c 100644 --- a/website/docs/seo.mdx +++ b/website/docs/seo.mdx @@ -211,7 +211,7 @@ For example, [`/examples/noIndex`](/examples/noIndex) is not included in the [Do ## Human readable links {#human-readable-links} -Docusaurus uses your file names as links, but you can always change that using slugs, see this [tutorial](./guides/docs/docs-introduction.mdx#document-id) for more details. +Docusaurus uses your file names as links, but you can always change that using slugs, see this [tutorial](./guides/docs/docs-introduction.mdx) for more details. ## Structured content {#structured-content} diff --git a/website/docs/using-plugins.mdx b/website/docs/using-plugins.mdx index 28e25e491541..92d86097d717 100644 --- a/website/docs/using-plugins.mdx +++ b/website/docs/using-plugins.mdx @@ -114,7 +114,7 @@ At most one plugin instance can be the "default plugin instance", by omitting th ## Using themes {#using-themes} -Themes are loaded in the exact same way as plugins. From the consumer perspective, the `themes` and `plugins` entries are interchangeable when installing and configuring a plugin. The only nuance is that themes are loaded after plugins, and it's possible for [a theme to override a plugin's default theme components](./swizzling.mdx#theme-aliases). +Themes are loaded in the exact same way as plugins. From the consumer perspective, the `themes` and `plugins` entries are interchangeable when installing and configuring a plugin. The only nuance is that themes are loaded after plugins, and it's possible for [a theme to override a plugin's default theme components](./advanced/client.mdx#theme-aliases). :::tip diff --git a/website/versioned_docs/version-2.x/advanced/routing.mdx b/website/versioned_docs/version-2.x/advanced/routing.mdx index e6513637e379..9e3551fdd37e 100644 --- a/website/versioned_docs/version-2.x/advanced/routing.mdx +++ b/website/versioned_docs/version-2.x/advanced/routing.mdx @@ -265,26 +265,18 @@ export function PageRoute() { Docusaurus builds a [single-page application](https://developer.mozilla.org/en-US/docs/Glossary/SPA), where route transitions are done through the `history.push()` method of React router. This operation is done on the client side. However, the prerequisite for a route transition to happen this way is that the target URL is known to our router. Otherwise, the router catches this path and displays a 404 page instead. -If you put some HTML pages under the `static` folder, they will be copied to the build output and therefore become accessible as part of your website, yet it's not part of the Docusaurus route system. We provide a `pathname://` protocol that allows you to redirect to another part of your domain in a non-SPA fashion, as if this route is an external link. Try the following two links: +If you put some HTML pages under the `static` folder, they will be copied to the build output and therefore become accessible as part of your website, yet it's not part of the Docusaurus route system. We provide a `pathname://` protocol that allows you to redirect to another part of your domain in a non-SPA fashion, as if this route is an external link. ```md -- [/pure-html](/pure-html) - [pathname:///pure-html](pathname:///pure-html) ``` -- [`/pure-html`](/pure-html) - [`pathname:///pure-html`](pathname:///pure-html) -:::tip - -The first link will **not** trigger a "broken links detected" check during the production build, because the respective file actually exists. Nevertheless, when you click on the link, a "page not found" will be displayed until you refresh. - -::: - The `pathname://` protocol is useful for referencing any content in the static folder. For example, Docusaurus would convert [all Markdown static assets to require() calls](../guides/markdown-features/markdown-features-assets.mdx#static-assets). You can use `pathname://` to keep it a regular link instead of being hashed by Webpack. ```md title="my-doc.md" diff --git a/website/versioned_docs/version-3.0.1/advanced/routing.mdx b/website/versioned_docs/version-3.0.1/advanced/routing.mdx index 5bf943072ba6..e9e96d4892c7 100644 --- a/website/versioned_docs/version-3.0.1/advanced/routing.mdx +++ b/website/versioned_docs/version-3.0.1/advanced/routing.mdx @@ -265,26 +265,18 @@ export function PageRoute() { Docusaurus builds a [single-page application](https://developer.mozilla.org/en-US/docs/Glossary/SPA), where route transitions are done through the `history.push()` method of React router. This operation is done on the client side. However, the prerequisite for a route transition to happen this way is that the target URL is known to our router. Otherwise, the router catches this path and displays a 404 page instead. -If you put some HTML pages under the `static` folder, they will be copied to the build output and therefore become accessible as part of your website, yet it's not part of the Docusaurus route system. We provide a `pathname://` protocol that allows you to redirect to another part of your domain in a non-SPA fashion, as if this route is an external link. Try the following two links: +If you put some HTML pages under the `static` folder, they will be copied to the build output and therefore become accessible as part of your website, yet it's not part of the Docusaurus route system. We provide a `pathname://` protocol that allows you to redirect to another part of your domain in a non-SPA fashion, as if this route is an external link. ```md -- [/pure-html](/pure-html) - [pathname:///pure-html](pathname:///pure-html) ``` -- [`/pure-html`](/pure-html) - [`pathname:///pure-html`](pathname:///pure-html) -:::tip - -The first link will **not** trigger a "broken links detected" check during the production build, because the respective file actually exists. Nevertheless, when you click on the link, a "page not found" will be displayed until you refresh. - -::: - The `pathname://` protocol is useful for referencing any content in the static folder. For example, Docusaurus would convert [all Markdown static assets to require() calls](../guides/markdown-features/markdown-features-assets.mdx#static-assets). You can use `pathname://` to keep it a regular link instead of being hashed by Webpack. ```md title="my-doc.md" From 1ad40e990c61a3eebad5ef7b6aaafc6ea3411a29 Mon Sep 17 00:00:00 2001 From: ozakione <29860391+OzakIOne@users.noreply.github.com> Date: Thu, 21 Dec 2023 23:36:31 +0100 Subject: [PATCH 18/69] docs: add onBrokenAnchor section --- website/docs/api/docusaurus.config.js.mdx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/website/docs/api/docusaurus.config.js.mdx b/website/docs/api/docusaurus.config.js.mdx index e7357f4bf520..ecb39b21aa7f 100644 --- a/website/docs/api/docusaurus.config.js.mdx +++ b/website/docs/api/docusaurus.config.js.mdx @@ -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` {#onBrokenLinks} + +- 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} From aa204ab44531475afbfe152bd8b05a5997c60717 Mon Sep 17 00:00:00 2001 From: ozakione <29860391+OzakIOne@users.noreply.github.com> Date: Thu, 21 Dec 2023 23:46:13 +0100 Subject: [PATCH 19/69] docs: add useBrokenLink hook section --- website/docs/docusaurus-core.mdx | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/website/docs/docusaurus-core.mdx b/website/docs/docusaurus-core.mdx index c14a53185e75..696fc4fc31ed 100644 --- a/website/docs/docusaurus-core.mdx +++ b/website/docs/docusaurus-core.mdx @@ -605,6 +605,38 @@ const MyComponent = () => { }; ``` +### `useBrokenLinks` {#useBrokenLinks} + +React hook to access the BrokenLinks context. The context provides methods for collecting and managing information about anchors and links. + +Usage example: + +```js +import useBrokenLinks from '@docusaurus/useBrokenLinks'; + +// MyHeading component +export default function MyHeading({id, ...props}): JSX.Element { + const brokenLinks = useBrokenLinks(); + + brokenLinks.collectAnchor(id); + + return

Heading

; +} +``` + +```js +import useBrokenLinks from '@docusaurus/useBrokenLinks'; + +// MyLink component +export default function MyLink({targetLink, ...props}): JSX.Element { + const brokenLinks = useBrokenLinks(); + + brokenLinks.collectLink(targetLink); + + return Link; +} +``` + ## Functions {#functions} ### `interpolate` {#interpolate-1} From 68f4bce38f56e208929537a38b774d83eb46e689 Mon Sep 17 00:00:00 2001 From: ozakione <29860391+OzakIOne@users.noreply.github.com> Date: Fri, 22 Dec 2023 10:00:28 +0100 Subject: [PATCH 20/69] fix: anchors --- .../version-2.x/api/plugins/plugin-content-docs.mdx | 11 +++++------ website/versioned_docs/version-2.x/cli.mdx | 2 +- website/versioned_docs/version-2.x/deployment.mdx | 2 +- .../version-2.x/guides/docs/sidebar/autogenerated.mdx | 2 +- .../version-2.x/guides/docs/versioning.mdx | 2 +- .../markdown-features/markdown-features-react.mdx | 2 +- .../versioned_docs/version-2.x/i18n/i18n-tutorial.mdx | 2 +- website/versioned_docs/version-2.x/seo.mdx | 2 +- website/versioned_docs/version-2.x/using-plugins.mdx | 2 +- 9 files changed, 13 insertions(+), 14 deletions(-) diff --git a/website/versioned_docs/version-2.x/api/plugins/plugin-content-docs.mdx b/website/versioned_docs/version-2.x/api/plugins/plugin-content-docs.mdx index e8343d9742cc..c2a6266d8ce9 100644 --- a/website/versioned_docs/version-2.x/api/plugins/plugin-content-docs.mdx +++ b/website/versioned_docs/version-2.x/api/plugins/plugin-content-docs.mdx @@ -42,10 +42,9 @@ Accepted fields: | `include` | `string[]` | `['**/*.{md,mdx}']` | Array of glob patterns matching Markdown files to be built, relative to the content path. | | `exclude` | `string[]` | _See example configuration_ | Array of glob patterns matching Markdown files to be excluded. Serves as refinement based on the `include` option. | | `sidebarPath` | false \| string | `undefined` | Path to sidebar configuration. Use `false` to disable sidebars, or `undefined` to create a fully autogenerated sidebar. | -| `sidebarCollapsible` | `boolean` | `true` | Whether sidebar categories are collapsible by default. See also [Collapsible categories](/docs/sidebar#collapsible-categories) | -| `sidebarCollapsed` | `boolean` | `true` | Whether sidebar categories are collapsed by default. See also [Expanded categories by default](/docs/sidebar#expanded-categories-by-default) | -| `sidebarItemsGenerator` | SidebarGenerator | _Omitted_ | Function used to replace the sidebar items of type `'autogenerated'` with real sidebar items (docs, categories, links...). See also [Customize the sidebar items generator](/docs/sidebar#customize-the-sidebar-items-generator) | -| `numberPrefixParser` | boolean \| PrefixParser | _Omitted_ | Custom parsing logic to extract number prefixes from file names. Use `false` to disable this behavior and leave the docs untouched, and `true` to use the default parser. See also [Using number prefixes](/docs/sidebar#using-number-prefixes) | +| `sidebarCollapsible` | `boolean` | `true` | Whether sidebar categories are collapsible by default. See also [Collapsible categories](/docs/sidebar/items#collapsible-categories) | +| `sidebarCollapsed` | `boolean` | `true` | Whether sidebar categories are collapsed by default. See also [Expanded categories by default](/docs/sidebar/items#expanded-categories-by-default) | +| `numberPrefixParser` | boolean \| PrefixParser | _Omitted_ | Custom parsing logic to extract number prefixes from file names. Use `false` to disable this behavior and leave the docs untouched, and `true` to use the default parser. See also [Using number prefixes](/docs/sidebar/autogenerated#using-number-prefixes) | | `docLayoutComponent` | `string` | `'@theme/DocPage'` | Root layout component of each doc page. Provides the version data context, and is not unmounted when switching docs. | | `docItemComponent` | `string` | `'@theme/DocItem'` | Main doc container, with TOC, pagination, etc. | | `docTagsListComponent` | `string` | `'@theme/DocTagsListPage'` | Root component of the tags list page | @@ -273,7 +272,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 +282,7 @@ Accepted fields: | `toc_max_heading_level` | `number` | `3` | The max heading level shown in the table of contents. Must be between 2 and 6. | | `pagination_next` | string \| null | Next doc in the sidebar | The ID of the documentation you want the "Next" pagination to link to. Use `null` to disable showing "Next" for this page. | | `pagination_prev` | string \| null | Previous doc in the sidebar | The ID of the documentation you want the "Previous" pagination to link to. Use `null` to disable showing "Previous" for this page. | -| `parse_number_prefixes` | `boolean` | `numberPrefixParser` plugin option | Whether number prefix parsing is disabled on this doc. See also [Using number prefixes](/docs/sidebar#using-number-prefixes). | +| `parse_number_prefixes` | `boolean` | `numberPrefixParser` plugin option | Whether number prefix parsing is disabled on this doc. See also [Using number prefixes](/docs/sidebar/autogenerated#using-number-prefixes). | | `custom_edit_url` | `string` | Computed using the `editUrl` plugin option | The URL for editing this document. | | `keywords` | `string[]` | `undefined` | Keywords meta tag for the document page, for search engines. | | `description` | `string` | The first line of Markdown content | The description of your document, which will become the `` and `` in ``, used by search engines. | diff --git a/website/versioned_docs/version-2.x/cli.mdx b/website/versioned_docs/version-2.x/cli.mdx index aaa652a48d5a..551b560aef84 100644 --- a/website/versioned_docs/version-2.x/cli.mdx +++ b/website/versioned_docs/version-2.x/cli.mdx @@ -176,7 +176,7 @@ By default, the files are written in `website/i18n//...`. ### `docusaurus write-heading-ids [siteDir] [files]` {#docusaurus-write-heading-ids-sitedir} -Add [explicit heading IDs](./guides/markdown-features/markdown-features-toc.mdx#explicit-ids) to the Markdown documents of your site. +Add [explicit heading IDs](./guides/markdown-features/markdown-features-toc.mdx#heading-ids) to the Markdown documents of your site. | Name | Default | Description | | --- | --- | --- | diff --git a/website/versioned_docs/version-2.x/deployment.mdx b/website/versioned_docs/version-2.x/deployment.mdx index 0c49003366ba..c45e7fc6c10d 100644 --- a/website/versioned_docs/version-2.x/deployment.mdx +++ b/website/versioned_docs/version-2.x/deployment.mdx @@ -57,7 +57,7 @@ Use [slorber/trailing-slash-guide](https://github.com/slorber/trailing-slash-gui ## Using environment variables {#using-environment-variables} -Putting potentially sensitive information in the environment is common practice. However, in a typical Docusaurus website, the `docusaurus.config.js` file is the only interface to the Node.js environment (see [our architecture overview](advanced/architecture.mdx)), while everything else—MDX pages, React components... are client side and do not have direct access to the `process` global. In this case, you can consider using [`customFields`](api/docusaurus.config.js.mdx#customFields) to pass environment variables to the client side. +Putting potentially sensitive information in the environment is common practice. However, in a typical Docusaurus website, the `docusaurus.config.js` file is the only interface to the Node.js environment (see [our architecture overview](advanced/architecture.mdx)), while everything else—MDX pages, React components... are client side and do not have direct access to the `process` global. In this case, you can consider using [`customFields`](api/docusaurus.config.js.mdx#customfields) to pass environment variables to the client side. ```js title="docusaurus.config.js" // If you are using dotenv (https://www.npmjs.com/package/dotenv) diff --git a/website/versioned_docs/version-2.x/guides/docs/sidebar/autogenerated.mdx b/website/versioned_docs/version-2.x/guides/docs/sidebar/autogenerated.mdx index 4ac6fa6e620f..6d9a074f19d6 100644 --- a/website/versioned_docs/version-2.x/guides/docs/sidebar/autogenerated.mdx +++ b/website/versioned_docs/version-2.x/guides/docs/sidebar/autogenerated.mdx @@ -371,7 +371,7 @@ customProps: :::info -If the `link` is explicitly specified, Docusaurus will not apply any [default conventions](items.mdx#category-index-convention). +If the `link` is explicitly specified, Docusaurus will not apply any [default conventions](#category-index-convention). The doc links can be specified relatively, e.g. if the category is generated with the `guides` directory, `"link": {"type": "doc", "id": "intro"}` will be resolved to the ID `guides/intro`, only falling back to `intro` if a doc with the former ID doesn't exist. diff --git a/website/versioned_docs/version-2.x/guides/docs/versioning.mdx b/website/versioned_docs/version-2.x/guides/docs/versioning.mdx index 2b5657f48d81..429cdd442496 100644 --- a/website/versioned_docs/version-2.x/guides/docs/versioning.mdx +++ b/website/versioned_docs/version-2.x/guides/docs/versioning.mdx @@ -106,7 +106,7 @@ npm run docusaurus docs:version 1.1.0 When tagging a new version, the document versioning mechanism will: - Copy the full `docs/` folder contents into a new `versioned_docs/version-[versionName]/` folder. -- Create a versioned sidebars file based from your current [sidebar](docs-introduction.mdx#sidebar) configuration (if it exists) - saved as `versioned_sidebars/version-[versionName]-sidebars.json`. +- Create a versioned sidebars file based from your current [sidebar](./sidebar#sidebar) configuration (if it exists) - saved as `versioned_sidebars/version-[versionName]-sidebars.json`. - Append the new version number to `versions.json`. ### Creating new docs {#creating-new-docs} diff --git a/website/versioned_docs/version-2.x/guides/markdown-features/markdown-features-react.mdx b/website/versioned_docs/version-2.x/guides/markdown-features/markdown-features-react.mdx index ac2c4a2ebc94..606ddf5e2451 100644 --- a/website/versioned_docs/version-2.x/guides/markdown-features/markdown-features-react.mdx +++ b/website/versioned_docs/version-2.x/guides/markdown-features/markdown-features-react.mdx @@ -84,7 +84,7 @@ Since all doc files are parsed using MDX, anything that looks like HTML is actua Foo ``` -This behavior is different from Docusaurus 1. See also [Migrating from v1 to v2](../../migration/migration-manual.mdx#convert-style-attributes-to-style-objects-in-mdx). +This behavior is different from Docusaurus 1. See also [Migrating from v1 to v2](../../migration/migration-manual.mdx). In addition, MDX is not [100% compatible with CommonMark](https://github.com/facebook/docusaurus/issues/3018). Use the **[MDX playground](https://mdx-git-renovate-babel-monorepo-mdx.vercel.app/playground)** to ensure that your syntax is valid MDX. diff --git a/website/versioned_docs/version-2.x/i18n/i18n-tutorial.mdx b/website/versioned_docs/version-2.x/i18n/i18n-tutorial.mdx index de7c22845af1..6c80b02dcc00 100644 --- a/website/versioned_docs/version-2.x/i18n/i18n-tutorial.mdx +++ b/website/versioned_docs/version-2.x/i18n/i18n-tutorial.mdx @@ -437,7 +437,7 @@ Generated IDs are not always a good fit for localized sites, as it requires you + [link](#bonjour-le-monde) ``` -For localized sites, it is recommended to use **[explicit heading IDs](../guides/markdown-features/markdown-features-toc.mdx#explicit-ids)**. +For localized sites, it is recommended to use **[explicit heading IDs](../guides/markdown-features/markdown-features-toc.mdx#heading-ids)**. ::: diff --git a/website/versioned_docs/version-2.x/seo.mdx b/website/versioned_docs/version-2.x/seo.mdx index ea09f87d4ae8..600423f0708a 100644 --- a/website/versioned_docs/version-2.x/seo.mdx +++ b/website/versioned_docs/version-2.x/seo.mdx @@ -152,7 +152,7 @@ For example, [`/examples/noIndex`](/examples/noIndex) is not included in the [Do ## Human readable links {#human-readable-links} -Docusaurus uses your file names as links, but you can always change that using slugs, see this [tutorial](./guides/docs/docs-introduction.mdx#document-id) for more details. +Docusaurus uses your file names as links, but you can always change that using slugs, see this [tutorial](./guides/docs/docs-introduction.mdx) for more details. ## Structured content {#structured-content} diff --git a/website/versioned_docs/version-2.x/using-plugins.mdx b/website/versioned_docs/version-2.x/using-plugins.mdx index c40672830ee6..8d7accf505b5 100644 --- a/website/versioned_docs/version-2.x/using-plugins.mdx +++ b/website/versioned_docs/version-2.x/using-plugins.mdx @@ -114,7 +114,7 @@ At most one plugin instance can be the "default plugin instance", by omitting th ## Using themes {#using-themes} -Themes are loaded in the exact same way as plugins. From the consumer perspective, the `themes` and `plugins` entries are interchangeable when installing and configuring a plugin. The only nuance is that themes are loaded after plugins, and it's possible for [a theme to override a plugin's default theme components](./swizzling.mdx#theme-aliases). +Themes are loaded in the exact same way as plugins. From the consumer perspective, the `themes` and `plugins` entries are interchangeable when installing and configuring a plugin. The only nuance is that themes are loaded after plugins, and it's possible for [a theme to override a plugin's default theme components](./advanced/client.mdx#theme-aliases). :::tip From 3fd42b6c6c5420de18661e868edaeef12a2e3fbd Mon Sep 17 00:00:00 2001 From: ozakione <29860391+OzakIOne@users.noreply.github.com> Date: Fri, 22 Dec 2023 10:26:40 +0100 Subject: [PATCH 21/69] fix: anchors --- .../api/plugins/plugin-content-docs.mdx | 12 ++++++------ website/versioned_docs/version-3.0.1/cli.mdx | 2 +- .../guides/docs/sidebar/autogenerated.mdx | 2 +- .../version-3.0.1/guides/docs/versioning.mdx | 2 +- .../version-3.0.1/i18n/i18n-tutorial.mdx | 2 +- website/versioned_docs/version-3.0.1/seo.mdx | 2 +- .../versioned_docs/version-3.0.1/using-plugins.mdx | 2 +- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/website/versioned_docs/version-3.0.1/api/plugins/plugin-content-docs.mdx b/website/versioned_docs/version-3.0.1/api/plugins/plugin-content-docs.mdx index d9238f65bb36..754a56d293d9 100644 --- a/website/versioned_docs/version-3.0.1/api/plugins/plugin-content-docs.mdx +++ b/website/versioned_docs/version-3.0.1/api/plugins/plugin-content-docs.mdx @@ -42,10 +42,10 @@ Accepted fields: | `include` | `string[]` | `['**/*.{md,mdx}']` | Array of glob patterns matching Markdown files to be built, relative to the content path. | | `exclude` | `string[]` | _See example configuration_ | Array of glob patterns matching Markdown files to be excluded. Serves as refinement based on the `include` option. | | `sidebarPath` | false \| string | `undefined` | Path to sidebar configuration. Use `false` to disable sidebars, or `undefined` to create a fully autogenerated sidebar. | -| `sidebarCollapsible` | `boolean` | `true` | Whether sidebar categories are collapsible by default. See also [Collapsible categories](/docs/sidebar#collapsible-categories) | -| `sidebarCollapsed` | `boolean` | `true` | Whether sidebar categories are collapsed by default. See also [Expanded categories by default](/docs/sidebar#expanded-categories-by-default) | -| `sidebarItemsGenerator` | SidebarGenerator | _Omitted_ | Function used to replace the sidebar items of type `'autogenerated'` with real sidebar items (docs, categories, links...). See also [Customize the sidebar items generator](/docs/sidebar#customize-the-sidebar-items-generator) | -| `numberPrefixParser` | boolean \| PrefixParser | _Omitted_ | Custom parsing logic to extract number prefixes from file names. Use `false` to disable this behavior and leave the docs untouched, and `true` to use the default parser. See also [Using number prefixes](/docs/sidebar#using-number-prefixes) | +| `sidebarCollapsible` | `boolean` | `true` | Whether sidebar categories are collapsible by default. See also [Collapsible categories](/docs/sidebar/items#collapsible-categories) | +| `sidebarCollapsed` | `boolean` | `true` | Whether sidebar categories are collapsed by default. See also [Expanded categories by default](/docs/sidebar/items#expanded-categories-by-default) | +| `sidebarItemsGenerator` | SidebarGenerator | _Omitted_ | Function used to replace the sidebar items of type `'autogenerated'` with real sidebar items (docs, categories, links...). See also [Customize the sidebar items generator](/docs/sidebar/autogenerated#customize-the-sidebar-items-generator) | +| `numberPrefixParser` | boolean \| PrefixParser | _Omitted_ | Custom parsing logic to extract number prefixes from file names. Use `false` to disable this behavior and leave the docs untouched, and `true` to use the default parser. See also [Using number prefixes](/docs/sidebar/autogenerated#using-number-prefixes) | | `docsRootComponent` | `string` | `'@theme/DocsRoot'` | Parent component of all the docs plugin pages (including all versions). Stays mounted when navigation between docs pages and versions. | | `docVersionRootComponent` | `string` | `'@theme/DocVersionLayout'` | Parent component of all docs pages of an individual version (doc pages with sidebars, tags pages). Stays mounted when navigation between pages of that specific version. | | `docRootComponent` | `string` | `'@theme/DocPage'` | Parent component of all doc pages with sidebars (regular docs pages, category generated index pages). Stays mounted when navigation between such pages. | @@ -275,7 +275,7 @@ Accepted fields: | `title` | `string` | Markdown title or `id` | The text title of your document. Used for the page metadata and as a fallback value in multiple places (sidebar, next/previous buttons...). Automatically added at the top of your doc if it does not contain any Markdown title. | | `pagination_label` | `string` | `sidebar_label` or `title` | The text used in the document next/previous buttons for this document. | | `sidebar_label` | `string` | `title` | The text shown in the document sidebar for this document. | -| `sidebar_position` | `number` | Default ordering | Controls the position of a doc inside the generated sidebar slice when using `autogenerated` sidebar items. See also [Autogenerated sidebar metadata](/docs/sidebar#autogenerated-sidebar-metadata). | +| `sidebar_position` | `number` | Default ordering | Controls the position of a doc inside the generated sidebar slice when using `autogenerated` sidebar items. See also [Autogenerated sidebar metadata](/docs/sidebar/autogenerated#autogenerated-sidebar-metadata). | | `sidebar_class_name` | `string` | `undefined` | Gives the corresponding sidebar label a special class name when using autogenerated sidebars. | | `sidebar_custom_props` | `object` | `undefined` | Assign [custom props](../../guides/docs/sidebar/index.mdx#passing-custom-props) to the sidebar item referencing this doc | | `displayed_sidebar` | `string` | `undefined` | Force the display of a given sidebar when browsing the current document. Read the [multiple sidebars guide](../../guides/docs/sidebar/multiple-sidebars.mdx) for details. | @@ -285,7 +285,7 @@ Accepted fields: | `toc_max_heading_level` | `number` | `3` | The max heading level shown in the table of contents. Must be between 2 and 6. | | `pagination_next` | string \| null | Next doc in the sidebar | The ID of the documentation you want the "Next" pagination to link to. Use `null` to disable showing "Next" for this page. | | `pagination_prev` | string \| null | Previous doc in the sidebar | The ID of the documentation you want the "Previous" pagination to link to. Use `null` to disable showing "Previous" for this page. | -| `parse_number_prefixes` | `boolean` | `numberPrefixParser` plugin option | Whether number prefix parsing is disabled on this doc. See also [Using number prefixes](/docs/sidebar#using-number-prefixes). | +| `parse_number_prefixes` | `boolean` | `numberPrefixParser` plugin option | Whether number prefix parsing is disabled on this doc. See also [Using number prefixes](/docs/sidebar/autogenerated#using-number-prefixes). | | `custom_edit_url` | string \| null | Computed using the `editUrl` plugin option | The URL for editing this document. Use `null` to disable showing "Edit this page" for this page. | | `keywords` | `string[]` | `undefined` | Keywords meta tag for the document page, for search engines. | | `description` | `string` | The first line of Markdown content | The description of your document, which will become the `` and `` in ``, used by search engines. | diff --git a/website/versioned_docs/version-3.0.1/cli.mdx b/website/versioned_docs/version-3.0.1/cli.mdx index 0b38c01776f7..bb1f32c91d68 100644 --- a/website/versioned_docs/version-3.0.1/cli.mdx +++ b/website/versioned_docs/version-3.0.1/cli.mdx @@ -177,7 +177,7 @@ By default, the files are written in `website/i18n//...`. ### `docusaurus write-heading-ids [siteDir] [files]` {#docusaurus-write-heading-ids-sitedir} -Add [explicit heading IDs](./guides/markdown-features/markdown-features-toc.mdx#explicit-ids) to the Markdown documents of your site. +Add [explicit heading IDs](./guides/markdown-features/markdown-features-toc.mdx#heading-ids) to the Markdown documents of your site. | Name | Default | Description | | --- | --- | --- | diff --git a/website/versioned_docs/version-3.0.1/guides/docs/sidebar/autogenerated.mdx b/website/versioned_docs/version-3.0.1/guides/docs/sidebar/autogenerated.mdx index 000e1e4cbdcf..7e3bfcf0a005 100644 --- a/website/versioned_docs/version-3.0.1/guides/docs/sidebar/autogenerated.mdx +++ b/website/versioned_docs/version-3.0.1/guides/docs/sidebar/autogenerated.mdx @@ -371,7 +371,7 @@ customProps: :::info -If the `link` is explicitly specified, Docusaurus will not apply any [default conventions](items.mdx#category-index-convention). +If the `link` is explicitly specified, Docusaurus will not apply any [default conventions](#category-index-convention). The doc links can be specified relatively, e.g. if the category is generated with the `guides` directory, `"link": {"type": "doc", "id": "intro"}` will be resolved to the ID `guides/intro`, only falling back to `intro` if a doc with the former ID doesn't exist. diff --git a/website/versioned_docs/version-3.0.1/guides/docs/versioning.mdx b/website/versioned_docs/version-3.0.1/guides/docs/versioning.mdx index b473d69a1268..97fe0152ff34 100644 --- a/website/versioned_docs/version-3.0.1/guides/docs/versioning.mdx +++ b/website/versioned_docs/version-3.0.1/guides/docs/versioning.mdx @@ -106,7 +106,7 @@ npm run docusaurus docs:version 1.1.0 When tagging a new version, the document versioning mechanism will: - Copy the full `docs/` folder contents into a new `versioned_docs/version-[versionName]/` folder. -- Create a versioned sidebars file based from your current [sidebar](docs-introduction.mdx#sidebar) configuration (if it exists) - saved as `versioned_sidebars/version-[versionName]-sidebars.json`. +- Create a versionedfrom your current [s sidebars file based idebar](./sidebar#sidebar) configuration (if it exists) - saved as `versioned_sidebars/version-[versionName]-sidebars.json`. - Append the new version number to `versions.json`. ### Creating new docs {#creating-new-docs} diff --git a/website/versioned_docs/version-3.0.1/i18n/i18n-tutorial.mdx b/website/versioned_docs/version-3.0.1/i18n/i18n-tutorial.mdx index b74896547cd7..a88e2f0a388b 100644 --- a/website/versioned_docs/version-3.0.1/i18n/i18n-tutorial.mdx +++ b/website/versioned_docs/version-3.0.1/i18n/i18n-tutorial.mdx @@ -445,7 +445,7 @@ Generated IDs are not always a good fit for localized sites, as it requires you + [link](#bonjour-le-monde) ``` -For localized sites, it is recommended to use **[explicit heading IDs](../guides/markdown-features/markdown-features-toc.mdx#explicit-ids)**. +For localized sites, it is recommended to use **[explicit heading IDs](../guides/markdown-features/markdown-features-toc.mdx#heading-ids)**. ::: diff --git a/website/versioned_docs/version-3.0.1/seo.mdx b/website/versioned_docs/version-3.0.1/seo.mdx index 147bf99657c0..b200ef35dd4c 100644 --- a/website/versioned_docs/version-3.0.1/seo.mdx +++ b/website/versioned_docs/version-3.0.1/seo.mdx @@ -211,7 +211,7 @@ For example, [`/examples/noIndex`](/examples/noIndex) is not included in the [Do ## Human readable links {#human-readable-links} -Docusaurus uses your file names as links, but you can always change that using slugs, see this [tutorial](./guides/docs/docs-introduction.mdx#document-id) for more details. +Docusaurus uses your file names as links, but you can always change that using slugs, see this [tutorial](./guides/docs/docs-introduction.mdx) for more details. ## Structured content {#structured-content} diff --git a/website/versioned_docs/version-3.0.1/using-plugins.mdx b/website/versioned_docs/version-3.0.1/using-plugins.mdx index 28e25e491541..92d86097d717 100644 --- a/website/versioned_docs/version-3.0.1/using-plugins.mdx +++ b/website/versioned_docs/version-3.0.1/using-plugins.mdx @@ -114,7 +114,7 @@ At most one plugin instance can be the "default plugin instance", by omitting th ## Using themes {#using-themes} -Themes are loaded in the exact same way as plugins. From the consumer perspective, the `themes` and `plugins` entries are interchangeable when installing and configuring a plugin. The only nuance is that themes are loaded after plugins, and it's possible for [a theme to override a plugin's default theme components](./swizzling.mdx#theme-aliases). +Themes are loaded in the exact same way as plugins. From the consumer perspective, the `themes` and `plugins` entries are interchangeable when installing and configuring a plugin. The only nuance is that themes are loaded after plugins, and it's possible for [a theme to override a plugin's default theme components](./advanced/client.mdx#theme-aliases). :::tip From 3fba43db955a586b285e06f2f2fb76121e946974 Mon Sep 17 00:00:00 2001 From: ozakione <29860391+OzakIOne@users.noreply.github.com> Date: Fri, 22 Dec 2023 11:08:28 +0100 Subject: [PATCH 22/69] fix: links --- website/docs/guides/docs/versioning.mdx | 2 +- website/versioned_docs/version-2.x/guides/docs/versioning.mdx | 2 +- website/versioned_docs/version-3.0.1/guides/docs/versioning.mdx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/website/docs/guides/docs/versioning.mdx b/website/docs/guides/docs/versioning.mdx index 3bb307c0fcf8..6303918e6a8f 100644 --- a/website/docs/guides/docs/versioning.mdx +++ b/website/docs/guides/docs/versioning.mdx @@ -106,7 +106,7 @@ npm run docusaurus docs:version 1.1.0 When tagging a new version, the document versioning mechanism will: - Copy the full `docs/` folder contents into a new `versioned_docs/version-[versionName]/` folder. -- Create a versionedfrom your current [s sidebars file based idebar](./sidebar#sidebar) configuration (if it exists) - saved as `versioned_sidebars/version-[versionName]-sidebars.json`. +- Create a versioned sidebars file based from your current [sidebar](sidebar.mdx#sidebar) configuration (if it exists) - saved as `versioned_sidebars/version-[versionName]-sidebars.json`. - Append the new version number to `versions.json`. ### Creating new docs {#creating-new-docs} diff --git a/website/versioned_docs/version-2.x/guides/docs/versioning.mdx b/website/versioned_docs/version-2.x/guides/docs/versioning.mdx index 429cdd442496..2079b01df85c 100644 --- a/website/versioned_docs/version-2.x/guides/docs/versioning.mdx +++ b/website/versioned_docs/version-2.x/guides/docs/versioning.mdx @@ -106,7 +106,7 @@ npm run docusaurus docs:version 1.1.0 When tagging a new version, the document versioning mechanism will: - Copy the full `docs/` folder contents into a new `versioned_docs/version-[versionName]/` folder. -- Create a versioned sidebars file based from your current [sidebar](./sidebar#sidebar) configuration (if it exists) - saved as `versioned_sidebars/version-[versionName]-sidebars.json`. +- Create a versioned sidebars file based from your current [sidebar](sidebar#sidebar) configuration (if it exists) - saved as `versioned_sidebars/version-[versionName]-sidebars.json`. - Append the new version number to `versions.json`. ### Creating new docs {#creating-new-docs} diff --git a/website/versioned_docs/version-3.0.1/guides/docs/versioning.mdx b/website/versioned_docs/version-3.0.1/guides/docs/versioning.mdx index 97fe0152ff34..159d99d77df8 100644 --- a/website/versioned_docs/version-3.0.1/guides/docs/versioning.mdx +++ b/website/versioned_docs/version-3.0.1/guides/docs/versioning.mdx @@ -106,7 +106,7 @@ npm run docusaurus docs:version 1.1.0 When tagging a new version, the document versioning mechanism will: - Copy the full `docs/` folder contents into a new `versioned_docs/version-[versionName]/` folder. -- Create a versionedfrom your current [s sidebars file based idebar](./sidebar#sidebar) configuration (if it exists) - saved as `versioned_sidebars/version-[versionName]-sidebars.json`. +- Create a versioned sidebars file based from your current [sidebar](sidebar#sidebar) 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} From b3487464e89279dd5b9a6bcf46c66941ecf73c81 Mon Sep 17 00:00:00 2001 From: ozakione <29860391+OzakIOne@users.noreply.github.com> Date: Fri, 22 Dec 2023 11:29:16 +0100 Subject: [PATCH 23/69] fix: links --- website/docs/guides/docs/versioning.mdx | 2 +- website/versioned_docs/version-2.x/guides/docs/versioning.mdx | 2 +- website/versioned_docs/version-3.0.1/guides/docs/versioning.mdx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/website/docs/guides/docs/versioning.mdx b/website/docs/guides/docs/versioning.mdx index 6303918e6a8f..08fab227b542 100644 --- a/website/docs/guides/docs/versioning.mdx +++ b/website/docs/guides/docs/versioning.mdx @@ -106,7 +106,7 @@ npm run docusaurus docs:version 1.1.0 When tagging a new version, the document versioning mechanism will: - Copy the full `docs/` folder contents into a new `versioned_docs/version-[versionName]/` folder. -- Create a versioned sidebars file based from your current [sidebar](sidebar.mdx#sidebar) configuration (if it exists) - saved as `versioned_sidebars/version-[versionName]-sidebars.json`. +- Create a versioned sidebars file based from your current [sidebar](./sidebar/index.mdx) configuration (if it exists) - saved as `versioned_sidebars/version-[versionName]-sidebars.json`. - Append the new version number to `versions.json`. ### Creating new docs {#creating-new-docs} diff --git a/website/versioned_docs/version-2.x/guides/docs/versioning.mdx b/website/versioned_docs/version-2.x/guides/docs/versioning.mdx index 2079b01df85c..60e41494d745 100644 --- a/website/versioned_docs/version-2.x/guides/docs/versioning.mdx +++ b/website/versioned_docs/version-2.x/guides/docs/versioning.mdx @@ -106,7 +106,7 @@ npm run docusaurus docs:version 1.1.0 When tagging a new version, the document versioning mechanism will: - Copy the full `docs/` folder contents into a new `versioned_docs/version-[versionName]/` folder. -- Create a versioned sidebars file based from your current [sidebar](sidebar#sidebar) configuration (if it exists) - saved as `versioned_sidebars/version-[versionName]-sidebars.json`. +- Create a versioned sidebars file based from your current [sidebar](./sidebar/index.mdx) configuration (if it exists) - saved as `versioned_sidebars/version-[versionName]-sidebars.json`. - Append the new version number to `versions.json`. ### Creating new docs {#creating-new-docs} diff --git a/website/versioned_docs/version-3.0.1/guides/docs/versioning.mdx b/website/versioned_docs/version-3.0.1/guides/docs/versioning.mdx index 159d99d77df8..1fa34fb1c5f4 100644 --- a/website/versioned_docs/version-3.0.1/guides/docs/versioning.mdx +++ b/website/versioned_docs/version-3.0.1/guides/docs/versioning.mdx @@ -106,7 +106,7 @@ npm run docusaurus docs:version 1.1.0 When tagging a new version, the document versioning mechanism will: - Copy the full `docs/` folder contents into a new `versioned_docs/version-[versionName]/` folder. -- Create a versioned sidebars file based from your current [sidebar](sidebar#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} From a90f6583108448f31b2c0054dae7e1998b2fa37b Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Fri, 22 Dec 2023 19:05:32 +0100 Subject: [PATCH 24/69] minor refactor --- packages/docusaurus/src/server/brokenLinks.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/docusaurus/src/server/brokenLinks.ts b/packages/docusaurus/src/server/brokenLinks.ts index f9f2f867f62f..d8516b71d79e 100644 --- a/packages/docusaurus/src/server/brokenLinks.ts +++ b/packages/docusaurus/src/server/brokenLinks.ts @@ -159,20 +159,20 @@ function brokenLinkMessage(brokenLink: BrokenLink): string { }`; } -function generalBrokenLinksMessage( +function createBrokenLinksMessage( type: 'link' | 'anchor', pagePath: string, - brokenLinks: BrokenLink[], + allBrokenLinks: BrokenLink[], ): string { - const anchorBrokenLinks = + const brokenLinks = type === 'anchor' - ? brokenLinks.filter((link) => link.anchor) - : brokenLinks.filter((link) => !link.anchor); + ? allBrokenLinks.filter((link) => link.anchor) + : allBrokenLinks.filter((link) => !link.anchor); const anchorMessage = - anchorBrokenLinks.length > 0 + brokenLinks.length > 0 ? `- Broken ${type} on source page path = ${pagePath}: - -> linking to ${anchorBrokenLinks + -> linking to ${brokenLinks .map(brokenLinkMessage) .join('\n -> linking to ')}` : ''; @@ -195,7 +195,7 @@ Note: it's possible to ignore broken anchors with the 'onBrokenAnchors' Docusaur Exhaustive list of all broken anchors found: ${Object.entries(allBrokenLinks) .map(([pagePath, brokenLinks]) => - generalBrokenLinksMessage('anchor', pagePath, brokenLinks), + createBrokenLinksMessage('anchor', pagePath, brokenLinks), ) .join('\n')} `; @@ -249,7 +249,7 @@ Note: it's possible to ignore broken links with the 'onBrokenLinks' Docusaurus c Exhaustive list of all broken links found: ${Object.entries(allBrokenLinks) .map(([pagePath, brokenLinks]) => - generalBrokenLinksMessage('link', pagePath, brokenLinks), + createBrokenLinksMessage('link', pagePath, brokenLinks), ) .join('\n')} `; From 294e7bf58d33add1f1768443a65abbf6f1231435 Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Fri, 22 Dec 2023 19:10:23 +0100 Subject: [PATCH 25/69] getAllBrokenLinks return object instead of tuple2 --- packages/docusaurus/src/server/brokenLinks.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/docusaurus/src/server/brokenLinks.ts b/packages/docusaurus/src/server/brokenLinks.ts index d8516b71d79e..f0ba09aec90f 100644 --- a/packages/docusaurus/src/server/brokenLinks.ts +++ b/packages/docusaurus/src/server/brokenLinks.ts @@ -18,6 +18,8 @@ type BrokenLink = { anchor: boolean; }; +type BrokenLinksByLocation = {[location: string]: BrokenLink[]}; + type CollectedLinks = { [location: string]: {links: string[]; anchors: string[]}; }; @@ -30,7 +32,7 @@ function onlyPathname(link: string) { function checkAnchorsInOtherRoutes(allCollectedCorrectLinks: CollectedLinks): { [location: string]: BrokenLink[]; } { - const brokenLinksByLocation: {[location: string]: BrokenLink[]} = {}; + const brokenLinksByLocation: BrokenLinksByLocation = {}; Object.entries(allCollectedCorrectLinks).forEach(([key, value]) => { const brokenLinks = value.links.flatMap((link) => { @@ -130,7 +132,7 @@ function getAllBrokenLinks({ }: { allCollectedLinks: CollectedLinks; routes: RouteConfig[]; -}): [{[location: string]: BrokenLink[]}, {[location: string]: BrokenLink[]}] { +}): {brokenLinks: BrokenLinksByLocation; brokenAnchors: BrokenLinksByLocation} { const filteredRoutes = filterIntermediateRoutes(routes); const allBrokenLinks = _.mapValues( @@ -143,13 +145,13 @@ function getAllBrokenLinks({ routes: filteredRoutes, }), ); - const filteredLinks = Object.fromEntries( + const brokenLinks = Object.fromEntries( Object.entries(allBrokenLinks).filter(([, value]) => value.length > 0), ); - const allBrokenAnchors = checkAnchorsInOtherRoutes(allCollectedLinks); + const brokenAnchors = checkAnchorsInOtherRoutes(allCollectedLinks); - return [filteredLinks, allBrokenAnchors]; + return {brokenLinks, brokenAnchors}; } function brokenLinkMessage(brokenLink: BrokenLink): string { @@ -270,7 +272,7 @@ export async function handleBrokenLinks({ return; } - const [brokenLinks, brokenAnchors] = getAllBrokenLinks({ + const {brokenLinks, brokenAnchors} = getAllBrokenLinks({ allCollectedLinks, routes, }); From 20e0cef567b76b3f0f11275676bf1922ec97ee4f Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Fri, 22 Dec 2023 19:13:10 +0100 Subject: [PATCH 26/69] line break --- packages/docusaurus/src/server/brokenLinks.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/docusaurus/src/server/brokenLinks.ts b/packages/docusaurus/src/server/brokenLinks.ts index f0ba09aec90f..8e5c200a4ce2 100644 --- a/packages/docusaurus/src/server/brokenLinks.ts +++ b/packages/docusaurus/src/server/brokenLinks.ts @@ -112,6 +112,7 @@ function getPageBrokenLinks({ } return []; }); + return brokenLinks; } From 3b68d3336eb7719f89a9dfc53531b72cc7542e53 Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Fri, 22 Dec 2023 19:22:56 +0100 Subject: [PATCH 27/69] add todo --- packages/docusaurus/src/server/__tests__/brokenLinks.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts index 2f9928e2760f..2fe4ae618985 100644 --- a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts +++ b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts @@ -181,6 +181,7 @@ describe('handleBrokenLinks', () => { }); it('no-op for ignore', async () => { + // TODO this mock is not future-proof, we may remove mapValues // In any case, _.mapValues will always be called, unless handleBrokenLinks // has already bailed const lodashMock = jest.spyOn(_, 'mapValues'); From d2837bfd990960cae76e87f2943a36bade70031f Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Fri, 22 Dec 2023 19:30:40 +0100 Subject: [PATCH 28/69] cleanup broken links tests --- .../__snapshots__/brokenLinks.test.ts.snap | 20 ---------- .../src/server/__tests__/brokenLinks.test.ts | 40 +------------------ 2 files changed, 2 insertions(+), 58 deletions(-) diff --git a/packages/docusaurus/src/server/__tests__/__snapshots__/brokenLinks.test.ts.snap b/packages/docusaurus/src/server/__tests__/__snapshots__/brokenLinks.test.ts.snap index e51aaa01c897..e8d6210724ff 100644 --- a/packages/docusaurus/src/server/__tests__/__snapshots__/brokenLinks.test.ts.snap +++ b/packages/docusaurus/src/server/__tests__/__snapshots__/brokenLinks.test.ts.snap @@ -21,19 +21,9 @@ Exhaustive list of all broken links found: -> linking to ./badLink3 (resolved as: /badLink3) - Broken link on source page path = /page1: -> linking to /link1 - -> linking to /files/hey.html - -> linking to /javadoc - -> linking to /files/hey - -> linking to /javadoc/index.html - -> linking to /javadoc/index.html#foo (resolved as: /javadoc/index.html) - -> linking to /emptyFolder - Broken link on source page path = /page2: -> linking to /docs/link2 - -> linking to /emptyFolder/ - -> linking to /javadoc/ -> linking to /hey/link3 - -> linking to /javadoc/index.html - -> linking to /files/file.zip " `; @@ -71,21 +61,11 @@ Exhaustive list of all broken links found: -> linking to ./maybe-not (resolved as: /maybe-not) - Broken link on source page path = /page1: -> linking to /link1 - -> linking to /files/hey.html - -> linking to /javadoc - -> linking to /files/hey - -> linking to /javadoc/index.html - -> linking to /javadoc/index.html#foo (resolved as: /javadoc/index.html) - -> linking to /emptyFolder -> linking to /frequent -> linking to ./maybe-not (resolved as: /maybe-not) - Broken link on source page path = /page2: -> linking to /docs/link2 - -> linking to /emptyFolder/ - -> linking to /javadoc/ -> linking to /hey/link3 - -> linking to /javadoc/index.html - -> linking to /files/file.zip -> linking to /frequent -> linking to ./maybe-not (resolved as: /maybe-not) " diff --git a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts index 2fe4ae618985..dde560591db6 100644 --- a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts +++ b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts @@ -37,17 +37,6 @@ describe('handleBrokenLinks', () => { 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': { links: [ @@ -93,26 +82,11 @@ describe('handleBrokenLinks', () => { anchors: [], }, '/page1': { - links: [ - link1, - linkToHtmlFile1, - linkToJavadoc1, - linkToHtmlFile2, - linkToJavadoc3, - linkToJavadoc4, - linkToEmptyFolder1, // Not filtered! - ], + links: [link1], anchors: [], }, '/page2': { - links: [ - link2, - linkToEmptyFolder2, // Not filtered! - linkToJavadoc2, - link3, - linkToJavadoc3, - linkToZipFile, - ], + links: [link2, link3], anchors: [], }, }; @@ -149,16 +123,6 @@ describe('handleBrokenLinks', () => { ], anchors: ['anchorFromGoodDoc'], }, - // '/page1': { - // links: [ - // linkToHtmlFile1, - // linkToJavadoc1, - // linkToHtmlFile2, - // linkToJavadoc3, - // linkToJavadoc4, - // ], - // anchors: [], - // }, }; await handleBrokenLinks({ allCollectedLinks: allCollectedCorrectLinks, From 27facd0c1940e664b2d1ff6e203422a3ab917d70 Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Fri, 22 Dec 2023 19:35:14 +0100 Subject: [PATCH 29/69] simplify test setup --- .../docusaurus/src/server/__tests__/brokenLinks.test.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts index dde560591db6..3ae64eca341b 100644 --- a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts +++ b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts @@ -92,9 +92,6 @@ describe('handleBrokenLinks', () => { }; it('do not report anything for correct paths', async () => { - const consoleMock = jest - .spyOn(console, 'warn') - .mockImplementation(() => {}); const allCollectedCorrectLinks = { '/docs/good doc with space': { links: [ @@ -124,13 +121,13 @@ describe('handleBrokenLinks', () => { anchors: ['anchorFromGoodDoc'], }, }; + await handleBrokenLinks({ allCollectedLinks: allCollectedCorrectLinks, - onBrokenLinks: 'warn', - onBrokenAnchors: 'warn', + onBrokenLinks: 'throw', + onBrokenAnchors: 'throw', routes, }); - expect(consoleMock).toHaveBeenCalledTimes(0); }); it('reports all broken links', async () => { From e368ac90ce5dce758c8e6d2d1831b23f2f9e4029 Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Fri, 22 Dec 2023 19:47:22 +0100 Subject: [PATCH 30/69] simplify test setup --- .../src/server/__tests__/brokenLinks.test.ts | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts index 3ae64eca341b..b41d83363ba3 100644 --- a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts +++ b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts @@ -55,10 +55,10 @@ describe('handleBrokenLinks', () => { '/docs/goodDoc': { links: [ // Good links - './anotherGoodDoc#someHash', - '/docs/anotherGoodDoc?someQueryString=true#someHash', + './anotherGoodDoc#anotherGoodDocHash', + '/docs/anotherGoodDoc?someQueryString=true#anotherGoodDocHash', '../docs/anotherGoodDoc?someQueryString=true', - '../docs/anotherGoodDoc#someHash', + '../docs/anotherGoodDoc#anotherGoodDocHash', // Bad links '../anotherGoodDoc#reported-because-of-bad-relative-path1', './docThatDoesNotExist2', @@ -71,8 +71,8 @@ describe('handleBrokenLinks', () => { links: [ // Good links '/docs/goodDoc', - '/docs/anotherGoodDoc#someHash', - './docs/goodDoc#someHash', + '/docs/anotherGoodDoc#anotherGoodDocHash', + './docs/goodDoc#goodDocHash', './docs/anotherGoodDoc', // Bad links '/someNonExistentDoc1', @@ -102,23 +102,23 @@ describe('handleBrokenLinks', () => { }, '/docs/goodDoc': { links: [ - '#someHash', - '/community#anchorFromGoodDoc', - './anotherGoodDoc#someHash', - '/docs/anotherGoodDoc?someQueryString=true#someHash', + '#goodDocHash', + '/community#communityAnchor', + './anotherGoodDoc#nonExistingHash', // TODO should be reported! + '/docs/anotherGoodDoc?someQueryString=true#anotherGoodDocHash', '../docs/anotherGoodDoc?someQueryString=true', - '../docs/anotherGoodDoc#someHash', + '../docs/anotherGoodDoc#anotherGoodDocHash', ], - anchors: ['someHash'], // anchors here are anchors of the page itself (/docs/goodDoc) not the links in it + anchors: ['goodDocHash'], // anchors here are anchors of the page itself (/docs/goodDoc) not the links in it }, '/community': { links: [ '/docs/goodDoc', - '/docs/anotherGoodDoc#someHash', // anchor here is an anchor of an other page, it should be checked against the anchors of the other page - './docs/goodDoc#someHash', + '/docs/anotherGoodDoc#anotherGoodDocHash', // anchor here is an anchor of an other page, it should be checked against the anchors of the other page + './docs/goodDoc#goodDocHash', './docs/anotherGoodDoc', ], - anchors: ['anchorFromGoodDoc'], + anchors: ['communityAnchor'], }, }; From 72571e93b70bdf188e6ca2e1d6d0e0e975417d11 Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Fri, 22 Dec 2023 20:12:46 +0100 Subject: [PATCH 31/69] add new helpers to create tests for broken links --- .../src/server/__tests__/brokenLinks.test.ts | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts index b41d83363ba3..9589270326bb 100644 --- a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts +++ b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts @@ -10,6 +10,59 @@ import _ from 'lodash'; import {handleBrokenLinks} from '../brokenLinks'; import type {RouteConfig} from '@docusaurus/types'; +type Params = Parameters[0]; + +// We don't need all the routes attributes for our tests +type SimpleRoute = {path: string; routes?: SimpleRoute[]}; + +// Conveniently apply defaults to function under test +async function testBrokenLinks(params: { + allCollectedLinks?: Params['allCollectedLinks']; + onBrokenLinks?: Params['onBrokenLinks']; + onBrokenAnchors?: Params['onBrokenAnchors']; + routes?: SimpleRoute[]; +}) { + await handleBrokenLinks({ + allCollectedLinks: {}, + onBrokenLinks: 'throw', + onBrokenAnchors: 'throw', + ...params, + // Unsafe but convenient for tests + routes: (params.routes ?? []) as RouteConfig[], + }); +} + +describe('handleBrokenLinks NEW TESTS', () => { + it('can accept simple valid link', async () => { + await testBrokenLinks({ + routes: [{path: '/page1'}, {path: '/page2'}], + allCollectedLinks: { + '/page1': {links: ['/page2'], anchors: []}, + }, + }); + }); + + it('can report simple broken link', async () => { + await expect(() => + testBrokenLinks({ + allCollectedLinks: { + '/pageWithBrokenLink': {links: ['/brokenLink'], anchors: []}, + }, + }), + ).rejects.toThrowErrorMatchingInlineSnapshot(` + "Docusaurus found broken links! + + Please check the pages of your site in the list below, and make sure you don't reference any path that does not exist. + Note: it's possible to ignore broken links with the 'onBrokenLinks' Docusaurus configuration, and let the build pass. + + Exhaustive list of all broken links found: + - Broken link on source page path = /pageWithBrokenLink: + -> linking to /brokenLink + " + `); + }); +}); + describe('handleBrokenLinks', () => { const routes: RouteConfig[] = [ { From 5870ac3b96e54a4e55381314daedd72dfa00c5a0 Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Sat, 23 Dec 2023 16:59:01 +0100 Subject: [PATCH 32/69] Add new unit failing tests --- .../src/server/__tests__/brokenLinks.test.ts | 117 +++++++++++++++++- 1 file changed, 113 insertions(+), 4 deletions(-) diff --git a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts index 9589270326bb..1bd81f42c343 100644 --- a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts +++ b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts @@ -33,7 +33,7 @@ async function testBrokenLinks(params: { } describe('handleBrokenLinks NEW TESTS', () => { - it('can accept simple valid link', async () => { + it('accepts valid link', async () => { await testBrokenLinks({ routes: [{path: '/page1'}, {path: '/page2'}], allCollectedLinks: { @@ -42,11 +42,56 @@ describe('handleBrokenLinks NEW TESTS', () => { }); }); - it('can report simple broken link', async () => { + it('accepts valid relative link', async () => { + await testBrokenLinks({ + routes: [{path: '/dir/page1'}, {path: '/dir/page2'}], + allCollectedLinks: { + '/dir/page1': { + links: ['./page2', '../dir/page2', '/dir/page2'], + anchors: [], + }, + }, + }); + }); + + it('accepts valid link with anchor', async () => { + await testBrokenLinks({ + routes: [{path: '/page1'}, {path: '/page2'}], + allCollectedLinks: { + '/page1': {links: ['/page2#page2anchor'], anchors: []}, + '/page2': {links: [], anchors: ['page2anchor']}, + }, + }); + }); + + it('accepts valid link with querystring + anchor', async () => { + await testBrokenLinks({ + routes: [{path: '/page1'}, {path: '/page2'}], + allCollectedLinks: { + '/page1': { + links: ['/page2?age=42&theme=dark#page2anchor'], + anchors: [], + }, + '/page2': {links: [], anchors: ['page2anchor']}, + }, + }); + }); + + it('accepts valid link with spaces and encoding', async () => { + await testBrokenLinks({ + routes: [{path: '/page 1'}, {path: '/page 2'}], + allCollectedLinks: { + '/page 1': {links: ['/page 2', '/page%202'], anchors: []}, + }, + }); + }); + + it('rejects broken link', async () => { await expect(() => testBrokenLinks({ + routes: [{path: '/page1'}, {path: '/page2'}], allCollectedLinks: { - '/pageWithBrokenLink': {links: ['/brokenLink'], anchors: []}, + '/page1': {links: ['/brokenLink'], anchors: []}, }, }), ).rejects.toThrowErrorMatchingInlineSnapshot(` @@ -56,11 +101,75 @@ describe('handleBrokenLinks NEW TESTS', () => { 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 = /pageWithBrokenLink: + - Broken link on source page path = /page1: -> linking to /brokenLink " `); }); + + it('rejects broken anchor', async () => { + await expect(() => + testBrokenLinks({ + routes: [{path: '/page1'}, {path: '/page2'}], + allCollectedLinks: { + '/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 path 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 (resolved as: /page2) + " + `); + }); + + it('rejects broken anchor with query-string', async () => { + await expect(() => + testBrokenLinks({ + routes: [{path: '/page1'}, {path: '/page2'}], + allCollectedLinks: { + '/page1': { + links: ['/page2?age=42&theme=dark#brokenAnchor'], + anchors: [], + }, + '/page2': {links: [], anchors: []}, + }, + }), + ).rejects.toThrowErrorMatchingInlineSnapshot(); + }); + + it('rejects broken anchor to uncollected page', async () => { + await expect(() => + testBrokenLinks({ + routes: [{path: '/page1'}, {path: '/page2'}], + allCollectedLinks: { + '/page1': {links: ['/page2#brokenAnchor'], anchors: []}, + // /page2 is absent on purpose: it doesn't contain any link/anchor + }, + }), + ).rejects.toThrowErrorMatchingInlineSnapshot(); + }); + + it('rejects broken anchor with query-string to uncollected page', async () => { + await expect(() => + testBrokenLinks({ + routes: [{path: '/page1'}, {path: '/page2'}], + allCollectedLinks: { + '/page1': { + links: ['/page2?age=42&theme=dark#brokenAnchor'], + anchors: [], + }, + // /page2 is absent on purpose: it doesn't contain any link/anchor + }, + }), + ).rejects.toThrowErrorMatchingInlineSnapshot(); + }); }); describe('handleBrokenLinks', () => { From 97822f4d299a3ba9cd93953fbdf85e583ed18f95 Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Sat, 23 Dec 2023 17:01:08 +0100 Subject: [PATCH 33/69] add test for nested subroutes --- .../src/server/__tests__/brokenLinks.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts index 1bd81f42c343..890db1f6e48e 100644 --- a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts +++ b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts @@ -42,6 +42,18 @@ describe('handleBrokenLinks NEW TESTS', () => { }); }); + it('accepts valid link of nested route', async () => { + await testBrokenLinks({ + routes: [ + {path: '/page1'}, + {path: '/nested/', routes: [{path: '/nested/page2'}]}, + ], + allCollectedLinks: { + '/page1': {links: ['/nested/page2'], anchors: []}, + }, + }); + }); + it('accepts valid relative link', async () => { await testBrokenLinks({ routes: [{path: '/dir/page1'}, {path: '/dir/page2'}], From 2640a5992986169508a7d50f4eb0a36c87ac1a1f Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Sat, 23 Dec 2023 17:01:20 +0100 Subject: [PATCH 34/69] add test for nested subroutes --- packages/docusaurus/src/server/__tests__/brokenLinks.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts index 890db1f6e48e..78d904e53fea 100644 --- a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts +++ b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts @@ -42,7 +42,7 @@ describe('handleBrokenLinks NEW TESTS', () => { }); }); - it('accepts valid link of nested route', async () => { + it('accepts valid link to nested route', async () => { await testBrokenLinks({ routes: [ {path: '/page1'}, From 4f450b05f3fde3809a99be4a258ff72850066ca2 Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Sat, 23 Dec 2023 17:07:28 +0100 Subject: [PATCH 35/69] test: rejects broken anchor to self --- .../src/server/__tests__/brokenLinks.test.ts | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts index 78d904e53fea..4b44eb08e2cf 100644 --- a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts +++ b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts @@ -89,6 +89,26 @@ describe('handleBrokenLinks NEW TESTS', () => { }); }); + it('accepts valid link to self', async () => { + await testBrokenLinks({ + routes: [{path: '/page1'}], + allCollectedLinks: { + '/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'}], @@ -156,6 +176,37 @@ describe('handleBrokenLinks NEW TESTS', () => { ).rejects.toThrowErrorMatchingInlineSnapshot(); }); + it('rejects broken anchor to self', async () => { + await expect(() => + testBrokenLinks({ + routes: [{path: '/page1'}], + allCollectedLinks: { + '/page1': { + links: [ + '#goodAnchor', + '/page1#goodAnchor', + '/page1?age=42#goodAnchor', + '#badAnchor1', + '/page1#badAnchor2', + '/page1?age=42#badAnchor3', + ], + + anchors: ['goodAnchor'], + }, + }, + }), + ).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: + + " + `); + }); + it('rejects broken anchor to uncollected page', async () => { await expect(() => testBrokenLinks({ From ad8483fa3e51382d867be5ff8ccaa9af06ecf4a3 Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Sat, 23 Dec 2023 17:24:16 +0100 Subject: [PATCH 36/69] can ignore broken links and broken anchors --- .../src/server/__tests__/brokenLinks.test.ts | 111 ++++++++++++++++-- 1 file changed, 103 insertions(+), 8 deletions(-) diff --git a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts index 4b44eb08e2cf..ca0880775fac 100644 --- a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts +++ b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts @@ -38,6 +38,17 @@ describe('handleBrokenLinks NEW TESTS', () => { routes: [{path: '/page1'}, {path: '/page2'}], allCollectedLinks: { '/page1': {links: ['/page2'], anchors: []}, + '/page2': {links: [], anchors: []}, + }, + }); + }); + + it('accepts valid link to uncollected page', async () => { + await testBrokenLinks({ + routes: [{path: '/page1'}, {path: '/page2'}], + allCollectedLinks: { + '/page1': {links: ['/page2'], anchors: []}, + // /page2 is absent on purpose: it doesn't contain any link/anchor }, }); }); @@ -113,7 +124,18 @@ describe('handleBrokenLinks NEW TESTS', () => { await testBrokenLinks({ routes: [{path: '/page 1'}, {path: '/page 2'}], allCollectedLinks: { - '/page 1': {links: ['/page 2', '/page%202'], anchors: []}, + '/page 1': { + links: [ + '/page 1', + '/page%201', + '/page%201?age=42', + '/page 2', + '/page%202', + '/page%202?age=42', + ], + anchors: [], + }, + '/page 2': {links: [], anchors: []}, }, }); }); @@ -139,7 +161,50 @@ describe('handleBrokenLinks NEW TESTS', () => { `); }); - it('rejects broken anchor', async () => { + it('rejects broken link with anchor', async () => { + await expect(() => + testBrokenLinks({ + routes: [{path: '/page1'}, {path: '/page2'}], + allCollectedLinks: { + '/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 (resolved as: /brokenLink) + " + `); + }); + + it('rejects broken link with querystring + anchor', async () => { + await expect(() => + testBrokenLinks({ + routes: [{path: '/page1'}, {path: '/page2'}], + allCollectedLinks: { + '/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 (resolved as: /brokenLink) + " + `); + }); + + // TODO it does not reject + it('rejects valid link with broken anchor', async () => { await expect(() => testBrokenLinks({ routes: [{path: '/page1'}, {path: '/page2'}], @@ -161,7 +226,7 @@ describe('handleBrokenLinks NEW TESTS', () => { `); }); - it('rejects broken anchor with query-string', async () => { + it('rejects valid link with broken anchor + query-string', async () => { await expect(() => testBrokenLinks({ routes: [{path: '/page1'}, {path: '/page2'}], @@ -176,7 +241,7 @@ describe('handleBrokenLinks NEW TESTS', () => { ).rejects.toThrowErrorMatchingInlineSnapshot(); }); - it('rejects broken anchor to self', async () => { + it('rejects valid link with broken anchor to self', async () => { await expect(() => testBrokenLinks({ routes: [{path: '/page1'}], @@ -190,12 +255,13 @@ describe('handleBrokenLinks NEW TESTS', () => { '/page1#badAnchor2', '/page1?age=42#badAnchor3', ], - anchors: ['goodAnchor'], }, }, }), - ).rejects.toThrowErrorMatchingInlineSnapshot(` + ).rejects.toThrowErrorMatchingInlineSnapshot( + // TODO bad error message + ` "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. @@ -204,10 +270,12 @@ describe('handleBrokenLinks NEW TESTS', () => { Exhaustive list of all broken links found: " - `); + `, + ); }); - it('rejects broken anchor to uncollected page', async () => { + // TODO it does not reject + it('rejects valid link with broken anchor to uncollected page', async () => { await expect(() => testBrokenLinks({ routes: [{path: '/page1'}, {path: '/page2'}], @@ -219,6 +287,7 @@ describe('handleBrokenLinks NEW TESTS', () => { ).rejects.toThrowErrorMatchingInlineSnapshot(); }); + // TODO it does not reject it('rejects broken anchor with query-string to uncollected page', async () => { await expect(() => testBrokenLinks({ @@ -233,6 +302,32 @@ describe('handleBrokenLinks NEW TESTS', () => { }), ).rejects.toThrowErrorMatchingInlineSnapshot(); }); + + it('can ignore broken links', async () => { + await testBrokenLinks({ + onBrokenLinks: 'ignore', + routes: [{path: '/page1'}], + allCollectedLinks: { + '/page1': { + links: ['/page2'], + anchors: [], + }, + }, + }); + }); + + it('can ignore broken anchors', async () => { + await testBrokenLinks({ + onBrokenAnchors: 'ignore', + routes: [{path: '/page1'}], + allCollectedLinks: { + '/page1': { + links: ['/page1#brokenAnchor'], + anchors: [], + }, + }, + }); + }); }); describe('handleBrokenLinks', () => { From 5cb9a3d8480b823aa9c30f2707c36df4d5de88d5 Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Sat, 23 Dec 2023 17:38:47 +0100 Subject: [PATCH 37/69] add coverage for broken link/anchor levels --- .../src/server/__tests__/brokenLinks.test.ts | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts index ca0880775fac..1cf493a57e7d 100644 --- a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts +++ b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts @@ -328,6 +328,117 @@ describe('handleBrokenLinks NEW TESTS', () => { }, }); }); + + it('can warn for broken links', async () => { + const warnMock = jest.spyOn(console, 'warn'); + + await testBrokenLinks({ + onBrokenLinks: 'warn', + routes: [{path: '/page1'}], + allCollectedLinks: { + '/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'}], + allCollectedLinks: { + '/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 path 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 (resolved as: /page1) + ", + ], + ] + `); + 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'}], + allCollectedLinks: { + '/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 path 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 (resolved as: /page1) + ", + ], + ] + `); + warnMock.mockRestore(); + }); }); describe('handleBrokenLinks', () => { From d3a78efc54808bd3bc5a4443b8f8583b811acc1b Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Sat, 23 Dec 2023 17:51:06 +0100 Subject: [PATCH 38/69] add coverage for reports frequent broken links differently --- .../src/server/__tests__/brokenLinks.test.ts | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts index 1cf493a57e7d..c00d2ae3df0f 100644 --- a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts +++ b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts @@ -439,6 +439,67 @@ describe('handleBrokenLinks NEW TESTS', () => { `); 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 allCollectedLinks: Params['allCollectedLinks'] = Object.fromEntries( + pagePaths.map((pagePath) => [ + pagePath, + { + links: ['/frequentBrokenLink', './relativeFrequentBrokenLink'], + anchors: [], + }, + ]), + ); + + await expect(() => + testBrokenLinks({ + routes, + allCollectedLinks, + }), + ).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) + " + `); + }); }); describe('handleBrokenLinks', () => { From 3e651370a59990813c2efec624993106fbf64025 Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Sat, 23 Dec 2023 17:55:50 +0100 Subject: [PATCH 39/69] minor broken anchor error message update --- .../src/server/__tests__/brokenLinks.test.ts | 30 ++++++++++++++++--- packages/docusaurus/src/server/brokenLinks.ts | 2 +- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts index c00d2ae3df0f..d31145f4c6eb 100644 --- a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts +++ b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts @@ -203,7 +203,6 @@ describe('handleBrokenLinks NEW TESTS', () => { `); }); - // TODO it does not reject it('rejects valid link with broken anchor', async () => { await expect(() => testBrokenLinks({ @@ -216,7 +215,7 @@ describe('handleBrokenLinks NEW TESTS', () => { ).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 path that does not exist. + 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: @@ -226,6 +225,29 @@ describe('handleBrokenLinks NEW TESTS', () => { `); }); + it('rejects valid link with empty broken anchor', async () => { + await expect(() => + testBrokenLinks({ + routes: [{path: '/page1'}, {path: '/page2'}], + allCollectedLinks: { + '/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# (resolved as: /page2) + " + `); + }); + + // TODO it does not reject it('rejects valid link with broken anchor + query-string', async () => { await expect(() => testBrokenLinks({ @@ -382,7 +404,7 @@ describe('handleBrokenLinks NEW TESTS', () => { [ "[WARNING] Docusaurus found broken anchors! - Please check the pages of your site in the list below, and make sure you don't reference any path that does not exist. + 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: @@ -427,7 +449,7 @@ describe('handleBrokenLinks NEW TESTS', () => { [ "[WARNING] Docusaurus found broken anchors! - Please check the pages of your site in the list below, and make sure you don't reference any path that does not exist. + 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: diff --git a/packages/docusaurus/src/server/brokenLinks.ts b/packages/docusaurus/src/server/brokenLinks.ts index 8e5c200a4ce2..9836af884013 100644 --- a/packages/docusaurus/src/server/brokenLinks.ts +++ b/packages/docusaurus/src/server/brokenLinks.ts @@ -192,7 +192,7 @@ function getAnchorBrokenLinksErrorMessage(allBrokenLinks: { return `Docusaurus found broken anchors! -Please check the pages of your site in the list below, and make sure you don't reference any path that does not exist. +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: From 57209a1781a928577a535f0113d0e01af2c07306 Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Sat, 23 Dec 2023 17:58:17 +0100 Subject: [PATCH 40/69] remove old brokenLinks test --- .../__snapshots__/brokenLinks.test.ts.snap | 72 -------- .../src/server/__tests__/brokenLinks.test.ts | 169 ------------------ 2 files changed, 241 deletions(-) delete mode 100644 packages/docusaurus/src/server/__tests__/__snapshots__/brokenLinks.test.ts.snap diff --git a/packages/docusaurus/src/server/__tests__/__snapshots__/brokenLinks.test.ts.snap b/packages/docusaurus/src/server/__tests__/__snapshots__/brokenLinks.test.ts.snap deleted file mode 100644 index e8d6210724ff..000000000000 --- a/packages/docusaurus/src/server/__tests__/__snapshots__/brokenLinks.test.ts.snap +++ /dev/null @@ -1,72 +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: -- Broken link 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) -- Broken link 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) -- Broken link on source page path = /community: - -> linking to /someNonExistentDoc1 - -> linking to /badLink2 - -> linking to ./badLink3 (resolved as: /badLink3) -- Broken link on source page path = /page1: - -> linking to /link1 -- Broken link on source page path = /page2: - -> linking to /docs/link2 - -> 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: -- Broken link 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) -- Broken link 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) -- Broken link 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) -- Broken link on source page path = /page1: - -> linking to /link1 - -> linking to /frequent - -> linking to ./maybe-not (resolved as: /maybe-not) -- Broken link on source page path = /page2: - -> linking to /docs/link2 - -> linking to /hey/link3 - -> linking to /frequent - -> linking to ./maybe-not (resolved as: /maybe-not) -" -`; diff --git a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts index d31145f4c6eb..a0de965f21df 100644 --- a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts +++ b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts @@ -6,7 +6,6 @@ */ import {jest} from '@jest/globals'; -import _ from 'lodash'; import {handleBrokenLinks} from '../brokenLinks'; import type {RouteConfig} from '@docusaurus/types'; @@ -523,171 +522,3 @@ describe('handleBrokenLinks NEW TESTS', () => { `); }); }); - -describe('handleBrokenLinks', () => { - const routes: RouteConfig[] = [ - { - path: '/community', - component: '', - }, - { - path: '/docs', - component: '', - routes: [ - {path: '/docs/goodDoc', component: ''}, - {path: '/docs/anotherGoodDoc', component: ''}, - {path: '/docs/good doc with space', component: ''}, - {path: '/docs/another good doc with space', component: ''}, - {path: '/docs/weird%20but%20good', component: ''}, - ], - }, - { - path: '*', - component: '', - }, - ]; - - const link1 = '/link1'; - const link2 = '/docs/link2'; - const link3 = '/hey/link3'; - - const allCollectedLinks = { - '/docs/good doc with space': { - links: [ - // 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', - ], - anchors: [], - }, - '/docs/goodDoc': { - links: [ - // Good links - './anotherGoodDoc#anotherGoodDocHash', - '/docs/anotherGoodDoc?someQueryString=true#anotherGoodDocHash', - '../docs/anotherGoodDoc?someQueryString=true', - '../docs/anotherGoodDoc#anotherGoodDocHash', - // Bad links - '../anotherGoodDoc#reported-because-of-bad-relative-path1', - './docThatDoesNotExist2', - './badRelativeLink3', - '../badRelativeLink4', - ], - anchors: [], - }, - '/community': { - links: [ - // Good links - '/docs/goodDoc', - '/docs/anotherGoodDoc#anotherGoodDocHash', - './docs/goodDoc#goodDocHash', - './docs/anotherGoodDoc', - // Bad links - '/someNonExistentDoc1', - '/badLink2', - './badLink3', - ], - anchors: [], - }, - '/page1': { - links: [link1], - anchors: [], - }, - '/page2': { - links: [link2, link3], - anchors: [], - }, - }; - - it('do not report anything for correct paths', async () => { - const allCollectedCorrectLinks = { - '/docs/good doc with space': { - links: [ - './another%20good%20doc%20with%20space', - './weird%20but%20good', - ], - anchors: [], - }, - '/docs/goodDoc': { - links: [ - '#goodDocHash', - '/community#communityAnchor', - './anotherGoodDoc#nonExistingHash', // TODO should be reported! - '/docs/anotherGoodDoc?someQueryString=true#anotherGoodDocHash', - '../docs/anotherGoodDoc?someQueryString=true', - '../docs/anotherGoodDoc#anotherGoodDocHash', - ], - anchors: ['goodDocHash'], // anchors here are anchors of the page itself (/docs/goodDoc) not the links in it - }, - '/community': { - links: [ - '/docs/goodDoc', - '/docs/anotherGoodDoc#anotherGoodDocHash', // anchor here is an anchor of an other page, it should be checked against the anchors of the other page - './docs/goodDoc#goodDocHash', - './docs/anotherGoodDoc', - ], - anchors: ['communityAnchor'], - }, - }; - - await handleBrokenLinks({ - allCollectedLinks: allCollectedCorrectLinks, - onBrokenLinks: 'throw', - onBrokenAnchors: 'throw', - routes, - }); - }); - - it('reports all broken links', async () => { - await expect(() => - handleBrokenLinks({ - allCollectedLinks, - onBrokenLinks: 'throw', - onBrokenAnchors: 'throw', - routes, - }), - ).rejects.toThrowErrorMatchingSnapshot(); - }); - - it('no-op for ignore', async () => { - // TODO this mock is not future-proof, we may remove mapValues - // In any case, _.mapValues will always be called, unless handleBrokenLinks - // has already bailed - const lodashMock = jest.spyOn(_, 'mapValues'); - await handleBrokenLinks({ - allCollectedLinks, - onBrokenLinks: 'ignore', - onBrokenAnchors: 'ignore', - routes, - }); - 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', - ); - }); - - await expect(() => - handleBrokenLinks({ - allCollectedLinks, - onBrokenLinks: 'throw', - onBrokenAnchors: 'throw', - routes, - }), - ).rejects.toThrowErrorMatchingSnapshot(); - }); -}); From 527717a4e5f682454e26fa314935787061f532d9 Mon Sep 17 00:00:00 2001 From: ozakione <29860391+OzakIOne@users.noreply.github.com> Date: Tue, 26 Dec 2023 09:40:53 +0100 Subject: [PATCH 41/69] docs: reviews --- website/docs/docusaurus-core.mdx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/website/docs/docusaurus-core.mdx b/website/docs/docusaurus-core.mdx index 696fc4fc31ed..cfa5fcd6b695 100644 --- a/website/docs/docusaurus-core.mdx +++ b/website/docs/docusaurus-core.mdx @@ -609,12 +609,17 @@ const MyComponent = () => { React hook to access the BrokenLinks context. The context provides methods for collecting and managing information about anchors and links. +:::info + +This is an advanced hook, most users won't need it. + +::: + Usage example: -```js +```js title=MyHeading.js import useBrokenLinks from '@docusaurus/useBrokenLinks'; -// MyHeading component export default function MyHeading({id, ...props}): JSX.Element { const brokenLinks = useBrokenLinks(); @@ -624,10 +629,9 @@ export default function MyHeading({id, ...props}): JSX.Element { } ``` -```js +```js title=MyLink.js import useBrokenLinks from '@docusaurus/useBrokenLinks'; -// MyLink component export default function MyLink({targetLink, ...props}): JSX.Element { const brokenLinks = useBrokenLinks(); From 9dfa6c790dde586f73b4db19523e774d318780fd Mon Sep 17 00:00:00 2001 From: ozakione <29860391+OzakIOne@users.noreply.github.com> Date: Tue, 26 Dec 2023 09:44:28 +0100 Subject: [PATCH 42/69] docs: fix title --- website/docs/docusaurus-core.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/docs/docusaurus-core.mdx b/website/docs/docusaurus-core.mdx index cfa5fcd6b695..5b302f8c1286 100644 --- a/website/docs/docusaurus-core.mdx +++ b/website/docs/docusaurus-core.mdx @@ -617,7 +617,7 @@ This is an advanced hook, most users won't need it. Usage example: -```js title=MyHeading.js +```js title="MyHeading.js" import useBrokenLinks from '@docusaurus/useBrokenLinks'; export default function MyHeading({id, ...props}): JSX.Element { @@ -629,7 +629,7 @@ export default function MyHeading({id, ...props}): JSX.Element { } ``` -```js title=MyLink.js +```js title="MyLink.js" import useBrokenLinks from '@docusaurus/useBrokenLinks'; export default function MyLink({targetLink, ...props}): JSX.Element { From c5e40d7d81c5f67b9afe0c07f930fac8338fd9dc Mon Sep 17 00:00:00 2001 From: ozakione <29860391+OzakIOne@users.noreply.github.com> Date: Tue, 26 Dec 2023 10:13:20 +0100 Subject: [PATCH 43/69] docs: add removed link --- .../version-2.x/api/plugins/plugin-content-docs.mdx | 1 + 1 file changed, 1 insertion(+) diff --git a/website/versioned_docs/version-2.x/api/plugins/plugin-content-docs.mdx b/website/versioned_docs/version-2.x/api/plugins/plugin-content-docs.mdx index c2a6266d8ce9..cd98d4e99e1c 100644 --- a/website/versioned_docs/version-2.x/api/plugins/plugin-content-docs.mdx +++ b/website/versioned_docs/version-2.x/api/plugins/plugin-content-docs.mdx @@ -44,6 +44,7 @@ Accepted fields: | `sidebarPath` | false \| string | `undefined` | Path to sidebar configuration. Use `false` to disable sidebars, or `undefined` to create a fully autogenerated sidebar. | | `sidebarCollapsible` | `boolean` | `true` | Whether sidebar categories are collapsible by default. See also [Collapsible categories](/docs/sidebar/items#collapsible-categories) | | `sidebarCollapsed` | `boolean` | `true` | Whether sidebar categories are collapsed by default. See also [Expanded categories by default](/docs/sidebar/items#expanded-categories-by-default) | +| `sidebarItemsGenerator` | SidebarGenerator | _Omitted_ | Function used to replace the sidebar items of type `'autogenerated'` with real sidebar items (docs, categories, links...). See also [Customize the sidebar items generator](/docs/sidebar/autogenerated#customize-the-sidebar-items-generator) | | `numberPrefixParser` | boolean \| PrefixParser | _Omitted_ | Custom parsing logic to extract number prefixes from file names. Use `false` to disable this behavior and leave the docs untouched, and `true` to use the default parser. See also [Using number prefixes](/docs/sidebar/autogenerated#using-number-prefixes) | | `docLayoutComponent` | `string` | `'@theme/DocPage'` | Root layout component of each doc page. Provides the version data context, and is not unmounted when switching docs. | | `docItemComponent` | `string` | `'@theme/DocItem'` | Main doc container, with TOC, pagination, etc. | From 0653c0e493e4f1ce92172bd6357cc778eb954f28 Mon Sep 17 00:00:00 2001 From: ozakione <29860391+OzakIOne@users.noreply.github.com> Date: Tue, 26 Dec 2023 10:19:45 +0100 Subject: [PATCH 44/69] remove any type --- packages/docusaurus/src/commands/build.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/docusaurus/src/commands/build.ts b/packages/docusaurus/src/commands/build.ts index 660b64d43f33..c61a06e02aa0 100644 --- a/packages/docusaurus/src/commands/build.ts +++ b/packages/docusaurus/src/commands/build.ts @@ -110,7 +110,7 @@ export async function build( ...i18n.locales.filter((locale) => locale !== i18n.defaultLocale), ]; - const results = await mapAsyncSequential(orderedLocales, (locale: any) => { + const results = await mapAsyncSequential(orderedLocales, (locale) => { const isLastLocale = orderedLocales.indexOf(locale) === orderedLocales.length - 1; return tryToBuildLocale({locale, isLastLocale}); From 5211d04d07df3abb240f1af17dc48458b266efdb Mon Sep 17 00:00:00 2001 From: ozakione <29860391+OzakIOne@users.noreply.github.com> Date: Tue, 26 Dec 2023 11:32:22 +0100 Subject: [PATCH 45/69] docs: update link --- website/docs/seo.mdx | 2 +- website/versioned_docs/version-2.x/seo.mdx | 2 +- website/versioned_docs/version-3.0.1/seo.mdx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/website/docs/seo.mdx b/website/docs/seo.mdx index b200ef35dd4c..031ab1ddf340 100644 --- a/website/docs/seo.mdx +++ b/website/docs/seo.mdx @@ -211,7 +211,7 @@ For example, [`/examples/noIndex`](/examples/noIndex) is not included in the [Do ## Human readable links {#human-readable-links} -Docusaurus uses your file names as links, but you can always change that using slugs, see this [tutorial](./guides/docs/docs-introduction.mdx) for more details. +Docusaurus uses your file names as links, but you can always change that using slugs, see this [tutorial](./guides/docs/docs-create-doc.mdx#document-id) for more details. ## Structured content {#structured-content} diff --git a/website/versioned_docs/version-2.x/seo.mdx b/website/versioned_docs/version-2.x/seo.mdx index 600423f0708a..a8af9c30c75a 100644 --- a/website/versioned_docs/version-2.x/seo.mdx +++ b/website/versioned_docs/version-2.x/seo.mdx @@ -152,7 +152,7 @@ For example, [`/examples/noIndex`](/examples/noIndex) is not included in the [Do ## Human readable links {#human-readable-links} -Docusaurus uses your file names as links, but you can always change that using slugs, see this [tutorial](./guides/docs/docs-introduction.mdx) for more details. +Docusaurus uses your file names as links, but you can always change that using slugs, see this [tutorial](./guides/docs/docs-create-doc.mdx#document-id) for more details. ## Structured content {#structured-content} diff --git a/website/versioned_docs/version-3.0.1/seo.mdx b/website/versioned_docs/version-3.0.1/seo.mdx index b200ef35dd4c..031ab1ddf340 100644 --- a/website/versioned_docs/version-3.0.1/seo.mdx +++ b/website/versioned_docs/version-3.0.1/seo.mdx @@ -211,7 +211,7 @@ For example, [`/examples/noIndex`](/examples/noIndex) is not included in the [Do ## Human readable links {#human-readable-links} -Docusaurus uses your file names as links, but you can always change that using slugs, see this [tutorial](./guides/docs/docs-introduction.mdx) 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} From 87af79c2605981d43ec79308d852cc160440d6fc Mon Sep 17 00:00:00 2001 From: ozakione <29860391+OzakIOne@users.noreply.github.com> Date: Tue, 26 Dec 2023 14:22:51 +0100 Subject: [PATCH 46/69] fix: handle query strings --- .../src/server/__tests__/brokenLinks.test.ts | 34 +++++++++++++++++-- packages/docusaurus/src/server/brokenLinks.ts | 25 ++++++++++++-- 2 files changed, 54 insertions(+), 5 deletions(-) diff --git a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts index a0de965f21df..10c183bc46ff 100644 --- a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts +++ b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts @@ -259,7 +259,17 @@ describe('handleBrokenLinks NEW TESTS', () => { '/page2': {links: [], anchors: []}, }, }), - ).rejects.toThrowErrorMatchingInlineSnapshot(); + ).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 (resolved as: /page2) + " + `); }); it('rejects valid link with broken anchor to self', async () => { @@ -305,7 +315,16 @@ describe('handleBrokenLinks NEW TESTS', () => { // /page2 is absent on purpose: it doesn't contain any link/anchor }, }), - ).rejects.toThrowErrorMatchingInlineSnapshot(); + ).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: + + " + `); }); // TODO it does not reject @@ -321,7 +340,16 @@ describe('handleBrokenLinks NEW TESTS', () => { // /page2 is absent on purpose: it doesn't contain any link/anchor }, }), - ).rejects.toThrowErrorMatchingInlineSnapshot(); + ).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: + + " + `); }); it('can ignore broken links', async () => { diff --git a/packages/docusaurus/src/server/brokenLinks.ts b/packages/docusaurus/src/server/brokenLinks.ts index 9836af884013..c8256edc8fe8 100644 --- a/packages/docusaurus/src/server/brokenLinks.ts +++ b/packages/docusaurus/src/server/brokenLinks.ts @@ -29,14 +29,33 @@ function onlyPathname(link: string) { return link.split('#')[0]!.split('?')[0]!; } +function getRouteAndAnchor(link: string) { + const url = new URL(link, 'https://example.com'); + const [, splitAnchor] = link.split('#'); + + const route = url.pathname; + const anchor = url.hash.slice(1) || undefined; + + if (splitAnchor === '') { + // rejects valid link with empty broken anchor + // new URL will return an empty string /docs# and /docs + return {route, anchor: ''}; + } + + return {route, anchor}; +} + function checkAnchorsInOtherRoutes(allCollectedCorrectLinks: CollectedLinks): { [location: string]: BrokenLink[]; } { const brokenLinksByLocation: BrokenLinksByLocation = {}; - Object.entries(allCollectedCorrectLinks).forEach(([key, value]) => { + const linkEntries = Object.entries(allCollectedCorrectLinks); + + linkEntries.forEach(([key, value]) => { const brokenLinks = value.links.flatMap((link) => { - const [route, anchor] = link.split('#'); + const {route, anchor} = getRouteAndAnchor(link); + // const [route, anchor] = link.split('#'); if (route !== '' && anchor !== undefined) { const targetRoute = allCollectedCorrectLinks[route!]; if (targetRoute && !targetRoute.anchors.includes(anchor)) { @@ -149,6 +168,8 @@ function getAllBrokenLinks({ const brokenLinks = Object.fromEntries( Object.entries(allBrokenLinks).filter(([, value]) => value.length > 0), ); + // console.log('brokenLinks:', brokenLinks); + // console.log('allBrokenLinks:', allBrokenLinks); const brokenAnchors = checkAnchorsInOtherRoutes(allCollectedLinks); From 8473fbc0e27c332790cc1c85443a9f2bfa53dccd Mon Sep 17 00:00:00 2001 From: ozakione <29860391+OzakIOne@users.noreply.github.com> Date: Tue, 26 Dec 2023 17:16:21 +0100 Subject: [PATCH 47/69] wip: fix todo, introduce side effect --- .../src/server/__tests__/brokenLinks.test.ts | 25 +++++++++---------- packages/docusaurus/src/server/brokenLinks.ts | 22 ++++++++++------ 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts index 10c183bc46ff..9ed17f1e5680 100644 --- a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts +++ b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts @@ -246,7 +246,6 @@ describe('handleBrokenLinks NEW TESTS', () => { `); }); - // TODO it does not reject it('rejects valid link with broken anchor + query-string', async () => { await expect(() => testBrokenLinks({ @@ -305,7 +304,6 @@ describe('handleBrokenLinks NEW TESTS', () => { ); }); - // TODO it does not reject it('rejects valid link with broken anchor to uncollected page', async () => { await expect(() => testBrokenLinks({ @@ -316,18 +314,18 @@ describe('handleBrokenLinks NEW TESTS', () => { }, }), ).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. + "Docusaurus found broken anchors! - Exhaustive list of all broken links found: + 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 (resolved as: /page2) " `); }); - // TODO it does not reject it('rejects broken anchor with query-string to uncollected page', async () => { await expect(() => testBrokenLinks({ @@ -341,13 +339,14 @@ describe('handleBrokenLinks NEW TESTS', () => { }, }), ).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. + "Docusaurus found broken anchors! - Exhaustive list of all broken links found: + 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 (resolved as: /page2) " `); }); diff --git a/packages/docusaurus/src/server/brokenLinks.ts b/packages/docusaurus/src/server/brokenLinks.ts index c8256edc8fe8..0c374fe26c70 100644 --- a/packages/docusaurus/src/server/brokenLinks.ts +++ b/packages/docusaurus/src/server/brokenLinks.ts @@ -50,14 +50,14 @@ function checkAnchorsInOtherRoutes(allCollectedCorrectLinks: CollectedLinks): { } { const brokenLinksByLocation: BrokenLinksByLocation = {}; - const linkEntries = Object.entries(allCollectedCorrectLinks); + const linkCollection = Object.entries(allCollectedCorrectLinks); - linkEntries.forEach(([key, value]) => { - const brokenLinks = value.links.flatMap((link) => { + linkCollection.forEach(([path, collection]) => { + const brokenLinks = collection.links.flatMap((link) => { const {route, anchor} = getRouteAndAnchor(link); - // const [route, anchor] = link.split('#'); - if (route !== '' && anchor !== undefined) { - const targetRoute = allCollectedCorrectLinks[route!]; + if (anchor !== undefined) { + const targetRoute = allCollectedCorrectLinks[route]; + if (targetRoute && !targetRoute.anchors.includes(anchor)) { return [ { @@ -66,13 +66,21 @@ function checkAnchorsInOtherRoutes(allCollectedCorrectLinks: CollectedLinks): { anchor: true, }, ]; + } else if (!targetRoute) { + return [ + { + link: `${route}#${anchor}`, + resolvedLink: route!, + anchor: true, + }, + ]; } } return []; }); if (brokenLinks.length > 0) { - brokenLinksByLocation[key] = brokenLinks; + brokenLinksByLocation[path] = brokenLinks; } }); From ee325bc30626a3621720997fe5e9f37d6a0a7e6c Mon Sep 17 00:00:00 2001 From: ozakione <29860391+OzakIOne@users.noreply.github.com> Date: Tue, 26 Dec 2023 18:15:52 +0100 Subject: [PATCH 48/69] fix: broken test --- packages/docusaurus/src/server/brokenLinks.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/docusaurus/src/server/brokenLinks.ts b/packages/docusaurus/src/server/brokenLinks.ts index 0c374fe26c70..91fe206d6d85 100644 --- a/packages/docusaurus/src/server/brokenLinks.ts +++ b/packages/docusaurus/src/server/brokenLinks.ts @@ -62,15 +62,15 @@ function checkAnchorsInOtherRoutes(allCollectedCorrectLinks: CollectedLinks): { return [ { link: `${route}#${anchor}`, - resolvedLink: route!, + resolvedLink: route, anchor: true, }, ]; - } else if (!targetRoute) { + } else if (!targetRoute && !collection.anchors.includes(anchor)) { return [ { link: `${route}#${anchor}`, - resolvedLink: route!, + resolvedLink: route, anchor: true, }, ]; From 2c24a89169668d926d1cebb98be2f2b5c60b0bbb Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Tue, 26 Dec 2023 18:40:07 +0100 Subject: [PATCH 49/69] fix resolution of relative links --- .../src/server/__tests__/brokenLinks.test.ts | 2 ++ packages/docusaurus/src/server/brokenLinks.ts | 19 ++++++++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts index 9ed17f1e5680..f46f1608b115 100644 --- a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts +++ b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts @@ -278,6 +278,8 @@ describe('handleBrokenLinks NEW TESTS', () => { allCollectedLinks: { '/page1': { links: [ + '/page1', + '', '#goodAnchor', '/page1#goodAnchor', '/page1?age=42#goodAnchor', diff --git a/packages/docusaurus/src/server/brokenLinks.ts b/packages/docusaurus/src/server/brokenLinks.ts index 91fe206d6d85..b67cda91a478 100644 --- a/packages/docusaurus/src/server/brokenLinks.ts +++ b/packages/docusaurus/src/server/brokenLinks.ts @@ -29,8 +29,21 @@ function onlyPathname(link: string) { return link.split('#')[0]!.split('?')[0]!; } -function getRouteAndAnchor(link: string) { - const url = new URL(link, 'https://example.com'); +function parseLocalPath(localUrl: string, base?: string | URL): URL { + try { + return new URL(localUrl, base ?? 'https://example.com'); + } catch (e) { + throw new Error(`Can't parse local path: ${localUrl}`); + } +} + +function parseLink(link: string, from: string): URL { + const base = parseLocalPath(from); + return parseLocalPath(link, base); +} + +function getRouteAndAnchor(link: string, fromPath: string) { + const url = parseLink(link, fromPath); const [, splitAnchor] = link.split('#'); const route = url.pathname; @@ -54,7 +67,7 @@ function checkAnchorsInOtherRoutes(allCollectedCorrectLinks: CollectedLinks): { linkCollection.forEach(([path, collection]) => { const brokenLinks = collection.links.flatMap((link) => { - const {route, anchor} = getRouteAndAnchor(link); + const {route, anchor} = getRouteAndAnchor(link, path); if (anchor !== undefined) { const targetRoute = allCollectedCorrectLinks[route]; From 4edc168b966682fd256ebfbd69a536f922f5010b Mon Sep 17 00:00:00 2001 From: ozakione <29860391+OzakIOne@users.noreply.github.com> Date: Wed, 27 Dec 2023 12:04:12 +0100 Subject: [PATCH 50/69] wip: fix anchor check --- .../src/server/__tests__/brokenLinks.test.ts | 44 +++++++++---------- packages/docusaurus/src/server/brokenLinks.ts | 40 ++++++++++++----- 2 files changed, 50 insertions(+), 34 deletions(-) diff --git a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts index f46f1608b115..6af2e521ca4b 100644 --- a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts +++ b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts @@ -219,7 +219,7 @@ describe('handleBrokenLinks NEW TESTS', () => { Exhaustive list of all broken anchors found: - Broken anchor on source page path = /page1: - -> linking to /page2#brokenAnchor (resolved as: /page2) + -> linking to /page2#brokenAnchor " `); }); @@ -241,7 +241,7 @@ describe('handleBrokenLinks NEW TESTS', () => { Exhaustive list of all broken anchors found: - Broken anchor on source page path = /page1: - -> linking to /page2# (resolved as: /page2) + -> linking to /page2# " `); }); @@ -266,7 +266,7 @@ describe('handleBrokenLinks NEW TESTS', () => { Exhaustive list of all broken anchors found: - Broken anchor on source page path = /page1: - -> linking to /page2#brokenAnchor (resolved as: /page2) + -> linking to /page2#brokenAnchor (resolved as: /page2?age=42&theme=dark#brokenAnchor) " `); }); @@ -280,30 +280,30 @@ describe('handleBrokenLinks NEW TESTS', () => { links: [ '/page1', '', - '#goodAnchor', - '/page1#goodAnchor', - '/page1?age=42#goodAnchor', - '#badAnchor1', + // '#goodAnchor1', // TODO brokenLink + '/page1#goodAnchor2', + '/page1?age=42#goodAnchor3', + // '#badAnchor1', // TODO brokenLink '/page1#badAnchor2', '/page1?age=42#badAnchor3', ], - anchors: ['goodAnchor'], + + anchors: ['goodAnchor1', 'goodAnchor2', 'goodAnchor3'], }, }, }), - ).rejects.toThrowErrorMatchingInlineSnapshot( - // TODO bad error message - ` - "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. + ).rejects.toThrowErrorMatchingInlineSnapshot(` + "Docusaurus found broken anchors! - Exhaustive list of all broken links found: + 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#badAnchor2 + -> linking to /page1#badAnchor3 (resolved as: /page1?age=42#badAnchor3) " - `, - ); + `); }); it('rejects valid link with broken anchor to uncollected page', async () => { @@ -323,7 +323,7 @@ describe('handleBrokenLinks NEW TESTS', () => { Exhaustive list of all broken anchors found: - Broken anchor on source page path = /page1: - -> linking to /page2#brokenAnchor (resolved as: /page2) + -> linking to /page2#brokenAnchor " `); }); @@ -348,7 +348,7 @@ describe('handleBrokenLinks NEW TESTS', () => { Exhaustive list of all broken anchors found: - Broken anchor on source page path = /page1: - -> linking to /page2#brokenAnchor (resolved as: /page2) + -> linking to /page2#brokenAnchor (resolved as: /page2?age=42&theme=dark#brokenAnchor) " `); }); @@ -437,7 +437,7 @@ describe('handleBrokenLinks NEW TESTS', () => { Exhaustive list of all broken anchors found: - Broken anchor on source page path = /page1: - -> linking to /page1#brokenAnchor (resolved as: /page1) + -> linking to /page1#brokenAnchor ", ], ] @@ -482,7 +482,7 @@ describe('handleBrokenLinks NEW TESTS', () => { Exhaustive list of all broken anchors found: - Broken anchor on source page path = /page1: - -> linking to /page1#brokenAnchor (resolved as: /page1) + -> linking to /page1#brokenAnchor ", ], ] diff --git a/packages/docusaurus/src/server/brokenLinks.ts b/packages/docusaurus/src/server/brokenLinks.ts index b67cda91a478..279efa043386 100644 --- a/packages/docusaurus/src/server/brokenLinks.ts +++ b/packages/docusaurus/src/server/brokenLinks.ts @@ -71,24 +71,42 @@ function checkAnchorsInOtherRoutes(allCollectedCorrectLinks: CollectedLinks): { if (anchor !== undefined) { const targetRoute = allCollectedCorrectLinks[route]; - if (targetRoute && !targetRoute.anchors.includes(anchor)) { + if (targetRoute) { + // means we have a link to an internal or external page that exists + if (route === path && !targetRoute.anchors.includes(anchor)) { + // internal page + return [ + { + link: `${route}#${anchor}`, + resolvedLink: link, + anchor: true, + }, + ]; + } + if (route !== path && !targetRoute.anchors.includes(anchor)) { + // external page + return [ + { + link: `${route}#${anchor}`, + resolvedLink: link, + anchor: true, + }, + ]; + } + } else { + // means we have link to an EXTERNAL not collected + // (means no anchor or link) + // but we have a link to this page with an anchor return [ { link: `${route}#${anchor}`, - resolvedLink: route, - anchor: true, - }, - ]; - } else if (!targetRoute && !collection.anchors.includes(anchor)) { - return [ - { - link: `${route}#${anchor}`, - resolvedLink: route, + resolvedLink: link, anchor: true, }, ]; } } + return []; }); @@ -189,8 +207,6 @@ function getAllBrokenLinks({ const brokenLinks = Object.fromEntries( Object.entries(allBrokenLinks).filter(([, value]) => value.length > 0), ); - // console.log('brokenLinks:', brokenLinks); - // console.log('allBrokenLinks:', allBrokenLinks); const brokenAnchors = checkAnchorsInOtherRoutes(allCollectedLinks); From 66ccc3be8da10a9ba00cb5dfa19ccb5c370fff86 Mon Sep 17 00:00:00 2001 From: ozakione <29860391+OzakIOne@users.noreply.github.com> Date: Wed, 27 Dec 2023 13:32:45 +0100 Subject: [PATCH 51/69] wip: simplify algorithm --- .../src/server/__tests__/brokenLinks.test.ts | 2 +- packages/docusaurus/src/server/brokenLinks.ts | 19 +++++++------------ yarn.lock | 12 ++++++++++++ 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts index 6af2e521ca4b..72f27d12e4e7 100644 --- a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts +++ b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts @@ -271,7 +271,7 @@ describe('handleBrokenLinks NEW TESTS', () => { `); }); - it('rejects valid link with broken anchor to self', async () => { + it('refactor rejects valid link with broken anchor to self', async () => { await expect(() => testBrokenLinks({ routes: [{path: '/page1'}], diff --git a/packages/docusaurus/src/server/brokenLinks.ts b/packages/docusaurus/src/server/brokenLinks.ts index 279efa043386..f4d8bbf597ab 100644 --- a/packages/docusaurus/src/server/brokenLinks.ts +++ b/packages/docusaurus/src/server/brokenLinks.ts @@ -73,18 +73,7 @@ function checkAnchorsInOtherRoutes(allCollectedCorrectLinks: CollectedLinks): { if (targetRoute) { // means we have a link to an internal or external page that exists - if (route === path && !targetRoute.anchors.includes(anchor)) { - // internal page - return [ - { - link: `${route}#${anchor}`, - resolvedLink: link, - anchor: true, - }, - ]; - } - if (route !== path && !targetRoute.anchors.includes(anchor)) { - // external page + if (!targetRoute.anchors.includes(anchor)) { return [ { link: `${route}#${anchor}`, @@ -144,6 +133,7 @@ function getPageBrokenLinks({ // We don't actually access component here, so it's fine. .map((l) => matchRoutes(routes, l)) .flat(); + console.log('matchedRoutes:', matchedRoutes.length === 0); return matchedRoutes.length === 0; } @@ -160,8 +150,13 @@ function getPageBrokenLinks({ return brokenAnchors.length > 0; } + // console.log('routes:', routes); + // console.log('pagePath:', pagePath); + // console.log('pageLinks:', pageLinks); + const brokenLinks = pageLinks.flatMap((pageLink) => { const resolvedLink = resolveLink(pageLink); + // console.log('resolvedLink:', resolvedLink); if (isPathBrokenLink(resolvedLink)) { return [{link: pageLink, resolvedLink, anchor: false}]; } diff --git a/yarn.lock b/yarn.lock index 80bf9294c8b1..2effc7b1eb4b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3863,6 +3863,13 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== +"@vercel/analytics@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@vercel/analytics/-/analytics-1.1.1.tgz#2a712378a95014a548b4f9d2ae1ea0721433908d" + integrity sha512-+NqgNmSabg3IFfxYhrWCfB/H+RCUOCR5ExRudNG2+pcRehq628DJB5e1u1xqwpLtn4pAYii4D98w7kofORAGQA== + dependencies: + server-only "^0.0.1" + "@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5": version "1.11.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.6.tgz#db046555d3c413f8966ca50a95176a0e2c642e24" @@ -14718,6 +14725,11 @@ serve-static@1.15.0: parseurl "~1.3.3" send "0.18.0" +server-only@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/server-only/-/server-only-0.0.1.tgz#0f366bb6afb618c37c9255a314535dc412cd1c9e" + integrity sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA== + set-blocking@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" From 660a527b5d754e1d4b10117b43d97f84505d9ab2 Mon Sep 17 00:00:00 2001 From: ozakione <29860391+OzakIOne@users.noreply.github.com> Date: Thu, 28 Dec 2023 11:09:33 +0100 Subject: [PATCH 52/69] wip: refactor --- .../src/server/__tests__/brokenLinks.test.ts | 28 ++-- packages/docusaurus/src/server/brokenLinks.ts | 150 ++++++++++-------- 2 files changed, 100 insertions(+), 78 deletions(-) diff --git a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts index 72f27d12e4e7..80a20813e3bd 100644 --- a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts +++ b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts @@ -219,7 +219,7 @@ describe('handleBrokenLinks NEW TESTS', () => { Exhaustive list of all broken anchors found: - Broken anchor on source page path = /page1: - -> linking to /page2#brokenAnchor + -> linking to /page2#brokenAnchor (resolved as: /page2) " `); }); @@ -241,7 +241,7 @@ describe('handleBrokenLinks NEW TESTS', () => { Exhaustive list of all broken anchors found: - Broken anchor on source page path = /page1: - -> linking to /page2# + -> linking to /page2# (resolved as: /page2) " `); }); @@ -266,7 +266,7 @@ describe('handleBrokenLinks NEW TESTS', () => { Exhaustive list of all broken anchors found: - Broken anchor on source page path = /page1: - -> linking to /page2#brokenAnchor (resolved as: /page2?age=42&theme=dark#brokenAnchor) + -> linking to /page2?age=42&theme=dark#brokenAnchor (resolved as: /page2) " `); }); @@ -278,17 +278,17 @@ describe('handleBrokenLinks NEW TESTS', () => { allCollectedLinks: { '/page1': { links: [ - '/page1', - '', - // '#goodAnchor1', // TODO brokenLink + // '/page1', + // '', + '#goodAnchor1', // TODO brokenLink '/page1#goodAnchor2', - '/page1?age=42#goodAnchor3', - // '#badAnchor1', // TODO brokenLink - '/page1#badAnchor2', - '/page1?age=42#badAnchor3', + // '/page1?age=42#goodAnchor3', + '#badAnchor1', // TODO brokenLink + // '/page1#badAnchor2', + // '/page1?age=42#badAnchor3', ], - anchors: ['goodAnchor1', 'goodAnchor2', 'goodAnchor3'], + anchors: ['goodAnchor1'], }, }, }), @@ -323,7 +323,7 @@ describe('handleBrokenLinks NEW TESTS', () => { Exhaustive list of all broken anchors found: - Broken anchor on source page path = /page1: - -> linking to /page2#brokenAnchor + -> linking to /page2#brokenAnchor (resolved as: /page2) " `); }); @@ -348,7 +348,7 @@ describe('handleBrokenLinks NEW TESTS', () => { Exhaustive list of all broken anchors found: - Broken anchor on source page path = /page1: - -> linking to /page2#brokenAnchor (resolved as: /page2?age=42&theme=dark#brokenAnchor) + -> linking to /page2?age=42&theme=dark#brokenAnchor (resolved as: /page2) " `); }); @@ -437,7 +437,7 @@ describe('handleBrokenLinks NEW TESTS', () => { Exhaustive list of all broken anchors found: - Broken anchor on source page path = /page1: - -> linking to /page1#brokenAnchor + -> linking to /page1#brokenAnchor (resolved as: /page1) ", ], ] diff --git a/packages/docusaurus/src/server/brokenLinks.ts b/packages/docusaurus/src/server/brokenLinks.ts index f4d8bbf597ab..267d71368933 100644 --- a/packages/docusaurus/src/server/brokenLinks.ts +++ b/packages/docusaurus/src/server/brokenLinks.ts @@ -58,61 +58,64 @@ function getRouteAndAnchor(link: string, fromPath: string) { return {route, anchor}; } -function checkAnchorsInOtherRoutes(allCollectedCorrectLinks: CollectedLinks): { - [location: string]: BrokenLink[]; -} { - const brokenLinksByLocation: BrokenLinksByLocation = {}; - - const linkCollection = Object.entries(allCollectedCorrectLinks); - - linkCollection.forEach(([path, collection]) => { - const brokenLinks = collection.links.flatMap((link) => { - const {route, anchor} = getRouteAndAnchor(link, path); - if (anchor !== undefined) { - const targetRoute = allCollectedCorrectLinks[route]; - - if (targetRoute) { - // means we have a link to an internal or external page that exists - if (!targetRoute.anchors.includes(anchor)) { - return [ - { - link: `${route}#${anchor}`, - resolvedLink: link, - anchor: true, - }, - ]; - } - } else { - // means we have link to an EXTERNAL not collected - // (means no anchor or link) - // but we have a link to this page with an anchor - return [ - { - link: `${route}#${anchor}`, - resolvedLink: link, - anchor: true, - }, - ]; - } - } - - return []; - }); - - if (brokenLinks.length > 0) { - brokenLinksByLocation[path] = brokenLinks; - } - }); - - return brokenLinksByLocation; -} +// function checkAnchorsInOtherRoutes(allCollectedCorrectLinks:CollectedLinks):{ +// [location: string]: BrokenLink[]; +// } { +// const brokenLinksByLocation: BrokenLinksByLocation = {}; + +// const linkCollection = Object.entries(allCollectedCorrectLinks); + +// linkCollection.forEach(([path, collection]) => { +// const brokenLinks = collection.links.flatMap((link) => { +// const {route, anchor} = getRouteAndAnchor(link, path); +// if (anchor !== undefined) { +// const targetRoute = allCollectedCorrectLinks[route]; + +// if (targetRoute) { +// // means we have a link to an internal or external page that exists +// // if (!targetRoute.anchors.find((el) => el === anchor)) { +// if (targetRoute.anchors.includes(anchor) === false) { +// return [ +// { +// link: `${route}#${anchor}`, +// resolvedLink: link, +// anchor: true, +// }, +// ]; +// } +// } else { +// // means we have link to an EXTERNAL not collected +// // (means no anchor or link) +// // but we have a link to this page with an anchor +// return [ +// { +// link: `${route}#${anchor}`, +// resolvedLink: link, +// anchor: true, +// }, +// ]; +// } +// } + +// return []; +// }); + +// if (brokenLinks.length > 0) { +// brokenLinksByLocation[path] = brokenLinks; +// } +// }); + +// return brokenLinksByLocation; +// } function getPageBrokenLinks({ + allCollectedLinks, pagePath, pageLinks, pageAnchors, routes, }: { + allCollectedLinks: CollectedLinks; pagePath: string; pageLinks: string[]; pageAnchors: string[]; @@ -126,6 +129,7 @@ function getPageBrokenLinks({ return resolvedLink; } + // console.log('routes:', routes); function isPathBrokenLink(link: string) { const matchedRoutes = [link, decodeURI(link)] // @ts-expect-error: React router types RouteConfig with an actual React @@ -133,30 +137,32 @@ function getPageBrokenLinks({ // We don't actually access component here, so it's fine. .map((l) => matchRoutes(routes, l)) .flat(); - console.log('matchedRoutes:', matchedRoutes.length === 0); return matchedRoutes.length === 0; } - function isAnchorBrokenLink(link: string) { - const [urlPath, urlHash] = link.split('#'); - - // ignore anchors that are not on the current page - if (urlHash === undefined || pageAnchors.length === 0 || urlPath !== '') { + function isAnchorBrokenLink(pageLink: string) { + const {route, anchor} = getRouteAndAnchor(pageLink, pagePath); + const targetRoute = allCollectedLinks[route]; + // console.log('d:', pagePath, pageLinks, pageLink, pageAnchors, routes); + // console.log('debug2', route, anchor, targetRoute); + console.log(pageAnchors); + if (anchor === undefined) { return false; } - const brokenAnchors = pageAnchors.filter((anchor) => anchor !== urlHash); - - return brokenAnchors.length > 0; + if (targetRoute) { + // console.log('debug3', targetRoute.anchors, anchor); + // means we have a link to an internal or external page that exists + if (targetRoute.anchors.includes(anchor) === false) { + return true; + } + return false; + } + return !allCollectedLinks[route]?.anchors.includes(anchor); } - // console.log('routes:', routes); - // console.log('pagePath:', pagePath); - // console.log('pageLinks:', pageLinks); - const brokenLinks = pageLinks.flatMap((pageLink) => { const resolvedLink = resolveLink(pageLink); - // console.log('resolvedLink:', resolvedLink); if (isPathBrokenLink(resolvedLink)) { return [{link: pageLink, resolvedLink, anchor: false}]; } @@ -193,17 +199,33 @@ function getAllBrokenLinks({ allCollectedLinks, (pageCollectedData, pagePath) => getPageBrokenLinks({ + allCollectedLinks, pageLinks: pageCollectedData.links, pageAnchors: pageCollectedData.anchors, pagePath, routes: filteredRoutes, }), ); + // const brokenLinks = Object.fromEntries( + // Object.entries(allBrokenLinks).filter(([, value]) => value.length > 0), + // ); + const brokenLinks = Object.fromEntries( - Object.entries(allBrokenLinks).filter(([, value]) => value.length > 0), + Object.entries(allBrokenLinks).filter( + ([, value]) => value[0]?.anchor === false, + ), ); - const brokenAnchors = checkAnchorsInOtherRoutes(allCollectedLinks); + // split allBrokenLinks object into two separate objects brokenLinks and + // brokenAnchors based on anchor boolean value + const brokenAnchors = Object.fromEntries( + Object.entries(allBrokenLinks).filter( + ([, value]) => value[0]?.anchor === true, + ), + ); + // console.log('allBrokenLinks:', allBrokenLinks); + // console.log('brokenAnchors:', brokenAnchors); + // console.log('brokenLinks:', brokenLinks); return {brokenLinks, brokenAnchors}; } From bf97a510a7da58ac6914fd281b4600a0d8f9052c Mon Sep 17 00:00:00 2001 From: ozakione <29860391+OzakIOne@users.noreply.github.com> Date: Thu, 28 Dec 2023 13:50:08 +0100 Subject: [PATCH 53/69] wip: refactor & add new tests --- .../src/server/__tests__/brokenLinks.test.ts | 82 +++++++++++++--- packages/docusaurus/src/server/brokenLinks.ts | 93 +++---------------- 2 files changed, 85 insertions(+), 90 deletions(-) diff --git a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts index 80a20813e3bd..0f3fabca6e91 100644 --- a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts +++ b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts @@ -271,24 +271,24 @@ describe('handleBrokenLinks NEW TESTS', () => { `); }); - it('refactor rejects valid link with broken anchor to self', async () => { + it('rejects valid link with broken anchor to self', async () => { await expect(() => testBrokenLinks({ routes: [{path: '/page1'}], allCollectedLinks: { '/page1': { links: [ - // '/page1', - // '', - '#goodAnchor1', // TODO brokenLink - '/page1#goodAnchor2', - // '/page1?age=42#goodAnchor3', - '#badAnchor1', // TODO brokenLink - // '/page1#badAnchor2', - // '/page1?age=42#badAnchor3', + '/page1', + '', + '#goodAnchor', + '/page1#goodAnchor', + '/page1?age=42#goodAnchor', + '#badAnchor', + '/page1#badAnchor', + '/page1?age=42#badAnchor', ], - anchors: ['goodAnchor1'], + anchors: ['goodAnchor'], }, }, }), @@ -300,8 +300,9 @@ describe('handleBrokenLinks NEW TESTS', () => { Exhaustive list of all broken anchors found: - Broken anchor on source page path = /page1: - -> linking to /page1#badAnchor2 - -> linking to /page1#badAnchor3 (resolved as: /page1?age=42#badAnchor3) + -> linking to #badAnchor (resolved as: /page1) + -> linking to /page1#badAnchor (resolved as: /page1) + -> linking to /page1?age=42#badAnchor (resolved as: /page1) " `); }); @@ -379,6 +380,63 @@ describe('handleBrokenLinks NEW TESTS', () => { }); }); + it('can ignore broken anchors but report broken link', async () => { + await expect(() => + testBrokenLinks({ + onBrokenAnchors: 'ignore', + routes: [{path: '/page1'}], + allCollectedLinks: { + '/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({ + routes: [{path: '/page1'}], + allCollectedLinks: { + '/page1': { + links: [ + '/page1#brokenAnchor', + '/page2', + '/page1#brokenAnchor', + '#brokenAnchor', + ], + + 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#brokenAnchor (resolved as: /page1) + -> linking to /page1#brokenAnchor (resolved as: /page1) + -> linking to #brokenAnchor (resolved as: /page1) + " + `); + }); + it('can warn for broken links', async () => { const warnMock = jest.spyOn(console, 'warn'); diff --git a/packages/docusaurus/src/server/brokenLinks.ts b/packages/docusaurus/src/server/brokenLinks.ts index 267d71368933..abb583cf5433 100644 --- a/packages/docusaurus/src/server/brokenLinks.ts +++ b/packages/docusaurus/src/server/brokenLinks.ts @@ -58,61 +58,10 @@ function getRouteAndAnchor(link: string, fromPath: string) { return {route, anchor}; } -// function checkAnchorsInOtherRoutes(allCollectedCorrectLinks:CollectedLinks):{ -// [location: string]: BrokenLink[]; -// } { -// const brokenLinksByLocation: BrokenLinksByLocation = {}; - -// const linkCollection = Object.entries(allCollectedCorrectLinks); - -// linkCollection.forEach(([path, collection]) => { -// const brokenLinks = collection.links.flatMap((link) => { -// const {route, anchor} = getRouteAndAnchor(link, path); -// if (anchor !== undefined) { -// const targetRoute = allCollectedCorrectLinks[route]; - -// if (targetRoute) { -// // means we have a link to an internal or external page that exists -// // if (!targetRoute.anchors.find((el) => el === anchor)) { -// if (targetRoute.anchors.includes(anchor) === false) { -// return [ -// { -// link: `${route}#${anchor}`, -// resolvedLink: link, -// anchor: true, -// }, -// ]; -// } -// } else { -// // means we have link to an EXTERNAL not collected -// // (means no anchor or link) -// // but we have a link to this page with an anchor -// return [ -// { -// link: `${route}#${anchor}`, -// resolvedLink: link, -// anchor: true, -// }, -// ]; -// } -// } - -// return []; -// }); - -// if (brokenLinks.length > 0) { -// brokenLinksByLocation[path] = brokenLinks; -// } -// }); - -// return brokenLinksByLocation; -// } - function getPageBrokenLinks({ allCollectedLinks, pagePath, pageLinks, - pageAnchors, routes, }: { allCollectedLinks: CollectedLinks; @@ -143,22 +92,19 @@ function getPageBrokenLinks({ function isAnchorBrokenLink(pageLink: string) { const {route, anchor} = getRouteAndAnchor(pageLink, pagePath); const targetRoute = allCollectedLinks[route]; - // console.log('d:', pagePath, pageLinks, pageLink, pageAnchors, routes); - // console.log('debug2', route, anchor, targetRoute); - console.log(pageAnchors); if (anchor === undefined) { return false; } if (targetRoute) { - // console.log('debug3', targetRoute.anchors, anchor); - // means we have a link to an internal or external page that exists + // link to an internal or external page that exists if (targetRoute.anchors.includes(anchor) === false) { return true; } return false; } - return !allCollectedLinks[route]?.anchors.includes(anchor); + // link with anchor to external page that does not exist + return anchor !== undefined; } const brokenLinks = pageLinks.flatMap((pageLink) => { @@ -206,9 +152,6 @@ function getAllBrokenLinks({ routes: filteredRoutes, }), ); - // const brokenLinks = Object.fromEntries( - // Object.entries(allBrokenLinks).filter(([, value]) => value.length > 0), - // ); const brokenLinks = Object.fromEntries( Object.entries(allBrokenLinks).filter( @@ -216,16 +159,11 @@ function getAllBrokenLinks({ ), ); - // split allBrokenLinks object into two separate objects brokenLinks and - // brokenAnchors based on anchor boolean value const brokenAnchors = Object.fromEntries( Object.entries(allBrokenLinks).filter( ([, value]) => value[0]?.anchor === true, ), ); - // console.log('allBrokenLinks:', allBrokenLinks); - // console.log('brokenAnchors:', brokenAnchors); - // console.log('brokenLinks:', brokenLinks); return {brokenLinks, brokenAnchors}; } @@ -242,15 +180,10 @@ function createBrokenLinksMessage( pagePath: string, allBrokenLinks: BrokenLink[], ): string { - const brokenLinks = - type === 'anchor' - ? allBrokenLinks.filter((link) => link.anchor) - : allBrokenLinks.filter((link) => !link.anchor); - const anchorMessage = - brokenLinks.length > 0 + allBrokenLinks.length > 0 ? `- Broken ${type} on source page path = ${pagePath}: - -> linking to ${brokenLinks + -> linking to ${allBrokenLinks .map(brokenLinkMessage) .join('\n -> linking to ')}` : ''; @@ -353,13 +286,17 @@ export async function handleBrokenLinks({ routes, }); - const pathErrorMessage = getPathBrokenLinksErrorMessage(brokenLinks); - if (pathErrorMessage) { - logger.report(onBrokenLinks)(pathErrorMessage); + if (onBrokenLinks !== 'ignore') { + const pathErrorMessage = getPathBrokenLinksErrorMessage(brokenLinks); + if (pathErrorMessage) { + logger.report(onBrokenLinks)(pathErrorMessage); + } } - const anchorErrorMessage = getAnchorBrokenLinksErrorMessage(brokenAnchors); - if (anchorErrorMessage) { - logger.report(onBrokenAnchors)(anchorErrorMessage); + if (onBrokenAnchors !== 'ignore') { + const anchorErrorMessage = getAnchorBrokenLinksErrorMessage(brokenAnchors); + if (anchorErrorMessage) { + logger.report(onBrokenAnchors)(anchorErrorMessage); + } } } From 9cde61b2d6ac96ef2b25b2741fa5e57122f70f99 Mon Sep 17 00:00:00 2001 From: ozakione <29860391+OzakIOne@users.noreply.github.com> Date: Thu, 28 Dec 2023 17:00:17 +0100 Subject: [PATCH 54/69] wip: refactor --- .../src/server/__tests__/brokenLinks.test.ts | 15 +++--- packages/docusaurus/src/server/brokenLinks.ts | 54 ++++++++++--------- 2 files changed, 38 insertions(+), 31 deletions(-) diff --git a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts index 0f3fabca6e91..2f27dc59c0c9 100644 --- a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts +++ b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts @@ -408,14 +408,15 @@ describe('handleBrokenLinks NEW TESTS', () => { it('can ignore broken link but report broken anchors', async () => { await expect(() => testBrokenLinks({ + onBrokenLinks: 'ignore', routes: [{path: '/page1'}], allCollectedLinks: { '/page1': { links: [ - '/page1#brokenAnchor', '/page2', - '/page1#brokenAnchor', - '#brokenAnchor', + '/page1#brokenAnchor1', + '/page1#brokenAnchor2', + '#brokenAnchor3', ], anchors: [], @@ -430,9 +431,9 @@ describe('handleBrokenLinks NEW TESTS', () => { Exhaustive list of all broken anchors found: - Broken anchor on source page path = /page1: - -> linking to /page1#brokenAnchor (resolved as: /page1) - -> linking to /page1#brokenAnchor (resolved as: /page1) - -> linking to #brokenAnchor (resolved as: /page1) + -> linking to /page1#brokenAnchor1 (resolved as: /page1) + -> linking to /page1#brokenAnchor2 (resolved as: /page1) + -> linking to #brokenAnchor3 (resolved as: /page1) " `); }); @@ -540,7 +541,7 @@ describe('handleBrokenLinks NEW TESTS', () => { Exhaustive list of all broken anchors found: - Broken anchor on source page path = /page1: - -> linking to /page1#brokenAnchor + -> linking to /page1#brokenAnchor (resolved as: /page1) ", ], ] diff --git a/packages/docusaurus/src/server/brokenLinks.ts b/packages/docusaurus/src/server/brokenLinks.ts index abb583cf5433..e5d07af401db 100644 --- a/packages/docusaurus/src/server/brokenLinks.ts +++ b/packages/docusaurus/src/server/brokenLinks.ts @@ -153,19 +153,28 @@ function getAllBrokenLinks({ }), ); - const brokenLinks = Object.fromEntries( - Object.entries(allBrokenLinks).filter( - ([, value]) => value[0]?.anchor === false, - ), - ); + function splitBrokenLinks(collect: {[x: string]: BrokenLink[]}) { + const brokenLinks: {[x: string]: BrokenLink[]} = {}; + const brokenAnchors: {[x: string]: BrokenLink[]} = {}; + + Object.entries(collect).forEach(([page, links]) => { + const [linksFiltered, anchorsFiltered] = _.partition( + links, + (link) => link.anchor === false, + ); + + if (linksFiltered.length > 0) { + brokenLinks[page] = linksFiltered; + } + if (anchorsFiltered.length > 0) { + brokenAnchors[page] = anchorsFiltered; + } + }); - const brokenAnchors = Object.fromEntries( - Object.entries(allBrokenLinks).filter( - ([, value]) => value[0]?.anchor === true, - ), - ); + return {brokenLinks, brokenAnchors}; + } - return {brokenLinks, brokenAnchors}; + return splitBrokenLinks(allBrokenLinks); } function brokenLinkMessage(brokenLink: BrokenLink): string { @@ -176,10 +185,11 @@ function brokenLinkMessage(brokenLink: BrokenLink): string { } function createBrokenLinksMessage( - type: 'link' | 'anchor', pagePath: string, allBrokenLinks: BrokenLink[], ): string { + const type = allBrokenLinks[0]?.anchor === true ? 'anchor' : 'link'; + const anchorMessage = allBrokenLinks.length > 0 ? `- Broken ${type} on source page path = ${pagePath}: @@ -206,7 +216,7 @@ Note: it's possible to ignore broken anchors with the 'onBrokenAnchors' Docusaur Exhaustive list of all broken anchors found: ${Object.entries(allBrokenLinks) .map(([pagePath, brokenLinks]) => - createBrokenLinksMessage('anchor', pagePath, brokenLinks), + createBrokenLinksMessage(pagePath, brokenLinks), ) .join('\n')} `; @@ -260,7 +270,7 @@ Note: it's possible to ignore broken links with the 'onBrokenLinks' Docusaurus c Exhaustive list of all broken links found: ${Object.entries(allBrokenLinks) .map(([pagePath, brokenLinks]) => - createBrokenLinksMessage('link', pagePath, brokenLinks), + createBrokenLinksMessage(pagePath, brokenLinks), ) .join('\n')} `; @@ -286,17 +296,13 @@ export async function handleBrokenLinks({ routes, }); - if (onBrokenLinks !== 'ignore') { - const pathErrorMessage = getPathBrokenLinksErrorMessage(brokenLinks); - if (pathErrorMessage) { - logger.report(onBrokenLinks)(pathErrorMessage); - } + const pathErrorMessage = getPathBrokenLinksErrorMessage(brokenLinks); + if (pathErrorMessage) { + logger.report(onBrokenLinks)(pathErrorMessage); } - if (onBrokenAnchors !== 'ignore') { - const anchorErrorMessage = getAnchorBrokenLinksErrorMessage(brokenAnchors); - if (anchorErrorMessage) { - logger.report(onBrokenAnchors)(anchorErrorMessage); - } + const anchorErrorMessage = getAnchorBrokenLinksErrorMessage(brokenAnchors); + if (anchorErrorMessage) { + logger.report(onBrokenAnchors)(anchorErrorMessage); } } From f61e77c3ed79150171571cd5c7039fd4dde6aaa3 Mon Sep 17 00:00:00 2001 From: ozakione <29860391+OzakIOne@users.noreply.github.com> Date: Thu, 28 Dec 2023 18:29:42 +0100 Subject: [PATCH 55/69] undo commit --- yarn.lock | 7 ------- 1 file changed, 7 deletions(-) diff --git a/yarn.lock b/yarn.lock index 2effc7b1eb4b..6f68648a0156 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3863,13 +3863,6 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== -"@vercel/analytics@^1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@vercel/analytics/-/analytics-1.1.1.tgz#2a712378a95014a548b4f9d2ae1ea0721433908d" - integrity sha512-+NqgNmSabg3IFfxYhrWCfB/H+RCUOCR5ExRudNG2+pcRehq628DJB5e1u1xqwpLtn4pAYii4D98w7kofORAGQA== - dependencies: - server-only "^0.0.1" - "@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5": version "1.11.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.6.tgz#db046555d3c413f8966ca50a95176a0e2c642e24" From 383db55baf580c40cd12e2aa665826dc290c2473 Mon Sep 17 00:00:00 2001 From: ozakione <29860391+OzakIOne@users.noreply.github.com> Date: Thu, 28 Dec 2023 19:17:59 +0100 Subject: [PATCH 56/69] undo commit --- yarn.lock | 5 ----- 1 file changed, 5 deletions(-) diff --git a/yarn.lock b/yarn.lock index 6f68648a0156..80bf9294c8b1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14718,11 +14718,6 @@ serve-static@1.15.0: parseurl "~1.3.3" send "0.18.0" -server-only@^0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/server-only/-/server-only-0.0.1.tgz#0f366bb6afb618c37c9255a314535dc412cd1c9e" - integrity sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA== - set-blocking@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" From f77d9a832cc0d1d114c01d215344fff50f5bc195 Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Tue, 2 Jan 2024 16:12:29 +0100 Subject: [PATCH 57/69] use different bad anchor names for tests --- .../src/server/__tests__/brokenLinks.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts index 2f27dc59c0c9..3a772eb5e7f8 100644 --- a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts +++ b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts @@ -283,9 +283,9 @@ describe('handleBrokenLinks NEW TESTS', () => { '#goodAnchor', '/page1#goodAnchor', '/page1?age=42#goodAnchor', - '#badAnchor', - '/page1#badAnchor', - '/page1?age=42#badAnchor', + '#badAnchor1', + '/page1#badAnchor2', + '/page1?age=42#badAnchor3', ], anchors: ['goodAnchor'], @@ -300,9 +300,9 @@ describe('handleBrokenLinks NEW TESTS', () => { Exhaustive list of all broken anchors found: - Broken anchor on source page path = /page1: - -> linking to #badAnchor (resolved as: /page1) - -> linking to /page1#badAnchor (resolved as: /page1) - -> linking to /page1?age=42#badAnchor (resolved as: /page1) + -> linking to #badAnchor1 (resolved as: /page1) + -> linking to /page1#badAnchor2 (resolved as: /page1) + -> linking to /page1?age=42#badAnchor3 (resolved as: /page1) " `); }); From 3625b1e139588b65e91d605862816ffce7df735c Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Tue, 2 Jan 2024 17:03:59 +0100 Subject: [PATCH 58/69] create proper parseURLPath util --- packages/docusaurus/src/server/brokenLinks.ts | 97 ++++++++++++------- 1 file changed, 61 insertions(+), 36 deletions(-) diff --git a/packages/docusaurus/src/server/brokenLinks.ts b/packages/docusaurus/src/server/brokenLinks.ts index e5d07af401db..123062363008 100644 --- a/packages/docusaurus/src/server/brokenLinks.ts +++ b/packages/docusaurus/src/server/brokenLinks.ts @@ -29,33 +29,55 @@ function onlyPathname(link: string) { return link.split('#')[0]!.split('?')[0]!; } -function parseLocalPath(localUrl: string, base?: string | URL): URL { - try { - return new URL(localUrl, base ?? 'https://example.com'); - } catch (e) { - throw new Error(`Can't parse local path: ${localUrl}`); - } -} - -function parseLink(link: string, from: string): URL { - const base = parseLocalPath(from); - return parseLocalPath(link, base); -} - -function getRouteAndAnchor(link: string, fromPath: string) { - const url = parseLink(link, fromPath); - const [, splitAnchor] = link.split('#'); - - const route = url.pathname; - const anchor = url.hash.slice(1) || undefined; - - if (splitAnchor === '') { - // rejects valid link with empty broken anchor - // new URL will return an empty string /docs# and /docs - return {route, anchor: ''}; +// TODO move to docusaurus-utils + add tests +// Let's name the concept of (pathname + search + hash) as URL path +// See also https://twitter.com/kettanaito/status/1741768992866308120 +// A possible alternative? https://github.com/unjs/ufo#url +function parseURLPath( + urlPath: string, + fromPath?: string, +): {pathname: string; search?: string; hash?: string} { + function parseURL(url: string, base?: string | URL): URL { + try { + return new URL(url, base ?? 'https://example.com'); + } catch (e) { + throw new Error( + `Can't parse URL ${url}${base ? ` with base ${base}` : ''}`, + {cause: e}, + ); + } } - return {route, anchor}; + 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, + }; } function getPageBrokenLinks({ @@ -90,21 +112,24 @@ function getPageBrokenLinks({ } function isAnchorBrokenLink(pageLink: string) { - const {route, anchor} = getRouteAndAnchor(pageLink, pagePath); - const targetRoute = allCollectedLinks[route]; - if (anchor === undefined) { + const {pathname, hash} = parseURLPath(pageLink, pagePath); + + // Link has no hash: it can't be a broken anchor link + if (hash === undefined) { return false; } - if (targetRoute) { - // link to an internal or external page that exists - if (targetRoute.anchors.includes(anchor) === false) { - return true; - } - return false; + const targetPage = allCollectedLinks[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; } - // link with anchor to external page that does not exist - return anchor !== undefined; + + // 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((pageLink) => { From 574eaf006c8afda25081553100955dc8a80172bc Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Tue, 2 Jan 2024 17:19:58 +0100 Subject: [PATCH 59/69] add todo --- packages/docusaurus/src/server/brokenLinks.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/docusaurus/src/server/brokenLinks.ts b/packages/docusaurus/src/server/brokenLinks.ts index 123062363008..a16da90ef06c 100644 --- a/packages/docusaurus/src/server/brokenLinks.ts +++ b/packages/docusaurus/src/server/brokenLinks.ts @@ -8,7 +8,6 @@ import _ from 'lodash'; import logger from '@docusaurus/logger'; import {matchRoutes} from 'react-router-config'; -import {resolvePathname} from '@docusaurus/utils'; import {getAllFinalRoutes} from './utils'; import type {RouteConfig, ReportingSeverity} from '@docusaurus/types'; @@ -24,12 +23,11 @@ type CollectedLinks = { [location: string]: {links: string[]; anchors: string[]}; }; -// matchRoutes does not support qs/anchors, so we remove it! -function onlyPathname(link: string) { - return link.split('#')[0]!.split('?')[0]!; -} - // TODO move to docusaurus-utils + add tests +// +// TODO: do we still need the urlUtils.resolvePathname ? +// this function also resolves the pathname while parsing +// // Let's name the concept of (pathname + search + hash) as URL path // See also https://twitter.com/kettanaito/status/1741768992866308120 // A possible alternative? https://github.com/unjs/ufo#url @@ -96,8 +94,8 @@ function getPageBrokenLinks({ // 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 resolvedLink; + const urlPath = parseURLPath(link, pagePath); + return urlPath.pathname; } // console.log('routes:', routes); From 93b1fb0ba074165644fb74799805b69e49dffa61 Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Tue, 2 Jan 2024 17:35:19 +0100 Subject: [PATCH 60/69] Refactor code to use new URLPath --- .../src/server/__tests__/brokenLinks.test.ts | 30 +++++----- packages/docusaurus/src/server/brokenLinks.ts | 57 +++++++++++-------- 2 files changed, 48 insertions(+), 39 deletions(-) diff --git a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts index 3a772eb5e7f8..7f246d416a44 100644 --- a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts +++ b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts @@ -176,7 +176,7 @@ describe('handleBrokenLinks NEW TESTS', () => { Exhaustive list of all broken links found: - Broken link on source page path = /page1: - -> linking to /brokenLink#anchor (resolved as: /brokenLink) + -> linking to /brokenLink#anchor " `); }); @@ -197,7 +197,7 @@ describe('handleBrokenLinks NEW TESTS', () => { Exhaustive list of all broken links found: - Broken link on source page path = /page1: - -> linking to /brokenLink?age=42#anchor (resolved as: /brokenLink) + -> linking to /brokenLink?age=42#anchor " `); }); @@ -219,7 +219,7 @@ describe('handleBrokenLinks NEW TESTS', () => { Exhaustive list of all broken anchors found: - Broken anchor on source page path = /page1: - -> linking to /page2#brokenAnchor (resolved as: /page2) + -> linking to /page2#brokenAnchor " `); }); @@ -241,7 +241,7 @@ describe('handleBrokenLinks NEW TESTS', () => { Exhaustive list of all broken anchors found: - Broken anchor on source page path = /page1: - -> linking to /page2# (resolved as: /page2) + -> linking to /page2# " `); }); @@ -266,7 +266,7 @@ describe('handleBrokenLinks NEW TESTS', () => { Exhaustive list of all broken anchors found: - Broken anchor on source page path = /page1: - -> linking to /page2?age=42&theme=dark#brokenAnchor (resolved as: /page2) + -> linking to /page2?age=42&theme=dark#brokenAnchor " `); }); @@ -300,9 +300,9 @@ describe('handleBrokenLinks NEW TESTS', () => { Exhaustive list of all broken anchors found: - Broken anchor on source page path = /page1: - -> linking to #badAnchor1 (resolved as: /page1) - -> linking to /page1#badAnchor2 (resolved as: /page1) - -> linking to /page1?age=42#badAnchor3 (resolved as: /page1) + -> linking to #badAnchor1 (resolved as: /page1#badAnchor1) + -> linking to /page1#badAnchor2 + -> linking to /page1?age=42#badAnchor3 " `); }); @@ -324,7 +324,7 @@ describe('handleBrokenLinks NEW TESTS', () => { Exhaustive list of all broken anchors found: - Broken anchor on source page path = /page1: - -> linking to /page2#brokenAnchor (resolved as: /page2) + -> linking to /page2#brokenAnchor " `); }); @@ -349,7 +349,7 @@ describe('handleBrokenLinks NEW TESTS', () => { Exhaustive list of all broken anchors found: - Broken anchor on source page path = /page1: - -> linking to /page2?age=42&theme=dark#brokenAnchor (resolved as: /page2) + -> linking to /page2?age=42&theme=dark#brokenAnchor " `); }); @@ -431,9 +431,9 @@ describe('handleBrokenLinks NEW TESTS', () => { Exhaustive list of all broken anchors found: - Broken anchor on source page path = /page1: - -> linking to /page1#brokenAnchor1 (resolved as: /page1) - -> linking to /page1#brokenAnchor2 (resolved as: /page1) - -> linking to #brokenAnchor3 (resolved as: /page1) + -> linking to /page1#brokenAnchor1 + -> linking to /page1#brokenAnchor2 + -> linking to #brokenAnchor3 (resolved as: /page1#brokenAnchor3) " `); }); @@ -496,7 +496,7 @@ describe('handleBrokenLinks NEW TESTS', () => { Exhaustive list of all broken anchors found: - Broken anchor on source page path = /page1: - -> linking to /page1#brokenAnchor (resolved as: /page1) + -> linking to /page1#brokenAnchor ", ], ] @@ -541,7 +541,7 @@ describe('handleBrokenLinks NEW TESTS', () => { Exhaustive list of all broken anchors found: - Broken anchor on source page path = /page1: - -> linking to /page1#brokenAnchor (resolved as: /page1) + -> linking to /page1#brokenAnchor ", ], ] diff --git a/packages/docusaurus/src/server/brokenLinks.ts b/packages/docusaurus/src/server/brokenLinks.ts index a16da90ef06c..780c89b4efc1 100644 --- a/packages/docusaurus/src/server/brokenLinks.ts +++ b/packages/docusaurus/src/server/brokenLinks.ts @@ -23,20 +23,19 @@ type CollectedLinks = { [location: string]: {links: string[]; anchors: string[]}; }; +type URLPath = {pathname: string; search?: string; hash?: string}; + // TODO move to docusaurus-utils + add tests // // TODO: do we still need the urlUtils.resolvePathname ? // this function also resolves the pathname while parsing // -// Let's name the concept of (pathname + search + hash) as URL path +// Let's name the concept of (pathname + search + hash) as URLPath // See also https://twitter.com/kettanaito/status/1741768992866308120 -// A possible alternative? https://github.com/unjs/ufo#url -function parseURLPath( - urlPath: string, - fromPath?: string, -): {pathname: string; search?: string; hash?: string} { +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( @@ -78,6 +77,12 @@ function parseURLPath( }; } +function serializeURLPath(urlPath: URLPath): string { + const search = urlPath.search === undefined ? '' : `?${urlPath.search}`; + const hash = urlPath.hash === undefined ? '' : `#${urlPath.hash}`; + return `${urlPath.pathname}${search}${hash}`; +} + function getPageBrokenLinks({ allCollectedLinks, pagePath, @@ -90,17 +95,9 @@ function getPageBrokenLinks({ 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 urlPath = parseURLPath(link, pagePath); - return urlPath.pathname; - } - // console.log('routes:', routes); - function isPathBrokenLink(link: string) { - const matchedRoutes = [link, decodeURI(link)] + 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. @@ -109,8 +106,8 @@ function getPageBrokenLinks({ return matchedRoutes.length === 0; } - function isAnchorBrokenLink(pageLink: string) { - const {pathname, hash} = parseURLPath(pageLink, pagePath); + function isAnchorBrokenLink(linkPath: URLPath) { + const {pathname, hash} = linkPath; // Link has no hash: it can't be a broken anchor link if (hash === undefined) { @@ -130,13 +127,25 @@ function getPageBrokenLinks({ return !targetPage.anchors.includes(hash); } - const brokenLinks = pageLinks.flatMap((pageLink) => { - const resolvedLink = resolveLink(pageLink); - if (isPathBrokenLink(resolvedLink)) { - return [{link: pageLink, resolvedLink, anchor: false}]; + const brokenLinks = pageLinks.flatMap((link) => { + const linkPath = parseURLPath(link, pagePath); + if (isPathBrokenLink(linkPath)) { + return [ + { + link, + resolvedLink: serializeURLPath(linkPath), + anchor: false, + }, + ]; } - if (isAnchorBrokenLink(pageLink)) { - return [{link: pageLink, resolvedLink, anchor: true}]; + if (isAnchorBrokenLink(linkPath)) { + return [ + { + link, + resolvedLink: serializeURLPath(linkPath), + anchor: true, + }, + ]; } return []; }); From 20edbd1fed6ae7f9efdb7db5c1444160c338a747 Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Tue, 2 Jan 2024 17:35:50 +0100 Subject: [PATCH 61/69] rename describe test --- packages/docusaurus/src/server/__tests__/brokenLinks.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts index 7f246d416a44..4048df847f2d 100644 --- a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts +++ b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts @@ -31,7 +31,7 @@ async function testBrokenLinks(params: { }); } -describe('handleBrokenLinks NEW TESTS', () => { +describe('handleBrokenLinks', () => { it('accepts valid link', async () => { await testBrokenLinks({ routes: [{path: '/page1'}, {path: '/page2'}], From ef814bd45ddd0dbdf4bae712a91f28d2bb821e3e Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Tue, 2 Jan 2024 17:38:22 +0100 Subject: [PATCH 62/69] fix edge case with broken anchors detection when using spaces and encoding --- packages/docusaurus/src/server/__tests__/brokenLinks.test.ts | 3 ++- packages/docusaurus/src/server/brokenLinks.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts index 4048df847f2d..0b015ede6726 100644 --- a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts +++ b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts @@ -131,10 +131,11 @@ describe('handleBrokenLinks', () => { '/page 2', '/page%202', '/page%202?age=42', + '/page%202?age=42#page2anchor', ], anchors: [], }, - '/page 2': {links: [], anchors: []}, + '/page 2': {links: [], anchors: ['page2anchor']}, }, }); }); diff --git a/packages/docusaurus/src/server/brokenLinks.ts b/packages/docusaurus/src/server/brokenLinks.ts index 780c89b4efc1..55a248b0476c 100644 --- a/packages/docusaurus/src/server/brokenLinks.ts +++ b/packages/docusaurus/src/server/brokenLinks.ts @@ -114,7 +114,8 @@ function getPageBrokenLinks({ return false; } - const targetPage = allCollectedLinks[pathname]; + const targetPage = + allCollectedLinks[pathname] || allCollectedLinks[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 From fcb0e65f3caf421a19d1361e787ff9002a52919d Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Tue, 2 Jan 2024 18:00:52 +0100 Subject: [PATCH 63/69] Move parseURLPath to urlUtils --- .../src/__tests__/urlUtils.test.ts | 133 ++++++++++++++++++ packages/docusaurus-utils/src/urlUtils.ts | 55 ++++++++ 2 files changed, 188 insertions(+) diff --git a/packages/docusaurus-utils/src/__tests__/urlUtils.test.ts b/packages/docusaurus-utils/src/__tests__/urlUtils.test.ts index 30625e400554..301a91ae3224 100644 --- a/packages/docusaurus-utils/src/__tests__/urlUtils.test.ts +++ b/packages/docusaurus-utils/src/__tests__/urlUtils.test.ts @@ -18,6 +18,8 @@ import { buildSshUrl, buildHttpsUrl, hasSSHProtocol, + parseURLPath, + serializeURLPath, } from '../urlUtils'; describe('normalizeUrl', () => { @@ -232,6 +234,137 @@ describe('removeTrailingSlash', () => { }); }); +describe('parseURLPath', () => { + it('parse and resolve pathname', () => { + expect(parseURLPath('')).toEqual({ + pathname: '/', + search: undefined, + hash: undefined, + }); + expect(parseURLPath('/')).toEqual({ + pathname: '/', + search: undefined, + hash: undefined, + }); + expect(parseURLPath('/page')).toEqual({ + pathname: '/page', + search: undefined, + hash: undefined, + }); + expect(parseURLPath('/dir1/page')).toEqual({ + pathname: '/dir1/page', + search: undefined, + hash: undefined, + }); + expect(parseURLPath('/dir1/dir2/./../page')).toEqual({ + pathname: '/dir1/page', + search: undefined, + hash: undefined, + }); + expect(parseURLPath('/dir1/dir2/../..')).toEqual({ + pathname: '/', + search: undefined, + hash: undefined, + }); + expect(parseURLPath('/dir1/dir2/../../..')).toEqual({ + pathname: '/', + search: undefined, + hash: undefined, + }); + expect(parseURLPath('./dir1/dir2./../page', '/dir3/dir4/page2')).toEqual({ + pathname: '/dir3/dir4/dir1/page', + search: undefined, + hash: undefined, + }); + }); + + it('parse query string', () => { + expect(parseURLPath('/page')).toEqual({ + pathname: '/page', + search: undefined, + hash: undefined, + }); + expect(parseURLPath('/page?')).toEqual({ + pathname: '/page', + search: '', + hash: undefined, + }); + expect(parseURLPath('/page?test')).toEqual({ + pathname: '/page', + search: 'test', + hash: undefined, + }); + expect(parseURLPath('/page?age=42&great=true')).toEqual({ + pathname: '/page', + search: 'age=42&great=true', + hash: undefined, + }); + }); + + it('parse hash', () => { + expect(parseURLPath('/page')).toEqual({ + pathname: '/page', + search: undefined, + hash: undefined, + }); + expect(parseURLPath('/page#')).toEqual({ + pathname: '/page', + search: undefined, + hash: '', + }); + expect(parseURLPath('/page#anchor')).toEqual({ + pathname: '/page', + search: undefined, + hash: 'anchor', + }); + }); + + it('parse fancy real-world edge cases', () => { + expect(parseURLPath('/page?#')).toEqual({ + pathname: '/page', + search: '', + hash: '', + }); + expect( + parseURLPath('dir1/dir2/../page?age=42#anchor', '/dir3/page2'), + ).toEqual({ + pathname: '/dir3/dir1/page', + search: 'age=42', + hash: 'anchor', + }); + }); +}); + +describe('serializeURLPath', () => { + function test(input: string, base?: string, expectedOutput?: string) { + expect(serializeURLPath(parseURLPath(input, base))).toEqual( + expectedOutput ?? input, + ); + } + + it('works for already resolved paths', () => { + test('/'); + test('/dir1/page'); + test('/dir1/page?'); + test('/dir1/page#'); + test('/dir1/page?#'); + test('/dir1/page?age=42#anchor'); + }); + + it('works for relative paths', () => { + test('', undefined, '/'); + test('', '/dir1/dir2/page2', '/dir1/dir2/page2'); + test('page', '/dir1/dir2/page2', '/dir1/dir2/page'); + test('../page', '/dir1/dir2/page2', '/dir1/page'); + test('/dir1/dir2/../page', undefined, '/dir1/page'); + test( + '/dir1/dir2/../page?age=42#anchor', + undefined, + '/dir1/page?age=42#anchor', + ); + }); +}); + describe('resolvePathname', () => { it('works', () => { // These tests are directly copied from https://github.com/mjackson/resolve-pathname/blob/master/modules/__tests__/resolvePathname-test.js diff --git a/packages/docusaurus-utils/src/urlUtils.ts b/packages/docusaurus-utils/src/urlUtils.ts index bb901a291d06..a01e66eb5c29 100644 --- a/packages/docusaurus-utils/src/urlUtils.ts +++ b/packages/docusaurus-utils/src/urlUtils.ts @@ -165,6 +165,61 @@ 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 +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 From b52fe316ffe3c8edd8ede68dc4c24ec10cd41371 Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Tue, 2 Jan 2024 18:06:07 +0100 Subject: [PATCH 64/69] Move parseURLPath to urlUtils --- packages/docusaurus-utils/src/index.ts | 3 + packages/docusaurus-utils/src/urlUtils.ts | 1 + packages/docusaurus/src/server/brokenLinks.ts | 61 +------------------ 3 files changed, 5 insertions(+), 60 deletions(-) diff --git a/packages/docusaurus-utils/src/index.ts b/packages/docusaurus-utils/src/index.ts index 5bb77a0c054f..7728846de0e5 100644 --- a/packages/docusaurus-utils/src/index.ts +++ b/packages/docusaurus-utils/src/index.ts @@ -48,6 +48,8 @@ export { encodePath, isValidPathname, resolvePathname, + parseURLPath, + serializeURLPath, addLeadingSlash, addTrailingSlash, removeTrailingSlash, @@ -55,6 +57,7 @@ export { buildHttpsUrl, buildSshUrl, } from './urlUtils'; +export type {URLPath} from './urlUtils'; export { type Tag, type TagsListItem, diff --git a/packages/docusaurus-utils/src/urlUtils.ts b/packages/docusaurus-utils/src/urlUtils.ts index a01e66eb5c29..78fad9268a79 100644 --- a/packages/docusaurus-utils/src/urlUtils.ts +++ b/packages/docusaurus-utils/src/urlUtils.ts @@ -169,6 +169,7 @@ 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 { diff --git a/packages/docusaurus/src/server/brokenLinks.ts b/packages/docusaurus/src/server/brokenLinks.ts index 55a248b0476c..533c3e5b84ee 100644 --- a/packages/docusaurus/src/server/brokenLinks.ts +++ b/packages/docusaurus/src/server/brokenLinks.ts @@ -8,6 +8,7 @@ import _ from 'lodash'; import logger from '@docusaurus/logger'; import {matchRoutes} from 'react-router-config'; +import {parseURLPath, serializeURLPath, type URLPath} from '@docusaurus/utils'; import {getAllFinalRoutes} from './utils'; import type {RouteConfig, ReportingSeverity} from '@docusaurus/types'; @@ -23,66 +24,6 @@ type CollectedLinks = { [location: string]: {links: string[]; anchors: string[]}; }; -type URLPath = {pathname: string; search?: string; hash?: string}; - -// TODO move to docusaurus-utils + add tests -// -// TODO: do we still need the urlUtils.resolvePathname ? -// this function also resolves the pathname while parsing -// -// Let's name the concept of (pathname + search + hash) as URLPath -// See also https://twitter.com/kettanaito/status/1741768992866308120 -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, - }; -} - -function serializeURLPath(urlPath: URLPath): string { - const search = urlPath.search === undefined ? '' : `?${urlPath.search}`; - const hash = urlPath.hash === undefined ? '' : `#${urlPath.hash}`; - return `${urlPath.pathname}${search}${hash}`; -} - function getPageBrokenLinks({ allCollectedLinks, pagePath, From 05aa80aa7bfc7fff5599114c9b6b17fcd65d6aa1 Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Tue, 2 Jan 2024 18:15:01 +0100 Subject: [PATCH 65/69] add comment: need for resolve-pathname lib? --- packages/docusaurus-utils/src/urlUtils.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/docusaurus-utils/src/urlUtils.ts b/packages/docusaurus-utils/src/urlUtils.ts index 78fad9268a79..8a7af4aa4b6d 100644 --- a/packages/docusaurus-utils/src/urlUtils.ts +++ b/packages/docusaurus-utils/src/urlUtils.ts @@ -227,8 +227,11 @@ export function serializeURLPath(urlPath: URLPath): string { * 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, '/'); From 7338242c388b98b767baf725c26a0a60c3894a2b Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Tue, 2 Jan 2024 18:43:08 +0100 Subject: [PATCH 66/69] broken links checker refactor --- packages/docusaurus/src/commands/build.ts | 8 +- .../src/server/__tests__/brokenLinks.test.ts | 56 +++---- packages/docusaurus/src/server/brokenLinks.ts | 150 ++++++++++-------- 3 files changed, 114 insertions(+), 100 deletions(-) diff --git a/packages/docusaurus/src/commands/build.ts b/packages/docusaurus/src/commands/build.ts index c61a06e02aa0..c0a38164092a 100644 --- a/packages/docusaurus/src/commands/build.ts +++ b/packages/docusaurus/src/commands/build.ts @@ -180,15 +180,15 @@ async function buildLocale({ }, ); - const allCollectedLinks: { - [location: string]: {links: string[]; anchors: string[]}; + const collectedLinks: { + [pathname: string]: {links: string[]; anchors: string[]}; } = {}; const headTags: {[location: string]: HelmetServerState} = {}; let serverConfig: Configuration = await createServerConfig({ props, onLinksCollected: ({staticPagePath, links, anchors}) => { - allCollectedLinks[staticPagePath] = {links, anchors}; + collectedLinks[staticPagePath] = {links, anchors}; }, onHeadTagsCollected: (staticPagePath, tags) => { headTags[staticPagePath] = tags; @@ -290,7 +290,7 @@ async function buildLocale({ ); await handleBrokenLinks({ - allCollectedLinks, + collectedLinks, routes, onBrokenLinks, onBrokenAnchors, diff --git a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts index 0b015ede6726..158af9165af7 100644 --- a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts +++ b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts @@ -16,13 +16,13 @@ type SimpleRoute = {path: string; routes?: SimpleRoute[]}; // Conveniently apply defaults to function under test async function testBrokenLinks(params: { - allCollectedLinks?: Params['allCollectedLinks']; + collectedLinks?: Params['collectedLinks']; onBrokenLinks?: Params['onBrokenLinks']; onBrokenAnchors?: Params['onBrokenAnchors']; routes?: SimpleRoute[]; }) { await handleBrokenLinks({ - allCollectedLinks: {}, + collectedLinks: {}, onBrokenLinks: 'throw', onBrokenAnchors: 'throw', ...params, @@ -35,7 +35,7 @@ describe('handleBrokenLinks', () => { it('accepts valid link', async () => { await testBrokenLinks({ routes: [{path: '/page1'}, {path: '/page2'}], - allCollectedLinks: { + collectedLinks: { '/page1': {links: ['/page2'], anchors: []}, '/page2': {links: [], anchors: []}, }, @@ -45,7 +45,7 @@ describe('handleBrokenLinks', () => { it('accepts valid link to uncollected page', async () => { await testBrokenLinks({ routes: [{path: '/page1'}, {path: '/page2'}], - allCollectedLinks: { + collectedLinks: { '/page1': {links: ['/page2'], anchors: []}, // /page2 is absent on purpose: it doesn't contain any link/anchor }, @@ -58,7 +58,7 @@ describe('handleBrokenLinks', () => { {path: '/page1'}, {path: '/nested/', routes: [{path: '/nested/page2'}]}, ], - allCollectedLinks: { + collectedLinks: { '/page1': {links: ['/nested/page2'], anchors: []}, }, }); @@ -67,7 +67,7 @@ describe('handleBrokenLinks', () => { it('accepts valid relative link', async () => { await testBrokenLinks({ routes: [{path: '/dir/page1'}, {path: '/dir/page2'}], - allCollectedLinks: { + collectedLinks: { '/dir/page1': { links: ['./page2', '../dir/page2', '/dir/page2'], anchors: [], @@ -79,7 +79,7 @@ describe('handleBrokenLinks', () => { it('accepts valid link with anchor', async () => { await testBrokenLinks({ routes: [{path: '/page1'}, {path: '/page2'}], - allCollectedLinks: { + collectedLinks: { '/page1': {links: ['/page2#page2anchor'], anchors: []}, '/page2': {links: [], anchors: ['page2anchor']}, }, @@ -89,7 +89,7 @@ describe('handleBrokenLinks', () => { it('accepts valid link with querystring + anchor', async () => { await testBrokenLinks({ routes: [{path: '/page1'}, {path: '/page2'}], - allCollectedLinks: { + collectedLinks: { '/page1': { links: ['/page2?age=42&theme=dark#page2anchor'], anchors: [], @@ -102,7 +102,7 @@ describe('handleBrokenLinks', () => { it('accepts valid link to self', async () => { await testBrokenLinks({ routes: [{path: '/page1'}], - allCollectedLinks: { + collectedLinks: { '/page1': { links: [ '/page1', @@ -122,7 +122,7 @@ describe('handleBrokenLinks', () => { it('accepts valid link with spaces and encoding', async () => { await testBrokenLinks({ routes: [{path: '/page 1'}, {path: '/page 2'}], - allCollectedLinks: { + collectedLinks: { '/page 1': { links: [ '/page 1', @@ -144,7 +144,7 @@ describe('handleBrokenLinks', () => { await expect(() => testBrokenLinks({ routes: [{path: '/page1'}, {path: '/page2'}], - allCollectedLinks: { + collectedLinks: { '/page1': {links: ['/brokenLink'], anchors: []}, }, }), @@ -165,7 +165,7 @@ describe('handleBrokenLinks', () => { await expect(() => testBrokenLinks({ routes: [{path: '/page1'}, {path: '/page2'}], - allCollectedLinks: { + collectedLinks: { '/page1': {links: ['/brokenLink#anchor'], anchors: []}, }, }), @@ -186,7 +186,7 @@ describe('handleBrokenLinks', () => { await expect(() => testBrokenLinks({ routes: [{path: '/page1'}, {path: '/page2'}], - allCollectedLinks: { + collectedLinks: { '/page1': {links: ['/brokenLink?age=42#anchor'], anchors: []}, }, }), @@ -207,7 +207,7 @@ describe('handleBrokenLinks', () => { await expect(() => testBrokenLinks({ routes: [{path: '/page1'}, {path: '/page2'}], - allCollectedLinks: { + collectedLinks: { '/page1': {links: ['/page2#brokenAnchor'], anchors: []}, '/page2': {links: [], anchors: []}, }, @@ -229,7 +229,7 @@ describe('handleBrokenLinks', () => { await expect(() => testBrokenLinks({ routes: [{path: '/page1'}, {path: '/page2'}], - allCollectedLinks: { + collectedLinks: { '/page1': {links: ['/page2#'], anchors: []}, '/page2': {links: [], anchors: []}, }, @@ -251,7 +251,7 @@ describe('handleBrokenLinks', () => { await expect(() => testBrokenLinks({ routes: [{path: '/page1'}, {path: '/page2'}], - allCollectedLinks: { + collectedLinks: { '/page1': { links: ['/page2?age=42&theme=dark#brokenAnchor'], anchors: [], @@ -276,7 +276,7 @@ describe('handleBrokenLinks', () => { await expect(() => testBrokenLinks({ routes: [{path: '/page1'}], - allCollectedLinks: { + collectedLinks: { '/page1': { links: [ '/page1', @@ -312,7 +312,7 @@ describe('handleBrokenLinks', () => { await expect(() => testBrokenLinks({ routes: [{path: '/page1'}, {path: '/page2'}], - allCollectedLinks: { + collectedLinks: { '/page1': {links: ['/page2#brokenAnchor'], anchors: []}, // /page2 is absent on purpose: it doesn't contain any link/anchor }, @@ -334,7 +334,7 @@ describe('handleBrokenLinks', () => { await expect(() => testBrokenLinks({ routes: [{path: '/page1'}, {path: '/page2'}], - allCollectedLinks: { + collectedLinks: { '/page1': { links: ['/page2?age=42&theme=dark#brokenAnchor'], anchors: [], @@ -359,7 +359,7 @@ describe('handleBrokenLinks', () => { await testBrokenLinks({ onBrokenLinks: 'ignore', routes: [{path: '/page1'}], - allCollectedLinks: { + collectedLinks: { '/page1': { links: ['/page2'], anchors: [], @@ -372,7 +372,7 @@ describe('handleBrokenLinks', () => { await testBrokenLinks({ onBrokenAnchors: 'ignore', routes: [{path: '/page1'}], - allCollectedLinks: { + collectedLinks: { '/page1': { links: ['/page1#brokenAnchor'], anchors: [], @@ -386,7 +386,7 @@ describe('handleBrokenLinks', () => { testBrokenLinks({ onBrokenAnchors: 'ignore', routes: [{path: '/page1'}], - allCollectedLinks: { + collectedLinks: { '/page1': { links: ['/page1#brokenAnchor', '/page2'], anchors: [], @@ -411,7 +411,7 @@ describe('handleBrokenLinks', () => { testBrokenLinks({ onBrokenLinks: 'ignore', routes: [{path: '/page1'}], - allCollectedLinks: { + collectedLinks: { '/page1': { links: [ '/page2', @@ -445,7 +445,7 @@ describe('handleBrokenLinks', () => { await testBrokenLinks({ onBrokenLinks: 'warn', routes: [{path: '/page1'}], - allCollectedLinks: { + collectedLinks: { '/page1': { links: ['/page2'], anchors: [], @@ -478,7 +478,7 @@ describe('handleBrokenLinks', () => { await testBrokenLinks({ onBrokenAnchors: 'warn', routes: [{path: '/page1'}], - allCollectedLinks: { + collectedLinks: { '/page1': { links: ['/page1#brokenAnchor'], anchors: [], @@ -512,7 +512,7 @@ describe('handleBrokenLinks', () => { onBrokenLinks: 'warn', onBrokenAnchors: 'warn', routes: [{path: '/page1'}], - allCollectedLinks: { + collectedLinks: { '/page1': { links: ['/page1#brokenAnchor', '/page2'], anchors: [], @@ -563,7 +563,7 @@ describe('handleBrokenLinks', () => { path: pagePath, })); - const allCollectedLinks: Params['allCollectedLinks'] = Object.fromEntries( + const collectedLinks: Params['collectedLinks'] = Object.fromEntries( pagePaths.map((pagePath) => [ pagePath, { @@ -576,7 +576,7 @@ describe('handleBrokenLinks', () => { await expect(() => testBrokenLinks({ routes, - allCollectedLinks, + collectedLinks, }), ).rejects.toThrowErrorMatchingInlineSnapshot(` "Docusaurus found broken links! diff --git a/packages/docusaurus/src/server/brokenLinks.ts b/packages/docusaurus/src/server/brokenLinks.ts index 533c3e5b84ee..bb477bb8bc1c 100644 --- a/packages/docusaurus/src/server/brokenLinks.ts +++ b/packages/docusaurus/src/server/brokenLinks.ts @@ -18,19 +18,21 @@ type BrokenLink = { anchor: boolean; }; -type BrokenLinksByLocation = {[location: string]: BrokenLink[]}; +type BrokenLinksMap = {[pathname: string]: BrokenLink[]}; +// The linking data that has been collected on Docusaurus pages during SSG +// {rendered page pathname => links and anchors collected on that page} type CollectedLinks = { - [location: string]: {links: string[]; anchors: string[]}; + [pathname: string]: {links: string[]; anchors: string[]}; }; -function getPageBrokenLinks({ - allCollectedLinks, +function getBrokenLinksForPage({ + collectedLinks, pagePath, pageLinks, routes, }: { - allCollectedLinks: CollectedLinks; + collectedLinks: CollectedLinks; pagePath: string; pageLinks: string[]; pageAnchors: string[]; @@ -56,7 +58,7 @@ function getPageBrokenLinks({ } const targetPage = - allCollectedLinks[pathname] || allCollectedLinks[decodeURI(pathname)]; + 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 @@ -106,49 +108,24 @@ function filterIntermediateRoutes(routesInput: RouteConfig[]): RouteConfig[] { return getAllFinalRoutes(routesWithout404); } -function getAllBrokenLinks({ - allCollectedLinks, +function getBrokenLinks({ + collectedLinks, routes, }: { - allCollectedLinks: CollectedLinks; + collectedLinks: CollectedLinks; routes: RouteConfig[]; -}): {brokenLinks: BrokenLinksByLocation; brokenAnchors: BrokenLinksByLocation} { +}): BrokenLinksMap { const filteredRoutes = filterIntermediateRoutes(routes); - const allBrokenLinks = _.mapValues( - allCollectedLinks, - (pageCollectedData, pagePath) => - getPageBrokenLinks({ - allCollectedLinks, - pageLinks: pageCollectedData.links, - pageAnchors: pageCollectedData.anchors, - pagePath, - routes: filteredRoutes, - }), + return _.mapValues(collectedLinks, (pageCollectedData, pagePath) => + getBrokenLinksForPage({ + collectedLinks, + pageLinks: pageCollectedData.links, + pageAnchors: pageCollectedData.anchors, + pagePath, + routes: filteredRoutes, + }), ); - - function splitBrokenLinks(collect: {[x: string]: BrokenLink[]}) { - const brokenLinks: {[x: string]: BrokenLink[]} = {}; - const brokenAnchors: {[x: string]: BrokenLink[]} = {}; - - Object.entries(collect).forEach(([page, links]) => { - const [linksFiltered, anchorsFiltered] = _.partition( - links, - (link) => link.anchor === false, - ); - - if (linksFiltered.length > 0) { - brokenLinks[page] = linksFiltered; - } - if (anchorsFiltered.length > 0) { - brokenAnchors[page] = anchorsFiltered; - } - }); - - return {brokenLinks, brokenAnchors}; - } - - return splitBrokenLinks(allBrokenLinks); } function brokenLinkMessage(brokenLink: BrokenLink): string { @@ -175,9 +152,9 @@ function createBrokenLinksMessage( return `${anchorMessage}`; } -function getAnchorBrokenLinksErrorMessage(allBrokenLinks: { - [location: string]: BrokenLink[]; -}): string | undefined { +function getAnchorBrokenLinksErrorMessage( + allBrokenLinks: BrokenLinksMap, +): string | undefined { if (Object.keys(allBrokenLinks).length === 0) { return undefined; } @@ -196,10 +173,10 @@ ${Object.entries(allBrokenLinks) `; } -function getPathBrokenLinksErrorMessage(allBrokenLinks: { - [location: string]: BrokenLink[]; -}): string | undefined { - if (Object.keys(allBrokenLinks).length === 0) { +function getPathBrokenLinksErrorMessage( + brokenLinksMap: BrokenLinksMap, +): string | undefined { + if (Object.keys(brokenLinksMap).length === 0) { return undefined; } @@ -209,7 +186,7 @@ function getPathBrokenLinksErrorMessage(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(brokenLinksMap).flatMap( ([pagePage, brokenLinks]) => brokenLinks.map((brokenLink) => ({pagePage, brokenLink})), ); @@ -242,7 +219,7 @@ 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) +${Object.entries(brokenLinksMap) .map(([pagePath, brokenLinks]) => createBrokenLinksMessage(pagePath, brokenLinks), ) @@ -250,27 +227,46 @@ ${Object.entries(allBrokenLinks) `; } -export async function handleBrokenLinks({ - allCollectedLinks, +function splitBrokenLinks(brokenLinks: BrokenLinksMap): { + brokenPaths: BrokenLinksMap; + brokenAnchors: BrokenLinksMap; +} { + const brokenPaths: BrokenLinksMap = {}; + const brokenAnchors: BrokenLinksMap = {}; + + Object.entries(brokenLinks).forEach(([pathname, pageBrokenLinks]) => { + const [anchorBrokenLinks, pathBrokenLinks] = _.partition( + pageBrokenLinks, + (link) => link.anchor, + ); + + if (pathBrokenLinks.length > 0) { + brokenPaths[pathname] = pathBrokenLinks; + } + if (anchorBrokenLinks.length > 0) { + brokenAnchors[pathname] = anchorBrokenLinks; + } + }); + + return {brokenPaths, brokenAnchors}; +} + +function reportBrokenLinks({ + brokenLinks, onBrokenLinks, onBrokenAnchors, - routes, }: { - allCollectedLinks: CollectedLinks; + brokenLinks: BrokenLinksMap; onBrokenLinks: ReportingSeverity; onBrokenAnchors: ReportingSeverity; - routes: RouteConfig[]; -}): Promise { - if (onBrokenLinks === 'ignore' && onBrokenAnchors === 'ignore') { - return; - } - - const {brokenLinks, brokenAnchors} = getAllBrokenLinks({ - allCollectedLinks, - routes, - }); - - const pathErrorMessage = getPathBrokenLinksErrorMessage(brokenLinks); +}) { + // 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 = getPathBrokenLinksErrorMessage(brokenPaths); if (pathErrorMessage) { logger.report(onBrokenLinks)(pathErrorMessage); } @@ -280,3 +276,21 @@ export async function handleBrokenLinks({ logger.report(onBrokenAnchors)(anchorErrorMessage); } } + +export async function handleBrokenLinks({ + collectedLinks, + onBrokenLinks, + onBrokenAnchors, + routes, +}: { + collectedLinks: CollectedLinks; + onBrokenLinks: ReportingSeverity; + onBrokenAnchors: ReportingSeverity; + routes: RouteConfig[]; +}): Promise { + if (onBrokenLinks === 'ignore' && onBrokenAnchors === 'ignore') { + return; + } + const brokenLinks = getBrokenLinks({routes, collectedLinks}); + reportBrokenLinks({brokenLinks, onBrokenLinks, onBrokenAnchors}); +} From 687f04f44cbfac2d875586f2c2b6fba586269243 Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Tue, 2 Jan 2024 18:50:31 +0100 Subject: [PATCH 67/69] fix onBrokenAnchors heading anchor --- packages/docusaurus-types/src/config.d.ts | 2 +- website/docs/api/docusaurus.config.js.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/docusaurus-types/src/config.d.ts b/packages/docusaurus-types/src/config.d.ts index e95c6768f10b..fd46895cec05 100644 --- a/packages/docusaurus-types/src/config.d.ts +++ b/packages/docusaurus-types/src/config.d.ts @@ -146,7 +146,7 @@ export type DocusaurusConfig = { /** * The behavior of Docusaurus when it detects any broken link. * - * @see // TODO + * @see https://docusaurus.io/docs/api/docusaurus-config#onBrokenAnchors * @default "warn" */ onBrokenAnchors: ReportingSeverity; diff --git a/website/docs/api/docusaurus.config.js.mdx b/website/docs/api/docusaurus.config.js.mdx index ecb39b21aa7f..d1a9186352f1 100644 --- a/website/docs/api/docusaurus.config.js.mdx +++ b/website/docs/api/docusaurus.config.js.mdx @@ -196,7 +196,7 @@ The broken links detection is only available for a production build (`docusaurus ::: -### `onBrokenAnchors` {#onBrokenLinks} +### `onBrokenAnchors` {#onBrokenAnchors} - Type: `'ignore' | 'log' | 'warn' | 'throw'` From 2bf56817771c12cdf1d852baaf7ef77518f462f4 Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Tue, 2 Jan 2024 19:10:04 +0100 Subject: [PATCH 68/69] minor broken links refactors --- packages/docusaurus/src/server/brokenLinks.ts | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/docusaurus/src/server/brokenLinks.ts b/packages/docusaurus/src/server/brokenLinks.ts index bb477bb8bc1c..ccbaadcd3ffb 100644 --- a/packages/docusaurus/src/server/brokenLinks.ts +++ b/packages/docusaurus/src/server/brokenLinks.ts @@ -137,14 +137,14 @@ function brokenLinkMessage(brokenLink: BrokenLink): string { function createBrokenLinksMessage( pagePath: string, - allBrokenLinks: BrokenLink[], + brokenLinks: BrokenLink[], ): string { - const type = allBrokenLinks[0]?.anchor === true ? 'anchor' : 'link'; + const type = brokenLinks[0]?.anchor === true ? 'anchor' : 'link'; const anchorMessage = - allBrokenLinks.length > 0 + brokenLinks.length > 0 ? `- Broken ${type} on source page path = ${pagePath}: - -> linking to ${allBrokenLinks + -> linking to ${brokenLinks .map(brokenLinkMessage) .join('\n -> linking to ')}` : ''; @@ -152,10 +152,10 @@ function createBrokenLinksMessage( return `${anchorMessage}`; } -function getAnchorBrokenLinksErrorMessage( - allBrokenLinks: BrokenLinksMap, +function createBrokenAnchorsMessage( + brokenAnchors: BrokenLinksMap, ): string | undefined { - if (Object.keys(allBrokenLinks).length === 0) { + if (Object.keys(brokenAnchors).length === 0) { return undefined; } @@ -165,7 +165,7 @@ 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 anchors with the 'onBrokenAnchors' Docusaurus configuration, and let the build pass. Exhaustive list of all broken anchors found: -${Object.entries(allBrokenLinks) +${Object.entries(brokenAnchors) .map(([pagePath, brokenLinks]) => createBrokenLinksMessage(pagePath, brokenLinks), ) @@ -173,10 +173,10 @@ ${Object.entries(allBrokenLinks) `; } -function getPathBrokenLinksErrorMessage( - brokenLinksMap: BrokenLinksMap, +function createBrokenPathsMessage( + brokenPathsMap: BrokenLinksMap, ): string | undefined { - if (Object.keys(brokenLinksMap).length === 0) { + if (Object.keys(brokenPathsMap).length === 0) { return undefined; } @@ -186,7 +186,7 @@ function getPathBrokenLinksErrorMessage( * this out. See https://github.com/facebook/docusaurus/issues/3567#issuecomment-706973805 */ function getLayoutBrokenLinksHelpMessage() { - const flatList = Object.entries(brokenLinksMap).flatMap( + const flatList = Object.entries(brokenPathsMap).flatMap( ([pagePage, brokenLinks]) => brokenLinks.map((brokenLink) => ({pagePage, brokenLink})), ); @@ -219,9 +219,9 @@ 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(brokenLinksMap) - .map(([pagePath, brokenLinks]) => - createBrokenLinksMessage(pagePath, brokenLinks), +${Object.entries(brokenPathsMap) + .map(([pagePath, brokenPaths]) => + createBrokenLinksMessage(pagePath, brokenPaths), ) .join('\n')} `; @@ -266,12 +266,12 @@ function reportBrokenLinks({ // TODO Docusaurus v4: make onBrokenAnchors throw by default? const {brokenPaths, brokenAnchors} = splitBrokenLinks(brokenLinks); - const pathErrorMessage = getPathBrokenLinksErrorMessage(brokenPaths); + const pathErrorMessage = createBrokenPathsMessage(brokenPaths); if (pathErrorMessage) { logger.report(onBrokenLinks)(pathErrorMessage); } - const anchorErrorMessage = getAnchorBrokenLinksErrorMessage(brokenAnchors); + const anchorErrorMessage = createBrokenAnchorsMessage(brokenAnchors); if (anchorErrorMessage) { logger.report(onBrokenAnchors)(anchorErrorMessage); } From cc74ef8a08feae3e502dade7a8aa13081b8ee634 Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Tue, 2 Jan 2024 19:29:09 +0100 Subject: [PATCH 69/69] improve useBrokenLinks docs --- website/docs/docusaurus-core.mdx | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/website/docs/docusaurus-core.mdx b/website/docs/docusaurus-core.mdx index 5b302f8c1286..cdb5a6fcf4a1 100644 --- a/website/docs/docusaurus-core.mdx +++ b/website/docs/docusaurus-core.mdx @@ -607,11 +607,18 @@ const MyComponent = () => { ### `useBrokenLinks` {#useBrokenLinks} -React hook to access the BrokenLinks context. The context provides methods for collecting and managing information about anchors and links. +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. -:::info +:::warning + +This is an **advanced** API that **most Docusaurus users don't need to use directly**. + +It is already **built-in** in existing high-level components: + +- the [``](#link) component will collect links for you +- the `@theme/Heading` (used for Markdown headings) will collect anchors -This is an advanced hook, most users won't need it. +Use `useBrokenLinks()` if you implement your own `` or `` component. ::: @@ -625,7 +632,7 @@ export default function MyHeading({id, ...props}): JSX.Element { brokenLinks.collectAnchor(id); - return

Heading

; + return

Heading

; } ```