diff --git a/package.json b/package.json index 2142852662..182f7510b0 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "start:v2:baseUrl": "yarn workspace docusaurus-2-website start:baseUrl", "start:v2:bootstrap": "yarn workspace docusaurus-2-website start:bootstrap", "start:v2:blogOnly": "yarn workspace docusaurus-2-website start:blogOnly", + "start:v2:deployPreview": "cross-env NETLIFY=true CONTEXT='deploy-preview' yarn workspace docusaurus-2-website start", "examples:generate": "node generateExamples", "build": "yarn build:packages && yarn build:v2", "build:packages": "lerna run build --no-private", @@ -22,7 +23,9 @@ "build:v2": "yarn workspace docusaurus-2-website build", "build:v2:baseUrl": "yarn workspace docusaurus-2-website build:baseUrl", "build:v2:blogOnly": "yarn workspace docusaurus-2-website build:blogOnly", + "build:v2:deployPreview": "cross-env NETLIFY=true CONTEXT='deploy-preview' yarn workspace docusaurus-2-website build", "build:v2:en": "yarn workspace docusaurus-2-website build --locale en", + "clear:v2": "yarn workspace docusaurus-2-website clear", "serve:v1": "serve website-1.x/build/docusaurus", "serve:v2": "yarn workspace docusaurus-2-website serve", "serve:v2:baseUrl": "serve website", diff --git a/packages/docusaurus-theme-classic/src/theme/DocPage/index.tsx b/packages/docusaurus-theme-classic/src/theme/DocPage/index.tsx index d73e5d3810..7bb23a6c4e 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocPage/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/DocPage/index.tsx @@ -31,15 +31,36 @@ type DocPageContentProps = { readonly children: ReactNode; }; +function getSidebar({versionMetadata, currentDocRoute}) { + function addTrailingSlash(str: string): string { + return str.endsWith('/') ? str : `${str}/`; + } + function removeTrailingSlash(str: string): string { + return str.endsWith('/') ? str.slice(0, -1) : str; + } + + const {permalinkToSidebar, docsSidebars} = versionMetadata; + + // With/without trailingSlash, we should always be able to get the appropriate sidebar + // note: docs plugin permalinks currently never have trailing slashes + // trailingSlash is handled globally at the framework level, not plugin level + const sidebarName = + permalinkToSidebar[currentDocRoute.path] || + permalinkToSidebar[addTrailingSlash(currentDocRoute.path)] || + permalinkToSidebar[removeTrailingSlash(currentDocRoute.path)]; + + const sidebar = docsSidebars[sidebarName]; + return {sidebar, sidebarName}; +} + function DocPageContent({ currentDocRoute, versionMetadata, children, }: DocPageContentProps): JSX.Element { const {siteConfig, isClient} = useDocusaurusContext(); - const {pluginId, permalinkToSidebar, docsSidebars, version} = versionMetadata; - const sidebarName = permalinkToSidebar[currentDocRoute.path]; - const sidebar = docsSidebars[sidebarName]; + const {pluginId, version} = versionMetadata; + const {sidebarName, sidebar} = getSidebar({versionMetadata, currentDocRoute}); const [hiddenSidebarContainer, setHiddenSidebarContainer] = useState(false); const [hiddenSidebar, setHiddenSidebar] = useState(false); diff --git a/packages/docusaurus-types/src/index.d.ts b/packages/docusaurus-types/src/index.d.ts index 49ca673c88..1ba356a86e 100644 --- a/packages/docusaurus-types/src/index.d.ts +++ b/packages/docusaurus-types/src/index.d.ts @@ -30,6 +30,8 @@ export interface DocusaurusConfig { tagline?: string; title: string; url: string; + // trailingSlash undefined = legacy retrocompatible behavior => /file => /file/index.html + trailingSlash: boolean | undefined; i18n: I18nConfig; onBrokenLinks: ReportingSeverity; onBrokenMarkdownLinks: ReportingSeverity; diff --git a/packages/docusaurus/package.json b/packages/docusaurus/package.json index 3cf1e51e9d..dbd407fa52 100644 --- a/packages/docusaurus/package.json +++ b/packages/docusaurus/package.json @@ -52,7 +52,7 @@ "@docusaurus/types": "2.0.0-beta.0", "@docusaurus/utils": "2.0.0-beta.0", "@docusaurus/utils-validation": "2.0.0-beta.0", - "@endiliey/static-site-generator-webpack-plugin": "^4.0.0", + "@slorber/static-site-generator-webpack-plugin": "^4.0.0", "@svgr/webpack": "^5.5.0", "autoprefixer": "^10.2.5", "babel-loader": "^8.2.2", diff --git a/packages/docusaurus/src/client/exports/Link.tsx b/packages/docusaurus/src/client/exports/Link.tsx index feb49924d6..9fcaa3c531 100644 --- a/packages/docusaurus/src/client/exports/Link.tsx +++ b/packages/docusaurus/src/client/exports/Link.tsx @@ -8,10 +8,12 @@ import React, {useEffect, useRef} from 'react'; import {NavLink, Link as RRLink} from 'react-router-dom'; +import useDocusaurusContext from './useDocusaurusContext'; import isInternalUrl from './isInternalUrl'; import ExecutionEnvironment from './ExecutionEnvironment'; import {useLinksCollector} from '../LinksCollector'; import {useBaseUrlUtils} from './useBaseUrl'; +import applyTrailingSlash from './applyTrailingSlash'; import type {LinkProps} from '@docusaurus/Link'; import type docusaurus from '../docusaurus'; @@ -39,6 +41,9 @@ function Link({ autoAddBaseUrl = true, ...props }: LinkProps): JSX.Element { + const { + siteConfig: {trailingSlash}, + } = useDocusaurusContext(); const {withBaseUrl} = useBaseUrlUtils(); const linksCollector = useLinksCollector(); @@ -69,11 +74,15 @@ function Link({ // TODO we should use ReactRouter basename feature instead! // Automatically apply base url in links that start with / - const targetLink = + let targetLink = typeof targetLinkWithoutPathnameProtocol !== 'undefined' ? maybeAddBaseUrl(targetLinkWithoutPathnameProtocol) : undefined; + if (targetLink && isInternal) { + targetLink = applyTrailingSlash(targetLink, trailingSlash); + } + const preloaded = useRef(false); const LinkComponent = isNavLink ? NavLink : RRLink; diff --git a/packages/docusaurus/src/client/exports/__tests__/applyTrailingSlash.test.ts b/packages/docusaurus/src/client/exports/__tests__/applyTrailingSlash.test.ts new file mode 100644 index 0000000000..49ea78d2a2 --- /dev/null +++ b/packages/docusaurus/src/client/exports/__tests__/applyTrailingSlash.test.ts @@ -0,0 +1,89 @@ +/** + * 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 applyTrailingSlash from '../applyTrailingSlash'; + +describe('applyTrailingSlash', () => { + test('should apply to empty', () => { + expect(applyTrailingSlash('', true)).toEqual('/'); + expect(applyTrailingSlash('', false)).toEqual(''); + expect(applyTrailingSlash('', undefined)).toEqual(''); + }); + + test('should apply to /', () => { + expect(applyTrailingSlash('/', true)).toEqual('/'); + expect(applyTrailingSlash('/', false)).toEqual(''); + expect(applyTrailingSlash('/', undefined)).toEqual('/'); + }); + + test('should not apply to #anchor links ', () => { + expect(applyTrailingSlash('#', true)).toEqual('#'); + expect(applyTrailingSlash('#', false)).toEqual('#'); + expect(applyTrailingSlash('#', undefined)).toEqual('#'); + expect(applyTrailingSlash('#anchor', true)).toEqual('#anchor'); + expect(applyTrailingSlash('#anchor', false)).toEqual('#anchor'); + expect(applyTrailingSlash('#anchor', undefined)).toEqual('#anchor'); + }); + + test('should apply to simple paths', () => { + expect(applyTrailingSlash('abc', true)).toEqual('abc/'); + expect(applyTrailingSlash('abc', false)).toEqual('abc'); + expect(applyTrailingSlash('abc', undefined)).toEqual('abc'); + expect(applyTrailingSlash('abc/', true)).toEqual('abc/'); + expect(applyTrailingSlash('abc/', false)).toEqual('abc'); + expect(applyTrailingSlash('abc/', undefined)).toEqual('abc/'); + expect(applyTrailingSlash('/abc', true)).toEqual('/abc/'); + expect(applyTrailingSlash('/abc', false)).toEqual('/abc'); + expect(applyTrailingSlash('/abc', undefined)).toEqual('/abc'); + expect(applyTrailingSlash('/abc/', true)).toEqual('/abc/'); + expect(applyTrailingSlash('/abc/', false)).toEqual('/abc'); + expect(applyTrailingSlash('/abc/', undefined)).toEqual('/abc/'); + }); + + test('should apply to path with #anchor', () => { + expect(applyTrailingSlash('/abc#anchor', true)).toEqual('/abc/#anchor'); + expect(applyTrailingSlash('/abc#anchor', false)).toEqual('/abc#anchor'); + expect(applyTrailingSlash('/abc#anchor', undefined)).toEqual('/abc#anchor'); + expect(applyTrailingSlash('/abc/#anchor', true)).toEqual('/abc/#anchor'); + expect(applyTrailingSlash('/abc/#anchor', false)).toEqual('/abc#anchor'); + expect(applyTrailingSlash('/abc/#anchor', undefined)).toEqual( + '/abc/#anchor', + ); + }); + + test('should apply to path with ?search', () => { + expect(applyTrailingSlash('/abc?search', true)).toEqual('/abc/?search'); + expect(applyTrailingSlash('/abc?search', false)).toEqual('/abc?search'); + expect(applyTrailingSlash('/abc?search', undefined)).toEqual('/abc?search'); + expect(applyTrailingSlash('/abc/?search', true)).toEqual('/abc/?search'); + expect(applyTrailingSlash('/abc/?search', false)).toEqual('/abc?search'); + expect(applyTrailingSlash('/abc/?search', undefined)).toEqual( + '/abc/?search', + ); + }); + + test('should apply to path with ?search#anchor', () => { + expect(applyTrailingSlash('/abc?search#anchor', true)).toEqual( + '/abc/?search#anchor', + ); + expect(applyTrailingSlash('/abc?search#anchor', false)).toEqual( + '/abc?search#anchor', + ); + expect(applyTrailingSlash('/abc?search#anchor', undefined)).toEqual( + '/abc?search#anchor', + ); + expect(applyTrailingSlash('/abc/?search#anchor', true)).toEqual( + '/abc/?search#anchor', + ); + expect(applyTrailingSlash('/abc/?search#anchor', false)).toEqual( + '/abc?search#anchor', + ); + expect(applyTrailingSlash('/abc/?search#anchor', undefined)).toEqual( + '/abc/?search#anchor', + ); + }); +}); diff --git a/packages/docusaurus/src/client/exports/__tests__/isInternalUrl.ts b/packages/docusaurus/src/client/exports/__tests__/isInternalUrl.test.ts similarity index 100% rename from packages/docusaurus/src/client/exports/__tests__/isInternalUrl.ts rename to packages/docusaurus/src/client/exports/__tests__/isInternalUrl.test.ts diff --git a/packages/docusaurus/src/client/exports/__tests__/useBaseUrl.ts b/packages/docusaurus/src/client/exports/__tests__/useBaseUrl.test.ts similarity index 100% rename from packages/docusaurus/src/client/exports/__tests__/useBaseUrl.ts rename to packages/docusaurus/src/client/exports/__tests__/useBaseUrl.test.ts diff --git a/packages/docusaurus/src/client/exports/applyTrailingSlash.tsx b/packages/docusaurus/src/client/exports/applyTrailingSlash.tsx new file mode 100644 index 0000000000..e49c27d011 --- /dev/null +++ b/packages/docusaurus/src/client/exports/applyTrailingSlash.tsx @@ -0,0 +1,34 @@ +/** + * 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 default function applyTrailingSlash( + path: string, + trailingSlash: boolean | undefined, +): string { + // Never apply trailing slash to an anchor link + if (path.startsWith('#')) { + return path; + } + + function addTrailingSlash(str: string): string { + return str.endsWith('/') ? str : `${str}/`; + } + function removeTrailingSlash(str: string): string { + return str.endsWith('/') ? str.slice(0, -1) : str; + } + // undefined = legacy retrocompatible behavior + if (typeof trailingSlash === 'undefined') { + return path; + } + + // The trailing slash should be handled before the ?search#hash ! + const [pathname] = path.split(/[#?]/); + const newPathname = trailingSlash + ? addTrailingSlash(pathname) + : removeTrailingSlash(pathname); + return path.replace(pathname, newPathname); +} diff --git a/packages/docusaurus/src/commands/serve.ts b/packages/docusaurus/src/commands/serve.ts index a37d97238c..d2dec0a5c2 100644 --- a/packages/docusaurus/src/commands/serve.ts +++ b/packages/docusaurus/src/commands/serve.ts @@ -42,7 +42,7 @@ export default async function serve( } const { - siteConfig: {baseUrl}, + siteConfig: {baseUrl, trailingSlash}, } = await loadSiteConfig({ siteDir, customConfigFilePath: cliOptions.config, @@ -67,6 +67,7 @@ export default async function serve( serveHandler(req, res, { cleanUrls: true, public: dir, + trailingSlash, }); }); diff --git a/packages/docusaurus/src/server/configValidation.ts b/packages/docusaurus/src/server/configValidation.ts index 194dfdda45..66f1e842cf 100644 --- a/packages/docusaurus/src/server/configValidation.ts +++ b/packages/docusaurus/src/server/configValidation.ts @@ -117,6 +117,7 @@ const ConfigSchema = Joi.object({ favicon: Joi.string().required(), title: Joi.string().required(), url: URISchema.required(), + trailingSlash: Joi.boolean(), // No default value! undefined = retrocompatible legacy behavior! i18n: I18N_CONFIG_SCHEMA, onBrokenLinks: Joi.string() .equal('ignore', 'log', 'warn', 'error', 'throw') diff --git a/packages/docusaurus/src/server/plugins/__tests__/applyRouteTrailingSlash.test.ts b/packages/docusaurus/src/server/plugins/__tests__/applyRouteTrailingSlash.test.ts new file mode 100644 index 0000000000..b445071b4e --- /dev/null +++ b/packages/docusaurus/src/server/plugins/__tests__/applyRouteTrailingSlash.test.ts @@ -0,0 +1,104 @@ +/** + * 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 applyRouteTrailingSlash from '../applyRouteTrailingSlash'; +import {RouteConfig} from '@docusaurus/types'; + +function route(path: string, subRoutes?: string[]): RouteConfig { + const result: RouteConfig = {path, component: 'any'}; + + if (subRoutes) { + result.routes = subRoutes.map((subRoute) => route(subRoute)); + } + + return result; +} + +describe('applyRouteTrailingSlash', () => { + test('apply to empty', () => { + expect(applyRouteTrailingSlash(route(''), true)).toEqual(route('/')); + expect(applyRouteTrailingSlash(route(''), false)).toEqual(route('')); + expect(applyRouteTrailingSlash(route(''), undefined)).toEqual(route('')); + }); + + test('apply to /', () => { + expect(applyRouteTrailingSlash(route('/'), true)).toEqual(route('/')); + expect(applyRouteTrailingSlash(route('/'), false)).toEqual(route('/')); + expect(applyRouteTrailingSlash(route('/'), undefined)).toEqual(route('/')); + }); + + test('apply to /abc', () => { + expect(applyRouteTrailingSlash(route('/abc'), true)).toEqual( + route('/abc/'), + ); + expect(applyRouteTrailingSlash(route('/abc'), false)).toEqual( + route('/abc'), + ); + expect(applyRouteTrailingSlash(route('/abc'), undefined)).toEqual( + route('/abc'), + ); + }); + + test('apply to /abc/', () => { + expect(applyRouteTrailingSlash(route('/abc/'), true)).toEqual( + route('/abc/'), + ); + expect(applyRouteTrailingSlash(route('/abc/'), false)).toEqual( + route('/abc'), + ); + expect(applyRouteTrailingSlash(route('/abc/'), undefined)).toEqual( + route('/abc/'), + ); + }); + + test('apply to /abc?search#anchor', () => { + expect(applyRouteTrailingSlash(route('/abc?search#anchor'), true)).toEqual( + route('/abc/?search#anchor'), + ); + expect(applyRouteTrailingSlash(route('/abc?search#anchor'), false)).toEqual( + route('/abc?search#anchor'), + ); + expect( + applyRouteTrailingSlash(route('/abc?search#anchor'), undefined), + ).toEqual(route('/abc?search#anchor')); + }); + + test('apply to /abc/?search#anchor', () => { + expect(applyRouteTrailingSlash(route('/abc/?search#anchor'), true)).toEqual( + route('/abc/?search#anchor'), + ); + expect( + applyRouteTrailingSlash(route('/abc/?search#anchor'), false), + ).toEqual(route('/abc?search#anchor')); + expect( + applyRouteTrailingSlash(route('/abc/?search#anchor'), undefined), + ).toEqual(route('/abc/?search#anchor')); + }); + + test('apply to subroutes', () => { + expect( + applyRouteTrailingSlash(route('/abc', ['/abc/1', '/abc/2']), true), + ).toEqual(route('/abc/', ['/abc/1/', '/abc/2/'])); + expect( + applyRouteTrailingSlash(route('/abc', ['/abc/1', '/abc/2']), false), + ).toEqual(route('/abc', ['/abc/1', '/abc/2'])); + expect( + applyRouteTrailingSlash(route('/abc', ['/abc/1', '/abc/2']), undefined), + ).toEqual(route('/abc', ['/abc/1', '/abc/2'])); + }); + + test('apply for complex case', () => { + expect( + applyRouteTrailingSlash( + route('/abc?search#anchor', ['/abc/1?search', '/abc/2#anchor']), + true, + ), + ).toEqual( + route('/abc/?search#anchor', ['/abc/1/?search', '/abc/2/#anchor']), + ); + }); +}); diff --git a/packages/docusaurus/src/server/plugins/applyRouteTrailingSlash.ts b/packages/docusaurus/src/server/plugins/applyRouteTrailingSlash.ts new file mode 100644 index 0000000000..b1b118a554 --- /dev/null +++ b/packages/docusaurus/src/server/plugins/applyRouteTrailingSlash.ts @@ -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 {RouteConfig} from '@docusaurus/types'; +import {addTrailingSlash, removeTrailingSlash} from '@docusaurus/utils'; + +export default function applyRouteTrailingSlash( + route: RouteConfig, + trailingSlash: boolean | undefined, +) { + // Never transform "/" to "" => cause router issues ("" catch everything) + if (route.path === '/') { + return route; + } + + function getNewRoutePath() { + // undefined = legacy retrocompatible behavior + if (typeof trailingSlash === 'undefined') { + return route.path; + } + // The trailing slash should be handled before the ?search#hash ! + // For routing #anchor is normally not possible, but querystring remains possible + const [pathname] = route.path.split(/[#?]/); + const newPathname = trailingSlash + ? addTrailingSlash(pathname) + : removeTrailingSlash(pathname); + return route.path.replace(pathname, newPathname); + } + + return { + ...route, + path: getNewRoutePath(), + ...(route.routes && { + routes: route.routes.map((subroute) => + applyRouteTrailingSlash(subroute, trailingSlash), + ), + }), + }; +} diff --git a/packages/docusaurus/src/server/plugins/index.ts b/packages/docusaurus/src/server/plugins/index.ts index cd996ca462..3a7b54689b 100644 --- a/packages/docusaurus/src/server/plugins/index.ts +++ b/packages/docusaurus/src/server/plugins/index.ts @@ -22,6 +22,7 @@ import chalk from 'chalk'; import {DEFAULT_PLUGIN_ID} from '../../constants'; import {chain} from 'lodash'; import {localizePluginTranslationFile} from '../translations/translations'; +import applyRouteTrailingSlash from './applyRouteTrailingSlash'; export function sortConfig(routeConfigs: RouteConfig[]): void { // Sort the route config. This ensures that route with nested @@ -136,8 +137,16 @@ export async function loadPlugins({ const dataDirRoot = path.join(context.generatedFilesDir, plugin.name); const dataDir = path.join(dataDirRoot, pluginId); - const addRoute: PluginContentLoadedActions['addRoute'] = (config) => - pluginsRouteConfigs.push(config); + const addRoute: PluginContentLoadedActions['addRoute'] = ( + initialRouteConfig, + ) => { + // Trailing slash behavior is handled in a generic way for all plugins + const finalRouteConfig = applyRouteTrailingSlash( + initialRouteConfig, + context.siteConfig.trailingSlash, + ); + pluginsRouteConfigs.push(finalRouteConfig); + }; const createData: PluginContentLoadedActions['createData'] = async ( name, diff --git a/packages/docusaurus/src/webpack/__tests__/__snapshots__/base.test.ts.snap b/packages/docusaurus/src/webpack/__tests__/__snapshots__/base.test.ts.snap index 779a239933..2994ee31f3 100644 --- a/packages/docusaurus/src/webpack/__tests__/__snapshots__/base.test.ts.snap +++ b/packages/docusaurus/src/webpack/__tests__/__snapshots__/base.test.ts.snap @@ -10,6 +10,7 @@ Object { "@docusaurus/Link": "../../client/exports/Link.tsx", "@docusaurus/Noop": "../../client/exports/Noop.ts", "@docusaurus/Translate": "../../client/exports/Translate.tsx", + "@docusaurus/applyTrailingSlash": "../../client/exports/applyTrailingSlash.tsx", "@docusaurus/constants": "../../client/exports/constants.ts", "@docusaurus/context": "../../client/exports/context.ts", "@docusaurus/isInternalUrl": "../../client/exports/isInternalUrl.ts", diff --git a/packages/docusaurus/src/webpack/server.ts b/packages/docusaurus/src/webpack/server.ts index 5b5940f497..d05f8bd4a5 100644 --- a/packages/docusaurus/src/webpack/server.ts +++ b/packages/docusaurus/src/webpack/server.ts @@ -6,7 +6,6 @@ */ import path from 'path'; -import StaticSiteGeneratorPlugin from '@endiliey/static-site-generator-webpack-plugin'; import {Configuration} from 'webpack'; import merge from 'webpack-merge'; @@ -16,6 +15,9 @@ import WaitPlugin from './plugins/WaitPlugin'; import LogPlugin from './plugins/LogPlugin'; import {NODE_MAJOR_VERSION, NODE_MINOR_VERSION} from '../constants'; +// Forked for Docusaurus: https://github.com/slorber/static-site-generator-webpack-plugin +import StaticSiteGeneratorPlugin from '@slorber/static-site-generator-webpack-plugin'; + export default function createServerConfig({ props, onLinksCollected = () => {}, @@ -31,7 +33,7 @@ export default function createServerConfig({ preBodyTags, postBodyTags, ssrTemplate, - siteConfig: {noIndex}, + siteConfig: {noIndex, trailingSlash}, } = props; const config = createBaseConfig(props, true); @@ -75,6 +77,7 @@ export default function createServerConfig({ noIndex, }, paths: ssgPaths, + preferFoldersOutput: trailingSlash, }), // Show compilation progress bar. diff --git a/website/docs/api/docusaurus.config.js.md b/website/docs/api/docusaurus.config.js.md index f39090326d..76d023de7b 100644 --- a/website/docs/api/docusaurus.config.js.md +++ b/website/docs/api/docusaurus.config.js.md @@ -81,6 +81,24 @@ module.exports = { ## Optional fields {#optional-fields} +### `trailingSlash` {#trailing-slash} + +- Type: `boolean | undefined` + +Allow to customize the presence/absence of a trailing slash at the end of URLs/links, and how static HTML files are generated: + +- `undefined` (default): keeps URLs untouched, and emit `/docs/myDoc/index.html` for `/docs/myDoc.md` +- `true`: add trailing slashes to URLs/links, and emit `/docs/myDoc/index.html` for `/docs/myDoc.md` +- `false`: remove trailing slashes from URLs/links, and emit `/docs/myDoc.html` for `/docs/myDoc.md` + +:::tip + +Each static hosting provider serve static files differently (this behavior may even change over time). + +Refer to the [deployment guide](../deployment.mdx) and [slorber/trailing-slash-guide](https://github.com/slorber/trailing-slash-guide) to choose the appropriate setting. + +::: + ### `i18n` {#i18n} - Type: `Object` diff --git a/website/docs/api/plugins/plugin-google-analytics.md b/website/docs/api/plugins/plugin-google-analytics.md index 05087ada07..738c5f6e34 100644 --- a/website/docs/api/plugins/plugin-google-analytics.md +++ b/website/docs/api/plugins/plugin-google-analytics.md @@ -4,7 +4,7 @@ title: '📦 plugin-google-analytics' slug: '/api/plugins/@docusaurus/plugin-google-analytics' --- -The default [Google Analytics](https://developers.google.com/analytics/devguides/collection/analyticsjs/) plugin. It is a JavaScript library for measuring how users interact with your website **in the production build**. If you are using Google Analytics 4 you might need to consider using [plugin-google-gtag](./plugin-google-gtag) instead. +The default [Google Analytics](https://developers.google.com/analytics/devguides/collection/analyticsjs/) plugin. It is a JavaScript library for measuring how users interact with your website **in the production build**. If you are using Google Analytics 4 you might need to consider using [plugin-google-gtag](./plugin-google-gtag.md) instead. ## Installation {#installation} diff --git a/website/docs/deployment.mdx b/website/docs/deployment.mdx index a6469b08a8..63aaf6260d 100644 --- a/website/docs/deployment.mdx +++ b/website/docs/deployment.mdx @@ -9,32 +9,60 @@ To build the static files of your website for production, run: npm run build ``` -Once it finishes, the static files will be generated within the `build/` directory. +Once it finishes, the static files will be generated within the `build` directory. -You can deploy your site to static site hosting services such as [Vercel](https://vercel.com/), [GitHub Pages](https://pages.github.com/), [Netlify](https://www.netlify.com/), [Render](https://render.com/docs/static-sites), and [Surge](https://surge.sh/help/getting-started-with-surge). Docusaurus sites are statically rendered so they work without JavaScript too! +:::note -## Testing Build Local {#testing-build-local} +The only responsibility of Docusaurus is to build your site and emit static files in `build`. -It is important to test build before deploying to a production. Docusaurus includes a [`docusaurus serve`](cli.md#docusaurus-serve) command to test build locally. +It is now up to you to choose how to host those static files. + +::: + +You can deploy your site to static site hosting services such as [Vercel](https://vercel.com/), [GitHub Pages](https://pages.github.com/), [Netlify](https://www.netlify.com/), [Render](https://render.com/docs/static-sites), [Surge](https://surge.sh/help/getting-started-with-surge)... + +A Docusaurus site is statically rendered, and it can generally work without JavaScript! + +## Testing your Build Locally {#testing-build-locally} + +It is important to test your build locally before deploying to production. + +Docusaurus includes a [`docusaurus serve`](cli.md#docusaurus-serve) command for that: ```bash npm2yarn npm run serve ``` -## Self Hosting {#self-hosting} +## Trailing slash configuration {#trailing-slashes} -:::warning +Docusaurus has a [`trailingSlash` config](./api/docusaurus.config.js.md#trailing-slash), to allow customizing URLs/links and emitted filename patterns. -It is not the most performant solution +The default value generally works fine. + +Unfortunately, each static hosting provider has a **different behavior**, and deploying the exact same site to various hosts can lead to distinct results. + +Depending on your host, it can be useful to change this config. + +:::tip + +Use [slorber/trailing-slash-guide](https://github.com/slorber/trailing-slash-guide) to understand better the behavior of your host and configure `trailingSlash` appropriately. ::: -Docusaurus can be self hosted using [`docusaurus serve`](cli.md#docusaurus-serve). Change port using `--port` and `--host` to change host. +## Self-Hosting {#self-hosting} + +Docusaurus can be self-hosted using [`docusaurus serve`](cli.md#docusaurus-serve). Change port using `--port` and `--host` to change host. ```bash npm2yarn npm run serve -- --build --port 80 --host 0.0.0.0 ``` +:::warning + +It is not the best option, compared to a static hosting provider / CDN. + +::: + ## Deploying to GitHub Pages {#deploying-to-github-pages} Docusaurus provides an easy way to publish to [GitHub Pages](https://pages.github.com/). Which is hosting that comes for free with every GitHub repository. @@ -54,10 +82,18 @@ First, modify your `docusaurus.config.js` and add the required params: In case you want to use your custom domain for GitHub Pages, create a `CNAME` file in the `static` directory. Anything within the `static` directory will be copied to the root of the `build` directory for deployment. +When using a custom domain, you should be able to move back from `baseUrl: '/projectName/'` to `baseUrl: '/'` + You may refer to GitHub Pages' documentation [User, Organization, and Project Pages](https://help.github.com/en/articles/user-organization-and-project-pages) for more details. ::: +:::caution + +GitHub Pages adds a trailing slash to Docusaurus URLs by default. Adjusting the `trailingSlash` setting can be useful. + +::: + Example: ```jsx {3-6} title="docusaurus.config.js" @@ -67,6 +103,7 @@ module.exports = { baseUrl: '/', projectName: 'endiliey.github.io', organizationName: 'endiliey', + trailingSlash: false, // ... }; ``` @@ -337,9 +374,17 @@ If you did not configure these build options, you may still go to "Site settings Once properly configured with the above options, your site should deploy and automatically redeploy upon merging to your deploy branch, which defaults to `master`. -:::important +:::warning -Make sure to disable Netlify setting `Pretty URLs` to prevent lowercased URLs, unnecessary redirects and 404 errors. +By default, Netlify adds trailing slashes to Docusaurus URLs. + +It is recommended to disable the Netlify setting `Post Processing > Asset Optimization > Pretty Urls` to prevent lowercased URLs, unnecessary redirects and 404 errors. + +**Be very careful**: the `Disable asset optimization` global checkbox is broken and does not really disable the `Pretty URLs` setting in practice. Please make sure to **uncheck it independently**. + +If you want to keep the `Pretty Urls` Netlify setting on, adjust the `trailingSlash` Docusaurus config appropriately. + +Refer to [slorber/trailing-slash-guide](https://github.com/slorber/trailing-slash-guide) for more information. ::: diff --git a/website/docs/guides/docs/docs-markdown-features.mdx b/website/docs/guides/docs/docs-markdown-features.mdx index 54735eed6e..63058ef056 100644 --- a/website/docs/guides/docs/docs-markdown-features.mdx +++ b/website/docs/guides/docs/docs-markdown-features.mdx @@ -13,16 +13,27 @@ Markdown docs have their own [Markdown frontmatter](../../api/plugins/plugin-con ## Referencing other documents {#referencing-other-documents} -If you want to reference another document file, you could use the name of the document you want to reference. Docusaurus will convert the file path to be the final website path (and remove the `.md`). +If you want to reference another document file, you could use the relative path of the document you want to link to. -For example, if you are in `doc2.md` and you want to reference `doc1.md` and `folder/doc3.md`: +Docusaurus will convert the file path to be the final document url path (and remove the `.md` extension). + +For example, if you are in `folder/doc1.md` and you want to reference `folder/doc2.md`, `folder/subfolder/doc3.md` and `otherFolder/doc4.md`: ```md -I am referencing a [document](doc1.md). Reference to another [document in a folder](folder/doc3.md). +I am referencing a [document](doc2.md). -[Relative document](../doc2.md) referencing works as well. +Reference to another [document in a subfolder](subfolder/doc3.md). + +[Relative document](../otherFolder/doc4.md) referencing works as well. ``` -One benefit of this approach is that the links to external files will still work if you are viewing the file on GitHub. +:::tip -Another benefit, for versioned docs, is that one versioned doc will link to another doc of the exact same version. +It is better to use relative file paths links instead of relative links: + +- links will keep working on the GitHub interface +- you can customize the document slugs without having to update all the links +- a versioned doc will link to another doc of the exact same version +- relative links are very likely to break if you update the [`trailingSlash` config](../../api/docusaurus.config.js.md#trailing-slash) + +::: diff --git a/website/docs/search.md b/website/docs/search.md index b607a6cead..3ec02e760c 100644 --- a/website/docs/search.md +++ b/website/docs/search.md @@ -105,7 +105,7 @@ module.exports = { By default, DocSearch comes with a fine-tuned theme that was designed for accessibility, making sure that colors and contrasts respect standards. -Still, you can reuse the [Infima CSS variables](styling-layout#styling-your-site-with-infima) from Docusaurus to style DocSearch by editing the `/src/css/custom.css` file. +Still, you can reuse the [Infima CSS variables](styling-layout.md#styling-your-site-with-infima) from Docusaurus to style DocSearch by editing the `/src/css/custom.css` file. ```css title="/src/css/custom.css" html[data-theme='light'] .DocSearch { diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js index 1b27cff79f..e76911f7fd 100644 --- a/website/docusaurus.config.js +++ b/website/docusaurus.config.js @@ -54,6 +54,10 @@ const isVersioningDisabled = !!process.env.DISABLE_VERSIONING || isI18nStaging; baseUrl, baseUrlIssueBanner: true, url: 'https://docusaurus.io', + // Dogfood both settings: + // - force trailing slashes for deploy previews + // - avoid trailing slashes in prod + trailingSlash: isDeployPreview, stylesheets: [ { href: 'https://cdn.jsdelivr.net/npm/katex@0.13.11/dist/katex.min.css', @@ -128,30 +132,34 @@ const isVersioningDisabled = !!process.env.DISABLE_VERSIONING || isI18nStaging; ], [ '@docusaurus/plugin-client-redirects', - { - fromExtensions: ['html'], - createRedirects: function (path) { - // redirect to /docs from /docs/introduction, - // as introduction has been made the home doc - if (allDocHomesPaths.includes(path)) { - return [`${path}/introduction`]; - } - }, - redirects: [ - { - from: ['/docs/support', '/docs/next/support'], - to: '/community/support', + 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'], + createRedirects: function (path) { + // redirect to /docs from /docs/introduction, + // as introduction has been made the home doc + if (allDocHomesPaths.includes(path)) { + return [`${path}/introduction`]; + } + }, + redirects: [ + { + from: ['/docs/support', '/docs/next/support'], + to: '/community/support', + }, + { + from: ['/docs/team', '/docs/next/team'], + to: '/community/team', + }, + { + from: ['/docs/resources', '/docs/next/resources'], + to: '/community/resources', + }, + ], }, - { - from: ['/docs/team', '/docs/next/team'], - to: '/community/team', - }, - { - from: ['/docs/resources', '/docs/next/resources'], - to: '/community/resources', - }, - ], - }, ], [ '@docusaurus/plugin-ideal-image', diff --git a/website/versioned_docs/version-2.0.0-alpha.73/search.md b/website/versioned_docs/version-2.0.0-alpha.73/search.md index b607a6cead..3ec02e760c 100644 --- a/website/versioned_docs/version-2.0.0-alpha.73/search.md +++ b/website/versioned_docs/version-2.0.0-alpha.73/search.md @@ -105,7 +105,7 @@ module.exports = { By default, DocSearch comes with a fine-tuned theme that was designed for accessibility, making sure that colors and contrasts respect standards. -Still, you can reuse the [Infima CSS variables](styling-layout#styling-your-site-with-infima) from Docusaurus to style DocSearch by editing the `/src/css/custom.css` file. +Still, you can reuse the [Infima CSS variables](styling-layout.md#styling-your-site-with-infima) from Docusaurus to style DocSearch by editing the `/src/css/custom.css` file. ```css title="/src/css/custom.css" html[data-theme='light'] .DocSearch { diff --git a/website/versioned_docs/version-2.0.0-alpha.74/search.md b/website/versioned_docs/version-2.0.0-alpha.74/search.md index b607a6cead..3ec02e760c 100644 --- a/website/versioned_docs/version-2.0.0-alpha.74/search.md +++ b/website/versioned_docs/version-2.0.0-alpha.74/search.md @@ -105,7 +105,7 @@ module.exports = { By default, DocSearch comes with a fine-tuned theme that was designed for accessibility, making sure that colors and contrasts respect standards. -Still, you can reuse the [Infima CSS variables](styling-layout#styling-your-site-with-infima) from Docusaurus to style DocSearch by editing the `/src/css/custom.css` file. +Still, you can reuse the [Infima CSS variables](styling-layout.md#styling-your-site-with-infima) from Docusaurus to style DocSearch by editing the `/src/css/custom.css` file. ```css title="/src/css/custom.css" html[data-theme='light'] .DocSearch { diff --git a/website/versioned_docs/version-2.0.0-alpha.75/search.md b/website/versioned_docs/version-2.0.0-alpha.75/search.md index b607a6cead..3ec02e760c 100644 --- a/website/versioned_docs/version-2.0.0-alpha.75/search.md +++ b/website/versioned_docs/version-2.0.0-alpha.75/search.md @@ -105,7 +105,7 @@ module.exports = { By default, DocSearch comes with a fine-tuned theme that was designed for accessibility, making sure that colors and contrasts respect standards. -Still, you can reuse the [Infima CSS variables](styling-layout#styling-your-site-with-infima) from Docusaurus to style DocSearch by editing the `/src/css/custom.css` file. +Still, you can reuse the [Infima CSS variables](styling-layout.md#styling-your-site-with-infima) from Docusaurus to style DocSearch by editing the `/src/css/custom.css` file. ```css title="/src/css/custom.css" html[data-theme='light'] .DocSearch { diff --git a/website/versioned_docs/version-2.0.0-beta.0/api/plugins/plugin-google-analytics.md b/website/versioned_docs/version-2.0.0-beta.0/api/plugins/plugin-google-analytics.md index 05087ada07..738c5f6e34 100644 --- a/website/versioned_docs/version-2.0.0-beta.0/api/plugins/plugin-google-analytics.md +++ b/website/versioned_docs/version-2.0.0-beta.0/api/plugins/plugin-google-analytics.md @@ -4,7 +4,7 @@ title: '📦 plugin-google-analytics' slug: '/api/plugins/@docusaurus/plugin-google-analytics' --- -The default [Google Analytics](https://developers.google.com/analytics/devguides/collection/analyticsjs/) plugin. It is a JavaScript library for measuring how users interact with your website **in the production build**. If you are using Google Analytics 4 you might need to consider using [plugin-google-gtag](./plugin-google-gtag) instead. +The default [Google Analytics](https://developers.google.com/analytics/devguides/collection/analyticsjs/) plugin. It is a JavaScript library for measuring how users interact with your website **in the production build**. If you are using Google Analytics 4 you might need to consider using [plugin-google-gtag](./plugin-google-gtag.md) instead. ## Installation {#installation} diff --git a/website/versioned_docs/version-2.0.0-beta.0/search.md b/website/versioned_docs/version-2.0.0-beta.0/search.md index b607a6cead..3ec02e760c 100644 --- a/website/versioned_docs/version-2.0.0-beta.0/search.md +++ b/website/versioned_docs/version-2.0.0-beta.0/search.md @@ -105,7 +105,7 @@ module.exports = { By default, DocSearch comes with a fine-tuned theme that was designed for accessibility, making sure that colors and contrasts respect standards. -Still, you can reuse the [Infima CSS variables](styling-layout#styling-your-site-with-infima) from Docusaurus to style DocSearch by editing the `/src/css/custom.css` file. +Still, you can reuse the [Infima CSS variables](styling-layout.md#styling-your-site-with-infima) from Docusaurus to style DocSearch by editing the `/src/css/custom.css` file. ```css title="/src/css/custom.css" html[data-theme='light'] .DocSearch { diff --git a/yarn.lock b/yarn.lock index f6bc1e731d..032831a6d7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1296,17 +1296,6 @@ resolved "https://registry.yarnpkg.com/@endiliey/react-ideal-image/-/react-ideal-image-0.0.11.tgz#dc3803d04e1409cf88efa4bba0f67667807bdf27" integrity sha512-QxMjt/Gvur/gLxSoCy7VIyGGGrGmDN+VHcXkN3R2ApoWX0EYUE+hMgPHSW/PV6VVebZ1Nd4t2UnGRBDihu16JQ== -"@endiliey/static-site-generator-webpack-plugin@^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@endiliey/static-site-generator-webpack-plugin/-/static-site-generator-webpack-plugin-4.0.0.tgz#94bfe58fd83aeda355de797fcb5112adaca3a6b1" - integrity sha512-3MBqYCs30qk1OBRC697NqhGouYbs71D1B8hrk/AFJC6GwF2QaJOQZtA1JYAaGSe650sZ8r5ppRTtCRXepDWlng== - dependencies: - bluebird "^3.7.1" - cheerio "^0.22.0" - eval "^0.1.4" - url "^0.11.0" - webpack-sources "^1.4.3" - "@eslint/eslintrc@^0.3.0": version "0.3.0" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.3.0.tgz#d736d6963d7003b6514e6324bec9c602ac340318" @@ -3058,6 +3047,17 @@ dependencies: "@sinonjs/commons" "^1.7.0" +"@slorber/static-site-generator-webpack-plugin@^4.0.0": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@slorber/static-site-generator-webpack-plugin/-/static-site-generator-webpack-plugin-4.0.1.tgz#0c8852146441aaa683693deaa5aee2f991d94841" + integrity sha512-PSv4RIVO1Y3kvHxjvqeVisk3E9XFoO04uwYBDWe217MFqKspplYswTuKLiJu0aLORQWzuQjfVsSlLPojwfYsLw== + dependencies: + bluebird "^3.7.1" + cheerio "^0.22.0" + eval "^0.1.4" + url "^0.11.0" + webpack-sources "^1.4.3" + "@stylelint/postcss-css-in-js@^0.37.2": version "0.37.2" resolved "https://registry.yarnpkg.com/@stylelint/postcss-css-in-js/-/postcss-css-in-js-0.37.2.tgz#7e5a84ad181f4234a2480803422a47b8749af3d2"