Skip to content

Commit 9df5aae

Browse files
authored
feat(core): new postBuild({routesBuildMetadata}) API, deprecate head attribute + v4 future flag (#10850)
Co-authored-by: slorber <[email protected]>
1 parent 67207bc commit 9df5aae

21 files changed

+403
-94
lines changed

packages/docusaurus-plugin-sitemap/src/__tests__/createSitemap.test.ts

+13-34
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8-
import {createElement} from 'react';
98
import {fromPartial} from '@total-typescript/shoehorn';
109
import createSitemap from '../createSitemap';
1110
import type {PluginOptions} from '../options';
@@ -39,7 +38,7 @@ describe('createSitemap', () => {
3938
const sitemap = await createSitemap({
4039
siteConfig,
4140
routes: routes(['/', '/test']),
42-
head: {},
41+
routesBuildMetadata: {},
4342
options,
4443
});
4544
expect(sitemap).toContain(
@@ -51,7 +50,7 @@ describe('createSitemap', () => {
5150
const sitemap = await createSitemap({
5251
siteConfig,
5352
routes: routes([]),
54-
head: {},
53+
routesBuildMetadata: {},
5554
options,
5655
});
5756
expect(sitemap).toBeNull();
@@ -67,7 +66,7 @@ describe('createSitemap', () => {
6766
'/search/foo',
6867
'/tags/foo/bar',
6968
]),
70-
head: {},
69+
routesBuildMetadata: {},
7170
options: {
7271
...options,
7372
ignorePatterns: [
@@ -94,7 +93,7 @@ describe('createSitemap', () => {
9493
'/search/foo',
9594
'/tags/foo/bar',
9695
]),
97-
head: {},
96+
routesBuildMetadata: {},
9897
options: {
9998
...options,
10099
createSitemapItems: async (params) => {
@@ -119,7 +118,7 @@ describe('createSitemap', () => {
119118
const sitemap = await createSitemap({
120119
siteConfig,
121120
routes: routes(['/', '/docs/myDoc/', '/blog/post']),
122-
head: {},
121+
routesBuildMetadata: {},
123122
options: {
124123
...options,
125124
createSitemapItems: async () => {
@@ -135,7 +134,7 @@ describe('createSitemap', () => {
135134
const sitemap = await createSitemap({
136135
siteConfig,
137136
routes: routes(['/', '/test', '/nested/test', '/nested/test2/']),
138-
head: {},
137+
routesBuildMetadata: {},
139138
options,
140139
});
141140

@@ -149,7 +148,7 @@ describe('createSitemap', () => {
149148
const sitemap = await createSitemap({
150149
siteConfig: {...siteConfig, trailingSlash: true},
151150
routes: routes(['/', '/test', '/nested/test', '/nested/test2/']),
152-
head: {},
151+
routesBuildMetadata: {},
153152
options,
154153
});
155154

@@ -167,7 +166,7 @@ describe('createSitemap', () => {
167166
trailingSlash: false,
168167
},
169168
routes: routes(['/', '/test', '/nested/test', '/nested/test2/']),
170-
head: {},
169+
routesBuildMetadata: {},
171170
options,
172171
});
173172

@@ -180,19 +179,10 @@ describe('createSitemap', () => {
180179
it('filters pages with noindex', async () => {
181180
const sitemap = await createSitemap({
182181
siteConfig,
183-
routesPaths: ['/', '/noindex', '/nested/test', '/nested/test2/'],
184182
routes: routes(['/', '/noindex', '/nested/test', '/nested/test2/']),
185-
head: {
183+
routesBuildMetadata: {
186184
'/noindex': {
187-
meta: {
188-
// @ts-expect-error: bad lib def
189-
toComponent: () => [
190-
createElement('meta', {
191-
name: 'robots',
192-
content: 'NoFolloW, NoiNDeX',
193-
}),
194-
],
195-
},
185+
noIndex: true,
196186
},
197187
},
198188
options,
@@ -204,24 +194,13 @@ describe('createSitemap', () => {
204194
it('does not generate anything for all pages with noindex', async () => {
205195
const sitemap = await createSitemap({
206196
siteConfig,
207-
routesPaths: ['/', '/noindex'],
208197
routes: routes(['/', '/noindex']),
209-
head: {
198+
routesBuildMetadata: {
210199
'/': {
211-
meta: {
212-
// @ts-expect-error: bad lib def
213-
toComponent: () => [
214-
createElement('meta', {name: 'robots', content: 'noindex'}),
215-
],
216-
},
200+
noIndex: true,
217201
},
218202
'/noindex': {
219-
meta: {
220-
// @ts-expect-error: bad lib def
221-
toComponent: () => [
222-
createElement('meta', {name: 'robots', content: 'noindex'}),
223-
],
224-
},
203+
noIndex: true,
225204
},
226205
},
227206
options,

packages/docusaurus-plugin-sitemap/src/createSitemap.ts

+12-9
Original file line numberDiff line numberDiff line change
@@ -10,32 +10,35 @@ import {sitemapItemsToXmlString} from './xml';
1010
import {createSitemapItem} from './createSitemapItem';
1111
import {isNoIndexMetaRoute} from './head';
1212
import type {CreateSitemapItemsFn, CreateSitemapItemsParams} from './types';
13-
import type {RouteConfig} from '@docusaurus/types';
13+
import type {RouteConfig, RouteBuildMetadata} from '@docusaurus/types';
1414
import type {PluginOptions} from './options';
15-
import type {HelmetServerState} from 'react-helmet-async';
1615

1716
// Not all routes should appear in the sitemap, and we should filter:
1817
// - parent routes, used for layouts
1918
// - routes matching options.ignorePatterns
2019
// - routes with no index metadata
21-
function getSitemapRoutes({routes, head, options}: CreateSitemapParams) {
20+
function getSitemapRoutes({
21+
routes,
22+
routesBuildMetadata,
23+
options,
24+
}: CreateSitemapParams) {
2225
const {ignorePatterns} = options;
2326

2427
const ignoreMatcher = createMatcher(ignorePatterns);
2528

2629
function isRouteExcluded(route: RouteConfig) {
2730
return (
28-
ignoreMatcher(route.path) || isNoIndexMetaRoute({head, route: route.path})
31+
ignoreMatcher(route.path) ||
32+
isNoIndexMetaRoute({routesBuildMetadata, route: route.path})
2933
);
3034
}
3135

3236
return flattenRoutes(routes).filter((route) => !isRouteExcluded(route));
3337
}
3438

3539
// Our default implementation receives some additional parameters on purpose
36-
// Params such as "head" are "messy" and not directly exposed to the user
3740
function createDefaultCreateSitemapItems(
38-
internalParams: Pick<CreateSitemapParams, 'head' | 'options'>,
41+
internalParams: Pick<CreateSitemapParams, 'routesBuildMetadata' | 'options'>,
3942
): CreateSitemapItemsFn {
4043
return async (params) => {
4144
const sitemapRoutes = getSitemapRoutes({...params, ...internalParams});
@@ -55,17 +58,17 @@ function createDefaultCreateSitemapItems(
5558
}
5659

5760
type CreateSitemapParams = CreateSitemapItemsParams & {
58-
head: {[location: string]: HelmetServerState};
61+
routesBuildMetadata: {[location: string]: RouteBuildMetadata};
5962
options: PluginOptions;
6063
};
6164

6265
export default async function createSitemap(
6366
params: CreateSitemapParams,
6467
): Promise<string | null> {
65-
const {head, options, routes, siteConfig} = params;
68+
const {routesBuildMetadata, options, routes, siteConfig} = params;
6669

6770
const defaultCreateSitemapItems: CreateSitemapItemsFn =
68-
createDefaultCreateSitemapItems({head, options});
71+
createDefaultCreateSitemapItems({routesBuildMetadata, options});
6972

7073
const sitemapItems = params.options.createSitemapItems
7174
? await params.options.createSitemapItems({

packages/docusaurus-plugin-sitemap/src/head.ts

+8-30
Original file line numberDiff line numberDiff line change
@@ -5,43 +5,21 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8-
import type {ReactElement} from 'react';
9-
import type {HelmetServerState} from 'react-helmet-async';
8+
import type {RouteBuildMetadata} from '@docusaurus/types';
109

1110
// Maybe we want to add a routeConfig.metadata.noIndex instead?
1211
// But using Helmet is more reliable for third-party plugins...
1312
export function isNoIndexMetaRoute({
14-
head,
13+
routesBuildMetadata,
1514
route,
1615
}: {
17-
head: {[location: string]: HelmetServerState};
16+
routesBuildMetadata: {[location: string]: RouteBuildMetadata};
1817
route: string;
1918
}): boolean {
20-
const isNoIndexMetaTag = ({
21-
name,
22-
content,
23-
}: {
24-
name?: string;
25-
content?: string;
26-
}): boolean => {
27-
if (!name || !content) {
28-
return false;
29-
}
30-
return (
31-
// meta name is not case-sensitive
32-
name.toLowerCase() === 'robots' &&
33-
// Robots directives are not case-sensitive
34-
content.toLowerCase().includes('noindex')
35-
);
36-
};
19+
const routeBuildMetadata = routesBuildMetadata[route];
3720

38-
// https://github.com/staylor/react-helmet-async/pull/167
39-
const meta = head[route]?.meta.toComponent() as unknown as
40-
| ReactElement<{name?: string; content?: string}>[]
41-
| undefined;
42-
return (
43-
meta?.some((tag) =>
44-
isNoIndexMetaTag({name: tag.props.name, content: tag.props.content}),
45-
) ?? false
46-
);
21+
if (routeBuildMetadata) {
22+
return routeBuildMetadata.noIndex;
23+
}
24+
return false;
4725
}

packages/docusaurus-plugin-sitemap/src/index.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,15 @@ export default function pluginSitemap(
2828
return {
2929
name: PluginName,
3030

31-
async postBuild({siteConfig, routes, outDir, head}) {
31+
async postBuild({siteConfig, routes, outDir, routesBuildMetadata}) {
3232
if (siteConfig.noIndex) {
3333
return;
3434
}
3535
// Generate sitemap.
3636
const generatedSitemap = await createSitemap({
3737
siteConfig,
3838
routes,
39-
head,
39+
routesBuildMetadata,
4040
options,
4141
});
4242
if (!generatedSitemap) {

packages/docusaurus-types/src/config.d.ts

+10
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,16 @@ export type FasterConfig = {
132132
rspackBundler: boolean;
133133
};
134134

135+
export type FutureV4Config = {
136+
removeLegacyPostBuildHeadAttribute: boolean;
137+
};
138+
135139
export type FutureConfig = {
140+
/**
141+
* Turns v4 future flags on
142+
*/
143+
v4: FutureV4Config;
144+
136145
experimental_faster: FasterConfig;
137146

138147
experimental_storage: StorageConfig;
@@ -451,6 +460,7 @@ export type Config = Overwrite<
451460
future?: Overwrite<
452461
DeepPartial<FutureConfig>,
453462
{
463+
v4?: boolean | FutureV4Config;
454464
experimental_faster?: boolean | FasterConfig;
455465
}
456466
>;

packages/docusaurus-types/src/index.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export {
6767
Validate,
6868
ValidationSchema,
6969
AllContent,
70+
RouteBuildMetadata,
7071
ConfigureWebpackUtils,
7172
PostCssOptions,
7273
HtmlTagObject,

packages/docusaurus-types/src/plugin.d.ts

+10
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,12 @@ export type ConfigureWebpackResult = WebpackConfiguration & {
114114
};
115115
};
116116

117+
export type RouteBuildMetadata = {
118+
// We'll add extra metadata on a case by case basis here
119+
// For now the only need is our sitemap plugin to filter noindex pages
120+
noIndex: boolean;
121+
};
122+
117123
export type Plugin<Content = unknown> = {
118124
name: string;
119125
loadContent?: () => Promise<Content> | Content;
@@ -129,7 +135,11 @@ export type Plugin<Content = unknown> = {
129135
postBuild?: (
130136
props: Props & {
131137
content: Content;
138+
// TODO Docusaurus v4: remove old messy unserializable "head" API
139+
// breaking change, replaced by routesBuildMetadata
140+
// Reason: https://github.com/facebook/docusaurus/pull/10826
132141
head: {[location: string]: HelmetServerState};
142+
routesBuildMetadata: {[location: string]: RouteBuildMetadata};
133143
},
134144
) => Promise<void> | void;
135145
// TODO Docusaurus v4 ?

packages/docusaurus/src/client/serverEntry.tsx

+16-5
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,13 @@ import {
1616
createStatefulBrokenLinks,
1717
BrokenLinksProvider,
1818
} from './BrokenLinksContext';
19+
import {toPageCollectedMetadata} from './serverHelmetUtils';
1920
import type {PageCollectedData, AppRenderer} from '../common';
2021

21-
const render: AppRenderer['render'] = async ({pathname}) => {
22+
const render: AppRenderer['render'] = async ({
23+
pathname,
24+
v4RemoveLegacyPostBuildHeadAttribute,
25+
}) => {
2226
await preload(pathname);
2327

2428
const modules = new Set<string>();
@@ -41,11 +45,18 @@ const render: AppRenderer['render'] = async ({pathname}) => {
4145

4246
const html = await renderToHtml(app);
4347

44-
const collectedData: PageCollectedData = {
45-
// TODO Docusaurus v4 refactor: helmet state is non-serializable
46-
// this makes it impossible to run SSG in a worker thread
47-
helmet: (helmetContext as FilledContext).helmet,
48+
const {helmet} = helmetContext as FilledContext;
49+
50+
const metadata = toPageCollectedMetadata({helmet});
4851

52+
// TODO Docusaurus v4 remove with deprecated postBuild({head}) API
53+
// the returned collectedData must be serializable to run in workers
54+
if (v4RemoveLegacyPostBuildHeadAttribute) {
55+
metadata.helmet = null;
56+
}
57+
58+
const collectedData: PageCollectedData = {
59+
metadata,
4960
anchors: statefulBrokenLinks.getCollectedAnchors(),
5061
links: statefulBrokenLinks.getCollectedLinks(),
5162
modules: Array.from(modules),

0 commit comments

Comments
 (0)