mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-15 18:17:35 +02:00
fix(v2): redirect plugin: use siteConfig.trailingSlash (#4988)
This commit is contained in:
parent
80b6d9728e
commit
b54ec72389
15 changed files with 443 additions and 73 deletions
|
@ -20,6 +20,7 @@
|
||||||
"@docusaurus/core": "2.0.0-beta.0",
|
"@docusaurus/core": "2.0.0-beta.0",
|
||||||
"@docusaurus/types": "2.0.0-beta.0",
|
"@docusaurus/types": "2.0.0-beta.0",
|
||||||
"@docusaurus/utils": "2.0.0-beta.0",
|
"@docusaurus/utils": "2.0.0-beta.0",
|
||||||
|
"@docusaurus/utils-common": "2.0.0-beta.0",
|
||||||
"@docusaurus/utils-validation": "2.0.0-beta.0",
|
"@docusaurus/utils-validation": "2.0.0-beta.0",
|
||||||
"chalk": "^4.1.1",
|
"chalk": "^4.1.1",
|
||||||
"eta": "^1.11.0",
|
"eta": "^1.11.0",
|
||||||
|
|
|
@ -32,7 +32,83 @@ Array [
|
||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`toRedirectFilesMetadata should create appropriate metadatas: fileContent 1`] = `
|
exports[`toRedirectFilesMetadata should create appropriate metadatas trailingSlash=false: fileContent 1`] = `
|
||||||
|
Array [
|
||||||
|
"<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset=\\"UTF-8\\">
|
||||||
|
<meta http-equiv=\\"refresh\\" content=\\"0; url=https://docusaurus.io/abc\\">
|
||||||
|
<link rel=\\"canonical\\" href=\\"https://docusaurus.io/abc\\" />
|
||||||
|
</head>
|
||||||
|
<script>
|
||||||
|
window.location.href = 'https://docusaurus.io/abc';
|
||||||
|
</script>
|
||||||
|
</html>",
|
||||||
|
"<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset=\\"UTF-8\\">
|
||||||
|
<meta http-equiv=\\"refresh\\" content=\\"0; url=https://docusaurus.io/def.html\\">
|
||||||
|
<link rel=\\"canonical\\" href=\\"https://docusaurus.io/def.html\\" />
|
||||||
|
</head>
|
||||||
|
<script>
|
||||||
|
window.location.href = 'https://docusaurus.io/def.html';
|
||||||
|
</script>
|
||||||
|
</html>",
|
||||||
|
"<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset=\\"UTF-8\\">
|
||||||
|
<meta http-equiv=\\"refresh\\" content=\\"0; url=https://docusaurus.io/\\">
|
||||||
|
<link rel=\\"canonical\\" href=\\"https://docusaurus.io/\\" />
|
||||||
|
</head>
|
||||||
|
<script>
|
||||||
|
window.location.href = 'https://docusaurus.io/';
|
||||||
|
</script>
|
||||||
|
</html>",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`toRedirectFilesMetadata should create appropriate metadatas trailingSlash=true: fileContent 1`] = `
|
||||||
|
Array [
|
||||||
|
"<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset=\\"UTF-8\\">
|
||||||
|
<meta http-equiv=\\"refresh\\" content=\\"0; url=https://docusaurus.io/abc\\">
|
||||||
|
<link rel=\\"canonical\\" href=\\"https://docusaurus.io/abc\\" />
|
||||||
|
</head>
|
||||||
|
<script>
|
||||||
|
window.location.href = 'https://docusaurus.io/abc';
|
||||||
|
</script>
|
||||||
|
</html>",
|
||||||
|
"<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset=\\"UTF-8\\">
|
||||||
|
<meta http-equiv=\\"refresh\\" content=\\"0; url=https://docusaurus.io/def.html\\">
|
||||||
|
<link rel=\\"canonical\\" href=\\"https://docusaurus.io/def.html\\" />
|
||||||
|
</head>
|
||||||
|
<script>
|
||||||
|
window.location.href = 'https://docusaurus.io/def.html';
|
||||||
|
</script>
|
||||||
|
</html>",
|
||||||
|
"<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset=\\"UTF-8\\">
|
||||||
|
<meta http-equiv=\\"refresh\\" content=\\"0; url=https://docusaurus.io/\\">
|
||||||
|
<link rel=\\"canonical\\" href=\\"https://docusaurus.io/\\" />
|
||||||
|
</head>
|
||||||
|
<script>
|
||||||
|
window.location.href = 'https://docusaurus.io/';
|
||||||
|
</script>
|
||||||
|
</html>",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`toRedirectFilesMetadata should create appropriate metadatas trailingSlash=undefined: fileContent 1`] = `
|
||||||
Array [
|
Array [
|
||||||
"<!DOCTYPE html>
|
"<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
|
|
|
@ -25,12 +25,17 @@ function createTestPluginContext(
|
||||||
describe('collectRedirects', () => {
|
describe('collectRedirects', () => {
|
||||||
test('should collect no redirect for undefined config', () => {
|
test('should collect no redirect for undefined config', () => {
|
||||||
expect(
|
expect(
|
||||||
collectRedirects(createTestPluginContext(undefined, ['/', '/path'])),
|
collectRedirects(
|
||||||
|
createTestPluginContext(undefined, ['/', '/path']),
|
||||||
|
undefined,
|
||||||
|
),
|
||||||
).toEqual([]);
|
).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should collect no redirect for empty config', () => {
|
test('should collect no redirect for empty config', () => {
|
||||||
expect(collectRedirects(createTestPluginContext({}))).toEqual([]);
|
expect(collectRedirects(createTestPluginContext({}), undefined)).toEqual(
|
||||||
|
[],
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should collect redirects to html/exe extension', () => {
|
test('should collect redirects to html/exe extension', () => {
|
||||||
|
@ -42,6 +47,7 @@ describe('collectRedirects', () => {
|
||||||
},
|
},
|
||||||
['/', '/somePath', '/otherPath.html'],
|
['/', '/somePath', '/otherPath.html'],
|
||||||
),
|
),
|
||||||
|
undefined,
|
||||||
),
|
),
|
||||||
).toEqual([
|
).toEqual([
|
||||||
{
|
{
|
||||||
|
@ -64,6 +70,7 @@ describe('collectRedirects', () => {
|
||||||
},
|
},
|
||||||
['/', '/somePath', '/otherPath.html'],
|
['/', '/somePath', '/otherPath.html'],
|
||||||
),
|
),
|
||||||
|
undefined,
|
||||||
),
|
),
|
||||||
).toEqual([
|
).toEqual([
|
||||||
{
|
{
|
||||||
|
@ -91,6 +98,79 @@ describe('collectRedirects', () => {
|
||||||
},
|
},
|
||||||
['/', '/somePath'],
|
['/', '/somePath'],
|
||||||
),
|
),
|
||||||
|
undefined,
|
||||||
|
),
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
|
from: '/someLegacyPath',
|
||||||
|
to: '/somePath',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: '/someLegacyPathArray1',
|
||||||
|
to: '/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: '/someLegacyPathArray2',
|
||||||
|
to: '/',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should collect redirects from plugin option redirects with trailingSlash=true', () => {
|
||||||
|
expect(
|
||||||
|
collectRedirects(
|
||||||
|
createTestPluginContext(
|
||||||
|
{
|
||||||
|
redirects: [
|
||||||
|
{
|
||||||
|
from: '/someLegacyPath',
|
||||||
|
to: '/somePath',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: ['/someLegacyPathArray1', '/someLegacyPathArray2'],
|
||||||
|
to: '/',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
['/', '/somePath/'],
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
|
from: '/someLegacyPath',
|
||||||
|
to: '/somePath/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: '/someLegacyPathArray1',
|
||||||
|
to: '/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: '/someLegacyPathArray2',
|
||||||
|
to: '/',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should collect redirects from plugin option redirects with trailingSlash=false', () => {
|
||||||
|
expect(
|
||||||
|
collectRedirects(
|
||||||
|
createTestPluginContext(
|
||||||
|
{
|
||||||
|
redirects: [
|
||||||
|
{
|
||||||
|
from: '/someLegacyPath',
|
||||||
|
to: '/somePath/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: ['/someLegacyPathArray1', '/someLegacyPathArray2'],
|
||||||
|
to: '/',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
['/', '/somePath'],
|
||||||
|
),
|
||||||
|
false,
|
||||||
),
|
),
|
||||||
).toEqual([
|
).toEqual([
|
||||||
{
|
{
|
||||||
|
@ -130,6 +210,7 @@ describe('collectRedirects', () => {
|
||||||
},
|
},
|
||||||
['/', '/someExistingPath', '/anotherExistingPath'],
|
['/', '/someExistingPath', '/anotherExistingPath'],
|
||||||
),
|
),
|
||||||
|
undefined,
|
||||||
),
|
),
|
||||||
).toThrowErrorMatchingSnapshot();
|
).toThrowErrorMatchingSnapshot();
|
||||||
});
|
});
|
||||||
|
@ -148,6 +229,7 @@ describe('collectRedirects', () => {
|
||||||
},
|
},
|
||||||
['/', '/testpath', '/otherPath.html'],
|
['/', '/testpath', '/otherPath.html'],
|
||||||
),
|
),
|
||||||
|
undefined,
|
||||||
),
|
),
|
||||||
).toEqual([
|
).toEqual([
|
||||||
{
|
{
|
||||||
|
@ -197,6 +279,7 @@ describe('collectRedirects', () => {
|
||||||
},
|
},
|
||||||
['/'],
|
['/'],
|
||||||
),
|
),
|
||||||
|
undefined,
|
||||||
),
|
),
|
||||||
).toThrowErrorMatchingSnapshot();
|
).toThrowErrorMatchingSnapshot();
|
||||||
});
|
});
|
||||||
|
@ -215,6 +298,7 @@ describe('collectRedirects', () => {
|
||||||
},
|
},
|
||||||
['/'],
|
['/'],
|
||||||
),
|
),
|
||||||
|
undefined,
|
||||||
),
|
),
|
||||||
).toThrowErrorMatchingSnapshot();
|
).toThrowErrorMatchingSnapshot();
|
||||||
});
|
});
|
||||||
|
@ -236,6 +320,7 @@ describe('collectRedirects', () => {
|
||||||
'/toShouldWork',
|
'/toShouldWork',
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
undefined,
|
||||||
),
|
),
|
||||||
).toEqual([
|
).toEqual([
|
||||||
{
|
{
|
||||||
|
|
|
@ -42,7 +42,7 @@ describe('createToUrl', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('toRedirectFilesMetadata', () => {
|
describe('toRedirectFilesMetadata', () => {
|
||||||
test('should create appropriate metadatas', async () => {
|
test('should create appropriate metadatas trailingSlash=undefined', async () => {
|
||||||
const pluginContext = {
|
const pluginContext = {
|
||||||
outDir: '/tmp/someFixedOutDir',
|
outDir: '/tmp/someFixedOutDir',
|
||||||
baseUrl: 'https://docusaurus.io',
|
baseUrl: 'https://docusaurus.io',
|
||||||
|
@ -55,10 +55,11 @@ describe('toRedirectFilesMetadata', () => {
|
||||||
{from: '/xyz', to: '/'},
|
{from: '/xyz', to: '/'},
|
||||||
],
|
],
|
||||||
pluginContext,
|
pluginContext,
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(redirectFiles.map((f) => f.fileAbsolutePath)).toEqual([
|
expect(redirectFiles.map((f) => f.fileAbsolutePath)).toEqual([
|
||||||
path.join(pluginContext.outDir, '/abc.html/index.html'),
|
path.join(pluginContext.outDir, '/abc.html'),
|
||||||
path.join(pluginContext.outDir, '/def/index.html'),
|
path.join(pluginContext.outDir, '/def/index.html'),
|
||||||
path.join(pluginContext.outDir, '/xyz/index.html'),
|
path.join(pluginContext.outDir, '/xyz/index.html'),
|
||||||
]);
|
]);
|
||||||
|
@ -68,6 +69,60 @@ describe('toRedirectFilesMetadata', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should create appropriate metadatas trailingSlash=true', async () => {
|
||||||
|
const pluginContext = {
|
||||||
|
outDir: '/tmp/someFixedOutDir',
|
||||||
|
baseUrl: 'https://docusaurus.io',
|
||||||
|
};
|
||||||
|
|
||||||
|
const redirectFiles = toRedirectFilesMetadata(
|
||||||
|
[
|
||||||
|
{from: '/abc.html', to: '/abc'},
|
||||||
|
{from: '/def', to: '/def.html'},
|
||||||
|
{from: '/xyz', to: '/'},
|
||||||
|
],
|
||||||
|
pluginContext,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(redirectFiles.map((f) => f.fileAbsolutePath)).toEqual([
|
||||||
|
path.join(pluginContext.outDir, '/abc.html'),
|
||||||
|
path.join(pluginContext.outDir, '/def/index.html'),
|
||||||
|
path.join(pluginContext.outDir, '/xyz/index.html'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(redirectFiles.map((f) => f.fileContent)).toMatchSnapshot(
|
||||||
|
'fileContent',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should create appropriate metadatas trailingSlash=false', async () => {
|
||||||
|
const pluginContext = {
|
||||||
|
outDir: '/tmp/someFixedOutDir',
|
||||||
|
baseUrl: 'https://docusaurus.io',
|
||||||
|
};
|
||||||
|
|
||||||
|
const redirectFiles = toRedirectFilesMetadata(
|
||||||
|
[
|
||||||
|
{from: '/abc.html', to: '/abc'},
|
||||||
|
{from: '/def', to: '/def.html'},
|
||||||
|
{from: '/xyz', to: '/'},
|
||||||
|
],
|
||||||
|
pluginContext,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(redirectFiles.map((f) => f.fileAbsolutePath)).toEqual([
|
||||||
|
path.join(pluginContext.outDir, '/abc.html'),
|
||||||
|
path.join(pluginContext.outDir, '/def.html'),
|
||||||
|
path.join(pluginContext.outDir, '/xyz.html'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(redirectFiles.map((f) => f.fileContent)).toMatchSnapshot(
|
||||||
|
'fileContent',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('should create appropriate metadatas for root baseUrl', async () => {
|
test('should create appropriate metadatas for root baseUrl', async () => {
|
||||||
const pluginContext = {
|
const pluginContext = {
|
||||||
outDir: '/tmp/someFixedOutDir',
|
outDir: '/tmp/someFixedOutDir',
|
||||||
|
@ -76,6 +131,7 @@ describe('toRedirectFilesMetadata', () => {
|
||||||
const redirectFiles = toRedirectFilesMetadata(
|
const redirectFiles = toRedirectFilesMetadata(
|
||||||
[{from: '/abc.html', to: '/abc'}],
|
[{from: '/abc.html', to: '/abc'}],
|
||||||
pluginContext,
|
pluginContext,
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
expect(redirectFiles.map((f) => f.fileContent)).toMatchSnapshot(
|
expect(redirectFiles.map((f) => f.fileContent)).toMatchSnapshot(
|
||||||
'fileContent baseUrl=/',
|
'fileContent baseUrl=/',
|
||||||
|
@ -90,6 +146,7 @@ describe('toRedirectFilesMetadata', () => {
|
||||||
const redirectFiles = toRedirectFilesMetadata(
|
const redirectFiles = toRedirectFilesMetadata(
|
||||||
[{from: '/abc.html', to: '/abc'}],
|
[{from: '/abc.html', to: '/abc'}],
|
||||||
pluginContext,
|
pluginContext,
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
expect(redirectFiles.map((f) => f.fileContent)).toMatchSnapshot(
|
expect(redirectFiles.map((f) => f.fileContent)).toMatchSnapshot(
|
||||||
'fileContent baseUrl=empty',
|
'fileContent baseUrl=empty',
|
||||||
|
|
|
@ -17,17 +17,36 @@ import {
|
||||||
createToExtensionsRedirects,
|
createToExtensionsRedirects,
|
||||||
} from './extensionRedirects';
|
} from './extensionRedirects';
|
||||||
import {validateRedirect} from './redirectValidation';
|
import {validateRedirect} from './redirectValidation';
|
||||||
|
import {applyTrailingSlash} from '@docusaurus/utils-common';
|
||||||
|
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
|
|
||||||
export default function collectRedirects(
|
export default function collectRedirects(
|
||||||
pluginContext: PluginContext,
|
pluginContext: PluginContext,
|
||||||
|
trailingSlash: boolean | undefined,
|
||||||
): RedirectMetadata[] {
|
): RedirectMetadata[] {
|
||||||
const redirects = doCollectRedirects(pluginContext);
|
let redirects = doCollectRedirects(pluginContext);
|
||||||
|
redirects = applyRedirectsTrailingSlash(redirects, trailingSlash);
|
||||||
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[],
|
||||||
|
trailingSlash: boolean | undefined,
|
||||||
|
) {
|
||||||
|
return redirects.map((redirect) => {
|
||||||
|
return {
|
||||||
|
...redirect,
|
||||||
|
to: applyTrailingSlash(redirect.to, trailingSlash),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function validateCollectedRedirects(
|
function validateCollectedRedirects(
|
||||||
redirects: RedirectMetadata[],
|
redirects: RedirectMetadata[],
|
||||||
pluginContext: PluginContext,
|
pluginContext: PluginContext,
|
||||||
|
|
|
@ -15,13 +15,28 @@ import writeRedirectFiles, {
|
||||||
RedirectFileMetadata,
|
RedirectFileMetadata,
|
||||||
} from './writeRedirectFiles';
|
} from './writeRedirectFiles';
|
||||||
import {removePrefix, addLeadingSlash} from '@docusaurus/utils';
|
import {removePrefix, addLeadingSlash} from '@docusaurus/utils';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
|
||||||
export default function pluginClientRedirectsPages(
|
export default function pluginClientRedirectsPages(
|
||||||
_context: LoadContext,
|
context: LoadContext,
|
||||||
opts: UserPluginOptions,
|
opts: UserPluginOptions,
|
||||||
): Plugin<unknown> {
|
): Plugin<unknown> {
|
||||||
|
const {trailingSlash} = context.siteConfig;
|
||||||
|
|
||||||
const options = normalizePluginOptions(opts);
|
const options = normalizePluginOptions(opts);
|
||||||
|
|
||||||
|
// Special case, when using trailingSlash=false we output /xyz.html files instead of /xyz/index.html
|
||||||
|
// It makes no sense to use option fromExtensions=["html"]: the redirect files would be the same as the original files
|
||||||
|
if (options.fromExtensions.includes('html') && trailingSlash === false) {
|
||||||
|
console.warn(
|
||||||
|
chalk.yellow(`Using the Docusaurus redirect plugin with fromExtensions=['html'] and siteConfig.trailingSlash=false is prevented.
|
||||||
|
It would lead the redirect plugin to override all the page.html files created by Docusaurus.`),
|
||||||
|
);
|
||||||
|
options.fromExtensions = options.fromExtensions.filter(
|
||||||
|
(ext) => ext !== 'html',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: 'docusaurus-plugin-client-redirects',
|
name: 'docusaurus-plugin-client-redirects',
|
||||||
async postBuild(props: Props) {
|
async postBuild(props: Props) {
|
||||||
|
@ -34,11 +49,15 @@ export default function pluginClientRedirectsPages(
|
||||||
options,
|
options,
|
||||||
};
|
};
|
||||||
|
|
||||||
const redirects: RedirectMetadata[] = collectRedirects(pluginContext);
|
const redirects: RedirectMetadata[] = collectRedirects(
|
||||||
|
pluginContext,
|
||||||
|
trailingSlash,
|
||||||
|
);
|
||||||
|
|
||||||
const redirectFiles: RedirectFileMetadata[] = toRedirectFilesMetadata(
|
const redirectFiles: RedirectFileMetadata[] = toRedirectFilesMetadata(
|
||||||
redirects,
|
redirects,
|
||||||
pluginContext,
|
pluginContext,
|
||||||
|
trailingSlash,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Write files only at the end: make code more easy to test without IO
|
// Write files only at the end: make code more easy to test without IO
|
||||||
|
|
|
@ -27,6 +27,7 @@ export function createToUrl(baseUrl: string, to: string): string {
|
||||||
export function toRedirectFilesMetadata(
|
export function toRedirectFilesMetadata(
|
||||||
redirects: RedirectMetadata[],
|
redirects: RedirectMetadata[],
|
||||||
pluginContext: WriteFilesPluginContext,
|
pluginContext: WriteFilesPluginContext,
|
||||||
|
trailingSlash: boolean | undefined,
|
||||||
): RedirectFileMetadata[] {
|
): RedirectFileMetadata[] {
|
||||||
// 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
|
||||||
|
@ -36,10 +37,8 @@ export function toRedirectFilesMetadata(
|
||||||
});
|
});
|
||||||
|
|
||||||
const createFileMetadata = (redirect: RedirectMetadata) => {
|
const createFileMetadata = (redirect: RedirectMetadata) => {
|
||||||
const fileAbsolutePath = path.join(
|
const filePath = getFilePathForRoutePath(redirect.from, trailingSlash);
|
||||||
pluginContext.outDir,
|
const fileAbsolutePath = path.join(pluginContext.outDir, filePath);
|
||||||
getFilePathForRoutePath(redirect.from),
|
|
||||||
);
|
|
||||||
const toUrl = createToUrl(pluginContext.baseUrl, redirect.to);
|
const toUrl = createToUrl(pluginContext.baseUrl, redirect.to);
|
||||||
const fileContent = createPageContentMemoized(toUrl);
|
const fileContent = createPageContentMemoized(toUrl);
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -14,10 +14,20 @@ describe('applyTrailingSlash', () => {
|
||||||
expect(applyTrailingSlash('', undefined)).toEqual('');
|
expect(applyTrailingSlash('', undefined)).toEqual('');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should apply to /', () => {
|
test('should not apply to /', () => {
|
||||||
expect(applyTrailingSlash('/', true)).toEqual('/');
|
expect(applyTrailingSlash('/', true)).toEqual('/');
|
||||||
expect(applyTrailingSlash('/', false)).toEqual('');
|
expect(applyTrailingSlash('/', false)).toEqual('/');
|
||||||
expect(applyTrailingSlash('/', undefined)).toEqual('/');
|
expect(applyTrailingSlash('/', undefined)).toEqual('/');
|
||||||
|
|
||||||
|
expect(applyTrailingSlash('/?query#anchor', true)).toEqual(
|
||||||
|
'/?query#anchor',
|
||||||
|
);
|
||||||
|
expect(applyTrailingSlash('/?query#anchor', false)).toEqual(
|
||||||
|
'/?query#anchor',
|
||||||
|
);
|
||||||
|
expect(applyTrailingSlash('/?query#anchor', undefined)).toEqual(
|
||||||
|
'/?query#anchor',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not apply to #anchor links ', () => {
|
test('should not apply to #anchor links ', () => {
|
||||||
|
|
|
@ -9,8 +9,8 @@ export default function applyTrailingSlash(
|
||||||
path: string,
|
path: string,
|
||||||
trailingSlash: boolean | undefined,
|
trailingSlash: boolean | undefined,
|
||||||
): string {
|
): string {
|
||||||
// Never apply trailing slash to an anchor link
|
|
||||||
if (path.startsWith('#')) {
|
if (path.startsWith('#')) {
|
||||||
|
// Never apply trailing slash to an anchor link
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,7 +29,12 @@ export default function applyTrailingSlash(
|
||||||
|
|
||||||
// The trailing slash should be handled before the ?search#hash !
|
// The trailing slash should be handled before the ?search#hash !
|
||||||
const [pathname] = path.split(/[#?]/);
|
const [pathname] = path.split(/[#?]/);
|
||||||
const newPathname = trailingSlash
|
|
||||||
|
// Never transform '/' to ''
|
||||||
|
const newPathname =
|
||||||
|
pathname === '/'
|
||||||
|
? '/'
|
||||||
|
: trailingSlash
|
||||||
? addTrailingSlash(pathname)
|
? addTrailingSlash(pathname)
|
||||||
: removeTrailingSlash(pathname);
|
: removeTrailingSlash(pathname);
|
||||||
return path.replace(pathname, newPathname);
|
return path.replace(pathname, newPathname);
|
||||||
|
|
|
@ -0,0 +1,87 @@
|
||||||
|
/**
|
||||||
|
* 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 {getFilePathForRoutePath} from '../getFilePathForRoutePath';
|
||||||
|
import {posixPath} from '../posixPath';
|
||||||
|
|
||||||
|
describe('getFilePathForRoutePath trailingSlash=undefined', () => {
|
||||||
|
test('works for /', () => {
|
||||||
|
expect(posixPath(getFilePathForRoutePath('/', undefined))).toEqual(
|
||||||
|
'/index.html',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('works for /somePath', () => {
|
||||||
|
expect(posixPath(getFilePathForRoutePath('/somePath', undefined))).toEqual(
|
||||||
|
'/somePath/index.html',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('works for /somePath/', () => {
|
||||||
|
expect(posixPath(getFilePathForRoutePath('/somePath/', undefined))).toEqual(
|
||||||
|
'/somePath/index.html',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('works for /somePath/xyz.html', () => {
|
||||||
|
expect(
|
||||||
|
posixPath(getFilePathForRoutePath('/somePath/xyz.html', undefined)),
|
||||||
|
).toEqual('/somePath/xyz.html');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getFilePathForRoutePath trailingSlash=true', () => {
|
||||||
|
test('works for /', () => {
|
||||||
|
expect(posixPath(getFilePathForRoutePath('/', true))).toEqual(
|
||||||
|
'/index.html',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('works for /somePath', () => {
|
||||||
|
expect(posixPath(getFilePathForRoutePath('/somePath', true))).toEqual(
|
||||||
|
'/somePath/index.html',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('works for /somePath/', () => {
|
||||||
|
expect(posixPath(getFilePathForRoutePath('/somePath/', true))).toEqual(
|
||||||
|
'/somePath/index.html',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('works for /somePath/xyz.html', () => {
|
||||||
|
expect(
|
||||||
|
posixPath(getFilePathForRoutePath('/somePath/xyz.html', true)),
|
||||||
|
).toEqual('/somePath/xyz.html');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getFilePathForRoutePath trailingSlash=false', () => {
|
||||||
|
test('works for /', () => {
|
||||||
|
expect(posixPath(getFilePathForRoutePath('/', false))).toEqual(
|
||||||
|
'/index.html',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('works for /somePath', () => {
|
||||||
|
expect(posixPath(getFilePathForRoutePath('/somePath', false))).toEqual(
|
||||||
|
'/somePath.html',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('works for /somePath/', () => {
|
||||||
|
expect(posixPath(getFilePathForRoutePath('/somePath/', false))).toEqual(
|
||||||
|
'/somePath/index.html',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('works for /somePath/xyz.html', () => {
|
||||||
|
expect(
|
||||||
|
posixPath(getFilePathForRoutePath('/somePath/xyz.html', false)),
|
||||||
|
).toEqual('/somePath/xyz.html');
|
||||||
|
});
|
||||||
|
});
|
|
@ -21,7 +21,6 @@ import {
|
||||||
removeTrailingSlash,
|
removeTrailingSlash,
|
||||||
removeSuffix,
|
removeSuffix,
|
||||||
removePrefix,
|
removePrefix,
|
||||||
getFilePathForRoutePath,
|
|
||||||
addLeadingSlash,
|
addLeadingSlash,
|
||||||
getElementsAround,
|
getElementsAround,
|
||||||
mergeTranslations,
|
mergeTranslations,
|
||||||
|
@ -401,22 +400,6 @@ describe('removePrefix', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getFilePathForRoutePath', () => {
|
|
||||||
test('works for /', () => {
|
|
||||||
expect(posixPath(getFilePathForRoutePath('/'))).toEqual('/index.html');
|
|
||||||
});
|
|
||||||
test('works for /somePath', () => {
|
|
||||||
expect(posixPath(getFilePathForRoutePath('/somePath'))).toEqual(
|
|
||||||
'/somePath/index.html',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
test('works for /somePath/', () => {
|
|
||||||
expect(posixPath(getFilePathForRoutePath('/somePath/'))).toEqual(
|
|
||||||
'/somePath/index.html',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getElementsAround', () => {
|
describe('getElementsAround', () => {
|
||||||
test('can return elements around', () => {
|
test('can return elements around', () => {
|
||||||
expect(getElementsAround(['a', 'b', 'c', 'd'], 0)).toEqual({
|
expect(getElementsAround(['a', 'b', 'c', 'd'], 0)).toEqual({
|
||||||
|
|
43
packages/docusaurus-utils/src/getFilePathForRoutePath.ts
Normal file
43
packages/docusaurus-utils/src/getFilePathForRoutePath.ts
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
/**
|
||||||
|
* 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 path from 'path';
|
||||||
|
|
||||||
|
/*
|
||||||
|
export function getFilePathForRoutePath(routePath: string): string {
|
||||||
|
const fileName = path.basename(routePath);
|
||||||
|
const filePath = path.dirname(routePath);
|
||||||
|
return path.join(filePath, `${fileName}/index.html`);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Almost exact copy of the behavior we implemented in our Docusaurus fork of the webpack static gen plugin
|
||||||
|
// See https://github.com/slorber/static-site-generator-webpack-plugin/blob/master/index.js#L167
|
||||||
|
export function getFilePathForRoutePath(
|
||||||
|
routePath: string,
|
||||||
|
trailingSlash: boolean | undefined,
|
||||||
|
): string {
|
||||||
|
// const outputFileName = routePath.replace(/^(\/|\\)/, ''); // Remove leading slashes for webpack-dev-server
|
||||||
|
|
||||||
|
// Paths ending with .html are left untouched
|
||||||
|
if (/\.(html?)$/i.test(routePath)) {
|
||||||
|
return routePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy retro-compatible behavior
|
||||||
|
if (typeof trailingSlash === 'undefined') {
|
||||||
|
return path.join(routePath, 'index.html');
|
||||||
|
}
|
||||||
|
|
||||||
|
// New behavior: we can say if we prefer file/folder output
|
||||||
|
// Useful resource: https://github.com/slorber/trailing-slash-guide
|
||||||
|
if (routePath === '' || routePath.endsWith('/') || trailingSlash) {
|
||||||
|
return path.join(routePath, 'index.html');
|
||||||
|
} else {
|
||||||
|
return `${routePath}.html`;
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,6 +27,7 @@ import {docuHash} from './docuHash';
|
||||||
|
|
||||||
export const posixPath = posixPathImport;
|
export const posixPath = posixPathImport;
|
||||||
|
|
||||||
|
export * from './getFilePathForRoutePath';
|
||||||
export * from './codeTranslationsUtils';
|
export * from './codeTranslationsUtils';
|
||||||
export * from './markdownParser';
|
export * from './markdownParser';
|
||||||
export * from './markdownLinks';
|
export * from './markdownLinks';
|
||||||
|
@ -325,12 +326,6 @@ export function removePrefix(str: string, prefix: string): string {
|
||||||
return str.startsWith(prefix) ? str.slice(prefix.length) : str;
|
return str.startsWith(prefix) ? str.slice(prefix.length) : str;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getFilePathForRoutePath(routePath: string): string {
|
|
||||||
const fileName = path.basename(routePath);
|
|
||||||
const filePath = path.dirname(routePath);
|
|
||||||
return path.join(filePath, `${fileName}/index.html`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getElementsAround<T extends unknown>(
|
export function getElementsAround<T extends unknown>(
|
||||||
array: T[],
|
array: T[],
|
||||||
aroundIndex: number,
|
aroundIndex: number,
|
||||||
|
|
|
@ -12,11 +12,6 @@ export default function applyRouteTrailingSlash(
|
||||||
route: RouteConfig,
|
route: RouteConfig,
|
||||||
trailingSlash: boolean | undefined,
|
trailingSlash: boolean | undefined,
|
||||||
) {
|
) {
|
||||||
// Never transform "/" to "" => cause router issues ("" catch everything)
|
|
||||||
if (route.path === '/') {
|
|
||||||
return route;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...route,
|
...route,
|
||||||
path: applyTrailingSlash(route.path, trailingSlash),
|
path: applyTrailingSlash(route.path, trailingSlash),
|
||||||
|
|
|
@ -132,11 +132,7 @@ const isVersioningDisabled = !!process.env.DISABLE_VERSIONING || isI18nStaging;
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'@docusaurus/plugin-client-redirects',
|
'@docusaurus/plugin-client-redirects',
|
||||||
isDeployPreview
|
{
|
||||||
? // Plugin is disabled for deploy preview because we use trailing slashes on deploy previews
|
|
||||||
// This plugin is sensitive to trailing slashes, and we don't care much about making it work on deploy previews
|
|
||||||
{}
|
|
||||||
: {
|
|
||||||
fromExtensions: ['html'],
|
fromExtensions: ['html'],
|
||||||
createRedirects: function (path) {
|
createRedirects: function (path) {
|
||||||
// redirect to /docs from /docs/introduction,
|
// redirect to /docs from /docs/introduction,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue