diff --git a/packages/docusaurus-plugin-client-redirects/src/__tests__/__snapshots__/writeRedirectFiles.test.ts.snap b/packages/docusaurus-plugin-client-redirects/src/__tests__/__snapshots__/writeRedirectFiles.test.ts.snap index 1405eefda432..a344a553cc34 100644 --- a/packages/docusaurus-plugin-client-redirects/src/__tests__/__snapshots__/writeRedirectFiles.test.ts.snap +++ b/packages/docusaurus-plugin-client-redirects/src/__tests__/__snapshots__/writeRedirectFiles.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`toRedirectFilesMetadata creates appropriate metadata for empty baseUrl: fileContent baseUrl=empty 1`] = ` +exports[`toRedirectFiles creates appropriate metadata for empty baseUrl: fileContent baseUrl=empty 1`] = ` [ " @@ -16,7 +16,7 @@ exports[`toRedirectFilesMetadata creates appropriate metadata for empty baseUrl: ] `; -exports[`toRedirectFilesMetadata creates appropriate metadata for root baseUrl: fileContent baseUrl=/ 1`] = ` +exports[`toRedirectFiles creates appropriate metadata for root baseUrl: fileContent baseUrl=/ 1`] = ` [ " @@ -32,7 +32,7 @@ exports[`toRedirectFilesMetadata creates appropriate metadata for root baseUrl: ] `; -exports[`toRedirectFilesMetadata creates appropriate metadata trailingSlash=false: fileContent 1`] = ` +exports[`toRedirectFiles creates appropriate metadata trailingSlash=false: fileContent 1`] = ` [ " @@ -70,7 +70,7 @@ exports[`toRedirectFilesMetadata creates appropriate metadata trailingSlash=fals ] `; -exports[`toRedirectFilesMetadata creates appropriate metadata trailingSlash=true: fileContent 1`] = ` +exports[`toRedirectFiles creates appropriate metadata trailingSlash=true: fileContent 1`] = ` [ " @@ -108,7 +108,7 @@ exports[`toRedirectFilesMetadata creates appropriate metadata trailingSlash=true ] `; -exports[`toRedirectFilesMetadata creates appropriate metadata trailingSlash=undefined: fileContent 1`] = ` +exports[`toRedirectFiles creates appropriate metadata trailingSlash=undefined: fileContent 1`] = ` [ " diff --git a/packages/docusaurus-plugin-client-redirects/src/__tests__/writeRedirectFiles.test.ts b/packages/docusaurus-plugin-client-redirects/src/__tests__/writeRedirectFiles.test.ts index 32b992c08650..f6cf9476f92f 100644 --- a/packages/docusaurus-plugin-client-redirects/src/__tests__/writeRedirectFiles.test.ts +++ b/packages/docusaurus-plugin-client-redirects/src/__tests__/writeRedirectFiles.test.ts @@ -9,7 +9,7 @@ import fs from 'fs-extra'; import path from 'path'; import writeRedirectFiles, { - toRedirectFilesMetadata, + toRedirectFiles, createToUrl, } from '../writeRedirectFiles'; @@ -42,14 +42,14 @@ describe('createToUrl', () => { }); }); -describe('toRedirectFilesMetadata', () => { +describe('toRedirectFiles', () => { it('creates appropriate metadata trailingSlash=undefined', () => { const pluginContext = { outDir: '/tmp/someFixedOutDir', baseUrl: 'https://docusaurus.io', }; - const redirectFiles = toRedirectFilesMetadata( + const redirectFiles = toRedirectFiles( [ {from: '/abc.html', to: '/abc'}, {from: '/def', to: '/def.html'}, @@ -76,7 +76,7 @@ describe('toRedirectFilesMetadata', () => { baseUrl: 'https://docusaurus.io', }; - const redirectFiles = toRedirectFilesMetadata( + const redirectFiles = toRedirectFiles( [ {from: '/abc.html', to: '/abc'}, {from: '/def', to: '/def.html'}, @@ -103,7 +103,7 @@ describe('toRedirectFilesMetadata', () => { baseUrl: 'https://docusaurus.io', }; - const redirectFiles = toRedirectFilesMetadata( + const redirectFiles = toRedirectFiles( [ {from: '/abc.html', to: '/abc'}, {from: '/def', to: '/def.html'}, @@ -132,7 +132,7 @@ describe('toRedirectFilesMetadata', () => { outDir: '/tmp/someFixedOutDir', baseUrl: '/', }; - const redirectFiles = toRedirectFilesMetadata( + const redirectFiles = toRedirectFiles( [{from: '/abc.html', to: '/abc'}], pluginContext, undefined, @@ -147,7 +147,7 @@ describe('toRedirectFilesMetadata', () => { outDir: '/tmp/someFixedOutDir', baseUrl: '', }; - const redirectFiles = toRedirectFilesMetadata( + const redirectFiles = toRedirectFiles( [{from: '/abc.html', to: '/abc'}], pluginContext, undefined, diff --git a/packages/docusaurus-plugin-client-redirects/src/collectRedirects.ts b/packages/docusaurus-plugin-client-redirects/src/collectRedirects.ts index fa4718eca32f..3cd0db4096cd 100644 --- a/packages/docusaurus-plugin-client-redirects/src/collectRedirects.ts +++ b/packages/docusaurus-plugin-client-redirects/src/collectRedirects.ts @@ -7,52 +7,57 @@ import _ from 'lodash'; import logger from '@docusaurus/logger'; -import { - applyTrailingSlash, - type ApplyTrailingSlashParams, -} from '@docusaurus/utils-common'; +import {applyTrailingSlash} from '@docusaurus/utils-common'; import { createFromExtensionsRedirects, createToExtensionsRedirects, } from './extensionRedirects'; import {validateRedirect} from './redirectValidation'; import type {PluginOptions, RedirectOption} from './options'; -import type {PluginContext, RedirectMetadata} from './types'; +import type {PluginContext, RedirectItem} from './types'; export default function collectRedirects( pluginContext: PluginContext, trailingSlash: boolean | undefined, -): RedirectMetadata[] { - let redirects = doCollectRedirects(pluginContext); - - redirects = applyRedirectsTrailingSlash(redirects, { - trailingSlash, - baseUrl: pluginContext.baseUrl, - }); +): RedirectItem[] { + // For each plugin config option, create the appropriate redirects + const redirects = [ + ...createFromExtensionsRedirects( + pluginContext.relativeRoutesPaths, + pluginContext.options.fromExtensions, + ), + ...createToExtensionsRedirects( + pluginContext.relativeRoutesPaths, + pluginContext.options.toExtensions, + ), + ...createRedirectsOptionRedirects(pluginContext.options.redirects), + ...createCreateRedirectsOptionRedirects( + pluginContext.relativeRoutesPaths, + pluginContext.options.createRedirects, + ), + ].map((redirect) => ({ + ...redirect, + // Given a redirect with `to: "/abc"` and `trailingSlash` enabled: + // + // - We don't want to reject `to: "/abc"`, as that unambiguously points to + // `/abc/` now; + // - We want to redirect `to: /abc/` without the user having to change all + // her redirect plugin options + // + // It should be easy to toggle `trailingSlash` option without having to + // change other configs + to: applyTrailingSlash(redirect.to, { + trailingSlash, + baseUrl: pluginContext.baseUrl, + }), + })); validateCollectedRedirects(redirects, pluginContext); return filterUnwantedRedirects(redirects, pluginContext); } -// If users wants to redirect to=/abc and they enable trailingSlash=true then -// => we don't want to reject the to=/abc (as only /abc/ is an existing/valid -// path now) -// => we want to redirect to=/abc/ without the user having to change all its -// redirect plugin options -// It should be easy to toggle siteConfig.trailingSlash option without having to -// change other configs -function applyRedirectsTrailingSlash( - redirects: RedirectMetadata[], - params: ApplyTrailingSlashParams, -) { - return redirects.map((redirect) => ({ - ...redirect, - to: applyTrailingSlash(redirect.to, params), - })); -} - function validateCollectedRedirects( - redirects: RedirectMetadata[], + redirects: RedirectItem[], pluginContext: PluginContext, ) { const redirectValidationErrors = redirects @@ -89,9 +94,9 @@ Valid paths you can redirect to: } function filterUnwantedRedirects( - redirects: RedirectMetadata[], + redirects: RedirectItem[], pluginContext: PluginContext, -): RedirectMetadata[] { +): RedirectItem[] { // We don't want to create the same redirect twice, since that would lead to // writing the same html redirection file twice. Object.entries(_.groupBy(redirects, (redirect) => redirect.from)).forEach( @@ -120,37 +125,15 @@ It is not possible to redirect the same pathname to multiple destinations: ${gro ); } -// For each plugin config option, create the appropriate redirects -function doCollectRedirects(pluginContext: PluginContext): RedirectMetadata[] { - return [ - ...createFromExtensionsRedirects( - pluginContext.relativeRoutesPaths, - pluginContext.options.fromExtensions, - ), - ...createToExtensionsRedirects( - pluginContext.relativeRoutesPaths, - pluginContext.options.toExtensions, - ), - ...createRedirectsOptionRedirects(pluginContext.options.redirects), - ...createCreateRedirectsOptionRedirects( - pluginContext.relativeRoutesPaths, - pluginContext.options.createRedirects, - ), - ]; -} - function createRedirectsOptionRedirects( redirectsOption: PluginOptions['redirects'], -): RedirectMetadata[] { +): RedirectItem[] { // For convenience, user can use a string or a string[] - function optionToRedirects(option: RedirectOption): RedirectMetadata[] { + function optionToRedirects(option: RedirectOption): RedirectItem[] { if (typeof option.from === 'string') { return [{from: option.from, to: option.to}]; } - return option.from.map((from) => ({ - from, - to: option.to, - })); + return option.from.map((from) => ({from, to: option.to})); } return redirectsOption.flatMap(optionToRedirects); @@ -160,17 +143,14 @@ function createRedirectsOptionRedirects( function createCreateRedirectsOptionRedirects( paths: string[], createRedirects: PluginOptions['createRedirects'], -): RedirectMetadata[] { - function createPathRedirects(path: string): RedirectMetadata[] { +): RedirectItem[] { + function createPathRedirects(path: string): RedirectItem[] { const fromsMixed: string | string[] = createRedirects?.(path) ?? []; const froms: string[] = typeof fromsMixed === 'string' ? [fromsMixed] : fromsMixed; - return froms.map((from) => ({ - from, - to: path, - })); + return froms.map((from) => ({from, to: path})); } return paths.flatMap(createPathRedirects); diff --git a/packages/docusaurus-plugin-client-redirects/src/extensionRedirects.ts b/packages/docusaurus-plugin-client-redirects/src/extensionRedirects.ts index a72452a7528c..86fa4aeb48de 100644 --- a/packages/docusaurus-plugin-client-redirects/src/extensionRedirects.ts +++ b/packages/docusaurus-plugin-client-redirects/src/extensionRedirects.ts @@ -10,7 +10,7 @@ import { removeSuffix, removeTrailingSlash, } from '@docusaurus/utils'; -import type {RedirectMetadata} from './types'; +import type {RedirectItem} from './types'; const ExtensionAdditionalMessage = 'If the redirect extension system is not good enough for your use case, you can create redirects yourself with the "createRedirects" plugin option.'; @@ -40,23 +40,21 @@ const validateExtension = (ext: string) => { const addLeadingDot = (extension: string) => `.${extension}`; -// Create new /path that redirects to existing an /path.html +/** + * Create new `/path` that redirects to existing an `/path.html` + */ export function createToExtensionsRedirects( paths: string[], extensions: string[], -): RedirectMetadata[] { +): RedirectItem[] { extensions.forEach(validateExtension); const dottedExtensions = extensions.map(addLeadingDot); - const createPathRedirects = (path: string): RedirectMetadata[] => { + const createPathRedirects = (path: string): RedirectItem[] => { const extensionFound = dottedExtensions.find((ext) => path.endsWith(ext)); if (extensionFound) { - const routePathWithoutExtension = removeSuffix(path, extensionFound); - return [routePathWithoutExtension].map((from) => ({ - from, - to: path, - })); + return [{from: removeSuffix(path, extensionFound), to: path}]; } return []; }; @@ -64,12 +62,15 @@ export function createToExtensionsRedirects( return paths.flatMap(createPathRedirects); } -// Create new /path.html/index.html that redirects to existing an /path -// The filename pattern might look weird but it's on purpose (see https://github.com/facebook/docusaurus/issues/5055) +/** + * Create new `/path.html/index.html` that redirects to existing an `/path` + * The filename pattern might look weird but it's on purpose (see + * https://github.com/facebook/docusaurus/issues/5055) + */ export function createFromExtensionsRedirects( paths: string[], extensions: string[], -): RedirectMetadata[] { +): RedirectItem[] { extensions.forEach(validateExtension); const dottedExtensions = extensions.map(addLeadingDot); @@ -77,7 +78,7 @@ export function createFromExtensionsRedirects( const alreadyEndsWithAnExtension = (str: string) => dottedExtensions.some((ext) => str.endsWith(ext)); - const createPathRedirects = (path: string): RedirectMetadata[] => { + const createPathRedirects = (path: string): RedirectItem[] => { if (path === '' || path === '/' || alreadyEndsWithAnExtension(path)) { return []; } diff --git a/packages/docusaurus-plugin-client-redirects/src/index.ts b/packages/docusaurus-plugin-client-redirects/src/index.ts index 237f3f099e9e..fa96902d19e5 100644 --- a/packages/docusaurus-plugin-client-redirects/src/index.ts +++ b/packages/docusaurus-plugin-client-redirects/src/index.ts @@ -8,11 +8,11 @@ import {removePrefix, addLeadingSlash} from '@docusaurus/utils'; import collectRedirects from './collectRedirects'; import writeRedirectFiles, { - toRedirectFilesMetadata, - type RedirectFileMetadata, + toRedirectFiles, + type RedirectFile, } from './writeRedirectFiles'; import type {LoadContext, Plugin} from '@docusaurus/types'; -import type {PluginContext, RedirectMetadata} from './types'; +import type {PluginContext, RedirectItem} from './types'; import type {PluginOptions, Options} from './options'; export default function pluginClientRedirectsPages( @@ -33,12 +33,12 @@ export default function pluginClientRedirectsPages( options, }; - const redirects: RedirectMetadata[] = collectRedirects( + const redirects: RedirectItem[] = collectRedirects( pluginContext, trailingSlash, ); - const redirectFiles: RedirectFileMetadata[] = toRedirectFilesMetadata( + const redirectFiles: RedirectFile[] = toRedirectFiles( redirects, pluginContext, trailingSlash, diff --git a/packages/docusaurus-plugin-client-redirects/src/options.ts b/packages/docusaurus-plugin-client-redirects/src/options.ts index 389e90b958aa..675e38902898 100644 --- a/packages/docusaurus-plugin-client-redirects/src/options.ts +++ b/packages/docusaurus-plugin-client-redirects/src/options.ts @@ -9,7 +9,9 @@ import {Joi, PathnameSchema} from '@docusaurus/utils-validation'; import type {OptionValidationContext} from '@docusaurus/types'; export type RedirectOption = { + /** Pathname of an existing Docusaurus page */ to: string; + /** Pathname of the new page(s) we should create */ from: string | string[]; }; @@ -23,7 +25,9 @@ export type PluginOptions = { /** The list of redirect rules, each one with multiple `from`s → one `to`. */ redirects: RedirectOption[]; /** - * A callback to create a redirect rule. + * A callback to create a redirect rule. Docusaurus query this callback + * against every path it has created, and use its return value to output more + * paths. * @returns All the paths from which we should redirect to `path` */ createRedirects?: ( diff --git a/packages/docusaurus-plugin-client-redirects/src/redirectValidation.ts b/packages/docusaurus-plugin-client-redirects/src/redirectValidation.ts index 334453274b1e..7b7a471f4b6f 100644 --- a/packages/docusaurus-plugin-client-redirects/src/redirectValidation.ts +++ b/packages/docusaurus-plugin-client-redirects/src/redirectValidation.ts @@ -6,14 +6,14 @@ */ import {Joi, PathnameSchema} from '@docusaurus/utils-validation'; -import type {RedirectMetadata} from './types'; +import type {RedirectItem} from './types'; -const RedirectSchema = Joi.object({ +const RedirectSchema = Joi.object({ from: PathnameSchema.required(), to: PathnameSchema.required(), }); -export function validateRedirect(redirect: RedirectMetadata): void { +export function validateRedirect(redirect: RedirectItem): void { const {error} = RedirectSchema.validate(redirect, { abortEarly: true, convert: false, diff --git a/packages/docusaurus-plugin-client-redirects/src/types.ts b/packages/docusaurus-plugin-client-redirects/src/types.ts index 8ad61ae06c47..110e189c2f48 100644 --- a/packages/docusaurus-plugin-client-redirects/src/types.ts +++ b/packages/docusaurus-plugin-client-redirects/src/types.ts @@ -21,7 +21,7 @@ export type PluginContext = Pick & { * /!\ easy to be confused: "from" is the new page we should create, * that redirects to "to": the existing Docusaurus page */ -export type RedirectMetadata = { +export type RedirectItem = { /** Pathname of the new page we should create */ from: string; /** Pathname of an existing Docusaurus page */ diff --git a/packages/docusaurus-plugin-client-redirects/src/writeRedirectFiles.ts b/packages/docusaurus-plugin-client-redirects/src/writeRedirectFiles.ts index 77a9316c829a..4f70e064ce02 100644 --- a/packages/docusaurus-plugin-client-redirects/src/writeRedirectFiles.ts +++ b/packages/docusaurus-plugin-client-redirects/src/writeRedirectFiles.ts @@ -13,11 +13,11 @@ import {normalizeUrl} from '@docusaurus/utils'; import createRedirectPageContent from './createRedirectPageContent'; -import type {PluginContext, RedirectMetadata} from './types'; +import type {PluginContext, RedirectItem} from './types'; export type WriteFilesPluginContext = Pick; -export type RedirectFileMetadata = { +export type RedirectFile = { fileAbsolutePath: string; fileContent: string; }; @@ -57,11 +57,11 @@ function getRedirectFilePath( return path.join(filePath, `${fileName}/index.html`); } -export function toRedirectFilesMetadata( - redirects: RedirectMetadata[], +export function toRedirectFiles( + redirects: RedirectItem[], pluginContext: WriteFilesPluginContext, trailingSlash: boolean | undefined, -): RedirectFileMetadata[] { +): RedirectFile[] { // Perf: avoid rendering the template twice with the exact same "props" // We might create multiple redirect pages for the same destination url // note: the first fn arg is the cache key! @@ -69,7 +69,7 @@ export function toRedirectFilesMetadata( createRedirectPageContent({toUrl}), ); - const createFileMetadata = (redirect: RedirectMetadata) => { + const createFileMetadata = (redirect: RedirectItem) => { const fileRelativePath = getRedirectFilePath(redirect.from, trailingSlash); const fileAbsolutePath = path.join(pluginContext.outDir, fileRelativePath); const toUrl = createToUrl(pluginContext.baseUrl, redirect.to); @@ -84,9 +84,7 @@ export function toRedirectFilesMetadata( return redirects.map(createFileMetadata); } -export async function writeRedirectFile( - file: RedirectFileMetadata, -): Promise { +export async function writeRedirectFile(file: RedirectFile): Promise { try { // User-friendly security to prevent file overrides if (await fs.pathExists(file.fileAbsolutePath)) { @@ -108,7 +106,7 @@ export async function writeRedirectFile( } export default async function writeRedirectFiles( - redirectFiles: RedirectFileMetadata[], + redirectFiles: RedirectFile[], ): Promise { await Promise.all(redirectFiles.map(writeRedirectFile)); } diff --git a/website/docs/api/plugins/plugin-client-redirects.md b/website/docs/api/plugins/plugin-client-redirects.md index 2211e8d28aad..8a1f161d1ccf 100644 --- a/website/docs/api/plugins/plugin-client-redirects.md +++ b/website/docs/api/plugins/plugin-client-redirects.md @@ -44,7 +44,7 @@ Accepted fields: | `fromExtensions` | `string[]` | `[]` | The extensions to be removed from the route after redirecting. | | `toExtensions` | `string[]` | `[]` | The extensions to be appended to the route after redirecting. | | `redirects` | RedirectRule[] | `[]` | The list of redirect rules. | -| `createRedirects` | CreateRedirectsFn | `undefined` | A callback to create a redirect rule. | +| `createRedirects` | CreateRedirectsFn | `undefined` | A callback to create a redirect rule. Docusaurus query this callback against every path it has created, and use its return value to output more paths. | ```mdx-code-block @@ -61,9 +61,20 @@ type RedirectRule = { }; ``` +:::note + +The idea of "from" and "to" is central in this plugin. "From" means a path that you want to _create_, i.e. an extra HTML file that will be written; "to" means a path to want to redirect _to_, usually a route that Docusaurus already knows about. + +This is why you can have multiple "from" for the same "to": we will create multiple HTML files that all redirect to the same destination. On the other hand, one "from" can never have more than one "to": the written HTML file needs to have a determinate destination. + +::: + #### `CreateRedirectsFn` {#CreateRedirectsFn} ```ts +// The parameter `path` is a route that Docusaurus has already created. It can +// be seen as the "to", and your return value is the "from". Returning a falsy +// value will not create any redirect pages for this particular path. type CreateRedirectsFn = (path: string) => string[] | string | null | undefined; ```