diff --git a/.eslintignore b/.eslintignore
index 1dc5c56918..c56f7af44c 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -13,6 +13,7 @@ packages/docusaurus-1.x/lib/core/__tests__/split-tab.test.js
packages/docusaurus-utils/lib/
packages/docusaurus/lib/
packages/docusaurus-init/lib/
+packages/docusaurus-plugin-client-redirects/lib/
packages/docusaurus-plugin-content-blog/lib/
packages/docusaurus-plugin-content-docs/lib/
packages/docusaurus-plugin-content-pages/lib/
diff --git a/.gitignore b/.gitignore
index 3462819d8b..418b6c11d1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,6 +17,7 @@ types
packages/docusaurus-utils/lib/
packages/docusaurus/lib/
packages/docusaurus-init/lib/
+packages/docusaurus-plugin-client-redirects/lib/
packages/docusaurus-plugin-content-blog/lib/
packages/docusaurus-plugin-content-docs/lib/
packages/docusaurus-plugin-content-pages/lib/
diff --git a/packages/docusaurus-plugin-client-redirects/package.json b/packages/docusaurus-plugin-client-redirects/package.json
new file mode 100644
index 0000000000..a3041d5e6b
--- /dev/null
+++ b/packages/docusaurus-plugin-client-redirects/package.json
@@ -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"
+ }
+}
diff --git a/packages/docusaurus-plugin-client-redirects/src/createRedirectPageContent.ts b/packages/docusaurus-plugin-client-redirects/src/createRedirectPageContent.ts
new file mode 100644
index 0000000000..140a098569
--- /dev/null
+++ b/packages/docusaurus-plugin-client-redirects/src/createRedirectPageContent.ts
@@ -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 `
+
+
+
+
+
+
+
+`;
+}
diff --git a/packages/docusaurus-plugin-client-redirects/src/index.ts b/packages/docusaurus-plugin-client-redirects/src/index.ts
new file mode 100644
index 0000000000..27a0927227
--- /dev/null
+++ b/packages/docusaurus-plugin-client-redirects/src/index.ts
@@ -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,
+): Plugin {
+ 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));
+}
diff --git a/packages/docusaurus-plugin-client-redirects/src/redirectCreators.ts b/packages/docusaurus-plugin-client-redirects/src/redirectCreators.ts
new file mode 100644
index 0000000000..a889f776f6
--- /dev/null
+++ b/packages/docusaurus-plugin-client-redirects/src/redirectCreators.ts
@@ -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}`);
+ }
+ };
+}
diff --git a/packages/docusaurus-plugin-client-redirects/src/types.ts b/packages/docusaurus-plugin-client-redirects/src/types.ts
new file mode 100644
index 0000000000..9e8a7fe024
--- /dev/null
+++ b/packages/docusaurus-plugin-client-redirects/src/types.ts
@@ -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;
diff --git a/packages/docusaurus-plugin-client-redirects/src/utils.ts b/packages/docusaurus-plugin-client-redirects/src/utils.ts
new file mode 100644
index 0000000000..33e954e902
--- /dev/null
+++ b/packages/docusaurus-plugin-client-redirects/src/utils.ts
@@ -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`);
+}
diff --git a/packages/docusaurus-plugin-client-redirects/tsconfig.json b/packages/docusaurus-plugin-client-redirects/tsconfig.json
new file mode 100644
index 0000000000..f5902ba108
--- /dev/null
+++ b/packages/docusaurus-plugin-client-redirects/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "incremental": true,
+ "tsBuildInfoFile": "./lib/.tsbuildinfo",
+ "rootDir": "src",
+ "outDir": "lib"
+ }
+}
diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js
index ff1d8b53a6..ca973602b7 100644
--- a/website/docusaurus.config.js
+++ b/website/docusaurus.config.js
@@ -21,6 +21,7 @@ module.exports = {
},
themes: ['@docusaurus/theme-live-codeblock'],
plugins: [
+ ['@docusaurus/plugin-client-redirects', {}],
[
'@docusaurus/plugin-ideal-image',
{
diff --git a/website/package.json b/website/package.json
index 885ff449e3..bb43e441c7 100644
--- a/website/package.json
+++ b/website/package.json
@@ -11,6 +11,7 @@
"dependencies": {
"@docusaurus/core": "^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",
"classnames": "^2.2.6",
"color": "^3.1.2",