From afff4389b0e25a737315f77a8a2a9618dd194f49 Mon Sep 17 00:00:00 2001 From: brunomartinspro Date: Tue, 1 Apr 2025 15:36:52 +0100 Subject: [PATCH 1/2] Enable managing broken links from a report --- .../docusaurus-types/src/bronkenLinks.d.ts | 9 +++++++ packages/docusaurus-types/src/config.d.ts | 10 +++++++ packages/docusaurus-types/src/index.d.ts | 6 +++++ .../src/commands/build/buildLocale.ts | 5 ++-- packages/docusaurus/src/server/brokenLinks.ts | 22 ++++++++------- .../docusaurus/src/server/configValidation.ts | 1 + website/docs/api/docusaurus.config.js.mdx | 27 +++++++++++++++++++ 7 files changed, 69 insertions(+), 11 deletions(-) create mode 100644 packages/docusaurus-types/src/bronkenLinks.d.ts diff --git a/packages/docusaurus-types/src/bronkenLinks.d.ts b/packages/docusaurus-types/src/bronkenLinks.d.ts new file mode 100644 index 0000000000..5124f773fd --- /dev/null +++ b/packages/docusaurus-types/src/bronkenLinks.d.ts @@ -0,0 +1,9 @@ +export type BrokenLink = { + link: string; + resolvedLink: string; + anchor: boolean; + }; + +export type BrokenLinksMap = { + [pathname: string]: BrokenLink[] +}; \ No newline at end of file diff --git a/packages/docusaurus-types/src/config.d.ts b/packages/docusaurus-types/src/config.d.ts index 26b7022cf3..d84c73c9f3 100644 --- a/packages/docusaurus-types/src/config.d.ts +++ b/packages/docusaurus-types/src/config.d.ts @@ -13,6 +13,8 @@ import type {PluginConfig, PresetConfig, HtmlTagObject} from './plugin'; import type {ProcessorOptions} from '@mdx-js/mdx'; +import type {BrokenLinksMap} from './bronkenLinks'; + export type RemarkRehypeOptions = ProcessorOptions['remarkRehypeOptions']; export type ReportingSeverity = 'ignore' | 'log' | 'warn' | 'throw'; @@ -251,6 +253,14 @@ export type DocusaurusConfig = { * @default "warn" */ onBrokenAnchors: ReportingSeverity; + /** + * The behavior of Docusaurus when it detects any broken link or anchor and generates a report. + * This functions runs before generating a report. + * + * @see https://docusaurus.io/docs/api/docusaurus-config#onReportBrokenLinks + * @default "warn" + */ + onReportBrokenLinks: (brokenLinksMap: BrokenLinksMap) => void; /** * The behavior of Docusaurus when it detects any broken markdown link. * diff --git a/packages/docusaurus-types/src/index.d.ts b/packages/docusaurus-types/src/index.d.ts index a6cb0b00b4..53439d186b 100644 --- a/packages/docusaurus-types/src/index.d.ts +++ b/packages/docusaurus-types/src/index.d.ts @@ -90,3 +90,9 @@ export { } from './routing'; export {UseDataOptions} from './utils'; + +export { + BrokenLinksMap, + BrokenLink +} from './bronkenLinks'; + diff --git a/packages/docusaurus/src/commands/build/buildLocale.ts b/packages/docusaurus/src/commands/build/buildLocale.ts index 84f65a63d3..2146d164c2 100644 --- a/packages/docusaurus/src/commands/build/buildLocale.ts +++ b/packages/docusaurus/src/commands/build/buildLocale.ts @@ -159,7 +159,7 @@ async function executePluginsPostBuild({ async function executeBrokenLinksCheck({ props: { routes, - siteConfig: {onBrokenLinks, onBrokenAnchors}, + siteConfig: {onBrokenLinks, onBrokenAnchors, onReportBrokenLinks}, }, collectedData, }: { @@ -172,9 +172,10 @@ async function executeBrokenLinksCheck({ })); await handleBrokenLinks({ collectedLinks, - routes, onBrokenLinks, onBrokenAnchors, + routes, + onReportBrokenLinks }); } diff --git a/packages/docusaurus/src/server/brokenLinks.ts b/packages/docusaurus/src/server/brokenLinks.ts index 3ee1971f81..76cb8e19d8 100644 --- a/packages/docusaurus/src/server/brokenLinks.ts +++ b/packages/docusaurus/src/server/brokenLinks.ts @@ -15,7 +15,12 @@ import { type URLPath, } from '@docusaurus/utils'; import {addTrailingSlash, removeTrailingSlash} from '@docusaurus/utils-common'; -import type {RouteConfig, ReportingSeverity} from '@docusaurus/types'; +import type { + RouteConfig, + ReportingSeverity, + BrokenLink, + BrokenLinksMap, +} from '@docusaurus/types'; function matchRoutes(routeConfig: RouteConfig[], pathname: string) { // @ts-expect-error: React router types RouteConfig with an actual React @@ -24,14 +29,6 @@ function matchRoutes(routeConfig: RouteConfig[], pathname: string) { return reactRouterMatchRoutes(routeConfig, pathname); } -type BrokenLink = { - link: string; - resolvedLink: string; - anchor: boolean; -}; - -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 = { @@ -404,11 +401,13 @@ export async function handleBrokenLinks({ onBrokenLinks, onBrokenAnchors, routes, + onReportBrokenLinks, }: { collectedLinks: CollectedLinks; onBrokenLinks: ReportingSeverity; onBrokenAnchors: ReportingSeverity; routes: RouteConfig[]; + onReportBrokenLinks?: (brokenLinksMap: BrokenLinksMap) => void; }): Promise { if (onBrokenLinks === 'ignore' && onBrokenAnchors === 'ignore') { return; @@ -417,5 +416,10 @@ export async function handleBrokenLinks({ routes, collectedLinks: normalizeCollectedLinks(collectedLinks), }); + + if (onReportBrokenLinks) { + onReportBrokenLinks(brokenLinks); + } + reportBrokenLinks({brokenLinks, onBrokenLinks, onBrokenAnchors}); } diff --git a/packages/docusaurus/src/server/configValidation.ts b/packages/docusaurus/src/server/configValidation.ts index 33d7199143..258bdd5ade 100644 --- a/packages/docusaurus/src/server/configValidation.ts +++ b/packages/docusaurus/src/server/configValidation.ts @@ -343,6 +343,7 @@ export const ConfigSchema = Joi.object({ onBrokenAnchors: Joi.string() .equal('ignore', 'log', 'warn', 'throw') .default(DEFAULT_CONFIG.onBrokenAnchors), + onReportBrokenLinks: Joi.function(), onBrokenMarkdownLinks: Joi.string() .equal('ignore', 'log', 'warn', 'throw') .default(DEFAULT_CONFIG.onBrokenMarkdownLinks), diff --git a/website/docs/api/docusaurus.config.js.mdx b/website/docs/api/docusaurus.config.js.mdx index 41f5c50a9f..950d438320 100644 --- a/website/docs/api/docusaurus.config.js.mdx +++ b/website/docs/api/docusaurus.config.js.mdx @@ -271,6 +271,33 @@ The behavior of Docusaurus when it detects any broken anchor declared with the ` By default, it prints a warning, to let you know about your broken anchors. +### `onReportBrokenLinks` {#onReportBrokenLinks} + +- Type: `function` + +When Docusaurus detects a broken link or anchor, it generates a report detailing the issues. + +This function runs before the report is generated, allowing you to modify its contents before final output. + +#### USAGE + +In this example we will delete broken links that start with the path "/api" from the final report. + +```javascript +import { Config, BrokenLinksMap } from '@docusaurus/types'; + +const config: Config = { + onReportBrokenLinks: (brokenLinksMap: BrokenLinksMap) => { + for (const pathname in brokenLinksMap) { + if (pathname.startsWith('/api')) { + delete brokenLinksMap[pathname]; + } + } + } +} + +``` + ### `onBrokenMarkdownLinks` {#onBrokenMarkdownLinks} - Type: `'ignore' | 'log' | 'warn' | 'throw'` From b0e642ee14a919258e7f295b6742b9308420fd09 Mon Sep 17 00:00:00 2001 From: brunomartinspro Date: Mon, 14 Apr 2025 14:38:20 +0100 Subject: [PATCH 2/2] unit tests --- .../src/server/__tests__/brokenLinks.test.ts | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts index b21d6c6e42..a34d7c9a4c 100644 --- a/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts +++ b/packages/docusaurus/src/server/__tests__/brokenLinks.test.ts @@ -8,7 +8,7 @@ import {jest} from '@jest/globals'; import reactRouterConfig from 'react-router-config'; import {handleBrokenLinks} from '../brokenLinks'; -import type {RouteConfig} from '@docusaurus/types'; +import type {BrokenLinksMap, RouteConfig} from '@docusaurus/types'; type Params = Parameters[0]; @@ -26,6 +26,7 @@ async function testBrokenLinks(params: { onBrokenLinks?: Params['onBrokenLinks']; onBrokenAnchors?: Params['onBrokenAnchors']; routes?: SimpleRoute[]; + onReportBrokenLinks?: Params['onReportBrokenLinks']; }) { await handleBrokenLinks({ collectedLinks: {}, @@ -34,6 +35,7 @@ async function testBrokenLinks(params: { ...params, // Unsafe but convenient for tests routes: (params.routes ?? []) as RouteConfig[], + onReportBrokenLinks: params.onReportBrokenLinks, }); } @@ -728,6 +730,31 @@ describe('handleBrokenLinks', () => { warnMock.mockRestore(); }); + it('can warn for broken links and remove them before building the report', async () => { + const warnMock = jest.spyOn(console, 'warn'); + + await testBrokenLinks({ + onBrokenLinks: 'warn', + routes: [{path: '/page1'}], + collectedLinks: { + '/page1': { + links: ['/page2'], + anchors: [], + }, + }, + onReportBrokenLinks: (brokenLinksMap: BrokenLinksMap) => { + for (const pathname in brokenLinksMap) { + if (pathname.startsWith('/page1')) { + delete brokenLinksMap[pathname]; + } + } + }, + }); + + expect(warnMock).toHaveBeenCalledTimes(0); + warnMock.mockRestore(); + }); + it('can warn for broken anchors', async () => { const warnMock = jest.spyOn(console, 'warn');