Compare commits

...

20 Commits

Author SHA1 Message Date
slorber 2f33aadc4e refactor: apply lint autofix 2024-02-23 18:57:53 +00:00
sebastien 6582ea79d7 Rework full build workflow for hash router support
Do not compile a server bundle that we won't use
2024-02-23 19:53:25 +01:00
sebastien 62aa2cb0cf Make it possible to switch router dynamically 2024-02-23 19:18:20 +01:00
sebastien 030688a026 Add router config type doc 2024-02-23 18:49:17 +01:00
sebastien bffb2f080c add unit test for sitemap with hash router 2024-02-23 18:15:47 +01:00
sebastien 6c072c3217 remove comment 2024-02-23 16:32:15 +01:00
sebastien 81dbd82091 blog feed support for Hash Router 2024-02-23 15:48:57 +01:00
sebastien 5a5d2f3127 simplify feeds unit tests 2024-02-23 15:46:39 +01:00
sebastien 4a22ebb2e4 urlUtils normalizeUrl should support hash router 2024-02-23 15:12:30 +01:00
sebastien c22da5a1ea Fix heading/TOC/anchor links with hash router 2024-02-22 19:39:42 +01:00
sebastien 4c5a9de268 Disable PWA/client-redirects plugins in Hash Router mode 2024-02-22 19:22:17 +01:00
sebastien 547d8abc68 sitemap should support hash router? 2024-02-22 18:46:45 +01:00
sebastien a1b9ba6f23 logging message 2024-02-22 18:25:01 +01:00
sebastien 904f73091b add siteConfig DOCUSAURUS_ROUTER env variable 2024-02-22 18:24:54 +01:00
sebastien 25a9004727 disable redirect plugin with the hash router? 2024-02-22 18:20:46 +01:00
sebastien 45c1c9d4b0 add link hack 2024-02-22 18:11:30 +01:00
sebastien 0c5034b809 fix tests 2024-02-22 18:09:30 +01:00
sebastien de0df2dcb3 Add router type to config file 2024-02-22 18:09:17 +01:00
sebastien da45fff262 Merge branch 'main' into slorber/offline-mode-poc-2 2024-02-22 17:38:54 +01:00
sebastien 27b1acfc36 attempt to use hash-based history and make it work offline 2024-02-17 00:44:20 +01:00
28 changed files with 722 additions and 201 deletions

View File

@ -45,6 +45,19 @@ declare module '@generated/routes' {
export default routes;
}
declare module '@generated/router' {
import type {ReactNode, ComponentType} from 'react';
export type Props = {
basename?: string | undefined;
children?: ReactNode;
};
const Router: ComponentType<Props>;
export default Router;
}
declare module '@generated/routesChunkNames' {
import type {RouteChunkNames} from '@docusaurus/types';

View File

@ -6,6 +6,7 @@
*/
import {removePrefix, addLeadingSlash} from '@docusaurus/utils';
import logger from '@docusaurus/logger';
import collectRedirects from './collectRedirects';
import writeRedirectFiles, {
toRedirectFiles,
@ -15,14 +16,21 @@ import type {LoadContext, Plugin} from '@docusaurus/types';
import type {PluginContext, RedirectItem} from './types';
import type {PluginOptions, Options} from './options';
const PluginName = 'docusaurus-plugin-client-redirects';
export default function pluginClientRedirectsPages(
context: LoadContext,
options: PluginOptions,
): Plugin<void> {
const {trailingSlash} = context.siteConfig;
const {trailingSlash, router} = context.siteConfig;
if (router === 'hash') {
logger.warn(`${PluginName} does not support the Hash Router`);
return {name: PluginName};
}
return {
name: 'docusaurus-plugin-client-redirects',
name: PluginName,
async postBuild(props) {
const pluginContext: PluginContext = {
relativeRoutesPaths: props.routesPaths.map(

View File

@ -220,6 +220,91 @@ exports[`atom has feed item for each post 1`] = `
]
`;
exports[`atom has feed item for each post using hash router 1`] = `
[
"<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<id>https://docusaurus.io/myBaseUrl/blog</id>
<title>Hello Blog</title>
<updated>2023-07-23T00:00:00.000Z</updated>
<generator>https://github.com/jpmonette/feed</generator>
<link rel="alternate" href="https://docusaurus.io/myBaseUrl/blog"/>
<subtitle>Hello Blog</subtitle>
<icon>https://docusaurus.io/myBaseUrl/image/favicon.ico</icon>
<rights>Copyright</rights>
<entry>
<title type="html"><![CDATA[test links]]></title>
<id>https://docusaurus.io/#/myBaseUrl/blog/blog-with-links</id>
<link href="https://docusaurus.io/#/myBaseUrl/blog/blog-with-links"/>
<updated>2023-07-23T00:00:00.000Z</updated>
<summary type="html"><![CDATA[absolute full url]]></summary>
</entry>
<entry>
<title type="html"><![CDATA[MDX Blog Sample with require calls]]></title>
<id>https://docusaurus.io/#/myBaseUrl/blog/mdx-require-blog-post</id>
<link href="https://docusaurus.io/#/myBaseUrl/blog/mdx-require-blog-post"/>
<updated>2021-03-06T00:00:00.000Z</updated>
<summary type="html"><![CDATA[Test MDX with require calls]]></summary>
</entry>
<entry>
<title type="html"><![CDATA[Full Blog Sample]]></title>
<id>https://docusaurus.io/#/myBaseUrl/blog/mdx-blog-post</id>
<link href="https://docusaurus.io/#/myBaseUrl/blog/mdx-blog-post"/>
<updated>2021-03-05T00:00:00.000Z</updated>
<summary type="html"><![CDATA[HTML Heading 1]]></summary>
</entry>
<entry>
<title type="html"><![CDATA[Complex Slug]]></title>
<id>https://docusaurus.io/#/myBaseUrl/blog/hey/my super path/héllô</id>
<link href="https://docusaurus.io/#/myBaseUrl/blog/hey/my super path/héllô"/>
<updated>2020-08-16T00:00:00.000Z</updated>
<summary type="html"><![CDATA[complex url slug]]></summary>
<category label="date" term="date"/>
<category label="complex" term="complex"/>
</entry>
<entry>
<title type="html"><![CDATA[Simple Slug]]></title>
<id>https://docusaurus.io/#/myBaseUrl/blog/simple/slug</id>
<link href="https://docusaurus.io/#/myBaseUrl/blog/simple/slug"/>
<updated>2020-08-15T00:00:00.000Z</updated>
<summary type="html"><![CDATA[simple url slug]]></summary>
<author>
<name>Sébastien Lorber</name>
<uri>https://sebastienlorber.com</uri>
</author>
</entry>
<entry>
<title type="html"><![CDATA[some heading]]></title>
<id>https://docusaurus.io/#/myBaseUrl/blog/heading-as-title</id>
<link href="https://docusaurus.io/#/myBaseUrl/blog/heading-as-title"/>
<updated>2019-01-02T00:00:00.000Z</updated>
</entry>
<entry>
<title type="html"><![CDATA[date-matter]]></title>
<id>https://docusaurus.io/#/myBaseUrl/blog/date-matter</id>
<link href="https://docusaurus.io/#/myBaseUrl/blog/date-matter"/>
<updated>2019-01-01T00:00:00.000Z</updated>
<summary type="html"><![CDATA[date inside front matter]]></summary>
<category label="date" term="date"/>
</entry>
<entry>
<title type="html"><![CDATA[Happy 1st Birthday Slash! (translated)]]></title>
<id>https://docusaurus.io/#/myBaseUrl/blog/2018/12/14/Happy-First-Birthday-Slash</id>
<link href="https://docusaurus.io/#/myBaseUrl/blog/2018/12/14/Happy-First-Birthday-Slash"/>
<updated>2018-12-14T00:00:00.000Z</updated>
<summary type="html"><![CDATA[Happy birthday! (translated)]]></summary>
<author>
<name>Yangshun Tay (translated)</name>
</author>
<author>
<name>Sébastien Lorber (translated)</name>
<email>lorber.sebastien@gmail.com</email>
</author>
</entry>
</feed>",
]
`;
exports[`json filters to the first two entries 1`] = `
[
"{
@ -378,6 +463,94 @@ exports[`json has feed item for each post 1`] = `
]
`;
exports[`json has feed item for each post using hash router 1`] = `
[
"{
"version": "https://jsonfeed.org/version/1",
"title": "Hello Blog",
"home_page_url": "https://docusaurus.io/myBaseUrl/blog",
"description": "Hello Blog",
"items": [
{
"id": "https://docusaurus.io/#/myBaseUrl/blog/blog-with-links",
"url": "https://docusaurus.io/#/myBaseUrl/blog/blog-with-links",
"title": "test links",
"summary": "absolute full url",
"date_modified": "2023-07-23T00:00:00.000Z",
"tags": []
},
{
"id": "https://docusaurus.io/#/myBaseUrl/blog/mdx-require-blog-post",
"url": "https://docusaurus.io/#/myBaseUrl/blog/mdx-require-blog-post",
"title": "MDX Blog Sample with require calls",
"summary": "Test MDX with require calls",
"date_modified": "2021-03-06T00:00:00.000Z",
"tags": []
},
{
"id": "https://docusaurus.io/#/myBaseUrl/blog/mdx-blog-post",
"url": "https://docusaurus.io/#/myBaseUrl/blog/mdx-blog-post",
"title": "Full Blog Sample",
"summary": "HTML Heading 1",
"date_modified": "2021-03-05T00:00:00.000Z",
"tags": []
},
{
"id": "https://docusaurus.io/#/myBaseUrl/blog/hey/my super path/héllô",
"url": "https://docusaurus.io/#/myBaseUrl/blog/hey/my super path/héllô",
"title": "Complex Slug",
"summary": "complex url slug",
"date_modified": "2020-08-16T00:00:00.000Z",
"tags": [
"date",
"complex"
]
},
{
"id": "https://docusaurus.io/#/myBaseUrl/blog/simple/slug",
"url": "https://docusaurus.io/#/myBaseUrl/blog/simple/slug",
"title": "Simple Slug",
"summary": "simple url slug",
"date_modified": "2020-08-15T00:00:00.000Z",
"author": {
"name": "Sébastien Lorber",
"url": "https://sebastienlorber.com"
},
"tags": []
},
{
"id": "https://docusaurus.io/#/myBaseUrl/blog/heading-as-title",
"url": "https://docusaurus.io/#/myBaseUrl/blog/heading-as-title",
"title": "some heading",
"date_modified": "2019-01-02T00:00:00.000Z",
"tags": []
},
{
"id": "https://docusaurus.io/#/myBaseUrl/blog/date-matter",
"url": "https://docusaurus.io/#/myBaseUrl/blog/date-matter",
"title": "date-matter",
"summary": "date inside front matter",
"date_modified": "2019-01-01T00:00:00.000Z",
"tags": [
"date"
]
},
{
"id": "https://docusaurus.io/#/myBaseUrl/blog/2018/12/14/Happy-First-Birthday-Slash",
"url": "https://docusaurus.io/#/myBaseUrl/blog/2018/12/14/Happy-First-Birthday-Slash",
"title": "Happy 1st Birthday Slash! (translated)",
"summary": "Happy birthday! (translated)",
"date_modified": "2018-12-14T00:00:00.000Z",
"author": {
"name": "Yangshun Tay (translated)"
},
"tags": []
}
]
}",
]
`;
exports[`rss filters to the first two entries 1`] = `
[
"<?xml version="1.0" encoding="utf-8"?>
@ -593,3 +766,80 @@ exports[`rss has feed item for each post 1`] = `
</rss>",
]
`;
exports[`rss has feed item for each post using hash router 1`] = `
[
"<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Hello Blog</title>
<link>https://docusaurus.io/myBaseUrl/blog</link>
<description>Hello Blog</description>
<lastBuildDate>Sun, 23 Jul 2023 00:00:00 GMT</lastBuildDate>
<docs>https://validator.w3.org/feed/docs/rss2.html</docs>
<generator>https://github.com/jpmonette/feed</generator>
<language>en</language>
<copyright>Copyright</copyright>
<item>
<title><![CDATA[test links]]></title>
<link>https://docusaurus.io/#/myBaseUrl/blog/blog-with-links</link>
<guid>https://docusaurus.io/#/myBaseUrl/blog/blog-with-links</guid>
<pubDate>Sun, 23 Jul 2023 00:00:00 GMT</pubDate>
<description><![CDATA[absolute full url]]></description>
</item>
<item>
<title><![CDATA[MDX Blog Sample with require calls]]></title>
<link>https://docusaurus.io/#/myBaseUrl/blog/mdx-require-blog-post</link>
<guid>https://docusaurus.io/#/myBaseUrl/blog/mdx-require-blog-post</guid>
<pubDate>Sat, 06 Mar 2021 00:00:00 GMT</pubDate>
<description><![CDATA[Test MDX with require calls]]></description>
</item>
<item>
<title><![CDATA[Full Blog Sample]]></title>
<link>https://docusaurus.io/#/myBaseUrl/blog/mdx-blog-post</link>
<guid>https://docusaurus.io/#/myBaseUrl/blog/mdx-blog-post</guid>
<pubDate>Fri, 05 Mar 2021 00:00:00 GMT</pubDate>
<description><![CDATA[HTML Heading 1]]></description>
</item>
<item>
<title><![CDATA[Complex Slug]]></title>
<link>https://docusaurus.io/#/myBaseUrl/blog/hey/my super path/héllô</link>
<guid>https://docusaurus.io/#/myBaseUrl/blog/hey/my super path/héllô</guid>
<pubDate>Sun, 16 Aug 2020 00:00:00 GMT</pubDate>
<description><![CDATA[complex url slug]]></description>
<category>date</category>
<category>complex</category>
</item>
<item>
<title><![CDATA[Simple Slug]]></title>
<link>https://docusaurus.io/#/myBaseUrl/blog/simple/slug</link>
<guid>https://docusaurus.io/#/myBaseUrl/blog/simple/slug</guid>
<pubDate>Sat, 15 Aug 2020 00:00:00 GMT</pubDate>
<description><![CDATA[simple url slug]]></description>
</item>
<item>
<title><![CDATA[some heading]]></title>
<link>https://docusaurus.io/#/myBaseUrl/blog/heading-as-title</link>
<guid>https://docusaurus.io/#/myBaseUrl/blog/heading-as-title</guid>
<pubDate>Wed, 02 Jan 2019 00:00:00 GMT</pubDate>
</item>
<item>
<title><![CDATA[date-matter]]></title>
<link>https://docusaurus.io/#/myBaseUrl/blog/date-matter</link>
<guid>https://docusaurus.io/#/myBaseUrl/blog/date-matter</guid>
<pubDate>Tue, 01 Jan 2019 00:00:00 GMT</pubDate>
<description><![CDATA[date inside front matter]]></description>
<category>date</category>
</item>
<item>
<title><![CDATA[Happy 1st Birthday Slash! (translated)]]></title>
<link>https://docusaurus.io/#/myBaseUrl/blog/2018/12/14/Happy-First-Birthday-Slash</link>
<guid>https://docusaurus.io/#/myBaseUrl/blog/2018/12/14/Happy-First-Birthday-Slash</guid>
<pubDate>Fri, 14 Dec 2018 00:00:00 GMT</pubDate>
<description><![CDATA[Happy birthday! (translated)]]></description>
<author>lorber.sebastien@gmail.com (Sébastien Lorber (translated))</author>
</item>
</channel>
</rss>",
]
`;

View File

@ -12,9 +12,15 @@ import {DEFAULT_PARSE_FRONT_MATTER} from '@docusaurus/utils';
import {DEFAULT_OPTIONS} from '../options';
import {generateBlogPosts} from '../blogUtils';
import {createBlogFeedFiles} from '../feed';
import type {LoadContext, I18n} from '@docusaurus/types';
import type {
LoadContext,
I18n,
DocusaurusConfig,
MarkdownConfig,
RouterType,
} from '@docusaurus/types';
import type {BlogContentPaths} from '../types';
import type {PluginOptions} from '@docusaurus/plugin-content-blog';
import type {FeedType, PluginOptions} from '@docusaurus/plugin-content-blog';
const DefaultI18N: I18n = {
currentLocale: 'en',
@ -32,7 +38,13 @@ const DefaultI18N: I18n = {
},
};
const markdown = {parseFrontMatter: DEFAULT_PARSE_FRONT_MATTER};
function partial<T>(t: Partial<T>): T {
return t as T;
}
const markdown = partial<MarkdownConfig>({
parseFrontMatter: DEFAULT_PARSE_FRONT_MATTER,
});
function getBlogContentPaths(siteDir: string): BlogContentPaths {
return {
@ -65,41 +77,61 @@ async function testGenerateFeeds(
});
}
describe.each(['atom', 'rss', 'json'])('%s', (feedType) => {
function pluginOptions(
feedType: FeedType,
options: Partial<PluginOptions> = {},
): PluginOptions {
return partial<PluginOptions>({
path: 'blog',
routeBasePath: 'blog',
tagsBasePath: 'tags',
authorsMapPath: 'authors.yml',
include: DEFAULT_OPTIONS.include,
exclude: DEFAULT_OPTIONS.exclude,
feedOptions: {
type: [feedType],
copyright: 'Copyright',
},
readingTime: ({content, defaultReadingTime}) =>
defaultReadingTime({content}),
truncateMarker: /<!--\s*truncate\s*-->/,
...options,
});
}
function siteFor(
siteDir: string,
siteOptions?: {baseUrl?: string; router?: RouterType},
) {
const siteConfig = partial<DocusaurusConfig>({
title: 'Hello',
router: siteOptions?.router,
baseUrl: siteOptions?.baseUrl,
url: 'https://docusaurus.io',
favicon: 'image/favicon.ico',
markdown,
});
const outDir = path.join(siteDir, 'build-snap');
const loadContext = partial<LoadContext>({
siteDir,
siteConfig,
i18n: DefaultI18N,
outDir,
});
return {siteConfig, outDir, loadContext};
}
describe.each(['atom', 'rss', 'json'] as FeedType[])('%s', (feedType) => {
const fsMock = jest.spyOn(fs, 'outputFile').mockImplementation(() => {});
it('does not get generated without posts', async () => {
const siteDir = __dirname;
const siteConfig = {
title: 'Hello',
baseUrl: '/',
url: 'https://docusaurus.io',
favicon: 'image/favicon.ico',
markdown,
};
const outDir = path.join(siteDir, 'build-snap');
const {loadContext} = siteFor(__dirname);
await testGenerateFeeds(
{
siteDir,
siteConfig,
i18n: DefaultI18N,
outDir,
} as LoadContext,
{
loadContext,
pluginOptions(feedType, {
path: 'invalid-blog-path',
routeBasePath: 'blog',
tagsBasePath: 'tags',
authorsMapPath: 'authors.yml',
include: ['*.md', '*.mdx'],
feedOptions: {
type: [feedType],
copyright: 'Copyright',
},
readingTime: ({content, defaultReadingTime}) =>
defaultReadingTime({content}),
truncateMarker: /<!--\s*truncate\s*-->/,
} as PluginOptions,
}),
);
expect(fsMock).toHaveBeenCalledTimes(0);
@ -108,40 +140,28 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => {
it('has feed item for each post', async () => {
const siteDir = path.join(__dirname, '__fixtures__', 'website');
const outDir = path.join(siteDir, 'build-snap');
const siteConfig = {
title: 'Hello',
baseUrl: '/myBaseUrl/',
url: 'https://docusaurus.io',
favicon: 'image/favicon.ico',
markdown,
};
const {loadContext} = siteFor(siteDir, {baseUrl: '/myBaseUrl/'});
// Build is quite difficult to mock, so we built the blog beforehand and
// copied the output to the fixture...
await testGenerateFeeds(
{
siteDir,
siteConfig,
i18n: DefaultI18N,
outDir,
} as LoadContext,
{
path: 'blog',
routeBasePath: 'blog',
tagsBasePath: 'tags',
authorsMapPath: 'authors.yml',
include: DEFAULT_OPTIONS.include,
exclude: DEFAULT_OPTIONS.exclude,
feedOptions: {
type: [feedType],
copyright: 'Copyright',
},
readingTime: ({content, defaultReadingTime}) =>
defaultReadingTime({content}),
truncateMarker: /<!--\s*truncate\s*-->/,
} as PluginOptions,
);
await testGenerateFeeds(loadContext, pluginOptions(feedType));
expect(
fsMock.mock.calls.map((call) => call[1] as string),
).toMatchSnapshot();
fsMock.mockClear();
});
it('has feed item for each post using hash router', async () => {
const siteDir = path.join(__dirname, '__fixtures__', 'website');
const {loadContext} = siteFor(siteDir, {
baseUrl: '/myBaseUrl/',
router: 'hash',
});
// Build is quite difficult to mock, so we built the blog beforehand and
// copied the output to the fixture...
await testGenerateFeeds(loadContext, pluginOptions(feedType));
expect(
fsMock.mock.calls.map((call) => call[1] as string),
@ -151,31 +171,15 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => {
it('filters to the first two entries', async () => {
const siteDir = path.join(__dirname, '__fixtures__', 'website');
const outDir = path.join(siteDir, 'build-snap');
const siteConfig = {
title: 'Hello',
const {loadContext} = siteFor(siteDir, {
baseUrl: '/myBaseUrl/',
url: 'https://docusaurus.io',
favicon: 'image/favicon.ico',
markdown,
};
});
// Build is quite difficult to mock, so we built the blog beforehand and
// copied the output to the fixture...
await testGenerateFeeds(
{
siteDir,
siteConfig,
i18n: DefaultI18N,
outDir,
} as LoadContext,
{
path: 'blog',
routeBasePath: 'blog',
tagsBasePath: 'tags',
authorsMapPath: 'authors.yml',
include: DEFAULT_OPTIONS.include,
exclude: DEFAULT_OPTIONS.exclude,
loadContext,
pluginOptions(feedType, {
feedOptions: {
type: [feedType],
copyright: 'Copyright',
@ -190,10 +194,7 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => {
});
},
},
readingTime: ({content, defaultReadingTime}) =>
defaultReadingTime({content}),
truncateMarker: /<!--\s*truncate\s*-->/,
} as PluginOptions,
}),
);
expect(
@ -204,40 +205,21 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => {
it('filters to the first two entries using limit', async () => {
const siteDir = path.join(__dirname, '__fixtures__', 'website');
const outDir = path.join(siteDir, 'build-snap');
const siteConfig = {
title: 'Hello',
const {loadContext} = siteFor(siteDir, {
baseUrl: '/myBaseUrl/',
url: 'https://docusaurus.io',
favicon: 'image/favicon.ico',
markdown,
};
});
// Build is quite difficult to mock, so we built the blog beforehand and
// copied the output to the fixture...
await testGenerateFeeds(
{
siteDir,
siteConfig,
i18n: DefaultI18N,
outDir,
} as LoadContext,
{
path: 'blog',
routeBasePath: 'blog',
tagsBasePath: 'tags',
authorsMapPath: 'authors.yml',
include: DEFAULT_OPTIONS.include,
exclude: DEFAULT_OPTIONS.exclude,
loadContext,
pluginOptions(feedType, {
feedOptions: {
type: [feedType],
copyright: 'Copyright',
limit: 2,
},
readingTime: ({content, defaultReadingTime}) =>
defaultReadingTime({content}),
truncateMarker: /<!--\s*truncate\s*-->/,
} as PluginOptions,
}),
);
expect(

View File

@ -76,6 +76,65 @@ async function generateBlogFeed({
return feed;
}
async function readBlogPostContent({
blogPost: post,
absoluteUrl,
siteConfig,
outDir,
}: {
blogPost: BlogPost;
absoluteUrl: string;
siteConfig: DocusaurusConfig;
outDir: string;
}): Promise<string | undefined> {
const {router, trailingSlash, baseUrl} = siteConfig;
const {
metadata: {permalink},
} = post;
// The hash router does not SSG: we can't read content from HTML files
if (router === 'hash') {
return undefined;
}
const content = await readOutputHTMLFile(
permalink.replace(baseUrl, ''),
outDir,
trailingSlash,
);
const $ = cheerioLoad(content);
const toAbsoluteUrl = (src: string) => String(new URL(src, absoluteUrl));
// Make links and image urls absolute
// See https://github.com/facebook/docusaurus/issues/9136
$(`div#${blogPostContainerID} a, div#${blogPostContainerID} img`).each(
(_, elm) => {
if (elm.tagName === 'a') {
const {href} = elm.attribs;
if (href) {
elm.attribs.href = toAbsoluteUrl(href);
}
} else if (elm.tagName === 'img') {
const {src, srcset: srcsetAttr} = elm.attribs;
if (src) {
elm.attribs.src = toAbsoluteUrl(src);
}
if (srcsetAttr) {
elm.attribs.srcset = srcset.stringify(
srcset.parse(srcsetAttr).map((props) => ({
...props,
url: toAbsoluteUrl(props.url),
})),
);
}
}
},
);
return $(`#${blogPostContainerID}`).html()!;
}
async function defaultCreateFeedItems({
blogPosts,
siteConfig,
@ -85,7 +144,7 @@ async function defaultCreateFeedItems({
siteConfig: DocusaurusConfig;
outDir: string;
}): Promise<BlogFeedItem[]> {
const {url: siteUrl} = siteConfig;
const {url: siteUrl, router} = siteConfig;
function toFeedAuthor(author: Author): FeedAuthor {
return {name: author.name, link: author.url, email: author.email};
@ -104,53 +163,28 @@ async function defaultCreateFeedItems({
},
} = post;
const content = await readOutputHTMLFile(
permalink.replace(siteConfig.baseUrl, ''),
const absoluteUrl = normalizeUrl([
siteUrl,
router === 'hash' ? '/#/' : '',
permalink,
]);
const content = await readBlogPostContent({
blogPost: post,
absoluteUrl,
siteConfig,
outDir,
siteConfig.trailingSlash,
);
const $ = cheerioLoad(content);
const blogPostAbsoluteUrl = normalizeUrl([siteUrl, permalink]);
const toAbsoluteUrl = (src: string) =>
String(new URL(src, blogPostAbsoluteUrl));
// Make links and image urls absolute
// See https://github.com/facebook/docusaurus/issues/9136
$(`div#${blogPostContainerID} a, div#${blogPostContainerID} img`).each(
(_, elm) => {
if (elm.tagName === 'a') {
const {href} = elm.attribs;
if (href) {
elm.attribs.href = toAbsoluteUrl(href);
}
} else if (elm.tagName === 'img') {
const {src, srcset: srcsetAttr} = elm.attribs;
if (src) {
elm.attribs.src = toAbsoluteUrl(src);
}
if (srcsetAttr) {
elm.attribs.srcset = srcset.stringify(
srcset.parse(srcsetAttr).map((props) => ({
...props,
url: toAbsoluteUrl(props.url),
})),
);
}
}
},
);
});
const feedItem: BlogFeedItem = {
title: metadataTitle,
id: blogPostAbsoluteUrl,
link: blogPostAbsoluteUrl,
id: absoluteUrl,
link: absoluteUrl,
date,
description,
// Atom feed demands the "term", while other feeds use "name"
category: tags.map((tag) => ({name: tag.label, term: tag.label})),
content: $(`#${blogPostContainerID}`).html()!,
content,
};
// json1() method takes the first item of authors array

View File

@ -23,6 +23,7 @@
"@babel/core": "^7.23.3",
"@babel/preset-env": "^7.23.3",
"@docusaurus/core": "3.0.0",
"@docusaurus/logger": "3.0.0",
"@docusaurus/theme-common": "3.0.0",
"@docusaurus/theme-translations": "3.0.0",
"@docusaurus/types": "3.0.0",

View File

@ -11,11 +11,14 @@ import WebpackBar from 'webpackbar';
import Terser from 'terser-webpack-plugin';
import {injectManifest} from 'workbox-build';
import {normalizeUrl} from '@docusaurus/utils';
import logger from '@docusaurus/logger';
import {compile} from '@docusaurus/core/lib/webpack/utils';
import {readDefaultCodeTranslationMessages} from '@docusaurus/theme-translations';
import type {HtmlTags, LoadContext, Plugin} from '@docusaurus/types';
import type {PluginOptions} from '@docusaurus/plugin-pwa';
const PluginName = 'docusaurus-plugin-pwa';
const isProd = process.env.NODE_ENV === 'production';
function getSWBabelLoader() {
@ -47,6 +50,7 @@ export default function pluginPWA(
outDir,
baseUrl,
i18n: {currentLocale},
siteConfig: {router},
} = context;
const {
debug,
@ -57,8 +61,13 @@ export default function pluginPWA(
swRegister,
} = options;
if (router === 'hash') {
logger.warn(`${PluginName} does not support the Hash Router`);
return {name: PluginName};
}
return {
name: 'docusaurus-plugin-pwa',
name: PluginName,
getThemePath() {
return '../lib/theme';

View File

@ -26,8 +26,28 @@ describe('createSitemap', () => {
filename: 'sitemap.xml',
},
);
expect(sitemap).toContain(
`<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">`,
expect(sitemap).toMatchInlineSnapshot(
`"<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1"><url><loc>https://example.com/</loc><changefreq>daily</changefreq><priority>0.7</priority></url><url><loc>https://example.com/test</loc><changefreq>daily</changefreq><priority>0.7</priority></url></urlset>"`,
);
});
it('simple site - hash router', async () => {
const sitemap = await createSitemap(
{
url: 'https://example.com',
router: 'hash',
} as DocusaurusConfig,
['/', '/test'],
{},
{
changefreq: EnumChangefreq.DAILY,
priority: 0.7,
ignorePatterns: [],
filename: 'sitemap.xml',
},
);
expect(sitemap).toMatchInlineSnapshot(
`"<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1"><url><loc>https://example.com/#/</loc><changefreq>daily</changefreq><priority>0.7</priority></url><url><loc>https://example.com/#/test</loc><changefreq>daily</changefreq><priority>0.7</priority></url></urlset>"`,
);
});

View File

@ -8,7 +8,7 @@
import type {ReactElement} from 'react';
import {SitemapStream, streamToPromise} from 'sitemap';
import {applyTrailingSlash} from '@docusaurus/utils-common';
import {createMatcher} from '@docusaurus/utils';
import {createMatcher, normalizeUrl} from '@docusaurus/utils';
import type {DocusaurusConfig} from '@docusaurus/types';
import type {HelmetServerState} from 'react-helmet-async';
import type {PluginOptions} from './options';
@ -53,7 +53,7 @@ export default async function createSitemap(
head: {[location: string]: HelmetServerState},
options: PluginOptions,
): Promise<string | null> {
const {url: hostname} = siteConfig;
const {url: hostname, router} = siteConfig;
if (!hostname) {
throw new Error('URL in docusaurus.config.js cannot be empty/undefined.');
}
@ -77,12 +77,17 @@ export default async function createSitemap(
const sitemapStream = new SitemapStream({hostname});
const createSitemapUrl = (routePath: string): string => {
const path = normalizeUrl([router === 'hash' ? '/#/' : '', routePath]);
return applyTrailingSlash(path, {
trailingSlash: siteConfig.trailingSlash,
baseUrl: siteConfig.baseUrl,
});
};
includedRoutes.forEach((routePath) =>
sitemapStream.write({
url: applyTrailingSlash(routePath, {
trailingSlash: siteConfig.trailingSlash,
baseUrl: siteConfig.baseUrl,
}),
url: createSitemapUrl(routePath),
changefreq,
priority,
}),

View File

@ -16,6 +16,8 @@ export type RemarkRehypeOptions = ProcessorOptions['remarkRehypeOptions'];
export type ReportingSeverity = 'ignore' | 'log' | 'warn' | 'throw';
export type RouterType = 'browser' | 'hash';
export type ThemeConfig = {
[key: string]: unknown;
};
@ -137,6 +139,23 @@ export type DocusaurusConfig = {
*/
favicon?: string;
/**
* Docusaurus can work with 2 router types.
*
* - The "browser" router is the main/default router of Docusaurus.
* It will use the browser history and regular urls to navigate from
* one page to another. A static file will be emitted for each page.
*
* - The "hash" router can be useful in very specific situations (such as
* distributing your app for offline-first usage), but should be avoided
* in most cases. All pages paths will be prefixed with a /#/.
* It will opt out of static site generation, only emit a single index.html
* entry point, and use the browser hash for routing. The Docusaurus site
* content will be rendered client-side, like a regular single page
* application.
* @see https://github.com/facebook/docusaurus/issues/3825
*/
router: RouterType;
/**
* Allow to customize the presence/absence of a trailing slash at the end of
* URLs/links, and how static HTML files are generated:
*

View File

@ -7,6 +7,7 @@
export {
ReportingSeverity,
RouterType,
ThemeConfig,
MarkdownConfig,
DefaultParseFrontMatter,

View File

@ -94,6 +94,30 @@ describe('normalizeUrl', () => {
output: 'http://foobar.com/test/',
},
{
input: ['http://foobar.com/', '', 'test', '/'],
output: 'http://foobar.com/test/',
},
{
input: ['http://foobar.com', '#', 'test'],
output: 'http://foobar.com/#/test',
},
{
input: ['http://foobar.com/', '#', 'test'],
output: 'http://foobar.com/#/test',
},
{
input: ['http://foobar.com', '/#/', 'test'],
output: 'http://foobar.com/#/test',
},
{
input: ['http://foobar.com', '#/', 'test'],
output: 'http://foobar.com/#/test',
},
{
input: ['http://foobar.com', '/#', 'test'],
output: 'http://foobar.com/#/test',
},
{
input: ['/', '', 'hello', '', '/', '/', '', '/', '/world'],
output: '/hello/world',
},

View File

@ -91,7 +91,7 @@ export function normalizeUrl(rawUrls: string[]): string {
// first plain protocol part.
// Remove trailing slash before parameters or hash.
str = str.replace(/\/(?<search>\?|&|#[^!])/g, '$1');
str = str.replace(/\/(?<search>\?|&|#[^!/])/g, '$1');
// Replace ? in parameters with &.
const parts = str.split('?');

View File

@ -7,7 +7,7 @@
import React from 'react';
import ReactDOM, {type ErrorInfo} from 'react-dom/client';
import {BrowserRouter} from 'react-router-dom';
import Router from '@generated/router';
import {HelmetProvider} from 'react-helmet-async';
import ExecutionEnvironment from './exports/ExecutionEnvironment';
@ -31,9 +31,9 @@ if (ExecutionEnvironment.canUseDOM) {
const app = (
<HelmetProvider>
<BrowserRouter>
<Router>
<App />
</BrowserRouter>
</Router>
</HelmetProvider>
);

View File

@ -41,7 +41,7 @@ function Link(
forwardedRef: React.ForwardedRef<HTMLAnchorElement>,
): JSX.Element {
const {
siteConfig: {trailingSlash, baseUrl},
siteConfig: {trailingSlash, baseUrl, router},
} = useDocusaurusContext();
const {withBaseUrl} = useBaseUrlUtils();
const brokenLinks = useBrokenLinks();
@ -81,6 +81,11 @@ function Link(
? maybeAddBaseUrl(targetLinkWithoutPathnameProtocol)
: undefined;
// TODO temporary hack
if (router === 'hash' && targetLink?.startsWith('./')) {
targetLink = targetLink?.slice(1);
}
if (targetLink && isInternal) {
targetLink = applyTrailingSlash(targetLink, {trailingSlash, baseUrl});
}
@ -148,8 +153,7 @@ function Link(
const hasInternalTarget = !props.target || props.target === '_self';
// Should we use a regular <a> tag instead of React-Router Link component?
const isRegularHtmlLink =
!targetLink || !isInternal || !hasInternalTarget || isAnchorLink;
const isRegularHtmlLink = !targetLink || !isInternal || !hasInternalTarget;
if (!noBrokenLinkCheck && (isAnchorLink || !isRegularHtmlLink)) {
brokenLinks.collectLink(targetLink!);

View File

@ -9,19 +9,32 @@ import {useCallback} from 'react';
import useDocusaurusContext from './useDocusaurusContext';
import {hasProtocol} from './isInternalUrl';
import type {BaseUrlOptions, BaseUrlUtils} from '@docusaurus/useBaseUrl';
import type {RouterType} from '@docusaurus/types';
function addBaseUrl(
siteUrl: string,
baseUrl: string,
url: string,
{forcePrependBaseUrl = false, absolute = false}: BaseUrlOptions = {},
): string {
function addBaseUrl({
siteUrl,
baseUrl,
url,
options: {forcePrependBaseUrl = false, absolute = false} = {},
router,
}: {
siteUrl: string;
baseUrl: string;
url: string;
router: RouterType;
options?: BaseUrlOptions;
}): string {
// It never makes sense to add base url to a local anchor url, or one with a
// protocol
if (!url || url.startsWith('#') || hasProtocol(url)) {
return url;
}
// TODO temp hack
if (router === 'hash' && url.startsWith('/')) {
return `.${url}`;
}
if (forcePrependBaseUrl) {
return baseUrl + url.replace(/^\//, '');
}
@ -42,13 +55,13 @@ function addBaseUrl(
export function useBaseUrlUtils(): BaseUrlUtils {
const {
siteConfig: {baseUrl, url: siteUrl},
siteConfig: {baseUrl, url: siteUrl, router},
} = useDocusaurusContext();
const withBaseUrl = useCallback(
(url: string, options?: BaseUrlOptions) =>
addBaseUrl(siteUrl, baseUrl, url, options),
[siteUrl, baseUrl],
addBaseUrl({siteUrl, baseUrl, url, options, router}),
[siteUrl, baseUrl, router],
);
return {

View File

@ -26,13 +26,29 @@ const render: AppRenderer = async ({pathname}) => {
const helmetContext = {};
const statefulBrokenLinks = createStatefulBrokenLinks();
const localBuild = true;
const appContent = localBuild ? (
<div
style={{
width: '100vw',
height: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}>
<div style={{fontSize: 200}}>... LOADING ...</div>
</div>
) : (
<App />
);
const app = (
// @ts-expect-error: we are migrating away from react-loadable anyways
<Loadable.Capture report={(moduleName) => modules.add(moduleName)}>
<HelmetProvider context={helmetContext}>
<StaticRouter location={pathname} context={routerContext}>
<BrokenLinksProvider brokenLinks={statefulBrokenLinks}>
<App />
{appContent}
</BrokenLinksProvider>
</StaticRouter>
</HelmetProvider>

View File

@ -23,12 +23,20 @@ import {
import {PerfLogger} from '../utils';
import {loadI18n} from '../server/i18n';
import {generateStaticFiles, loadAppRenderer} from '../ssg';
import {compileSSRTemplate} from '../templates/templates';
import {
generateHashRouterEntrypoint,
generateStaticFiles,
loadAppRenderer,
} from '../ssg';
import {
compileSSRTemplate,
renderHashRouterTemplate,
} from '../templates/templates';
import defaultSSRTemplate from '../templates/ssr.html.template';
import type {SSGParams} from '../ssg';
import type {Manifest} from 'react-loadable-ssr-addon-v5-slorber';
import type {LoadedPlugin, Props} from '@docusaurus/types';
import type {LoadedPlugin, Props, RouterType} from '@docusaurus/types';
import type {SiteCollectedData} from '../common';
export type BuildCLIOptions = Pick<
@ -171,7 +179,11 @@ async function buildLocale({
PerfLogger.end('Loading site');
// Apply user webpack config.
const {outDir, plugins} = props;
const {
outDir,
plugins,
siteConfig: {router},
} = props;
// We can build the 2 configs in parallel
PerfLogger.start('Creating webpack configs');
@ -196,7 +208,11 @@ async function buildLocale({
// Run webpack to build JS bundle (client) and static html files (server).
PerfLogger.start('Bundling');
await compile([clientConfig, serverConfig]);
if (router === 'hash') {
await compile([clientConfig]);
} else {
await compile([clientConfig, serverConfig]);
}
PerfLogger.end('Bundling');
PerfLogger.start('Executing static site generation');
@ -204,6 +220,7 @@ async function buildLocale({
props,
serverBundlePath,
clientManifestPath,
router,
});
PerfLogger.end('Executing static site generation');
@ -242,11 +259,13 @@ async function executeSSG({
props,
serverBundlePath,
clientManifestPath,
router,
}: {
props: Props;
serverBundlePath: string;
clientManifestPath: string;
}) {
router: RouterType;
}): Promise<{collectedData: SiteCollectedData}> {
PerfLogger.start('Reading client manifest');
const manifest: Manifest = await fs.readJSON(clientManifestPath, 'utf-8');
PerfLogger.end('Reading client manifest');
@ -257,6 +276,27 @@ async function executeSSG({
);
PerfLogger.end('Compiling SSR template');
const params: SSGParams = {
trailingSlash: props.siteConfig.trailingSlash,
outDir: props.outDir,
baseUrl: props.baseUrl,
manifest,
headTags: props.headTags,
preBodyTags: props.preBodyTags,
postBodyTags: props.postBodyTags,
ssrTemplate,
noIndex: props.siteConfig.noIndex,
DOCUSAURUS_VERSION,
};
if (router === 'hash') {
PerfLogger.start('Generate Hash Router entry point');
const content = renderHashRouterTemplate({params});
await generateHashRouterEntrypoint({content, params});
PerfLogger.end('Generate Hash Router entry point');
return {collectedData: {}};
}
PerfLogger.start('Loading App renderer');
const renderer = await loadAppRenderer({
serverBundlePath,
@ -267,18 +307,7 @@ async function executeSSG({
const ssgResult = await generateStaticFiles({
pathnames: props.routesPaths,
renderer,
params: {
trailingSlash: props.siteConfig.trailingSlash,
outDir: props.outDir,
baseUrl: props.baseUrl,
manifest,
headTags: props.headTags,
preBodyTags: props.preBodyTags,
postBodyTags: props.postBodyTags,
ssrTemplate,
noIndex: props.siteConfig.noIndex,
DOCUSAURUS_VERSION,
},
params,
});
PerfLogger.end('Generate static files');

View File

@ -35,6 +35,7 @@ exports[`loadSiteConfig website with .cjs siteConfig 1`] = `
"onDuplicateRoutes": "warn",
"plugins": [],
"presets": [],
"router": "browser",
"scripts": [],
"staticDirectories": [
"static",
@ -86,6 +87,7 @@ exports[`loadSiteConfig website with ts + js config 1`] = `
"onDuplicateRoutes": "warn",
"plugins": [],
"presets": [],
"router": "browser",
"scripts": [],
"staticDirectories": [
"static",
@ -137,6 +139,7 @@ exports[`loadSiteConfig website with valid JS CJS config 1`] = `
"onDuplicateRoutes": "warn",
"plugins": [],
"presets": [],
"router": "browser",
"scripts": [],
"staticDirectories": [
"static",
@ -188,6 +191,7 @@ exports[`loadSiteConfig website with valid JS ESM config 1`] = `
"onDuplicateRoutes": "warn",
"plugins": [],
"presets": [],
"router": "browser",
"scripts": [],
"staticDirectories": [
"static",
@ -239,6 +243,7 @@ exports[`loadSiteConfig website with valid TypeScript CJS config 1`] = `
"onDuplicateRoutes": "warn",
"plugins": [],
"presets": [],
"router": "browser",
"scripts": [],
"staticDirectories": [
"static",
@ -290,6 +295,7 @@ exports[`loadSiteConfig website with valid TypeScript ESM config 1`] = `
"onDuplicateRoutes": "warn",
"plugins": [],
"presets": [],
"router": "browser",
"scripts": [],
"staticDirectories": [
"static",
@ -343,6 +349,7 @@ exports[`loadSiteConfig website with valid async config 1`] = `
"plugins": [],
"presets": [],
"projectName": "hello",
"router": "browser",
"scripts": [],
"staticDirectories": [
"static",
@ -396,6 +403,7 @@ exports[`loadSiteConfig website with valid async config creator function 1`] = `
"plugins": [],
"presets": [],
"projectName": "hello",
"router": "browser",
"scripts": [],
"staticDirectories": [
"static",
@ -449,6 +457,7 @@ exports[`loadSiteConfig website with valid config creator function 1`] = `
"plugins": [],
"presets": [],
"projectName": "hello",
"router": "browser",
"scripts": [],
"staticDirectories": [
"static",
@ -513,6 +522,7 @@ exports[`loadSiteConfig website with valid siteConfig 1`] = `
],
"presets": [],
"projectName": "hello",
"router": "browser",
"scripts": [],
"staticDirectories": [
"static",

View File

@ -109,6 +109,7 @@ exports[`load loads props for site with custom i18n path 1`] = `
"onDuplicateRoutes": "warn",
"plugins": [],
"presets": [],
"router": "browser",
"scripts": [],
"staticDirectories": [
"static",

View File

@ -45,6 +45,7 @@ export const DEFAULT_MARKDOWN_CONFIG: MarkdownConfig = {
export const DEFAULT_CONFIG: Pick<
DocusaurusConfig,
| 'i18n'
| 'router'
| 'onBrokenLinks'
| 'onBrokenAnchors'
| 'onBrokenMarkdownLinks'
@ -66,6 +67,7 @@ export const DEFAULT_CONFIG: Pick<
| 'markdown'
> = {
i18n: DEFAULT_I18N_CONFIG,
router: 'browser',
onBrokenLinks: 'throw',
onBrokenAnchors: 'warn', // TODO Docusaurus v4: change to throw
onBrokenMarkdownLinks: 'warn',
@ -208,6 +210,7 @@ export const ConfigSchema = Joi.object<DocusaurusConfig>({
favicon: Joi.string().optional(),
title: Joi.string().required(),
url: SiteUrlSchema,
router: Joi.string().equal('browser', 'hash').default(DEFAULT_CONFIG.router),
trailingSlash: Joi.boolean(), // No default value! undefined = retrocompatible legacy behavior!
i18n: I18N_CONFIG_SCHEMA,
onBrokenLinks: Joi.string()

View File

@ -36,16 +36,24 @@ function assertIsHtmlTagObject(val: unknown): asserts val is HtmlTagObject {
}
}
function absoluteToRelativeTagAttribute(name: string, value: string): string {
if ((name === 'src' || name === 'href') && value.startsWith('/')) {
return `.${value}`; // TODO would only work for homepage
}
return value;
}
function htmlTagObjectToString(tag: unknown): string {
assertIsHtmlTagObject(tag);
const isVoidTag = (voidHtmlTags as string[]).includes(tag.tagName);
const tagAttributes = tag.attributes ?? {};
const attributes = Object.keys(tagAttributes)
.map((attr) => {
const value = tagAttributes[attr]!;
let value = tagAttributes[attr]!;
if (typeof value === 'boolean') {
return value ? attr : undefined;
}
value = absoluteToRelativeTagAttribute(attr, value);
return `${attr}="${escapeHTML(value)}"`;
})
.filter((str): str is string => Boolean(str));

View File

@ -232,6 +232,14 @@ ${Object.entries(registry)
JSON.stringify(siteMetadata, null, 2),
);
const routerImport =
siteConfig.router === 'hash' ? 'HashRouter' : 'BrowserRouter';
const genRouter = generate(
generatedFilesDir,
'router.js',
`export {${routerImport} as default} from 'react-router-dom';`,
);
await Promise.all([
genWarning,
genClientModules,
@ -243,6 +251,7 @@ ${Object.entries(registry)
genSiteMetadata,
genI18n,
genCodeTranslations,
genRouter,
]);
return {

View File

@ -226,6 +226,20 @@ It might also require to wrap your client code in ${logger.code(
return parts.join('\n');
}
export async function generateHashRouterEntrypoint({
content,
params,
}: {
content: string;
params: SSGParams;
}): Promise<void> {
await writeStaticFile({
pathname: '/',
content,
params,
});
}
async function writeStaticFile({
content,
pathname,

View File

@ -113,3 +113,41 @@ export function renderSSRTemplate({
return ssrTemplate(data);
}
export function renderHashRouterTemplate({
params,
}: {
params: SSGParams;
}): string {
const {
// baseUrl,
headTags,
preBodyTags,
postBodyTags,
manifest,
DOCUSAURUS_VERSION,
ssrTemplate,
} = params;
const {scripts, stylesheets} = getScriptsAndStylesheets({
manifest,
modules: [],
});
const data: SSRTemplateData = {
appHtml: '',
baseUrl: './',
htmlAttributes: '',
bodyAttributes: '',
headTags,
preBodyTags,
postBodyTags,
metaAttributes: [],
scripts,
stylesheets,
noIndex: false,
version: DOCUSAURUS_VERSION,
};
return ssrTemplate(data);
}

View File

@ -112,7 +112,7 @@ export async function createBaseConfig({
chunkFilename: isProd
? 'assets/js/[name].[contenthash:8].js'
: '[name].js',
publicPath: baseUrl,
publicPath: siteConfig.router === 'hash' ? 'auto' : baseUrl,
hashFunction: 'xxhash64',
},
// Don't throw warning when asset created is over 250kb

View File

@ -129,7 +129,14 @@ export async function createBuildClientConfig({
bundleAnalyzer: boolean;
}): Promise<{config: Configuration; clientManifestPath: string}> {
// Apply user webpack config.
const {generatedFilesDir} = props;
const {
generatedFilesDir,
siteConfig: {router},
} = props;
// With the hash router, we don't hydrate the React app, even in build mode!
// This is because it will always be a client-rendered React app
const hydrate = router !== 'hash';
const clientManifestPath = path.join(
generatedFilesDir,
@ -137,7 +144,7 @@ export async function createBuildClientConfig({
);
const config: Configuration = merge(
await createBaseClientConfig({props, minify, hydrate: true}),
await createBaseClientConfig({props, minify, hydrate}),
{
plugins: [
new ForceTerminatePlugin(),

View File

@ -74,6 +74,8 @@ function getNextVersionName() {
// Test with: DOCUSAURUS_CRASH_TEST=true yarn build:website:fast
const crashTest = process.env.DOCUSAURUS_CRASH_TEST === 'true';
const router = process.env.DOCUSAURUS_ROUTER as Config['router'];
const isDev = process.env.NODE_ENV === 'development';
const isDeployPreview =
@ -126,6 +128,7 @@ export default async function createConfigAsync() {
baseUrl,
baseUrlIssueBanner: true,
url: 'https://docusaurus.io',
router,
// Dogfood both settings:
// - force trailing slashes for deploy previews
// - avoid trailing slashes in prod