mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-24 06:27:02 +02:00
feat(core): new postBuild({routesBuildMetadata})
API, deprecate head
attribute + v4 future flag (#10850)
Co-authored-by: slorber <749374+slorber@users.noreply.github.com>
This commit is contained in:
parent
67207bc5e5
commit
9df5aae6de
21 changed files with 403 additions and 94 deletions
|
@ -5,7 +5,6 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {createElement} from 'react';
|
||||
import {fromPartial} from '@total-typescript/shoehorn';
|
||||
import createSitemap from '../createSitemap';
|
||||
import type {PluginOptions} from '../options';
|
||||
|
@ -39,7 +38,7 @@ describe('createSitemap', () => {
|
|||
const sitemap = await createSitemap({
|
||||
siteConfig,
|
||||
routes: routes(['/', '/test']),
|
||||
head: {},
|
||||
routesBuildMetadata: {},
|
||||
options,
|
||||
});
|
||||
expect(sitemap).toContain(
|
||||
|
@ -51,7 +50,7 @@ describe('createSitemap', () => {
|
|||
const sitemap = await createSitemap({
|
||||
siteConfig,
|
||||
routes: routes([]),
|
||||
head: {},
|
||||
routesBuildMetadata: {},
|
||||
options,
|
||||
});
|
||||
expect(sitemap).toBeNull();
|
||||
|
@ -67,7 +66,7 @@ describe('createSitemap', () => {
|
|||
'/search/foo',
|
||||
'/tags/foo/bar',
|
||||
]),
|
||||
head: {},
|
||||
routesBuildMetadata: {},
|
||||
options: {
|
||||
...options,
|
||||
ignorePatterns: [
|
||||
|
@ -94,7 +93,7 @@ describe('createSitemap', () => {
|
|||
'/search/foo',
|
||||
'/tags/foo/bar',
|
||||
]),
|
||||
head: {},
|
||||
routesBuildMetadata: {},
|
||||
options: {
|
||||
...options,
|
||||
createSitemapItems: async (params) => {
|
||||
|
@ -119,7 +118,7 @@ describe('createSitemap', () => {
|
|||
const sitemap = await createSitemap({
|
||||
siteConfig,
|
||||
routes: routes(['/', '/docs/myDoc/', '/blog/post']),
|
||||
head: {},
|
||||
routesBuildMetadata: {},
|
||||
options: {
|
||||
...options,
|
||||
createSitemapItems: async () => {
|
||||
|
@ -135,7 +134,7 @@ describe('createSitemap', () => {
|
|||
const sitemap = await createSitemap({
|
||||
siteConfig,
|
||||
routes: routes(['/', '/test', '/nested/test', '/nested/test2/']),
|
||||
head: {},
|
||||
routesBuildMetadata: {},
|
||||
options,
|
||||
});
|
||||
|
||||
|
@ -149,7 +148,7 @@ describe('createSitemap', () => {
|
|||
const sitemap = await createSitemap({
|
||||
siteConfig: {...siteConfig, trailingSlash: true},
|
||||
routes: routes(['/', '/test', '/nested/test', '/nested/test2/']),
|
||||
head: {},
|
||||
routesBuildMetadata: {},
|
||||
options,
|
||||
});
|
||||
|
||||
|
@ -167,7 +166,7 @@ describe('createSitemap', () => {
|
|||
trailingSlash: false,
|
||||
},
|
||||
routes: routes(['/', '/test', '/nested/test', '/nested/test2/']),
|
||||
head: {},
|
||||
routesBuildMetadata: {},
|
||||
options,
|
||||
});
|
||||
|
||||
|
@ -180,19 +179,10 @@ describe('createSitemap', () => {
|
|||
it('filters pages with noindex', async () => {
|
||||
const sitemap = await createSitemap({
|
||||
siteConfig,
|
||||
routesPaths: ['/', '/noindex', '/nested/test', '/nested/test2/'],
|
||||
routes: routes(['/', '/noindex', '/nested/test', '/nested/test2/']),
|
||||
head: {
|
||||
routesBuildMetadata: {
|
||||
'/noindex': {
|
||||
meta: {
|
||||
// @ts-expect-error: bad lib def
|
||||
toComponent: () => [
|
||||
createElement('meta', {
|
||||
name: 'robots',
|
||||
content: 'NoFolloW, NoiNDeX',
|
||||
}),
|
||||
],
|
||||
},
|
||||
noIndex: true,
|
||||
},
|
||||
},
|
||||
options,
|
||||
|
@ -204,24 +194,13 @@ describe('createSitemap', () => {
|
|||
it('does not generate anything for all pages with noindex', async () => {
|
||||
const sitemap = await createSitemap({
|
||||
siteConfig,
|
||||
routesPaths: ['/', '/noindex'],
|
||||
routes: routes(['/', '/noindex']),
|
||||
head: {
|
||||
routesBuildMetadata: {
|
||||
'/': {
|
||||
meta: {
|
||||
// @ts-expect-error: bad lib def
|
||||
toComponent: () => [
|
||||
createElement('meta', {name: 'robots', content: 'noindex'}),
|
||||
],
|
||||
},
|
||||
noIndex: true,
|
||||
},
|
||||
'/noindex': {
|
||||
meta: {
|
||||
// @ts-expect-error: bad lib def
|
||||
toComponent: () => [
|
||||
createElement('meta', {name: 'robots', content: 'noindex'}),
|
||||
],
|
||||
},
|
||||
noIndex: true,
|
||||
},
|
||||
},
|
||||
options,
|
||||
|
|
|
@ -10,22 +10,26 @@ import {sitemapItemsToXmlString} from './xml';
|
|||
import {createSitemapItem} from './createSitemapItem';
|
||||
import {isNoIndexMetaRoute} from './head';
|
||||
import type {CreateSitemapItemsFn, CreateSitemapItemsParams} from './types';
|
||||
import type {RouteConfig} from '@docusaurus/types';
|
||||
import type {RouteConfig, RouteBuildMetadata} from '@docusaurus/types';
|
||||
import type {PluginOptions} from './options';
|
||||
import type {HelmetServerState} from 'react-helmet-async';
|
||||
|
||||
// Not all routes should appear in the sitemap, and we should filter:
|
||||
// - parent routes, used for layouts
|
||||
// - routes matching options.ignorePatterns
|
||||
// - routes with no index metadata
|
||||
function getSitemapRoutes({routes, head, options}: CreateSitemapParams) {
|
||||
function getSitemapRoutes({
|
||||
routes,
|
||||
routesBuildMetadata,
|
||||
options,
|
||||
}: CreateSitemapParams) {
|
||||
const {ignorePatterns} = options;
|
||||
|
||||
const ignoreMatcher = createMatcher(ignorePatterns);
|
||||
|
||||
function isRouteExcluded(route: RouteConfig) {
|
||||
return (
|
||||
ignoreMatcher(route.path) || isNoIndexMetaRoute({head, route: route.path})
|
||||
ignoreMatcher(route.path) ||
|
||||
isNoIndexMetaRoute({routesBuildMetadata, route: route.path})
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -33,9 +37,8 @@ function getSitemapRoutes({routes, head, options}: CreateSitemapParams) {
|
|||
}
|
||||
|
||||
// Our default implementation receives some additional parameters on purpose
|
||||
// Params such as "head" are "messy" and not directly exposed to the user
|
||||
function createDefaultCreateSitemapItems(
|
||||
internalParams: Pick<CreateSitemapParams, 'head' | 'options'>,
|
||||
internalParams: Pick<CreateSitemapParams, 'routesBuildMetadata' | 'options'>,
|
||||
): CreateSitemapItemsFn {
|
||||
return async (params) => {
|
||||
const sitemapRoutes = getSitemapRoutes({...params, ...internalParams});
|
||||
|
@ -55,17 +58,17 @@ function createDefaultCreateSitemapItems(
|
|||
}
|
||||
|
||||
type CreateSitemapParams = CreateSitemapItemsParams & {
|
||||
head: {[location: string]: HelmetServerState};
|
||||
routesBuildMetadata: {[location: string]: RouteBuildMetadata};
|
||||
options: PluginOptions;
|
||||
};
|
||||
|
||||
export default async function createSitemap(
|
||||
params: CreateSitemapParams,
|
||||
): Promise<string | null> {
|
||||
const {head, options, routes, siteConfig} = params;
|
||||
const {routesBuildMetadata, options, routes, siteConfig} = params;
|
||||
|
||||
const defaultCreateSitemapItems: CreateSitemapItemsFn =
|
||||
createDefaultCreateSitemapItems({head, options});
|
||||
createDefaultCreateSitemapItems({routesBuildMetadata, options});
|
||||
|
||||
const sitemapItems = params.options.createSitemapItems
|
||||
? await params.options.createSitemapItems({
|
||||
|
|
|
@ -5,43 +5,21 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import type {ReactElement} from 'react';
|
||||
import type {HelmetServerState} from 'react-helmet-async';
|
||||
import type {RouteBuildMetadata} from '@docusaurus/types';
|
||||
|
||||
// Maybe we want to add a routeConfig.metadata.noIndex instead?
|
||||
// But using Helmet is more reliable for third-party plugins...
|
||||
export function isNoIndexMetaRoute({
|
||||
head,
|
||||
routesBuildMetadata,
|
||||
route,
|
||||
}: {
|
||||
head: {[location: string]: HelmetServerState};
|
||||
routesBuildMetadata: {[location: string]: RouteBuildMetadata};
|
||||
route: string;
|
||||
}): boolean {
|
||||
const isNoIndexMetaTag = ({
|
||||
name,
|
||||
content,
|
||||
}: {
|
||||
name?: string;
|
||||
content?: string;
|
||||
}): boolean => {
|
||||
if (!name || !content) {
|
||||
const routeBuildMetadata = routesBuildMetadata[route];
|
||||
|
||||
if (routeBuildMetadata) {
|
||||
return routeBuildMetadata.noIndex;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
// meta name is not case-sensitive
|
||||
name.toLowerCase() === 'robots' &&
|
||||
// Robots directives are not case-sensitive
|
||||
content.toLowerCase().includes('noindex')
|
||||
);
|
||||
};
|
||||
|
||||
// https://github.com/staylor/react-helmet-async/pull/167
|
||||
const meta = head[route]?.meta.toComponent() as unknown as
|
||||
| ReactElement<{name?: string; content?: string}>[]
|
||||
| undefined;
|
||||
return (
|
||||
meta?.some((tag) =>
|
||||
isNoIndexMetaTag({name: tag.props.name, content: tag.props.content}),
|
||||
) ?? false
|
||||
);
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ export default function pluginSitemap(
|
|||
return {
|
||||
name: PluginName,
|
||||
|
||||
async postBuild({siteConfig, routes, outDir, head}) {
|
||||
async postBuild({siteConfig, routes, outDir, routesBuildMetadata}) {
|
||||
if (siteConfig.noIndex) {
|
||||
return;
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ export default function pluginSitemap(
|
|||
const generatedSitemap = await createSitemap({
|
||||
siteConfig,
|
||||
routes,
|
||||
head,
|
||||
routesBuildMetadata,
|
||||
options,
|
||||
});
|
||||
if (!generatedSitemap) {
|
||||
|
|
10
packages/docusaurus-types/src/config.d.ts
vendored
10
packages/docusaurus-types/src/config.d.ts
vendored
|
@ -132,7 +132,16 @@ export type FasterConfig = {
|
|||
rspackBundler: boolean;
|
||||
};
|
||||
|
||||
export type FutureV4Config = {
|
||||
removeLegacyPostBuildHeadAttribute: boolean;
|
||||
};
|
||||
|
||||
export type FutureConfig = {
|
||||
/**
|
||||
* Turns v4 future flags on
|
||||
*/
|
||||
v4: FutureV4Config;
|
||||
|
||||
experimental_faster: FasterConfig;
|
||||
|
||||
experimental_storage: StorageConfig;
|
||||
|
@ -451,6 +460,7 @@ export type Config = Overwrite<
|
|||
future?: Overwrite<
|
||||
DeepPartial<FutureConfig>,
|
||||
{
|
||||
v4?: boolean | FutureV4Config;
|
||||
experimental_faster?: boolean | FasterConfig;
|
||||
}
|
||||
>;
|
||||
|
|
1
packages/docusaurus-types/src/index.d.ts
vendored
1
packages/docusaurus-types/src/index.d.ts
vendored
|
@ -67,6 +67,7 @@ export {
|
|||
Validate,
|
||||
ValidationSchema,
|
||||
AllContent,
|
||||
RouteBuildMetadata,
|
||||
ConfigureWebpackUtils,
|
||||
PostCssOptions,
|
||||
HtmlTagObject,
|
||||
|
|
10
packages/docusaurus-types/src/plugin.d.ts
vendored
10
packages/docusaurus-types/src/plugin.d.ts
vendored
|
@ -114,6 +114,12 @@ export type ConfigureWebpackResult = WebpackConfiguration & {
|
|||
};
|
||||
};
|
||||
|
||||
export type RouteBuildMetadata = {
|
||||
// We'll add extra metadata on a case by case basis here
|
||||
// For now the only need is our sitemap plugin to filter noindex pages
|
||||
noIndex: boolean;
|
||||
};
|
||||
|
||||
export type Plugin<Content = unknown> = {
|
||||
name: string;
|
||||
loadContent?: () => Promise<Content> | Content;
|
||||
|
@ -129,7 +135,11 @@ export type Plugin<Content = unknown> = {
|
|||
postBuild?: (
|
||||
props: Props & {
|
||||
content: Content;
|
||||
// TODO Docusaurus v4: remove old messy unserializable "head" API
|
||||
// breaking change, replaced by routesBuildMetadata
|
||||
// Reason: https://github.com/facebook/docusaurus/pull/10826
|
||||
head: {[location: string]: HelmetServerState};
|
||||
routesBuildMetadata: {[location: string]: RouteBuildMetadata};
|
||||
},
|
||||
) => Promise<void> | void;
|
||||
// TODO Docusaurus v4 ?
|
||||
|
|
|
@ -16,9 +16,13 @@ import {
|
|||
createStatefulBrokenLinks,
|
||||
BrokenLinksProvider,
|
||||
} from './BrokenLinksContext';
|
||||
import {toPageCollectedMetadata} from './serverHelmetUtils';
|
||||
import type {PageCollectedData, AppRenderer} from '../common';
|
||||
|
||||
const render: AppRenderer['render'] = async ({pathname}) => {
|
||||
const render: AppRenderer['render'] = async ({
|
||||
pathname,
|
||||
v4RemoveLegacyPostBuildHeadAttribute,
|
||||
}) => {
|
||||
await preload(pathname);
|
||||
|
||||
const modules = new Set<string>();
|
||||
|
@ -41,11 +45,18 @@ const render: AppRenderer['render'] = async ({pathname}) => {
|
|||
|
||||
const html = await renderToHtml(app);
|
||||
|
||||
const collectedData: PageCollectedData = {
|
||||
// TODO Docusaurus v4 refactor: helmet state is non-serializable
|
||||
// this makes it impossible to run SSG in a worker thread
|
||||
helmet: (helmetContext as FilledContext).helmet,
|
||||
const {helmet} = helmetContext as FilledContext;
|
||||
|
||||
const metadata = toPageCollectedMetadata({helmet});
|
||||
|
||||
// TODO Docusaurus v4 remove with deprecated postBuild({head}) API
|
||||
// the returned collectedData must be serializable to run in workers
|
||||
if (v4RemoveLegacyPostBuildHeadAttribute) {
|
||||
metadata.helmet = null;
|
||||
}
|
||||
|
||||
const collectedData: PageCollectedData = {
|
||||
metadata,
|
||||
anchors: statefulBrokenLinks.getCollectedAnchors(),
|
||||
links: statefulBrokenLinks.getCollectedLinks(),
|
||||
modules: Array.from(modules),
|
||||
|
|
55
packages/docusaurus/src/client/serverHelmetUtils.tsx
Normal file
55
packages/docusaurus/src/client/serverHelmetUtils.tsx
Normal file
|
@ -0,0 +1,55 @@
|
|||
/**
|
||||
* 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 type {ReactElement} from 'react';
|
||||
import type {PageCollectedMetadata} from '../common';
|
||||
import type {HelmetServerState} from 'react-helmet-async';
|
||||
|
||||
type BuildMetaTag = {name?: string; content?: string};
|
||||
|
||||
function getBuildMetaTags(helmet: HelmetServerState): BuildMetaTag[] {
|
||||
// @ts-expect-error: see https://github.com/staylor/react-helmet-async/pull/167
|
||||
const metaElements: ReactElement<BuildMetaTag>[] =
|
||||
helmet.meta.toComponent() ?? [];
|
||||
return metaElements.map((el) => el.props);
|
||||
}
|
||||
|
||||
function isNoIndexTag(tag: BuildMetaTag): boolean {
|
||||
if (!tag.name || !tag.content) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
// meta name is not case-sensitive
|
||||
tag.name.toLowerCase() === 'robots' &&
|
||||
// Robots directives are not case-sensitive
|
||||
tag.content.toLowerCase().includes('noindex')
|
||||
);
|
||||
}
|
||||
|
||||
export function toPageCollectedMetadata({
|
||||
helmet,
|
||||
}: {
|
||||
helmet: HelmetServerState;
|
||||
}): PageCollectedMetadata {
|
||||
const tags = getBuildMetaTags(helmet);
|
||||
const noIndex = tags.some(isNoIndexTag);
|
||||
|
||||
return {
|
||||
helmet, // TODO Docusaurus v4 remove
|
||||
public: {
|
||||
noIndex,
|
||||
},
|
||||
internal: {
|
||||
htmlAttributes: helmet.htmlAttributes.toString(),
|
||||
bodyAttributes: helmet.bodyAttributes.toString(),
|
||||
title: helmet.title.toString(),
|
||||
meta: helmet.meta.toString(),
|
||||
link: helmet.link.toString(),
|
||||
script: helmet.script.toString(),
|
||||
},
|
||||
};
|
||||
}
|
|
@ -126,7 +126,15 @@ async function executePluginsPostBuild({
|
|||
props: Props;
|
||||
collectedData: SiteCollectedData;
|
||||
}) {
|
||||
const head = _.mapValues(collectedData, (d) => d.helmet);
|
||||
const head = props.siteConfig.future.v4.removeLegacyPostBuildHeadAttribute
|
||||
? {}
|
||||
: _.mapValues(collectedData, (d) => d.metadata.helmet!);
|
||||
|
||||
const routesBuildMetadata = _.mapValues(
|
||||
collectedData,
|
||||
(d) => d.metadata.public,
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
plugins.map(async (plugin) => {
|
||||
if (!plugin.postBuild) {
|
||||
|
@ -135,6 +143,7 @@ async function executePluginsPostBuild({
|
|||
await plugin.postBuild({
|
||||
...props,
|
||||
head,
|
||||
routesBuildMetadata,
|
||||
content: plugin.content,
|
||||
});
|
||||
}),
|
||||
|
|
34
packages/docusaurus/src/common.d.ts
vendored
34
packages/docusaurus/src/common.d.ts
vendored
|
@ -9,6 +9,7 @@
|
|||
// In particular the interface between SSG and serverEntry code
|
||||
|
||||
import type {HelmetServerState} from 'react-helmet-async';
|
||||
import type {RouteBuildMetadata} from '@docusaurus/types';
|
||||
|
||||
export type AppRenderResult = {
|
||||
html: string;
|
||||
|
@ -16,18 +17,41 @@ export type AppRenderResult = {
|
|||
};
|
||||
|
||||
export type AppRenderer = {
|
||||
render: (params: {pathname: string}) => Promise<AppRenderResult>;
|
||||
render: (params: {
|
||||
pathname: string;
|
||||
|
||||
// TODO Docusaurus v4: remove deprecated postBuild({head}) API
|
||||
v4RemoveLegacyPostBuildHeadAttribute: boolean;
|
||||
}) => Promise<AppRenderResult>;
|
||||
|
||||
// It's important to shut down the app renderer
|
||||
// Otherwise Node.js require cache leaks memory
|
||||
shutdown: () => Promise<void>;
|
||||
};
|
||||
|
||||
export type PageCollectedData = {
|
||||
// TODO Docusaurus v4 refactor: helmet state is non-serializable
|
||||
// this makes it impossible to run SSG in a worker thread
|
||||
helmet: HelmetServerState;
|
||||
// Attributes we need internally, for the SSG html template
|
||||
// They are not exposed to the user in postBuild({routesBuildMetadata})
|
||||
export type RouteBuildMetadataInternal = {
|
||||
htmlAttributes: string;
|
||||
bodyAttributes: string;
|
||||
title: string;
|
||||
meta: string;
|
||||
link: string;
|
||||
script: string;
|
||||
};
|
||||
|
||||
// This data structure must remain serializable!
|
||||
// See why: https://github.com/facebook/docusaurus/pull/10826
|
||||
export type PageCollectedMetadata = {
|
||||
public: RouteBuildMetadata;
|
||||
internal: RouteBuildMetadataInternal;
|
||||
// TODO Docusaurus v4 remove legacy unserializable helmet data structure
|
||||
// See https://github.com/facebook/docusaurus/pull/10850
|
||||
helmet: HelmetServerState | null;
|
||||
};
|
||||
|
||||
export type PageCollectedData = {
|
||||
metadata: PageCollectedMetadata;
|
||||
links: string[];
|
||||
anchors: string[];
|
||||
modules: string[];
|
||||
|
|
|
@ -21,6 +21,9 @@ exports[`loadSiteConfig website with .cjs siteConfig 1`] = `
|
|||
"namespace": false,
|
||||
"type": "localStorage",
|
||||
},
|
||||
"v4": {
|
||||
"removeLegacyPostBuildHeadAttribute": false,
|
||||
},
|
||||
},
|
||||
"headTags": [],
|
||||
"i18n": {
|
||||
|
@ -90,6 +93,9 @@ exports[`loadSiteConfig website with ts + js config 1`] = `
|
|||
"namespace": false,
|
||||
"type": "localStorage",
|
||||
},
|
||||
"v4": {
|
||||
"removeLegacyPostBuildHeadAttribute": false,
|
||||
},
|
||||
},
|
||||
"headTags": [],
|
||||
"i18n": {
|
||||
|
@ -159,6 +165,9 @@ exports[`loadSiteConfig website with valid JS CJS config 1`] = `
|
|||
"namespace": false,
|
||||
"type": "localStorage",
|
||||
},
|
||||
"v4": {
|
||||
"removeLegacyPostBuildHeadAttribute": false,
|
||||
},
|
||||
},
|
||||
"headTags": [],
|
||||
"i18n": {
|
||||
|
@ -228,6 +237,9 @@ exports[`loadSiteConfig website with valid JS ESM config 1`] = `
|
|||
"namespace": false,
|
||||
"type": "localStorage",
|
||||
},
|
||||
"v4": {
|
||||
"removeLegacyPostBuildHeadAttribute": false,
|
||||
},
|
||||
},
|
||||
"headTags": [],
|
||||
"i18n": {
|
||||
|
@ -297,6 +309,9 @@ exports[`loadSiteConfig website with valid TypeScript CJS config 1`] = `
|
|||
"namespace": false,
|
||||
"type": "localStorage",
|
||||
},
|
||||
"v4": {
|
||||
"removeLegacyPostBuildHeadAttribute": false,
|
||||
},
|
||||
},
|
||||
"headTags": [],
|
||||
"i18n": {
|
||||
|
@ -366,6 +381,9 @@ exports[`loadSiteConfig website with valid TypeScript ESM config 1`] = `
|
|||
"namespace": false,
|
||||
"type": "localStorage",
|
||||
},
|
||||
"v4": {
|
||||
"removeLegacyPostBuildHeadAttribute": false,
|
||||
},
|
||||
},
|
||||
"headTags": [],
|
||||
"i18n": {
|
||||
|
@ -435,6 +453,9 @@ exports[`loadSiteConfig website with valid async config 1`] = `
|
|||
"namespace": false,
|
||||
"type": "localStorage",
|
||||
},
|
||||
"v4": {
|
||||
"removeLegacyPostBuildHeadAttribute": false,
|
||||
},
|
||||
},
|
||||
"headTags": [],
|
||||
"i18n": {
|
||||
|
@ -506,6 +527,9 @@ exports[`loadSiteConfig website with valid async config creator function 1`] = `
|
|||
"namespace": false,
|
||||
"type": "localStorage",
|
||||
},
|
||||
"v4": {
|
||||
"removeLegacyPostBuildHeadAttribute": false,
|
||||
},
|
||||
},
|
||||
"headTags": [],
|
||||
"i18n": {
|
||||
|
@ -577,6 +601,9 @@ exports[`loadSiteConfig website with valid config creator function 1`] = `
|
|||
"namespace": false,
|
||||
"type": "localStorage",
|
||||
},
|
||||
"v4": {
|
||||
"removeLegacyPostBuildHeadAttribute": false,
|
||||
},
|
||||
},
|
||||
"headTags": [],
|
||||
"i18n": {
|
||||
|
@ -651,6 +678,9 @@ exports[`loadSiteConfig website with valid siteConfig 1`] = `
|
|||
"namespace": false,
|
||||
"type": "localStorage",
|
||||
},
|
||||
"v4": {
|
||||
"removeLegacyPostBuildHeadAttribute": false,
|
||||
},
|
||||
},
|
||||
"headTags": [],
|
||||
"i18n": {
|
||||
|
|
|
@ -95,6 +95,9 @@ exports[`load loads props for site with custom i18n path 1`] = `
|
|||
"namespace": false,
|
||||
"type": "localStorage",
|
||||
},
|
||||
"v4": {
|
||||
"removeLegacyPostBuildHeadAttribute": false,
|
||||
},
|
||||
},
|
||||
"headTags": [],
|
||||
"i18n": {
|
||||
|
|
|
@ -11,12 +11,15 @@ import {
|
|||
DEFAULT_FASTER_CONFIG,
|
||||
DEFAULT_FASTER_CONFIG_TRUE,
|
||||
DEFAULT_FUTURE_CONFIG,
|
||||
DEFAULT_FUTURE_V4_CONFIG,
|
||||
DEFAULT_FUTURE_V4_CONFIG_TRUE,
|
||||
DEFAULT_STORAGE_CONFIG,
|
||||
validateConfig,
|
||||
} from '../configValidation';
|
||||
import type {
|
||||
FasterConfig,
|
||||
FutureConfig,
|
||||
FutureV4Config,
|
||||
StorageConfig,
|
||||
} from '@docusaurus/types/src/config';
|
||||
import type {Config, DocusaurusConfig, PluginConfig} from '@docusaurus/types';
|
||||
|
@ -45,6 +48,9 @@ describe('normalizeConfig', () => {
|
|||
...DEFAULT_CONFIG,
|
||||
...baseConfig,
|
||||
future: {
|
||||
v4: {
|
||||
removeLegacyPostBuildHeadAttribute: true,
|
||||
},
|
||||
experimental_faster: {
|
||||
swcJsLoader: true,
|
||||
swcJsMinimizer: true,
|
||||
|
@ -744,6 +750,9 @@ describe('future', () => {
|
|||
|
||||
it('accepts future - full', () => {
|
||||
const future: DocusaurusConfig['future'] = {
|
||||
v4: {
|
||||
removeLegacyPostBuildHeadAttribute: true,
|
||||
},
|
||||
experimental_faster: {
|
||||
swcJsLoader: true,
|
||||
swcJsMinimizer: true,
|
||||
|
@ -1571,4 +1580,149 @@ describe('future', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('v4', () => {
|
||||
function v4Containing(v4: Partial<FutureV4Config>) {
|
||||
return futureContaining({
|
||||
v4: expect.objectContaining(v4),
|
||||
});
|
||||
}
|
||||
|
||||
it('accepts v4 - undefined', () => {
|
||||
expect(
|
||||
normalizeConfig({
|
||||
future: {
|
||||
v4: undefined,
|
||||
},
|
||||
}),
|
||||
).toEqual(futureContaining(DEFAULT_FUTURE_CONFIG));
|
||||
});
|
||||
|
||||
it('accepts v4 - empty', () => {
|
||||
expect(
|
||||
normalizeConfig({
|
||||
future: {v4: {}},
|
||||
}),
|
||||
).toEqual(futureContaining(DEFAULT_FUTURE_CONFIG));
|
||||
});
|
||||
|
||||
it('accepts v4 - full', () => {
|
||||
const v4: FutureV4Config = {
|
||||
removeLegacyPostBuildHeadAttribute: true,
|
||||
};
|
||||
expect(
|
||||
normalizeConfig({
|
||||
future: {
|
||||
v4,
|
||||
},
|
||||
}),
|
||||
).toEqual(v4Containing(v4));
|
||||
});
|
||||
|
||||
it('accepts v4 - false', () => {
|
||||
expect(
|
||||
normalizeConfig({
|
||||
future: {v4: false},
|
||||
}),
|
||||
).toEqual(v4Containing(DEFAULT_FUTURE_V4_CONFIG));
|
||||
});
|
||||
|
||||
it('accepts v4 - true', () => {
|
||||
expect(
|
||||
normalizeConfig({
|
||||
future: {v4: true},
|
||||
}),
|
||||
).toEqual(v4Containing(DEFAULT_FUTURE_V4_CONFIG_TRUE));
|
||||
});
|
||||
|
||||
it('rejects v4 - number', () => {
|
||||
// @ts-expect-error: invalid
|
||||
const v4: Partial<FutureV4Config> = 42;
|
||||
expect(() =>
|
||||
normalizeConfig({
|
||||
future: {
|
||||
v4,
|
||||
},
|
||||
}),
|
||||
).toThrowErrorMatchingInlineSnapshot(`
|
||||
""future.v4" must be one of [object, boolean]
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
describe('removeLegacyPostBuildHeadAttribute', () => {
|
||||
it('accepts - undefined', () => {
|
||||
const v4: Partial<FutureV4Config> = {
|
||||
removeLegacyPostBuildHeadAttribute: undefined,
|
||||
};
|
||||
expect(
|
||||
normalizeConfig({
|
||||
future: {
|
||||
v4,
|
||||
},
|
||||
}),
|
||||
).toEqual(v4Containing({removeLegacyPostBuildHeadAttribute: false}));
|
||||
});
|
||||
|
||||
it('accepts - true', () => {
|
||||
const v4: Partial<FutureV4Config> = {
|
||||
removeLegacyPostBuildHeadAttribute: true,
|
||||
};
|
||||
expect(
|
||||
normalizeConfig({
|
||||
future: {
|
||||
v4,
|
||||
},
|
||||
}),
|
||||
).toEqual(v4Containing({removeLegacyPostBuildHeadAttribute: true}));
|
||||
});
|
||||
|
||||
it('accepts - false', () => {
|
||||
const v4: Partial<FutureV4Config> = {
|
||||
removeLegacyPostBuildHeadAttribute: false,
|
||||
};
|
||||
expect(
|
||||
normalizeConfig({
|
||||
future: {
|
||||
v4,
|
||||
},
|
||||
}),
|
||||
).toEqual(v4Containing({removeLegacyPostBuildHeadAttribute: false}));
|
||||
});
|
||||
|
||||
it('rejects - null', () => {
|
||||
const v4: Partial<FutureV4Config> = {
|
||||
// @ts-expect-error: invalid
|
||||
removeLegacyPostBuildHeadAttribute: 42,
|
||||
};
|
||||
expect(() =>
|
||||
normalizeConfig({
|
||||
future: {
|
||||
v4,
|
||||
},
|
||||
}),
|
||||
).toThrowErrorMatchingInlineSnapshot(`
|
||||
""future.v4.removeLegacyPostBuildHeadAttribute" must be a boolean
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('rejects - number', () => {
|
||||
const v4: Partial<FutureV4Config> = {
|
||||
// @ts-expect-error: invalid
|
||||
removeLegacyPostBuildHeadAttribute: 42,
|
||||
};
|
||||
expect(() =>
|
||||
normalizeConfig({
|
||||
future: {
|
||||
v4,
|
||||
},
|
||||
}),
|
||||
).toThrowErrorMatchingInlineSnapshot(`
|
||||
""future.v4.removeLegacyPostBuildHeadAttribute" must be a boolean
|
||||
"
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
import type {
|
||||
FasterConfig,
|
||||
FutureConfig,
|
||||
FutureV4Config,
|
||||
StorageConfig,
|
||||
} from '@docusaurus/types/src/config';
|
||||
import type {
|
||||
|
@ -60,7 +61,17 @@ export const DEFAULT_FASTER_CONFIG_TRUE: FasterConfig = {
|
|||
rspackBundler: true,
|
||||
};
|
||||
|
||||
export const DEFAULT_FUTURE_V4_CONFIG: FutureV4Config = {
|
||||
removeLegacyPostBuildHeadAttribute: false,
|
||||
};
|
||||
|
||||
// When using the "v4: true" shortcut
|
||||
export const DEFAULT_FUTURE_V4_CONFIG_TRUE: FutureV4Config = {
|
||||
removeLegacyPostBuildHeadAttribute: true,
|
||||
};
|
||||
|
||||
export const DEFAULT_FUTURE_CONFIG: FutureConfig = {
|
||||
v4: DEFAULT_FUTURE_V4_CONFIG,
|
||||
experimental_faster: DEFAULT_FASTER_CONFIG,
|
||||
experimental_storage: DEFAULT_STORAGE_CONFIG,
|
||||
experimental_router: 'browser',
|
||||
|
@ -242,6 +253,22 @@ const FASTER_CONFIG_SCHEMA = Joi.alternatives()
|
|||
.optional()
|
||||
.default(DEFAULT_FASTER_CONFIG);
|
||||
|
||||
const FUTURE_V4_SCHEMA = Joi.alternatives()
|
||||
.try(
|
||||
Joi.object<FutureV4Config>({
|
||||
removeLegacyPostBuildHeadAttribute: Joi.boolean().default(
|
||||
DEFAULT_FUTURE_V4_CONFIG.removeLegacyPostBuildHeadAttribute,
|
||||
),
|
||||
}),
|
||||
Joi.boolean()
|
||||
.required()
|
||||
.custom((bool) =>
|
||||
bool ? DEFAULT_FUTURE_V4_CONFIG_TRUE : DEFAULT_FUTURE_V4_CONFIG,
|
||||
),
|
||||
)
|
||||
.optional()
|
||||
.default(DEFAULT_FUTURE_V4_CONFIG);
|
||||
|
||||
const STORAGE_CONFIG_SCHEMA = Joi.object({
|
||||
type: Joi.string()
|
||||
.equal('localStorage', 'sessionStorage')
|
||||
|
@ -254,6 +281,7 @@ const STORAGE_CONFIG_SCHEMA = Joi.object({
|
|||
.default(DEFAULT_STORAGE_CONFIG);
|
||||
|
||||
const FUTURE_CONFIG_SCHEMA = Joi.object<FutureConfig>({
|
||||
v4: FUTURE_V4_SCHEMA,
|
||||
experimental_faster: FASTER_CONFIG_SCHEMA,
|
||||
experimental_storage: STORAGE_CONFIG_SCHEMA,
|
||||
experimental_router: Joi.string()
|
||||
|
|
|
@ -251,6 +251,8 @@ async function generateStaticFile({
|
|||
// This only renders the app HTML
|
||||
const result = await renderer.render({
|
||||
pathname,
|
||||
v4RemoveLegacyPostBuildHeadAttribute:
|
||||
params.v4RemoveLegacyPostBuildHeadAttribute,
|
||||
});
|
||||
// This renders the full page HTML, including head tags...
|
||||
const fullPageHtml = renderSSGTemplate({
|
||||
|
|
|
@ -30,6 +30,9 @@ export type SSGParams = {
|
|||
htmlMinifierType: HtmlMinifierType;
|
||||
serverBundlePath: string;
|
||||
ssgTemplateContent: string;
|
||||
|
||||
// TODO Docusaurus v4: remove deprecated postBuild({head}) API
|
||||
v4RemoveLegacyPostBuildHeadAttribute: boolean;
|
||||
};
|
||||
|
||||
export async function createSSGParams({
|
||||
|
@ -62,6 +65,9 @@ export async function createSSGParams({
|
|||
.swcHtmlMinimizer
|
||||
? 'swc'
|
||||
: 'terser',
|
||||
|
||||
v4RemoveLegacyPostBuildHeadAttribute:
|
||||
props.siteConfig.future.v4.removeLegacyPostBuildHeadAttribute,
|
||||
};
|
||||
|
||||
// Useless but ensures that SSG params remain serializable
|
||||
|
|
|
@ -83,18 +83,17 @@ export function renderSSGTemplate({
|
|||
} = params;
|
||||
const {
|
||||
html: appHtml,
|
||||
collectedData: {modules, helmet},
|
||||
collectedData: {modules, metadata},
|
||||
} = result;
|
||||
|
||||
const {scripts, stylesheets} = getScriptsAndStylesheets({manifest, modules});
|
||||
|
||||
const htmlAttributes = helmet.htmlAttributes.toString();
|
||||
const bodyAttributes = helmet.bodyAttributes.toString();
|
||||
const {htmlAttributes, bodyAttributes} = metadata.internal;
|
||||
const metaStrings = [
|
||||
helmet.title.toString(),
|
||||
helmet.meta.toString(),
|
||||
helmet.link.toString(),
|
||||
helmet.script.toString(),
|
||||
metadata.internal.title,
|
||||
metadata.internal.meta,
|
||||
metadata.internal.link,
|
||||
metadata.internal.script,
|
||||
];
|
||||
const metaAttributes = metaStrings.filter(Boolean);
|
||||
|
||||
|
|
|
@ -186,7 +186,7 @@ It is also a way to opt-in for upcoming breaking changes coming in the next majo
|
|||
|
||||
Features prefixed by `experimental_` or `unstable_` are subject to changes in **minor versions**, and not considered as [Semantic Versioning breaking changes](/community/release-process).
|
||||
|
||||
Features prefixed by `v<MajorVersion>_` (`v6_` `v7_`, etc.) are future flags that are expected to be turned on by default in the next major versions. These are less likely to change, but we keep the possibility to do so.
|
||||
Features namespaced by `v<MajorVersion>` (`v6` `v7`, etc.) are future flags that are expected to be turned on by default in the next major versions. These are less likely to change, but we keep the possibility to do so.
|
||||
|
||||
`future` API breaking changes should be easy to handle, and will be documented in minor/major version blog posts.
|
||||
|
||||
|
@ -197,6 +197,9 @@ Example:
|
|||
```js title="docusaurus.config.js"
|
||||
export default {
|
||||
future: {
|
||||
v4: {
|
||||
removeLegacyPostBuildHeadAttribute: true,
|
||||
},
|
||||
experimental_faster: {
|
||||
swcJsLoader: true,
|
||||
swcJsMinimizer: true,
|
||||
|
@ -214,6 +217,8 @@ export default {
|
|||
};
|
||||
```
|
||||
|
||||
- `v4`: Permits to opt-in for upcoming Docusaurus v4 breaking changes and features, to prepare your site in advance for this new version. Use `true` as a shorthand to enable all the flags.
|
||||
- [`removeLegacyPostBuildHeadAttribute`](https://github.com/facebook/docusaurus/pull/10435): Removes the legacy `plugin.postBuild({head})` API that prevents us from applying useful SSG optimizations ([explanations](https://github.com/facebook/docusaurus/pull/10850)).
|
||||
- `experimental_faster`: An object containing feature flags to make the Docusaurus build faster. This requires adding the `@docusaurus/faster` package to your site's dependencies. Use `true` as a shorthand to enable all flags. Read more on the [Docusaurus Faster](https://github.com/facebook/docusaurus/issues/10556) issue. Available feature flags:
|
||||
- [`swcJsLoader`](https://github.com/facebook/docusaurus/pull/10435): Use [SWC](https://swc.rs/) to transpile JS (instead of [Babel](https://babeljs.io/)).
|
||||
- [`swcJsMinimizer`](https://github.com/facebook/docusaurus/pull/10441): Use [SWC](https://swc.rs/) to minify JS (instead of [Terser](https://github.com/terser/terser)).
|
||||
|
|
|
@ -369,6 +369,7 @@ interface Props {
|
|||
preBodyTags: string;
|
||||
postBodyTags: string;
|
||||
routesPaths: string[];
|
||||
routesBuildMetadata: {[location: string]: {noIndex: boolean}};
|
||||
plugins: Plugin<any>[];
|
||||
content: Content;
|
||||
}
|
||||
|
|
|
@ -163,6 +163,7 @@ export default async function createConfigAsync() {
|
|||
baseUrlIssueBanner: true,
|
||||
url: 'https://docusaurus.io',
|
||||
future: {
|
||||
v4: !isSlower, // Not accurate, but good enough
|
||||
experimental_faster: !isSlower,
|
||||
experimental_storage: {
|
||||
namespace: true,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue