diff --git a/packages/docusaurus-types/src/index.d.ts b/packages/docusaurus-types/src/index.d.ts index cc607086d703..80aef41cdb2a 100644 --- a/packages/docusaurus-types/src/index.d.ts +++ b/packages/docusaurus-types/src/index.d.ts @@ -10,7 +10,7 @@ import {Command} from 'commander'; import {ParsedUrlQueryInput} from 'querystring'; import {MergeStrategy} from 'webpack-merge'; -export type OnBrokenLinks = 'ignore' | 'log' | 'error' | 'throw'; +export type ReportingSeverity = 'ignore' | 'log' | 'warn' | 'error' | 'throw'; export interface DocusaurusConfig { baseUrl: string; @@ -18,7 +18,8 @@ export interface DocusaurusConfig { tagline?: string; title: string; url: string; - onBrokenLinks: OnBrokenLinks; + onBrokenLinks: ReportingSeverity; + onDuplicateRoutes: ReportingSeverity; organizationName?: string; projectName?: string; githubHost?: string; diff --git a/packages/docusaurus/src/server/__tests__/__snapshots__/config.test.ts.snap b/packages/docusaurus/src/server/__tests__/__snapshots__/config.test.ts.snap index 6b24e10a1bf1..3d2e8b03e480 100644 --- a/packages/docusaurus/src/server/__tests__/__snapshots__/config.test.ts.snap +++ b/packages/docusaurus/src/server/__tests__/__snapshots__/config.test.ts.snap @@ -21,6 +21,7 @@ Object { "customFields": Object {}, "favicon": "img/docusaurus.ico", "onBrokenLinks": "throw", + "onDuplicateRoutes": "warn", "organizationName": "endiliey", "plugins": Array [ Array [ diff --git a/packages/docusaurus/src/server/__tests__/__snapshots__/duplicateRoutes.test.ts.snap b/packages/docusaurus/src/server/__tests__/__snapshots__/duplicateRoutes.test.ts.snap new file mode 100644 index 000000000000..072719acc483 --- /dev/null +++ b/packages/docusaurus/src/server/__tests__/__snapshots__/duplicateRoutes.test.ts.snap @@ -0,0 +1,15 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`duplicateRoutes getDuplicateRoutesMessage 1`] = ` +"Attempting to create page at /, but a page already exists at this route +Attempting to create page at /, but a page already exists at this route +Attempting to create page at /blog, but a page already exists at this route +Attempting to create page at /doc/search, but a page already exists at this route" +`; + +exports[`duplicateRoutes handleDuplicateRoutes 1`] = ` +"Duplicate routes found! +Attempting to create page at /search, but a page already exists at this route +Attempting to create page at /sameDoc, but a page already exists at this route +This could lead to non-deterministic routing behavior" +`; diff --git a/packages/docusaurus/src/server/__tests__/duplicateRoutes.test.ts b/packages/docusaurus/src/server/__tests__/duplicateRoutes.test.ts new file mode 100644 index 000000000000..f8fc65cd790b --- /dev/null +++ b/packages/docusaurus/src/server/__tests__/duplicateRoutes.test.ts @@ -0,0 +1,54 @@ +/** + * 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 { + getAllDuplicateRoutes, + getDuplicateRoutesMessage, + handleDuplicateRoutes, +} from '../duplicateRoutes'; +import {RouteConfig} from '@docusaurus/types'; + +const routes: RouteConfig[] = [ + { + path: '/', + component: '', + routes: [ + {path: '/search', component: ''}, + {path: '/sameDoc', component: ''}, + ], + }, + { + path: '/', + component: '', + routes: [ + {path: '/search', component: ''}, + {path: '/sameDoc', component: ''}, + {path: '/uniqueDoc', component: ''}, + ], + }, +]; + +describe('duplicateRoutes', () => { + test('getDuplicateRoutesMessage', () => { + const message = getDuplicateRoutesMessage([ + '/', + '/', + '/blog', + '/doc/search', + ]); + expect(message).toMatchSnapshot(); + }); + + test('getAllDuplicateRoutes', () => { + expect(getAllDuplicateRoutes(routes)).toEqual(['/search', '/sameDoc']); + }); + + test('handleDuplicateRoutes', () => { + expect(() => { + handleDuplicateRoutes(routes, 'throw'); + }).toThrowErrorMatchingSnapshot(); + }); +}); diff --git a/packages/docusaurus/src/server/__tests__/utils.test.ts b/packages/docusaurus/src/server/__tests__/utils.test.ts new file mode 100644 index 000000000000..6f395e755738 --- /dev/null +++ b/packages/docusaurus/src/server/__tests__/utils.test.ts @@ -0,0 +1,32 @@ +/** + * 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 {RouteConfig} from '@docusaurus/types'; +import {getAllFinalRoutes} from '../utils'; + +describe('getAllFinalRoutes', () => { + test('should get final routes correctly', () => { + const routes: RouteConfig[] = [ + { + path: '/docs', + component: '', + routes: [ + {path: '/docs/someDoc', component: ''}, + {path: '/docs/someOtherDoc', component: ''}, + ], + }, + { + path: '/community', + component: '', + }, + ]; + expect(getAllFinalRoutes(routes)).toEqual([ + routes[0].routes[0], + routes[0].routes[1], + routes[1], + ]); + }); +}); diff --git a/packages/docusaurus/src/server/brokenLinks.ts b/packages/docusaurus/src/server/brokenLinks.ts index 31080b58cc24..30b9637386ad 100644 --- a/packages/docusaurus/src/server/brokenLinks.ts +++ b/packages/docusaurus/src/server/brokenLinks.ts @@ -7,11 +7,11 @@ import {matchRoutes, RouteConfig as RRRouteConfig} from 'react-router-config'; import resolvePathname from 'resolve-pathname'; -import chalk from 'chalk'; import fs from 'fs-extra'; -import {mapValues, pickBy, flatMap} from 'lodash'; -import {RouteConfig, OnBrokenLinks} from '@docusaurus/types'; +import {mapValues, pickBy} from 'lodash'; +import {RouteConfig, ReportingSeverity} from '@docusaurus/types'; import {removePrefix} from '@docusaurus/utils'; +import {getAllFinalRoutes, reportMessage} from './utils'; function toReactRouterRoutes(routes: RouteConfig[]): RRRouteConfig[] { // @ts-expect-error: types incompatible??? @@ -59,12 +59,8 @@ function getPageBrokenLinks({ // For this reason, we only consider the "final routes", that do not have subroutes // We also need to remove the match all 404 route function filterIntermediateRoutes(routesInput: RouteConfig[]): RouteConfig[] { - function getFinalRoutes(route: RouteConfig): RouteConfig[] { - return route.routes ? flatMap(route.routes, getFinalRoutes) : [route]; - } - const routesWithout404 = routesInput.filter((route) => route.path !== '*'); - return flatMap(routesWithout404, getFinalRoutes); + return getAllFinalRoutes(routesWithout404); } export function getAllBrokenLinks({ @@ -152,7 +148,7 @@ export async function handleBrokenLinks({ outDir, }: { allCollectedLinks: Record; - onBrokenLinks: OnBrokenLinks; + onBrokenLinks: ReportingSeverity; routes: RouteConfig[]; baseUrl: string; outDir: string; @@ -177,16 +173,6 @@ export async function handleBrokenLinks({ const errorMessage = getBrokenLinksErrorMessage(allBrokenLinks); if (errorMessage) { const finalMessage = `${errorMessage}\nNote: it's possible to ignore broken links with the 'onBrokenLinks' Docusaurus configuration.\n\n`; - - // Useful to ensure the CI fails in case of broken link - if (onBrokenLinks === 'throw') { - throw new Error(finalMessage); - } else if (onBrokenLinks === 'error') { - console.error(chalk.red(finalMessage)); - } else if (onBrokenLinks === 'log') { - console.log(chalk.blue(finalMessage)); - } else { - throw new Error(`unexpected onBrokenLinks value=${onBrokenLinks}`); - } + reportMessage(finalMessage, onBrokenLinks); } } diff --git a/packages/docusaurus/src/server/configValidation.ts b/packages/docusaurus/src/server/configValidation.ts index aae824ccf076..0672cd69cf9a 100644 --- a/packages/docusaurus/src/server/configValidation.ts +++ b/packages/docusaurus/src/server/configValidation.ts @@ -11,11 +11,12 @@ import Joi from '@hapi/joi'; import { isValidationDisabledEscapeHatch, logValidationBugReportHint, -} from './validationUtils'; +} from './utils'; export const DEFAULT_CONFIG: Pick< DocusaurusConfig, | 'onBrokenLinks' + | 'onDuplicateRoutes' | 'plugins' | 'themes' | 'presets' @@ -23,6 +24,7 @@ export const DEFAULT_CONFIG: Pick< | 'themeConfig' > = { onBrokenLinks: 'throw', + onDuplicateRoutes: 'warn', plugins: [], themes: [], presets: [], @@ -54,8 +56,11 @@ const ConfigSchema = Joi.object({ title: Joi.string().required(), url: Joi.string().uri().required(), onBrokenLinks: Joi.string() - .equal('ignore', 'log', 'error', 'throw') + .equal('ignore', 'log', 'warn', 'error', 'throw') .default(DEFAULT_CONFIG.onBrokenLinks), + onDuplicateRoutes: Joi.string() + .equal('ignore', 'log', 'warn', 'error', 'throw') + .default(DEFAULT_CONFIG.onDuplicateRoutes), organizationName: Joi.string().allow(''), projectName: Joi.string().allow(''), customFields: Joi.object().unknown().default(DEFAULT_CONFIG.customFields), diff --git a/packages/docusaurus/src/server/duplicateRoutes.ts b/packages/docusaurus/src/server/duplicateRoutes.ts new file mode 100644 index 000000000000..869157b1994b --- /dev/null +++ b/packages/docusaurus/src/server/duplicateRoutes.ts @@ -0,0 +1,53 @@ +/** + * 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 {RouteConfig, ReportingSeverity} from '@docusaurus/types'; +import {getAllFinalRoutes, reportMessage} from './utils'; + +export function getAllDuplicateRoutes( + pluginsRouteConfigs: RouteConfig[], +): string[] { + const allRoutes: string[] = getAllFinalRoutes(pluginsRouteConfigs).map( + (routeConfig) => routeConfig.path, + ); + const seenRoutes: Record = {}; + const duplicateRoutes: string[] = allRoutes.filter(function (route) { + if (seenRoutes.hasOwnProperty(route)) { + return true; + } else { + seenRoutes[route] = true; + return false; + } + }); + return duplicateRoutes; +} + +export function getDuplicateRoutesMessage( + allDuplicateRoutes: string[], +): string { + const message = allDuplicateRoutes + .map( + (duplicateRoute) => + `Attempting to create page at ${duplicateRoute}, but a page already exists at this route`, + ) + .join('\n'); + return message; +} + +export function handleDuplicateRoutes( + pluginsRouteConfigs: RouteConfig[], + onDuplicateRoutes: ReportingSeverity, +): void { + if (onDuplicateRoutes === 'ignore') { + return; + } + const duplicatePaths: string[] = getAllDuplicateRoutes(pluginsRouteConfigs); + const message: string = getDuplicateRoutesMessage(duplicatePaths); + if (message) { + const finalMessage = `Duplicate routes found!\n${message}\nThis could lead to non-deterministic routing behavior`; + reportMessage(finalMessage, onDuplicateRoutes); + } +} diff --git a/packages/docusaurus/src/server/index.ts b/packages/docusaurus/src/server/index.ts index f00056a3b676..785ebb7ff9b7 100644 --- a/packages/docusaurus/src/server/index.ts +++ b/packages/docusaurus/src/server/index.ts @@ -28,6 +28,7 @@ import { } from '@docusaurus/types'; import {loadHtmlTags} from './html-tags'; import {getPackageJsonVersion} from './versions'; +import {handleDuplicateRoutes} from './duplicateRoutes'; export function loadContext( siteDir: string, @@ -79,6 +80,8 @@ export async function load( context, }); + handleDuplicateRoutes(pluginsRouteConfigs, siteConfig.onDuplicateRoutes); + // Site config must be generated after plugins // We want the generated config to have been normalized by the plugins! const genSiteConfig = generate( diff --git a/packages/docusaurus/src/server/plugins/init.ts b/packages/docusaurus/src/server/plugins/init.ts index 4e0dd1cc5c1c..9a2cb7ed36b7 100644 --- a/packages/docusaurus/src/server/plugins/init.ts +++ b/packages/docusaurus/src/server/plugins/init.ts @@ -23,7 +23,7 @@ import * as Joi from '@hapi/joi'; import { isValidationDisabledEscapeHatch, logValidationBugReportHint, -} from '../validationUtils'; +} from '../utils'; function pluginOptionsValidator( schema: ValidationSchema, diff --git a/packages/docusaurus/src/server/validationUtils.ts b/packages/docusaurus/src/server/utils.ts similarity index 55% rename from packages/docusaurus/src/server/validationUtils.ts rename to packages/docusaurus/src/server/utils.ts index 2fe418482d7e..12f2247993cb 100644 --- a/packages/docusaurus/src/server/validationUtils.ts +++ b/packages/docusaurus/src/server/utils.ts @@ -5,6 +5,8 @@ * LICENSE file in the root directory of this source tree. */ import chalk from 'chalk'; +import flatMap from 'lodash.flatmap'; +import {RouteConfig, ReportingSeverity} from '@docusaurus/types'; // TODO temporary escape hatch for alpha-60: to be removed soon // Our validation schemas might be buggy at first @@ -31,3 +33,36 @@ export const logValidationBugReportHint = () => { )}\n`, ); }; + +// Recursively get the final routes (routes with no subroutes) +export function getAllFinalRoutes(routeConfig: RouteConfig[]): RouteConfig[] { + function getFinalRoutes(route: RouteConfig): RouteConfig[] { + return route.routes ? flatMap(route.routes, getFinalRoutes) : [route]; + } + return flatMap(routeConfig, getFinalRoutes); +} + +export function reportMessage( + message: string, + reportingSeverity: ReportingSeverity, +): void { + switch (reportingSeverity) { + case 'ignore': + break; + case 'log': + console.log(chalk.bold.blue('info ') + chalk.blue(message)); + break; + case 'warn': + console.warn(chalk.bold.yellow('warn ') + chalk.yellow(message)); + break; + case 'error': + console.error(chalk.bold.red('error ') + chalk.red(message)); + break; + case 'throw': + throw new Error(message); + default: + throw new Error( + `unexpected reportingSeverity value: ${reportingSeverity}`, + ); + } +} diff --git a/website/docs/docusaurus.config.js.md b/website/docs/api/docusaurus.config.js.md similarity index 94% rename from website/docs/docusaurus.config.js.md rename to website/docs/api/docusaurus.config.js.md index 20a0914c822a..c7422f7ab924 100644 --- a/website/docs/docusaurus.config.js.md +++ b/website/docs/api/docusaurus.config.js.md @@ -2,6 +2,7 @@ id: docusaurus.config.js title: docusaurus.config.js description: API reference for Docusaurus configuration file. +slug: /docusaurus.config.js --- ## Overview @@ -81,7 +82,7 @@ module.exports = { ### `onBrokenLinks` -- Type: `'ignore' | 'log' | 'error' | 'throw'` +- Type: `'ignore' | 'log' | 'warn' | 'error' | 'throw'` The behavior of Docusaurus, when it detects any broken link. @@ -93,6 +94,14 @@ The broken links detection is only available for a production build (`docusaurus ::: +### `onDuplicateRoutes` + +- Type: `'ignore' | 'log' | 'warn' | 'error' | 'throw'` + +The behavior of Docusaurus when it detects any [duplicate routes](/guides/creating-pages.md#duplicate-routes). + +By default, it displays a warning after you run `yarn start` or `yarn build`. + ### `tagline` - Type: `string` diff --git a/website/docs/configuration.md b/website/docs/configuration.md index 16c443625c11..c84a39b0500d 100644 --- a/website/docs/configuration.md +++ b/website/docs/configuration.md @@ -22,7 +22,7 @@ The high-level overview of Docusaurus configuration can be categorized into: - [Custom configurations](#custom-configurations) - [Customizing Babel Configuration](#customizing-babel-configuration) -For exact reference to each of the configurable fields, you may refer to [**`docusaurus.config.js` API reference**](docusaurus.config.js.md). +For exact reference to each of the configurable fields, you may refer to [**`docusaurus.config.js` API reference**](api/docusaurus.config.js.md). ### Site metadata diff --git a/website/docs/docusaurus-core.md b/website/docs/docusaurus-core.md index 593040bd0716..d5c78ea5809b 100644 --- a/website/docs/docusaurus-core.md +++ b/website/docs/docusaurus-core.md @@ -132,7 +132,7 @@ function MyComponent() { ### `useDocusaurusContext` -React hook to access Docusaurus Context. Context contains `siteConfig` object from [docusaurus.config.js](docusaurus.config.js.md), and some additional site metadata. +React hook to access Docusaurus Context. Context contains `siteConfig` object from [docusaurus.config.js](api/docusaurus.config.js.md), and some additional site metadata. ```ts type DocusaurusPluginVersionInformation = diff --git a/website/docs/creating-pages.md b/website/docs/guides/creating-pages.md similarity index 86% rename from website/docs/creating-pages.md rename to website/docs/guides/creating-pages.md index 9b1bef9c8929..ed33097f6053 100644 --- a/website/docs/creating-pages.md +++ b/website/docs/guides/creating-pages.md @@ -1,6 +1,7 @@ --- id: creating-pages title: Creating Pages +slug: /creating-pages --- In this section, we will learn about creating ad-hoc pages in Docusaurus using React. This is most useful for creating one-off standalone pages like a showcase page, playground page or support page. @@ -112,4 +113,10 @@ All JavaScript/TypeScript files within the `src/pages/` directory will have corr ## Using React +React is used as the UI library to create pages. Every page component should export a React component and you can leverage on the expressiveness of React to build rich and interactive content. + React is used as the UI library to create pages. Every page component should export a React component, and you can leverage on the expressiveness of React to build rich and interactive content. + +## Duplicate Routes + +You may accidentally create multiple pages that are meant to be accessed on the same route. When this happens, Docusaurus will warn you about duplicate routes when you run `yarn start` or `yarn build`, but the site will still be built successfully. The page that was created last will be accessible, but it will override other conflicting pages. To resolve this issue, you should modify or remove any conflicting routes. diff --git a/website/docs/guides/migrating-from-v1-to-v2.md b/website/docs/guides/migrating-from-v1-to-v2.md index 8a4a5e652d51..6996a0425ba5 100644 --- a/website/docs/guides/migrating-from-v1-to-v2.md +++ b/website/docs/guides/migrating-from-v1-to-v2.md @@ -538,7 +538,7 @@ module.exports = { ### Pages -Please refer to [creating pages](creating-pages.md) to learn how Docusaurus 2 pages work. After reading that, notice that you have to move `pages/en` files in v1 to `src/pages` instead. +Please refer to [creating pages](guides/creating-pages.md) to learn how Docusaurus 2 pages work. After reading that, notice that you have to move `pages/en` files in v1 to `src/pages` instead. In Docusaurus v1, pages received the `siteConfig` object as props. diff --git a/website/docs/installation.md b/website/docs/installation.md index 2c49151524e1..774103fbbfde 100644 --- a/website/docs/installation.md +++ b/website/docs/installation.md @@ -67,7 +67,7 @@ my-website - `/blog/` - Contains the blog Markdown files. You can delete the directory if you do not want/need a blog. More details can be found in the [blog guide](blog.md). - `/docs/` - Contains the Markdown files for the docs. Customize the order of the docs sidebar in `sidebars.js`. More details can be found in the [docs guide](markdown-features.mdx). - `/src/` - Non-documentation files like pages or custom React components. You don't have to strictly put your non-documentation files in here but putting them under a centralized directory makes it easier to specify in case you need to do some sort of linting/processing - - `/src/pages` - Any files within this directory will be converted into a website page. More details can be found in the [pages guide](creating-pages.md). + - `/src/pages` - Any files within this directory will be converted into a website page. More details can be found in the [pages guide](guides/creating-pages.md). - `/static/` - Static directory. Any contents inside here will be copied into the root of the final `build` directory. - `/docusaurus.config.js` - A config file containing the site configuration. This is the equivalent of `siteConfig.js` in Docusaurus 1. - `/package.json` - A Docusaurus website is a React app. You can install and use any npm packages you like in them. diff --git a/website/docs/using-plugins.md b/website/docs/using-plugins.md index d02052829377..2acc349b026c 100644 --- a/website/docs/using-plugins.md +++ b/website/docs/using-plugins.md @@ -344,7 +344,7 @@ module.exports = { ### `@docusaurus/plugin-content-pages` -The default pages plugin for Docusaurus. The classic template ships with this plugin with default configurations. This plugin provides [creating pages](creating-pages.md) functionality. +The default pages plugin for Docusaurus. The classic template ships with this plugin with default configurations. This plugin provides [creating pages](guides/creating-pages.md) functionality. **Installation** diff --git a/website/sidebars.js b/website/sidebars.js index 8d75d8c8ef51..1114e81c5f9f 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -22,7 +22,7 @@ module.exports = { type: 'category', label: 'Guides', items: [ - 'creating-pages', + 'guides/creating-pages', 'styling-layout', 'static-assets', { @@ -45,7 +45,7 @@ module.exports = { items: [ 'cli', 'docusaurus-core', - 'docusaurus.config.js', + 'api/docusaurus.config.js', 'lifecycle-apis', 'theme-classic', ],