mirror of
https://github.com/facebook/docusaurus.git
synced 2025-06-15 09:12:24 +02:00
feat(v2): docusaurus-plugin-client-redirects
This commit is contained in:
parent
05c3aa31f4
commit
fd47ca1925
11 changed files with 300 additions and 0 deletions
|
@ -13,6 +13,7 @@ packages/docusaurus-1.x/lib/core/__tests__/split-tab.test.js
|
||||||
packages/docusaurus-utils/lib/
|
packages/docusaurus-utils/lib/
|
||||||
packages/docusaurus/lib/
|
packages/docusaurus/lib/
|
||||||
packages/docusaurus-init/lib/
|
packages/docusaurus-init/lib/
|
||||||
|
packages/docusaurus-plugin-client-redirects/lib/
|
||||||
packages/docusaurus-plugin-content-blog/lib/
|
packages/docusaurus-plugin-content-blog/lib/
|
||||||
packages/docusaurus-plugin-content-docs/lib/
|
packages/docusaurus-plugin-content-docs/lib/
|
||||||
packages/docusaurus-plugin-content-pages/lib/
|
packages/docusaurus-plugin-content-pages/lib/
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -17,6 +17,7 @@ types
|
||||||
packages/docusaurus-utils/lib/
|
packages/docusaurus-utils/lib/
|
||||||
packages/docusaurus/lib/
|
packages/docusaurus/lib/
|
||||||
packages/docusaurus-init/lib/
|
packages/docusaurus-init/lib/
|
||||||
|
packages/docusaurus-plugin-client-redirects/lib/
|
||||||
packages/docusaurus-plugin-content-blog/lib/
|
packages/docusaurus-plugin-content-blog/lib/
|
||||||
packages/docusaurus-plugin-content-docs/lib/
|
packages/docusaurus-plugin-content-docs/lib/
|
||||||
packages/docusaurus-plugin-content-pages/lib/
|
packages/docusaurus-plugin-content-pages/lib/
|
||||||
|
|
26
packages/docusaurus-plugin-client-redirects/package.json
Normal file
26
packages/docusaurus-plugin-client-redirects/package.json
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"name": "@docusaurus/plugin-client-redirects",
|
||||||
|
"version": "2.0.0-alpha.55",
|
||||||
|
"description": "Client redirects plugin for Docusaurus",
|
||||||
|
"main": "lib/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"tsc": "tsc"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@docusaurus/types": "^2.0.0-alpha.55",
|
||||||
|
"@docusaurus/utils": "^2.0.0-alpha.55",
|
||||||
|
"globby": "^10.0.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@docusaurus/core": "^2.0.0",
|
||||||
|
"react": "^16.8.4",
|
||||||
|
"react-dom": "^16.8.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.9.0"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
type CreateRedirectPageOptions = {
|
||||||
|
toUrl: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function createRedirectPageContent({
|
||||||
|
toUrl,
|
||||||
|
}: CreateRedirectPageOptions) {
|
||||||
|
return `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en-US">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="refresh" content="0; url=${toUrl}">
|
||||||
|
<script>
|
||||||
|
const redirectLink = '${toUrl}' + location.search + location.hash;
|
||||||
|
document.write('<link rel="canonical" href="' + redirectLink + '">');
|
||||||
|
document.write('<title>Redirecting to ' + redirectLink + '</title>');
|
||||||
|
document.write('</head>')
|
||||||
|
document.write('<body>')
|
||||||
|
document.write('If you are not redirected automatically, follow this <a href="' + redirectLink + '">link</a>.')
|
||||||
|
document.write('</body>')
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.assign(redirectLink)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
}
|
144
packages/docusaurus-plugin-client-redirects/src/index.ts
Normal file
144
packages/docusaurus-plugin-client-redirects/src/index.ts
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
/**
|
||||||
|
* 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 fs from 'fs-extra';
|
||||||
|
import path from 'path';
|
||||||
|
import {flatten} from 'lodash';
|
||||||
|
|
||||||
|
import {LoadContext, Plugin, Props} from '@docusaurus/types';
|
||||||
|
|
||||||
|
import {PluginOptions, RedirectsCreator} from './types';
|
||||||
|
import createRedirectPageContent from './createRedirectPageContent';
|
||||||
|
import {addTrailingSlash, getFilePathForRoutePath} from './utils';
|
||||||
|
import {
|
||||||
|
fromExtensionsRedirectCreator,
|
||||||
|
toExtensionsRedirectCreator,
|
||||||
|
} from './redirectCreators';
|
||||||
|
|
||||||
|
const DEFAULT_OPTIONS: PluginOptions = {
|
||||||
|
fromExtensions: [],
|
||||||
|
toExtensions: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
type RedirectMetadata = {
|
||||||
|
fromRoutePath: string;
|
||||||
|
toRoutePath: string;
|
||||||
|
toUrl: string;
|
||||||
|
redirectPageContent: string;
|
||||||
|
redirectAbsoluteFilePath: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PluginContext = {
|
||||||
|
props: Props;
|
||||||
|
options: PluginOptions;
|
||||||
|
redirectsCreators: RedirectsCreator[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function pluginClientRedirectsPages(
|
||||||
|
_context: LoadContext,
|
||||||
|
opts: Partial<PluginOptions>,
|
||||||
|
): Plugin<unknown> {
|
||||||
|
const options = {...DEFAULT_OPTIONS, ...opts};
|
||||||
|
return {
|
||||||
|
name: 'docusaurus-plugin-client-redirects',
|
||||||
|
async postBuild(props: Props) {
|
||||||
|
const redirectsCreators: RedirectsCreator[] = buildRedirectCreators(
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
|
||||||
|
const pluginContext: PluginContext = {props, options, redirectsCreators};
|
||||||
|
// Process in 2 steps, to make code more easy to test
|
||||||
|
const redirects: RedirectMetadata[] = collectRoutePathRedirects(
|
||||||
|
pluginContext,
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('redirects=', redirects);
|
||||||
|
|
||||||
|
await writeRedirectFiles(redirects);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRedirectCreators(options: PluginOptions): RedirectsCreator[] {
|
||||||
|
const noopRedirectCreator: RedirectsCreator = (_routePath: string) => [];
|
||||||
|
return [
|
||||||
|
fromExtensionsRedirectCreator(options.fromExtensions),
|
||||||
|
toExtensionsRedirectCreator(options.toExtensions),
|
||||||
|
options.createRedirects ?? noopRedirectCreator,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectRoutePathRedirects(
|
||||||
|
pluginContext: PluginContext,
|
||||||
|
): RedirectMetadata[] {
|
||||||
|
return flatten(
|
||||||
|
pluginContext.redirectsCreators.map((redirectCreator) => {
|
||||||
|
return createRoutesPathsRedirects(redirectCreator, pluginContext);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create all redirects for a list of route path
|
||||||
|
function createRoutesPathsRedirects(
|
||||||
|
redirectCreator: RedirectsCreator,
|
||||||
|
pluginContext: PluginContext,
|
||||||
|
): RedirectMetadata[] {
|
||||||
|
return flatten(
|
||||||
|
pluginContext.props.routesPaths.map((routePath) =>
|
||||||
|
createRoutePathRedirects(routePath, redirectCreator, pluginContext),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create all redirects for a single route path
|
||||||
|
function createRoutePathRedirects(
|
||||||
|
routePath: string,
|
||||||
|
redirectCreator: RedirectsCreator,
|
||||||
|
{props}: PluginContext,
|
||||||
|
): RedirectMetadata[] {
|
||||||
|
const {siteConfig, outDir} = props;
|
||||||
|
|
||||||
|
// TODO do we receive absolute urls???
|
||||||
|
if (!path.isAbsolute(routePath)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO addTrailingSlash ?
|
||||||
|
const toUrl = addTrailingSlash(`${siteConfig.url}${routePath}`);
|
||||||
|
|
||||||
|
const redirectPageContent = createRedirectPageContent({toUrl});
|
||||||
|
|
||||||
|
const fromRoutePaths: string[] = redirectCreator(routePath) ?? [];
|
||||||
|
|
||||||
|
return fromRoutePaths.map((fromRoutePath) => {
|
||||||
|
const redirectAbsoluteFilePath = path.join(
|
||||||
|
outDir,
|
||||||
|
getFilePathForRoutePath(fromRoutePath),
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
fromRoutePath,
|
||||||
|
toRoutePath: routePath,
|
||||||
|
toUrl,
|
||||||
|
redirectPageContent,
|
||||||
|
redirectAbsoluteFilePath,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeRedirectFiles(redirects: RedirectMetadata[]) {
|
||||||
|
async function writeRedirectFile(redirect: RedirectMetadata) {
|
||||||
|
try {
|
||||||
|
await fs.writeFile(
|
||||||
|
redirect.redirectAbsoluteFilePath,
|
||||||
|
redirect.redirectPageContent,
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(`Redirect file creation error: ${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await Promise.all(redirects.map(writeRedirectFile));
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
/**
|
||||||
|
* 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 {RedirectsCreator} from './types';
|
||||||
|
import {removeTrailingSlash} from './utils';
|
||||||
|
|
||||||
|
export function fromExtensionsRedirectCreator(
|
||||||
|
extensions: string[],
|
||||||
|
): RedirectsCreator {
|
||||||
|
const dottedExtensions = extensions.map((ext) => `.${ext}`);
|
||||||
|
return (fromRoutePath: string) => {
|
||||||
|
const extensionMatch = dottedExtensions.find((ext) =>
|
||||||
|
fromRoutePath.endsWith(`.${ext}`),
|
||||||
|
);
|
||||||
|
if (extensionMatch) {
|
||||||
|
const routePathWithoutExtension = fromRoutePath.substr(
|
||||||
|
0,
|
||||||
|
fromRoutePath.length - extensionMatch.length - 1,
|
||||||
|
);
|
||||||
|
return [routePathWithoutExtension];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toExtensionsRedirectCreator(
|
||||||
|
extensions: string[],
|
||||||
|
): RedirectsCreator {
|
||||||
|
return (fromRoutePath: string) => {
|
||||||
|
if (fromRoutePath === '/') {
|
||||||
|
return [];
|
||||||
|
} else {
|
||||||
|
const fromRoutePathNoSlash = removeTrailingSlash(fromRoutePath);
|
||||||
|
return extensions.map((ext) => `${fromRoutePathNoSlash}.${ext}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
18
packages/docusaurus-plugin-client-redirects/src/types.ts
Normal file
18
packages/docusaurus-plugin-client-redirects/src/types.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type PluginOptions = {
|
||||||
|
fromExtensions: string[];
|
||||||
|
toExtensions: string[];
|
||||||
|
createRedirects?: RedirectsCreator;
|
||||||
|
};
|
||||||
|
|
||||||
|
// For a given existing route path,
|
||||||
|
// return all the paths from which we should redirect from
|
||||||
|
export type RedirectsCreator = (
|
||||||
|
routePath: string,
|
||||||
|
) => string[] | null | undefined;
|
23
packages/docusaurus-plugin-client-redirects/src/utils.ts
Normal file
23
packages/docusaurus-plugin-client-redirects/src/utils.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
/**
|
||||||
|
* 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 addTrailingSlash(str: string) {
|
||||||
|
return str.endsWith('/') ? '' : '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeTrailingSlash(str: string) {
|
||||||
|
return str.endsWith('/') ? str.substr(0, str.length - 2) : str;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO does this function already exist?
|
||||||
|
export function getFilePathForRoutePath(routePath: string) {
|
||||||
|
const fileName = path.basename(routePath);
|
||||||
|
const filePath = path.dirname(routePath);
|
||||||
|
return path.join(filePath, `${fileName}.html`);
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"incremental": true,
|
||||||
|
"tsBuildInfoFile": "./lib/.tsbuildinfo",
|
||||||
|
"rootDir": "src",
|
||||||
|
"outDir": "lib"
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,6 +21,7 @@ module.exports = {
|
||||||
},
|
},
|
||||||
themes: ['@docusaurus/theme-live-codeblock'],
|
themes: ['@docusaurus/theme-live-codeblock'],
|
||||||
plugins: [
|
plugins: [
|
||||||
|
['@docusaurus/plugin-client-redirects', {}],
|
||||||
[
|
[
|
||||||
'@docusaurus/plugin-ideal-image',
|
'@docusaurus/plugin-ideal-image',
|
||||||
{
|
{
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@docusaurus/core": "^2.0.0-alpha.55",
|
"@docusaurus/core": "^2.0.0-alpha.55",
|
||||||
"@docusaurus/plugin-ideal-image": "^2.0.0-alpha.55",
|
"@docusaurus/plugin-ideal-image": "^2.0.0-alpha.55",
|
||||||
|
"@docusaurus/plugin-client-redirects": "^2.0.0-alpha.55",
|
||||||
"@docusaurus/preset-classic": "^2.0.0-alpha.55",
|
"@docusaurus/preset-classic": "^2.0.0-alpha.55",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"color": "^3.1.2",
|
"color": "^3.1.2",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue