mirror of
https://github.com/facebook/docusaurus.git
synced 2025-06-12 15:52:39 +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
|
// 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>
|
"<!DOCTYPE html>
|
||||||
<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>
|
"<!DOCTYPE html>
|
||||||
<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>
|
"<!DOCTYPE html>
|
||||||
<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>
|
"<!DOCTYPE html>
|
||||||
<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>
|
"<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
|
|
|
@ -9,7 +9,7 @@ import fs from 'fs-extra';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
import writeRedirectFiles, {
|
import writeRedirectFiles, {
|
||||||
toRedirectFilesMetadata,
|
toRedirectFiles,
|
||||||
createToUrl,
|
createToUrl,
|
||||||
} from '../writeRedirectFiles';
|
} from '../writeRedirectFiles';
|
||||||
|
|
||||||
|
@ -42,14 +42,14 @@ describe('createToUrl', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('toRedirectFilesMetadata', () => {
|
describe('toRedirectFiles', () => {
|
||||||
it('creates appropriate metadata trailingSlash=undefined', () => {
|
it('creates appropriate metadata trailingSlash=undefined', () => {
|
||||||
const pluginContext = {
|
const pluginContext = {
|
||||||
outDir: '/tmp/someFixedOutDir',
|
outDir: '/tmp/someFixedOutDir',
|
||||||
baseUrl: 'https://docusaurus.io',
|
baseUrl: 'https://docusaurus.io',
|
||||||
};
|
};
|
||||||
|
|
||||||
const redirectFiles = toRedirectFilesMetadata(
|
const redirectFiles = toRedirectFiles(
|
||||||
[
|
[
|
||||||
{from: '/abc.html', to: '/abc'},
|
{from: '/abc.html', to: '/abc'},
|
||||||
{from: '/def', to: '/def.html'},
|
{from: '/def', to: '/def.html'},
|
||||||
|
@ -76,7 +76,7 @@ describe('toRedirectFilesMetadata', () => {
|
||||||
baseUrl: 'https://docusaurus.io',
|
baseUrl: 'https://docusaurus.io',
|
||||||
};
|
};
|
||||||
|
|
||||||
const redirectFiles = toRedirectFilesMetadata(
|
const redirectFiles = toRedirectFiles(
|
||||||
[
|
[
|
||||||
{from: '/abc.html', to: '/abc'},
|
{from: '/abc.html', to: '/abc'},
|
||||||
{from: '/def', to: '/def.html'},
|
{from: '/def', to: '/def.html'},
|
||||||
|
@ -103,7 +103,7 @@ describe('toRedirectFilesMetadata', () => {
|
||||||
baseUrl: 'https://docusaurus.io',
|
baseUrl: 'https://docusaurus.io',
|
||||||
};
|
};
|
||||||
|
|
||||||
const redirectFiles = toRedirectFilesMetadata(
|
const redirectFiles = toRedirectFiles(
|
||||||
[
|
[
|
||||||
{from: '/abc.html', to: '/abc'},
|
{from: '/abc.html', to: '/abc'},
|
||||||
{from: '/def', to: '/def.html'},
|
{from: '/def', to: '/def.html'},
|
||||||
|
@ -132,7 +132,7 @@ describe('toRedirectFilesMetadata', () => {
|
||||||
outDir: '/tmp/someFixedOutDir',
|
outDir: '/tmp/someFixedOutDir',
|
||||||
baseUrl: '/',
|
baseUrl: '/',
|
||||||
};
|
};
|
||||||
const redirectFiles = toRedirectFilesMetadata(
|
const redirectFiles = toRedirectFiles(
|
||||||
[{from: '/abc.html', to: '/abc'}],
|
[{from: '/abc.html', to: '/abc'}],
|
||||||
pluginContext,
|
pluginContext,
|
||||||
undefined,
|
undefined,
|
||||||
|
@ -147,7 +147,7 @@ describe('toRedirectFilesMetadata', () => {
|
||||||
outDir: '/tmp/someFixedOutDir',
|
outDir: '/tmp/someFixedOutDir',
|
||||||
baseUrl: '',
|
baseUrl: '',
|
||||||
};
|
};
|
||||||
const redirectFiles = toRedirectFilesMetadata(
|
const redirectFiles = toRedirectFiles(
|
||||||
[{from: '/abc.html', to: '/abc'}],
|
[{from: '/abc.html', to: '/abc'}],
|
||||||
pluginContext,
|
pluginContext,
|
||||||
undefined,
|
undefined,
|
||||||
|
|
|
@ -7,52 +7,57 @@
|
||||||
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import logger from '@docusaurus/logger';
|
import logger from '@docusaurus/logger';
|
||||||
import {
|
import {applyTrailingSlash} from '@docusaurus/utils-common';
|
||||||
applyTrailingSlash,
|
|
||||||
type ApplyTrailingSlashParams,
|
|
||||||
} from '@docusaurus/utils-common';
|
|
||||||
import {
|
import {
|
||||||
createFromExtensionsRedirects,
|
createFromExtensionsRedirects,
|
||||||
createToExtensionsRedirects,
|
createToExtensionsRedirects,
|
||||||
} from './extensionRedirects';
|
} from './extensionRedirects';
|
||||||
import {validateRedirect} from './redirectValidation';
|
import {validateRedirect} from './redirectValidation';
|
||||||
import type {PluginOptions, RedirectOption} from './options';
|
import type {PluginOptions, RedirectOption} from './options';
|
||||||
import type {PluginContext, RedirectMetadata} from './types';
|
import type {PluginContext, RedirectItem} from './types';
|
||||||
|
|
||||||
export default function collectRedirects(
|
export default function collectRedirects(
|
||||||
pluginContext: PluginContext,
|
pluginContext: PluginContext,
|
||||||
trailingSlash: boolean | undefined,
|
trailingSlash: boolean | undefined,
|
||||||
): RedirectMetadata[] {
|
): RedirectItem[] {
|
||||||
let redirects = doCollectRedirects(pluginContext);
|
// For each plugin config option, create the appropriate redirects
|
||||||
|
const redirects = [
|
||||||
redirects = applyRedirectsTrailingSlash(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,
|
trailingSlash,
|
||||||
baseUrl: pluginContext.baseUrl,
|
baseUrl: pluginContext.baseUrl,
|
||||||
});
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
validateCollectedRedirects(redirects, pluginContext);
|
validateCollectedRedirects(redirects, pluginContext);
|
||||||
return filterUnwantedRedirects(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(
|
function validateCollectedRedirects(
|
||||||
redirects: RedirectMetadata[],
|
redirects: RedirectItem[],
|
||||||
pluginContext: PluginContext,
|
pluginContext: PluginContext,
|
||||||
) {
|
) {
|
||||||
const redirectValidationErrors = redirects
|
const redirectValidationErrors = redirects
|
||||||
|
@ -89,9 +94,9 @@ Valid paths you can redirect to:
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterUnwantedRedirects(
|
function filterUnwantedRedirects(
|
||||||
redirects: RedirectMetadata[],
|
redirects: RedirectItem[],
|
||||||
pluginContext: PluginContext,
|
pluginContext: PluginContext,
|
||||||
): RedirectMetadata[] {
|
): RedirectItem[] {
|
||||||
// We don't want to create the same redirect twice, since that would lead to
|
// We don't want to create the same redirect twice, since that would lead to
|
||||||
// writing the same html redirection file twice.
|
// writing the same html redirection file twice.
|
||||||
Object.entries(_.groupBy(redirects, (redirect) => redirect.from)).forEach(
|
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(
|
function createRedirectsOptionRedirects(
|
||||||
redirectsOption: PluginOptions['redirects'],
|
redirectsOption: PluginOptions['redirects'],
|
||||||
): RedirectMetadata[] {
|
): RedirectItem[] {
|
||||||
// For convenience, user can use a string or a string[]
|
// 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') {
|
if (typeof option.from === 'string') {
|
||||||
return [{from: option.from, to: option.to}];
|
return [{from: option.from, to: option.to}];
|
||||||
}
|
}
|
||||||
return option.from.map((from) => ({
|
return option.from.map((from) => ({from, to: option.to}));
|
||||||
from,
|
|
||||||
to: option.to,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirectsOption.flatMap(optionToRedirects);
|
return redirectsOption.flatMap(optionToRedirects);
|
||||||
|
@ -160,17 +143,14 @@ function createRedirectsOptionRedirects(
|
||||||
function createCreateRedirectsOptionRedirects(
|
function createCreateRedirectsOptionRedirects(
|
||||||
paths: string[],
|
paths: string[],
|
||||||
createRedirects: PluginOptions['createRedirects'],
|
createRedirects: PluginOptions['createRedirects'],
|
||||||
): RedirectMetadata[] {
|
): RedirectItem[] {
|
||||||
function createPathRedirects(path: string): RedirectMetadata[] {
|
function createPathRedirects(path: string): RedirectItem[] {
|
||||||
const fromsMixed: string | string[] = createRedirects?.(path) ?? [];
|
const fromsMixed: string | string[] = createRedirects?.(path) ?? [];
|
||||||
|
|
||||||
const froms: string[] =
|
const froms: string[] =
|
||||||
typeof fromsMixed === 'string' ? [fromsMixed] : fromsMixed;
|
typeof fromsMixed === 'string' ? [fromsMixed] : fromsMixed;
|
||||||
|
|
||||||
return froms.map((from) => ({
|
return froms.map((from) => ({from, to: path}));
|
||||||
from,
|
|
||||||
to: path,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return paths.flatMap(createPathRedirects);
|
return paths.flatMap(createPathRedirects);
|
||||||
|
|
|
@ -10,7 +10,7 @@ import {
|
||||||
removeSuffix,
|
removeSuffix,
|
||||||
removeTrailingSlash,
|
removeTrailingSlash,
|
||||||
} from '@docusaurus/utils';
|
} from '@docusaurus/utils';
|
||||||
import type {RedirectMetadata} from './types';
|
import type {RedirectItem} from './types';
|
||||||
|
|
||||||
const ExtensionAdditionalMessage =
|
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.';
|
'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}`;
|
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(
|
export function createToExtensionsRedirects(
|
||||||
paths: string[],
|
paths: string[],
|
||||||
extensions: string[],
|
extensions: string[],
|
||||||
): RedirectMetadata[] {
|
): RedirectItem[] {
|
||||||
extensions.forEach(validateExtension);
|
extensions.forEach(validateExtension);
|
||||||
|
|
||||||
const dottedExtensions = extensions.map(addLeadingDot);
|
const dottedExtensions = extensions.map(addLeadingDot);
|
||||||
|
|
||||||
const createPathRedirects = (path: string): RedirectMetadata[] => {
|
const createPathRedirects = (path: string): RedirectItem[] => {
|
||||||
const extensionFound = dottedExtensions.find((ext) => path.endsWith(ext));
|
const extensionFound = dottedExtensions.find((ext) => path.endsWith(ext));
|
||||||
if (extensionFound) {
|
if (extensionFound) {
|
||||||
const routePathWithoutExtension = removeSuffix(path, extensionFound);
|
return [{from: removeSuffix(path, extensionFound), to: path}];
|
||||||
return [routePathWithoutExtension].map((from) => ({
|
|
||||||
from,
|
|
||||||
to: path,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
};
|
};
|
||||||
|
@ -64,12 +62,15 @@ export function createToExtensionsRedirects(
|
||||||
return paths.flatMap(createPathRedirects);
|
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(
|
export function createFromExtensionsRedirects(
|
||||||
paths: string[],
|
paths: string[],
|
||||||
extensions: string[],
|
extensions: string[],
|
||||||
): RedirectMetadata[] {
|
): RedirectItem[] {
|
||||||
extensions.forEach(validateExtension);
|
extensions.forEach(validateExtension);
|
||||||
|
|
||||||
const dottedExtensions = extensions.map(addLeadingDot);
|
const dottedExtensions = extensions.map(addLeadingDot);
|
||||||
|
@ -77,7 +78,7 @@ export function createFromExtensionsRedirects(
|
||||||
const alreadyEndsWithAnExtension = (str: string) =>
|
const alreadyEndsWithAnExtension = (str: string) =>
|
||||||
dottedExtensions.some((ext) => str.endsWith(ext));
|
dottedExtensions.some((ext) => str.endsWith(ext));
|
||||||
|
|
||||||
const createPathRedirects = (path: string): RedirectMetadata[] => {
|
const createPathRedirects = (path: string): RedirectItem[] => {
|
||||||
if (path === '' || path === '/' || alreadyEndsWithAnExtension(path)) {
|
if (path === '' || path === '/' || alreadyEndsWithAnExtension(path)) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,11 +8,11 @@
|
||||||
import {removePrefix, addLeadingSlash} from '@docusaurus/utils';
|
import {removePrefix, addLeadingSlash} from '@docusaurus/utils';
|
||||||
import collectRedirects from './collectRedirects';
|
import collectRedirects from './collectRedirects';
|
||||||
import writeRedirectFiles, {
|
import writeRedirectFiles, {
|
||||||
toRedirectFilesMetadata,
|
toRedirectFiles,
|
||||||
type RedirectFileMetadata,
|
type RedirectFile,
|
||||||
} from './writeRedirectFiles';
|
} from './writeRedirectFiles';
|
||||||
import type {LoadContext, Plugin} from '@docusaurus/types';
|
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';
|
import type {PluginOptions, Options} from './options';
|
||||||
|
|
||||||
export default function pluginClientRedirectsPages(
|
export default function pluginClientRedirectsPages(
|
||||||
|
@ -33,12 +33,12 @@ export default function pluginClientRedirectsPages(
|
||||||
options,
|
options,
|
||||||
};
|
};
|
||||||
|
|
||||||
const redirects: RedirectMetadata[] = collectRedirects(
|
const redirects: RedirectItem[] = collectRedirects(
|
||||||
pluginContext,
|
pluginContext,
|
||||||
trailingSlash,
|
trailingSlash,
|
||||||
);
|
);
|
||||||
|
|
||||||
const redirectFiles: RedirectFileMetadata[] = toRedirectFilesMetadata(
|
const redirectFiles: RedirectFile[] = toRedirectFiles(
|
||||||
redirects,
|
redirects,
|
||||||
pluginContext,
|
pluginContext,
|
||||||
trailingSlash,
|
trailingSlash,
|
||||||
|
|
|
@ -9,7 +9,9 @@ import {Joi, PathnameSchema} from '@docusaurus/utils-validation';
|
||||||
import type {OptionValidationContext} from '@docusaurus/types';
|
import type {OptionValidationContext} from '@docusaurus/types';
|
||||||
|
|
||||||
export type RedirectOption = {
|
export type RedirectOption = {
|
||||||
|
/** Pathname of an existing Docusaurus page */
|
||||||
to: string;
|
to: string;
|
||||||
|
/** Pathname of the new page(s) we should create */
|
||||||
from: string | string[];
|
from: string | string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -23,7 +25,9 @@ export type PluginOptions = {
|
||||||
/** The list of redirect rules, each one with multiple `from`s → one `to`. */
|
/** The list of redirect rules, each one with multiple `from`s → one `to`. */
|
||||||
redirects: RedirectOption[];
|
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`
|
* @returns All the paths from which we should redirect to `path`
|
||||||
*/
|
*/
|
||||||
createRedirects?: (
|
createRedirects?: (
|
||||||
|
|
|
@ -6,14 +6,14 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {Joi, PathnameSchema} from '@docusaurus/utils-validation';
|
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(),
|
from: PathnameSchema.required(),
|
||||||
to: PathnameSchema.required(),
|
to: PathnameSchema.required(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export function validateRedirect(redirect: RedirectMetadata): void {
|
export function validateRedirect(redirect: RedirectItem): void {
|
||||||
const {error} = RedirectSchema.validate(redirect, {
|
const {error} = RedirectSchema.validate(redirect, {
|
||||||
abortEarly: true,
|
abortEarly: true,
|
||||||
convert: false,
|
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,
|
* /!\ easy to be confused: "from" is the new page we should create,
|
||||||
* that redirects to "to": the existing Docusaurus page
|
* that redirects to "to": the existing Docusaurus page
|
||||||
*/
|
*/
|
||||||
export type RedirectMetadata = {
|
export type RedirectItem = {
|
||||||
/** Pathname of the new page we should create */
|
/** Pathname of the new page we should create */
|
||||||
from: string;
|
from: string;
|
||||||
/** Pathname of an existing Docusaurus page */
|
/** Pathname of an existing Docusaurus page */
|
||||||
|
|
|
@ -13,11 +13,11 @@ import {normalizeUrl} from '@docusaurus/utils';
|
||||||
|
|
||||||
import createRedirectPageContent from './createRedirectPageContent';
|
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 WriteFilesPluginContext = Pick<PluginContext, 'baseUrl' | 'outDir'>;
|
||||||
|
|
||||||
export type RedirectFileMetadata = {
|
export type RedirectFile = {
|
||||||
fileAbsolutePath: string;
|
fileAbsolutePath: string;
|
||||||
fileContent: string;
|
fileContent: string;
|
||||||
};
|
};
|
||||||
|
@ -57,11 +57,11 @@ function getRedirectFilePath(
|
||||||
return path.join(filePath, `${fileName}/index.html`);
|
return path.join(filePath, `${fileName}/index.html`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toRedirectFilesMetadata(
|
export function toRedirectFiles(
|
||||||
redirects: RedirectMetadata[],
|
redirects: RedirectItem[],
|
||||||
pluginContext: WriteFilesPluginContext,
|
pluginContext: WriteFilesPluginContext,
|
||||||
trailingSlash: boolean | undefined,
|
trailingSlash: boolean | undefined,
|
||||||
): RedirectFileMetadata[] {
|
): RedirectFile[] {
|
||||||
// Perf: avoid rendering the template twice with the exact same "props"
|
// Perf: avoid rendering the template twice with the exact same "props"
|
||||||
// We might create multiple redirect pages for the same destination url
|
// We might create multiple redirect pages for the same destination url
|
||||||
// note: the first fn arg is the cache key!
|
// note: the first fn arg is the cache key!
|
||||||
|
@ -69,7 +69,7 @@ export function toRedirectFilesMetadata(
|
||||||
createRedirectPageContent({toUrl}),
|
createRedirectPageContent({toUrl}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const createFileMetadata = (redirect: RedirectMetadata) => {
|
const createFileMetadata = (redirect: RedirectItem) => {
|
||||||
const fileRelativePath = getRedirectFilePath(redirect.from, trailingSlash);
|
const fileRelativePath = getRedirectFilePath(redirect.from, trailingSlash);
|
||||||
const fileAbsolutePath = path.join(pluginContext.outDir, fileRelativePath);
|
const fileAbsolutePath = path.join(pluginContext.outDir, fileRelativePath);
|
||||||
const toUrl = createToUrl(pluginContext.baseUrl, redirect.to);
|
const toUrl = createToUrl(pluginContext.baseUrl, redirect.to);
|
||||||
|
@ -84,9 +84,7 @@ export function toRedirectFilesMetadata(
|
||||||
return redirects.map(createFileMetadata);
|
return redirects.map(createFileMetadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function writeRedirectFile(
|
export async function writeRedirectFile(file: RedirectFile): Promise<void> {
|
||||||
file: RedirectFileMetadata,
|
|
||||||
): Promise<void> {
|
|
||||||
try {
|
try {
|
||||||
// User-friendly security to prevent file overrides
|
// User-friendly security to prevent file overrides
|
||||||
if (await fs.pathExists(file.fileAbsolutePath)) {
|
if (await fs.pathExists(file.fileAbsolutePath)) {
|
||||||
|
@ -108,7 +106,7 @@ export async function writeRedirectFile(
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function writeRedirectFiles(
|
export default async function writeRedirectFiles(
|
||||||
redirectFiles: RedirectFileMetadata[],
|
redirectFiles: RedirectFile[],
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await Promise.all(redirectFiles.map(writeRedirectFile));
|
await Promise.all(redirectFiles.map(writeRedirectFile));
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,7 +44,7 @@ Accepted fields:
|
||||||
| `fromExtensions` | `string[]` | `[]` | The extensions to be removed from the route after redirecting. |
|
| `fromExtensions` | `string[]` | `[]` | The extensions to be removed from the route after redirecting. |
|
||||||
| `toExtensions` | `string[]` | `[]` | The extensions to be appended to 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. |
|
| `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
|
```mdx-code-block
|
||||||
</APITable>
|
</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}
|
#### `CreateRedirectsFn` {#CreateRedirectsFn}
|
||||||
|
|
||||||
```ts
|
```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;
|
type CreateRedirectsFn = (path: string) => string[] | string | null | undefined;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue