Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/docusaurus-types/src/bronkenLinks.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export type BrokenLink = {
link: string;
resolvedLink: string;
anchor: boolean;
};

export type BrokenLinksMap = {
[pathname: string]: BrokenLink[]
};
10 changes: 10 additions & 0 deletions packages/docusaurus-types/src/config.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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.
*
Expand Down
6 changes: 6 additions & 0 deletions packages/docusaurus-types/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,9 @@ export {
} from './routing';

export {UseDataOptions} from './utils';

export {
BrokenLinksMap,
BrokenLink
} from './bronkenLinks';

5 changes: 3 additions & 2 deletions packages/docusaurus/src/commands/build/buildLocale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ async function executePluginsPostBuild({
async function executeBrokenLinksCheck({
props: {
routes,
siteConfig: {onBrokenLinks, onBrokenAnchors},
siteConfig: {onBrokenLinks, onBrokenAnchors, onReportBrokenLinks},
},
collectedData,
}: {
Expand All @@ -172,9 +172,10 @@ async function executeBrokenLinksCheck({
}));
await handleBrokenLinks({
collectedLinks,
routes,
onBrokenLinks,
onBrokenAnchors,
routes,
onReportBrokenLinks
});
}

Expand Down
29 changes: 28 additions & 1 deletion packages/docusaurus/src/server/__tests__/brokenLinks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof handleBrokenLinks>[0];

Expand All @@ -26,6 +26,7 @@ async function testBrokenLinks(params: {
onBrokenLinks?: Params['onBrokenLinks'];
onBrokenAnchors?: Params['onBrokenAnchors'];
routes?: SimpleRoute[];
onReportBrokenLinks?: Params['onReportBrokenLinks'];
}) {
await handleBrokenLinks({
collectedLinks: {},
Expand All @@ -34,6 +35,7 @@ async function testBrokenLinks(params: {
...params,
// Unsafe but convenient for tests
routes: (params.routes ?? []) as RouteConfig[],
onReportBrokenLinks: params.onReportBrokenLinks,
});
}

Expand Down Expand Up @@ -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');

Expand Down
22 changes: 13 additions & 9 deletions packages/docusaurus/src/server/brokenLinks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 = {
Expand Down Expand Up @@ -404,11 +401,13 @@ export async function handleBrokenLinks({
onBrokenLinks,
onBrokenAnchors,
routes,
onReportBrokenLinks,
}: {
collectedLinks: CollectedLinks;
onBrokenLinks: ReportingSeverity;
onBrokenAnchors: ReportingSeverity;
routes: RouteConfig[];
onReportBrokenLinks?: (brokenLinksMap: BrokenLinksMap) => void;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer to avoid introducing a new API when we already have one

}): Promise<void> {
if (onBrokenLinks === 'ignore' && onBrokenAnchors === 'ignore') {
return;
Expand All @@ -417,5 +416,10 @@ export async function handleBrokenLinks({
routes,
collectedLinks: normalizeCollectedLinks(collectedLinks),
});

if (onReportBrokenLinks) {
onReportBrokenLinks(brokenLinks);
}

reportBrokenLinks({brokenLinks, onBrokenLinks, onBrokenAnchors});
}
1 change: 1 addition & 0 deletions packages/docusaurus/src/server/configValidation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@ export const ConfigSchema = Joi.object<DocusaurusConfig>({
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),
Expand Down
27 changes: 27 additions & 0 deletions website/docs/api/docusaurus.config.js.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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'`
Expand Down