Skip to content

Commit

Permalink
Merge branch 'main' into slorber/poc-remove-helmet-nonserializable-data
Browse files Browse the repository at this point in the history
  • Loading branch information
slorber committed Jan 17, 2025
2 parents 1fd3e21 + 9df5aae commit 788d617
Show file tree
Hide file tree
Showing 21 changed files with 405 additions and 103 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -39,7 +38,7 @@ describe('createSitemap', () => {
const sitemap = await createSitemap({
siteConfig,
routes: routes(['/', '/test']),
head: {},
routesBuildMetadata: {},
options,
});
expect(sitemap).toContain(
Expand All @@ -51,7 +50,7 @@ describe('createSitemap', () => {
const sitemap = await createSitemap({
siteConfig,
routes: routes([]),
head: {},
routesBuildMetadata: {},
options,
});
expect(sitemap).toBeNull();
Expand All @@ -67,7 +66,7 @@ describe('createSitemap', () => {
'/search/foo',
'/tags/foo/bar',
]),
head: {},
routesBuildMetadata: {},
options: {
...options,
ignorePatterns: [
Expand All @@ -94,7 +93,7 @@ describe('createSitemap', () => {
'/search/foo',
'/tags/foo/bar',
]),
head: {},
routesBuildMetadata: {},
options: {
...options,
createSitemapItems: async (params) => {
Expand All @@ -119,7 +118,7 @@ describe('createSitemap', () => {
const sitemap = await createSitemap({
siteConfig,
routes: routes(['/', '/docs/myDoc/', '/blog/post']),
head: {},
routesBuildMetadata: {},
options: {
...options,
createSitemapItems: async () => {
Expand All @@ -135,7 +134,7 @@ describe('createSitemap', () => {
const sitemap = await createSitemap({
siteConfig,
routes: routes(['/', '/test', '/nested/test', '/nested/test2/']),
head: {},
routesBuildMetadata: {},
options,
});

Expand All @@ -149,7 +148,7 @@ describe('createSitemap', () => {
const sitemap = await createSitemap({
siteConfig: {...siteConfig, trailingSlash: true},
routes: routes(['/', '/test', '/nested/test', '/nested/test2/']),
head: {},
routesBuildMetadata: {},
options,
});

Expand All @@ -167,7 +166,7 @@ describe('createSitemap', () => {
trailingSlash: false,
},
routes: routes(['/', '/test', '/nested/test', '/nested/test2/']),
head: {},
routesBuildMetadata: {},
options,
});

Expand All @@ -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,
Expand All @@ -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,
Expand Down
21 changes: 12 additions & 9 deletions packages/docusaurus-plugin-sitemap/src/createSitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,35 @@ 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})
);
}

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

// 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});
Expand All @@ -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({
Expand Down
38 changes: 8 additions & 30 deletions packages/docusaurus-plugin-sitemap/src/head.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
return false;
}
return (
// meta name is not case-sensitive
name.toLowerCase() === 'robots' &&
// Robots directives are not case-sensitive
content.toLowerCase().includes('noindex')
);
};
const routeBuildMetadata = routesBuildMetadata[route];

// 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
);
if (routeBuildMetadata) {
return routeBuildMetadata.noIndex;
}
return false;
}
4 changes: 2 additions & 2 deletions packages/docusaurus-plugin-sitemap/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,15 @@ export default function pluginSitemap(
return {
name: PluginName,

async postBuild({siteConfig, routes, outDir, head}) {
async postBuild({siteConfig, routes, outDir, routesBuildMetadata}) {
if (siteConfig.noIndex) {
return;
}
// Generate sitemap.
const generatedSitemap = await createSitemap({
siteConfig,
routes,
head,
routesBuildMetadata,
options,
});
if (!generatedSitemap) {
Expand Down
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 @@ -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;
Expand Down Expand Up @@ -451,6 +460,7 @@ export type Config = Overwrite<
future?: Overwrite<
DeepPartial<FutureConfig>,
{
v4?: boolean | FutureV4Config;
experimental_faster?: boolean | FasterConfig;
}
>;
Expand Down
1 change: 1 addition & 0 deletions packages/docusaurus-types/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export {
Validate,
ValidationSchema,
AllContent,
RouteBuildMetadata,
ConfigureWebpackUtils,
PostCssOptions,
HtmlTagObject,
Expand Down
10 changes: 10 additions & 0 deletions packages/docusaurus-types/src/plugin.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 ?
Expand Down
25 changes: 17 additions & 8 deletions packages/docusaurus/src/client/serverEntry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import React from 'react';
import {StaticRouter} from 'react-router-dom';
import {HelmetProvider} from 'react-helmet-async';
import {HelmetProvider, type FilledContext} from 'react-helmet-async';
import Loadable from 'react-loadable';
import {renderToHtml} from './renderToHtml';
import preload from './preload';
Expand All @@ -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>();
Expand All @@ -41,13 +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,
// @ts-expect-error: temp disabled
helmet: undefined,
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),
Expand Down
Loading

0 comments on commit 788d617

Please sign in to comment.