mirror of
https://github.com/facebook/docusaurus.git
synced 2025-06-06 04:42:40 +02:00
refactor(client-redirects): elaborate documentation, minor refactor (#7607)
This commit is contained in:
parent
27834dc23a
commit
fb3138d722
10 changed files with 103 additions and 109 deletions
|
@ -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`] = `
|
||||
[
|
||||
"<!DOCTYPE html>
|
||||
<html>
|
||||
|
@ -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`] = `
|
||||
[
|
||||
"<!DOCTYPE html>
|
||||
<html>
|
||||
|
@ -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`] = `
|
||||
[
|
||||
"<!DOCTYPE html>
|
||||
<html>
|
||||
|
@ -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`] = `
|
||||
[
|
||||
"<!DOCTYPE html>
|
||||
<html>
|
||||
|
@ -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`] = `
|
||||
[
|
||||
"<!DOCTYPE html>
|
||||
<html>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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, {
|
||||
): 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);
|
||||
|
|
|
@ -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 [];
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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?: (
|
||||
|
|
|
@ -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<RedirectMetadata>({
|
||||
const RedirectSchema = Joi.object<RedirectItem>({
|
||||
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,
|
||||
|
|
|
@ -21,7 +21,7 @@ export type PluginContext = Pick<Props, 'outDir' | 'baseUrl'> & {
|
|||
* /!\ 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 */
|
||||
|
|
|
@ -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<PluginContext, 'baseUrl' | 'outDir'>;
|
||||
|
||||
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<void> {
|
||||
export async function writeRedirectFile(file: RedirectFile): Promise<void> {
|
||||
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<void> {
|
||||
await Promise.all(redirectFiles.map(writeRedirectFile));
|
||||
}
|
||||
|
|
|
@ -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` | <code><a href="#RedirectRule">RedirectRule</a>[]</code> | `[]` | The list of redirect rules. |
|
||||
| `createRedirects` | <code><a href="#CreateRedirectsFn">CreateRedirectsFn</a></code> | `undefined` | A callback to create a redirect rule. |
|
||||
| `createRedirects` | <code><a href="#CreateRedirectsFn">CreateRedirectsFn</a></code> | `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
|
||||
</APITable>
|
||||
|
@ -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;
|
||||
```
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue