Compare commits
20 Commits
main
...
slorber/of
| Author | SHA1 | Date |
|---|---|---|
|
|
2f33aadc4e | |
|
|
6582ea79d7 | |
|
|
62aa2cb0cf | |
|
|
030688a026 | |
|
|
bffb2f080c | |
|
|
6c072c3217 | |
|
|
81dbd82091 | |
|
|
5a5d2f3127 | |
|
|
4a22ebb2e4 | |
|
|
c22da5a1ea | |
|
|
4c5a9de268 | |
|
|
547d8abc68 | |
|
|
a1b9ba6f23 | |
|
|
904f73091b | |
|
|
25a9004727 | |
|
|
45c1c9d4b0 | |
|
|
0c5034b809 | |
|
|
de0df2dcb3 | |
|
|
da45fff262 | |
|
|
27b1acfc36 |
|
|
@ -45,6 +45,19 @@ declare module '@generated/routes' {
|
||||||
export default 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' {
|
declare module '@generated/routesChunkNames' {
|
||||||
import type {RouteChunkNames} from '@docusaurus/types';
|
import type {RouteChunkNames} from '@docusaurus/types';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {removePrefix, addLeadingSlash} from '@docusaurus/utils';
|
import {removePrefix, addLeadingSlash} from '@docusaurus/utils';
|
||||||
|
import logger from '@docusaurus/logger';
|
||||||
import collectRedirects from './collectRedirects';
|
import collectRedirects from './collectRedirects';
|
||||||
import writeRedirectFiles, {
|
import writeRedirectFiles, {
|
||||||
toRedirectFiles,
|
toRedirectFiles,
|
||||||
|
|
@ -15,14 +16,21 @@ import type {LoadContext, Plugin} from '@docusaurus/types';
|
||||||
import type {PluginContext, RedirectItem} from './types';
|
import type {PluginContext, RedirectItem} from './types';
|
||||||
import type {PluginOptions, Options} from './options';
|
import type {PluginOptions, Options} from './options';
|
||||||
|
|
||||||
|
const PluginName = 'docusaurus-plugin-client-redirects';
|
||||||
|
|
||||||
export default function pluginClientRedirectsPages(
|
export default function pluginClientRedirectsPages(
|
||||||
context: LoadContext,
|
context: LoadContext,
|
||||||
options: PluginOptions,
|
options: PluginOptions,
|
||||||
): Plugin<void> {
|
): 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 {
|
return {
|
||||||
name: 'docusaurus-plugin-client-redirects',
|
name: PluginName,
|
||||||
async postBuild(props) {
|
async postBuild(props) {
|
||||||
const pluginContext: PluginContext = {
|
const pluginContext: PluginContext = {
|
||||||
relativeRoutesPaths: props.routesPaths.map(
|
relativeRoutesPaths: props.routesPaths.map(
|
||||||
|
|
|
||||||
|
|
@ -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`] = `
|
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`] = `
|
exports[`rss filters to the first two entries 1`] = `
|
||||||
[
|
[
|
||||||
"<?xml version="1.0" encoding="utf-8"?>
|
"<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
@ -593,3 +766,80 @@ exports[`rss has feed item for each post 1`] = `
|
||||||
</rss>",
|
</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>",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,15 @@ import {DEFAULT_PARSE_FRONT_MATTER} from '@docusaurus/utils';
|
||||||
import {DEFAULT_OPTIONS} from '../options';
|
import {DEFAULT_OPTIONS} from '../options';
|
||||||
import {generateBlogPosts} from '../blogUtils';
|
import {generateBlogPosts} from '../blogUtils';
|
||||||
import {createBlogFeedFiles} from '../feed';
|
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 {BlogContentPaths} from '../types';
|
||||||
import type {PluginOptions} from '@docusaurus/plugin-content-blog';
|
import type {FeedType, PluginOptions} from '@docusaurus/plugin-content-blog';
|
||||||
|
|
||||||
const DefaultI18N: I18n = {
|
const DefaultI18N: I18n = {
|
||||||
currentLocale: 'en',
|
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 {
|
function getBlogContentPaths(siteDir: string): BlogContentPaths {
|
||||||
return {
|
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(() => {});
|
const fsMock = jest.spyOn(fs, 'outputFile').mockImplementation(() => {});
|
||||||
|
|
||||||
it('does not get generated without posts', async () => {
|
it('does not get generated without posts', async () => {
|
||||||
const siteDir = __dirname;
|
const {loadContext} = siteFor(__dirname);
|
||||||
const siteConfig = {
|
|
||||||
title: 'Hello',
|
|
||||||
baseUrl: '/',
|
|
||||||
url: 'https://docusaurus.io',
|
|
||||||
favicon: 'image/favicon.ico',
|
|
||||||
markdown,
|
|
||||||
};
|
|
||||||
const outDir = path.join(siteDir, 'build-snap');
|
|
||||||
|
|
||||||
await testGenerateFeeds(
|
await testGenerateFeeds(
|
||||||
{
|
loadContext,
|
||||||
siteDir,
|
pluginOptions(feedType, {
|
||||||
siteConfig,
|
|
||||||
i18n: DefaultI18N,
|
|
||||||
outDir,
|
|
||||||
} as LoadContext,
|
|
||||||
{
|
|
||||||
path: 'invalid-blog-path',
|
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);
|
expect(fsMock).toHaveBeenCalledTimes(0);
|
||||||
|
|
@ -108,40 +140,28 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => {
|
||||||
|
|
||||||
it('has feed item for each post', async () => {
|
it('has feed item for each post', async () => {
|
||||||
const siteDir = path.join(__dirname, '__fixtures__', 'website');
|
const siteDir = path.join(__dirname, '__fixtures__', 'website');
|
||||||
const outDir = path.join(siteDir, 'build-snap');
|
const {loadContext} = siteFor(siteDir, {baseUrl: '/myBaseUrl/'});
|
||||||
const siteConfig = {
|
|
||||||
title: 'Hello',
|
|
||||||
baseUrl: '/myBaseUrl/',
|
|
||||||
url: 'https://docusaurus.io',
|
|
||||||
favicon: 'image/favicon.ico',
|
|
||||||
markdown,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Build is quite difficult to mock, so we built the blog beforehand and
|
// Build is quite difficult to mock, so we built the blog beforehand and
|
||||||
// copied the output to the fixture...
|
// copied the output to the fixture...
|
||||||
await testGenerateFeeds(
|
await testGenerateFeeds(loadContext, pluginOptions(feedType));
|
||||||
{
|
|
||||||
siteDir,
|
expect(
|
||||||
siteConfig,
|
fsMock.mock.calls.map((call) => call[1] as string),
|
||||||
i18n: DefaultI18N,
|
).toMatchSnapshot();
|
||||||
outDir,
|
fsMock.mockClear();
|
||||||
} as LoadContext,
|
});
|
||||||
{
|
|
||||||
path: 'blog',
|
it('has feed item for each post using hash router', async () => {
|
||||||
routeBasePath: 'blog',
|
const siteDir = path.join(__dirname, '__fixtures__', 'website');
|
||||||
tagsBasePath: 'tags',
|
const {loadContext} = siteFor(siteDir, {
|
||||||
authorsMapPath: 'authors.yml',
|
baseUrl: '/myBaseUrl/',
|
||||||
include: DEFAULT_OPTIONS.include,
|
router: 'hash',
|
||||||
exclude: DEFAULT_OPTIONS.exclude,
|
});
|
||||||
feedOptions: {
|
|
||||||
type: [feedType],
|
// Build is quite difficult to mock, so we built the blog beforehand and
|
||||||
copyright: 'Copyright',
|
// copied the output to the fixture...
|
||||||
},
|
await testGenerateFeeds(loadContext, pluginOptions(feedType));
|
||||||
readingTime: ({content, defaultReadingTime}) =>
|
|
||||||
defaultReadingTime({content}),
|
|
||||||
truncateMarker: /<!--\s*truncate\s*-->/,
|
|
||||||
} as PluginOptions,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
fsMock.mock.calls.map((call) => call[1] as string),
|
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 () => {
|
it('filters to the first two entries', async () => {
|
||||||
const siteDir = path.join(__dirname, '__fixtures__', 'website');
|
const siteDir = path.join(__dirname, '__fixtures__', 'website');
|
||||||
const outDir = path.join(siteDir, 'build-snap');
|
const {loadContext} = siteFor(siteDir, {
|
||||||
const siteConfig = {
|
|
||||||
title: 'Hello',
|
|
||||||
baseUrl: '/myBaseUrl/',
|
baseUrl: '/myBaseUrl/',
|
||||||
url: 'https://docusaurus.io',
|
});
|
||||||
favicon: 'image/favicon.ico',
|
|
||||||
markdown,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Build is quite difficult to mock, so we built the blog beforehand and
|
// Build is quite difficult to mock, so we built the blog beforehand and
|
||||||
// copied the output to the fixture...
|
// copied the output to the fixture...
|
||||||
await testGenerateFeeds(
|
await testGenerateFeeds(
|
||||||
{
|
loadContext,
|
||||||
siteDir,
|
pluginOptions(feedType, {
|
||||||
siteConfig,
|
|
||||||
i18n: DefaultI18N,
|
|
||||||
outDir,
|
|
||||||
} as LoadContext,
|
|
||||||
{
|
|
||||||
path: 'blog',
|
|
||||||
routeBasePath: 'blog',
|
|
||||||
tagsBasePath: 'tags',
|
|
||||||
authorsMapPath: 'authors.yml',
|
|
||||||
include: DEFAULT_OPTIONS.include,
|
|
||||||
exclude: DEFAULT_OPTIONS.exclude,
|
|
||||||
feedOptions: {
|
feedOptions: {
|
||||||
type: [feedType],
|
type: [feedType],
|
||||||
copyright: 'Copyright',
|
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(
|
expect(
|
||||||
|
|
@ -204,40 +205,21 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => {
|
||||||
|
|
||||||
it('filters to the first two entries using limit', async () => {
|
it('filters to the first two entries using limit', async () => {
|
||||||
const siteDir = path.join(__dirname, '__fixtures__', 'website');
|
const siteDir = path.join(__dirname, '__fixtures__', 'website');
|
||||||
const outDir = path.join(siteDir, 'build-snap');
|
const {loadContext} = siteFor(siteDir, {
|
||||||
const siteConfig = {
|
|
||||||
title: 'Hello',
|
|
||||||
baseUrl: '/myBaseUrl/',
|
baseUrl: '/myBaseUrl/',
|
||||||
url: 'https://docusaurus.io',
|
});
|
||||||
favicon: 'image/favicon.ico',
|
|
||||||
markdown,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Build is quite difficult to mock, so we built the blog beforehand and
|
// Build is quite difficult to mock, so we built the blog beforehand and
|
||||||
// copied the output to the fixture...
|
// copied the output to the fixture...
|
||||||
await testGenerateFeeds(
|
await testGenerateFeeds(
|
||||||
{
|
loadContext,
|
||||||
siteDir,
|
pluginOptions(feedType, {
|
||||||
siteConfig,
|
|
||||||
i18n: DefaultI18N,
|
|
||||||
outDir,
|
|
||||||
} as LoadContext,
|
|
||||||
{
|
|
||||||
path: 'blog',
|
|
||||||
routeBasePath: 'blog',
|
|
||||||
tagsBasePath: 'tags',
|
|
||||||
authorsMapPath: 'authors.yml',
|
|
||||||
include: DEFAULT_OPTIONS.include,
|
|
||||||
exclude: DEFAULT_OPTIONS.exclude,
|
|
||||||
feedOptions: {
|
feedOptions: {
|
||||||
type: [feedType],
|
type: [feedType],
|
||||||
copyright: 'Copyright',
|
copyright: 'Copyright',
|
||||||
limit: 2,
|
limit: 2,
|
||||||
},
|
},
|
||||||
readingTime: ({content, defaultReadingTime}) =>
|
}),
|
||||||
defaultReadingTime({content}),
|
|
||||||
truncateMarker: /<!--\s*truncate\s*-->/,
|
|
||||||
} as PluginOptions,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,65 @@ async function generateBlogFeed({
|
||||||
return feed;
|
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({
|
async function defaultCreateFeedItems({
|
||||||
blogPosts,
|
blogPosts,
|
||||||
siteConfig,
|
siteConfig,
|
||||||
|
|
@ -85,7 +144,7 @@ async function defaultCreateFeedItems({
|
||||||
siteConfig: DocusaurusConfig;
|
siteConfig: DocusaurusConfig;
|
||||||
outDir: string;
|
outDir: string;
|
||||||
}): Promise<BlogFeedItem[]> {
|
}): Promise<BlogFeedItem[]> {
|
||||||
const {url: siteUrl} = siteConfig;
|
const {url: siteUrl, router} = siteConfig;
|
||||||
|
|
||||||
function toFeedAuthor(author: Author): FeedAuthor {
|
function toFeedAuthor(author: Author): FeedAuthor {
|
||||||
return {name: author.name, link: author.url, email: author.email};
|
return {name: author.name, link: author.url, email: author.email};
|
||||||
|
|
@ -104,53 +163,28 @@ async function defaultCreateFeedItems({
|
||||||
},
|
},
|
||||||
} = post;
|
} = post;
|
||||||
|
|
||||||
const content = await readOutputHTMLFile(
|
const absoluteUrl = normalizeUrl([
|
||||||
permalink.replace(siteConfig.baseUrl, ''),
|
siteUrl,
|
||||||
|
router === 'hash' ? '/#/' : '',
|
||||||
|
permalink,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const content = await readBlogPostContent({
|
||||||
|
blogPost: post,
|
||||||
|
absoluteUrl,
|
||||||
|
siteConfig,
|
||||||
outDir,
|
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 = {
|
const feedItem: BlogFeedItem = {
|
||||||
title: metadataTitle,
|
title: metadataTitle,
|
||||||
id: blogPostAbsoluteUrl,
|
id: absoluteUrl,
|
||||||
link: blogPostAbsoluteUrl,
|
link: absoluteUrl,
|
||||||
date,
|
date,
|
||||||
description,
|
description,
|
||||||
// Atom feed demands the "term", while other feeds use "name"
|
// Atom feed demands the "term", while other feeds use "name"
|
||||||
category: tags.map((tag) => ({name: tag.label, term: tag.label})),
|
category: tags.map((tag) => ({name: tag.label, term: tag.label})),
|
||||||
content: $(`#${blogPostContainerID}`).html()!,
|
content,
|
||||||
};
|
};
|
||||||
|
|
||||||
// json1() method takes the first item of authors array
|
// json1() method takes the first item of authors array
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
"@babel/core": "^7.23.3",
|
"@babel/core": "^7.23.3",
|
||||||
"@babel/preset-env": "^7.23.3",
|
"@babel/preset-env": "^7.23.3",
|
||||||
"@docusaurus/core": "3.0.0",
|
"@docusaurus/core": "3.0.0",
|
||||||
|
"@docusaurus/logger": "3.0.0",
|
||||||
"@docusaurus/theme-common": "3.0.0",
|
"@docusaurus/theme-common": "3.0.0",
|
||||||
"@docusaurus/theme-translations": "3.0.0",
|
"@docusaurus/theme-translations": "3.0.0",
|
||||||
"@docusaurus/types": "3.0.0",
|
"@docusaurus/types": "3.0.0",
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,14 @@ import WebpackBar from 'webpackbar';
|
||||||
import Terser from 'terser-webpack-plugin';
|
import Terser from 'terser-webpack-plugin';
|
||||||
import {injectManifest} from 'workbox-build';
|
import {injectManifest} from 'workbox-build';
|
||||||
import {normalizeUrl} from '@docusaurus/utils';
|
import {normalizeUrl} from '@docusaurus/utils';
|
||||||
|
import logger from '@docusaurus/logger';
|
||||||
import {compile} from '@docusaurus/core/lib/webpack/utils';
|
import {compile} from '@docusaurus/core/lib/webpack/utils';
|
||||||
import {readDefaultCodeTranslationMessages} from '@docusaurus/theme-translations';
|
import {readDefaultCodeTranslationMessages} from '@docusaurus/theme-translations';
|
||||||
import type {HtmlTags, LoadContext, Plugin} from '@docusaurus/types';
|
import type {HtmlTags, LoadContext, Plugin} from '@docusaurus/types';
|
||||||
import type {PluginOptions} from '@docusaurus/plugin-pwa';
|
import type {PluginOptions} from '@docusaurus/plugin-pwa';
|
||||||
|
|
||||||
|
const PluginName = 'docusaurus-plugin-pwa';
|
||||||
|
|
||||||
const isProd = process.env.NODE_ENV === 'production';
|
const isProd = process.env.NODE_ENV === 'production';
|
||||||
|
|
||||||
function getSWBabelLoader() {
|
function getSWBabelLoader() {
|
||||||
|
|
@ -47,6 +50,7 @@ export default function pluginPWA(
|
||||||
outDir,
|
outDir,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
i18n: {currentLocale},
|
i18n: {currentLocale},
|
||||||
|
siteConfig: {router},
|
||||||
} = context;
|
} = context;
|
||||||
const {
|
const {
|
||||||
debug,
|
debug,
|
||||||
|
|
@ -57,8 +61,13 @@ export default function pluginPWA(
|
||||||
swRegister,
|
swRegister,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
|
if (router === 'hash') {
|
||||||
|
logger.warn(`${PluginName} does not support the Hash Router`);
|
||||||
|
return {name: PluginName};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: 'docusaurus-plugin-pwa',
|
name: PluginName,
|
||||||
|
|
||||||
getThemePath() {
|
getThemePath() {
|
||||||
return '../lib/theme';
|
return '../lib/theme';
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,28 @@ describe('createSitemap', () => {
|
||||||
filename: 'sitemap.xml',
|
filename: 'sitemap.xml',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(sitemap).toContain(
|
expect(sitemap).toMatchInlineSnapshot(
|
||||||
`<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">`,
|
`"<?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>"`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
import type {ReactElement} from 'react';
|
import type {ReactElement} from 'react';
|
||||||
import {SitemapStream, streamToPromise} from 'sitemap';
|
import {SitemapStream, streamToPromise} from 'sitemap';
|
||||||
import {applyTrailingSlash} from '@docusaurus/utils-common';
|
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 {DocusaurusConfig} from '@docusaurus/types';
|
||||||
import type {HelmetServerState} from 'react-helmet-async';
|
import type {HelmetServerState} from 'react-helmet-async';
|
||||||
import type {PluginOptions} from './options';
|
import type {PluginOptions} from './options';
|
||||||
|
|
@ -53,7 +53,7 @@ export default async function createSitemap(
|
||||||
head: {[location: string]: HelmetServerState},
|
head: {[location: string]: HelmetServerState},
|
||||||
options: PluginOptions,
|
options: PluginOptions,
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
const {url: hostname} = siteConfig;
|
const {url: hostname, router} = siteConfig;
|
||||||
if (!hostname) {
|
if (!hostname) {
|
||||||
throw new Error('URL in docusaurus.config.js cannot be empty/undefined.');
|
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 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) =>
|
includedRoutes.forEach((routePath) =>
|
||||||
sitemapStream.write({
|
sitemapStream.write({
|
||||||
url: applyTrailingSlash(routePath, {
|
url: createSitemapUrl(routePath),
|
||||||
trailingSlash: siteConfig.trailingSlash,
|
|
||||||
baseUrl: siteConfig.baseUrl,
|
|
||||||
}),
|
|
||||||
changefreq,
|
changefreq,
|
||||||
priority,
|
priority,
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ export type RemarkRehypeOptions = ProcessorOptions['remarkRehypeOptions'];
|
||||||
|
|
||||||
export type ReportingSeverity = 'ignore' | 'log' | 'warn' | 'throw';
|
export type ReportingSeverity = 'ignore' | 'log' | 'warn' | 'throw';
|
||||||
|
|
||||||
|
export type RouterType = 'browser' | 'hash';
|
||||||
|
|
||||||
export type ThemeConfig = {
|
export type ThemeConfig = {
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
};
|
};
|
||||||
|
|
@ -137,6 +139,23 @@ export type DocusaurusConfig = {
|
||||||
*/
|
*/
|
||||||
favicon?: string;
|
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
|
* Allow to customize the presence/absence of a trailing slash at the end of
|
||||||
* URLs/links, and how static HTML files are generated:
|
* URLs/links, and how static HTML files are generated:
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ReportingSeverity,
|
ReportingSeverity,
|
||||||
|
RouterType,
|
||||||
ThemeConfig,
|
ThemeConfig,
|
||||||
MarkdownConfig,
|
MarkdownConfig,
|
||||||
DefaultParseFrontMatter,
|
DefaultParseFrontMatter,
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,30 @@ describe('normalizeUrl', () => {
|
||||||
output: '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: ['http://foobar.com', '/#', 'test'],
|
||||||
|
output: 'http://foobar.com/#/test',
|
||||||
|
},
|
||||||
|
{
|
||||||
input: ['/', '', 'hello', '', '/', '/', '', '/', '/world'],
|
input: ['/', '', 'hello', '', '/', '/', '', '/', '/world'],
|
||||||
output: '/hello/world',
|
output: '/hello/world',
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@ export function normalizeUrl(rawUrls: string[]): string {
|
||||||
// first plain protocol part.
|
// first plain protocol part.
|
||||||
|
|
||||||
// Remove trailing slash before parameters or hash.
|
// Remove trailing slash before parameters or hash.
|
||||||
str = str.replace(/\/(?<search>\?|&|#[^!])/g, '$1');
|
str = str.replace(/\/(?<search>\?|&|#[^!/])/g, '$1');
|
||||||
|
|
||||||
// Replace ? in parameters with &.
|
// Replace ? in parameters with &.
|
||||||
const parts = str.split('?');
|
const parts = str.split('?');
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM, {type ErrorInfo} from 'react-dom/client';
|
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 {HelmetProvider} from 'react-helmet-async';
|
||||||
|
|
||||||
import ExecutionEnvironment from './exports/ExecutionEnvironment';
|
import ExecutionEnvironment from './exports/ExecutionEnvironment';
|
||||||
|
|
@ -31,9 +31,9 @@ if (ExecutionEnvironment.canUseDOM) {
|
||||||
|
|
||||||
const app = (
|
const app = (
|
||||||
<HelmetProvider>
|
<HelmetProvider>
|
||||||
<BrowserRouter>
|
<Router>
|
||||||
<App />
|
<App />
|
||||||
</BrowserRouter>
|
</Router>
|
||||||
</HelmetProvider>
|
</HelmetProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ function Link(
|
||||||
forwardedRef: React.ForwardedRef<HTMLAnchorElement>,
|
forwardedRef: React.ForwardedRef<HTMLAnchorElement>,
|
||||||
): JSX.Element {
|
): JSX.Element {
|
||||||
const {
|
const {
|
||||||
siteConfig: {trailingSlash, baseUrl},
|
siteConfig: {trailingSlash, baseUrl, router},
|
||||||
} = useDocusaurusContext();
|
} = useDocusaurusContext();
|
||||||
const {withBaseUrl} = useBaseUrlUtils();
|
const {withBaseUrl} = useBaseUrlUtils();
|
||||||
const brokenLinks = useBrokenLinks();
|
const brokenLinks = useBrokenLinks();
|
||||||
|
|
@ -81,6 +81,11 @@ function Link(
|
||||||
? maybeAddBaseUrl(targetLinkWithoutPathnameProtocol)
|
? maybeAddBaseUrl(targetLinkWithoutPathnameProtocol)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
// TODO temporary hack
|
||||||
|
if (router === 'hash' && targetLink?.startsWith('./')) {
|
||||||
|
targetLink = targetLink?.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
if (targetLink && isInternal) {
|
if (targetLink && isInternal) {
|
||||||
targetLink = applyTrailingSlash(targetLink, {trailingSlash, baseUrl});
|
targetLink = applyTrailingSlash(targetLink, {trailingSlash, baseUrl});
|
||||||
}
|
}
|
||||||
|
|
@ -148,8 +153,7 @@ function Link(
|
||||||
const hasInternalTarget = !props.target || props.target === '_self';
|
const hasInternalTarget = !props.target || props.target === '_self';
|
||||||
|
|
||||||
// Should we use a regular <a> tag instead of React-Router Link component?
|
// Should we use a regular <a> tag instead of React-Router Link component?
|
||||||
const isRegularHtmlLink =
|
const isRegularHtmlLink = !targetLink || !isInternal || !hasInternalTarget;
|
||||||
!targetLink || !isInternal || !hasInternalTarget || isAnchorLink;
|
|
||||||
|
|
||||||
if (!noBrokenLinkCheck && (isAnchorLink || !isRegularHtmlLink)) {
|
if (!noBrokenLinkCheck && (isAnchorLink || !isRegularHtmlLink)) {
|
||||||
brokenLinks.collectLink(targetLink!);
|
brokenLinks.collectLink(targetLink!);
|
||||||
|
|
|
||||||
|
|
@ -9,19 +9,32 @@ import {useCallback} from 'react';
|
||||||
import useDocusaurusContext from './useDocusaurusContext';
|
import useDocusaurusContext from './useDocusaurusContext';
|
||||||
import {hasProtocol} from './isInternalUrl';
|
import {hasProtocol} from './isInternalUrl';
|
||||||
import type {BaseUrlOptions, BaseUrlUtils} from '@docusaurus/useBaseUrl';
|
import type {BaseUrlOptions, BaseUrlUtils} from '@docusaurus/useBaseUrl';
|
||||||
|
import type {RouterType} from '@docusaurus/types';
|
||||||
|
|
||||||
function addBaseUrl(
|
function addBaseUrl({
|
||||||
siteUrl: string,
|
siteUrl,
|
||||||
baseUrl: string,
|
baseUrl,
|
||||||
url: string,
|
url,
|
||||||
{forcePrependBaseUrl = false, absolute = false}: BaseUrlOptions = {},
|
options: {forcePrependBaseUrl = false, absolute = false} = {},
|
||||||
): string {
|
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
|
// It never makes sense to add base url to a local anchor url, or one with a
|
||||||
// protocol
|
// protocol
|
||||||
if (!url || url.startsWith('#') || hasProtocol(url)) {
|
if (!url || url.startsWith('#') || hasProtocol(url)) {
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO temp hack
|
||||||
|
if (router === 'hash' && url.startsWith('/')) {
|
||||||
|
return `.${url}`;
|
||||||
|
}
|
||||||
|
|
||||||
if (forcePrependBaseUrl) {
|
if (forcePrependBaseUrl) {
|
||||||
return baseUrl + url.replace(/^\//, '');
|
return baseUrl + url.replace(/^\//, '');
|
||||||
}
|
}
|
||||||
|
|
@ -42,13 +55,13 @@ function addBaseUrl(
|
||||||
|
|
||||||
export function useBaseUrlUtils(): BaseUrlUtils {
|
export function useBaseUrlUtils(): BaseUrlUtils {
|
||||||
const {
|
const {
|
||||||
siteConfig: {baseUrl, url: siteUrl},
|
siteConfig: {baseUrl, url: siteUrl, router},
|
||||||
} = useDocusaurusContext();
|
} = useDocusaurusContext();
|
||||||
|
|
||||||
const withBaseUrl = useCallback(
|
const withBaseUrl = useCallback(
|
||||||
(url: string, options?: BaseUrlOptions) =>
|
(url: string, options?: BaseUrlOptions) =>
|
||||||
addBaseUrl(siteUrl, baseUrl, url, options),
|
addBaseUrl({siteUrl, baseUrl, url, options, router}),
|
||||||
[siteUrl, baseUrl],
|
[siteUrl, baseUrl, router],
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -26,13 +26,29 @@ const render: AppRenderer = async ({pathname}) => {
|
||||||
const helmetContext = {};
|
const helmetContext = {};
|
||||||
const statefulBrokenLinks = createStatefulBrokenLinks();
|
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 = (
|
const app = (
|
||||||
// @ts-expect-error: we are migrating away from react-loadable anyways
|
// @ts-expect-error: we are migrating away from react-loadable anyways
|
||||||
<Loadable.Capture report={(moduleName) => modules.add(moduleName)}>
|
<Loadable.Capture report={(moduleName) => modules.add(moduleName)}>
|
||||||
<HelmetProvider context={helmetContext}>
|
<HelmetProvider context={helmetContext}>
|
||||||
<StaticRouter location={pathname} context={routerContext}>
|
<StaticRouter location={pathname} context={routerContext}>
|
||||||
<BrokenLinksProvider brokenLinks={statefulBrokenLinks}>
|
<BrokenLinksProvider brokenLinks={statefulBrokenLinks}>
|
||||||
<App />
|
{appContent}
|
||||||
</BrokenLinksProvider>
|
</BrokenLinksProvider>
|
||||||
</StaticRouter>
|
</StaticRouter>
|
||||||
</HelmetProvider>
|
</HelmetProvider>
|
||||||
|
|
|
||||||
|
|
@ -23,12 +23,20 @@ import {
|
||||||
import {PerfLogger} from '../utils';
|
import {PerfLogger} from '../utils';
|
||||||
|
|
||||||
import {loadI18n} from '../server/i18n';
|
import {loadI18n} from '../server/i18n';
|
||||||
import {generateStaticFiles, loadAppRenderer} from '../ssg';
|
import {
|
||||||
import {compileSSRTemplate} from '../templates/templates';
|
generateHashRouterEntrypoint,
|
||||||
|
generateStaticFiles,
|
||||||
|
loadAppRenderer,
|
||||||
|
} from '../ssg';
|
||||||
|
import {
|
||||||
|
compileSSRTemplate,
|
||||||
|
renderHashRouterTemplate,
|
||||||
|
} from '../templates/templates';
|
||||||
import defaultSSRTemplate from '../templates/ssr.html.template';
|
import defaultSSRTemplate from '../templates/ssr.html.template';
|
||||||
|
import type {SSGParams} from '../ssg';
|
||||||
|
|
||||||
import type {Manifest} from 'react-loadable-ssr-addon-v5-slorber';
|
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';
|
import type {SiteCollectedData} from '../common';
|
||||||
|
|
||||||
export type BuildCLIOptions = Pick<
|
export type BuildCLIOptions = Pick<
|
||||||
|
|
@ -171,7 +179,11 @@ async function buildLocale({
|
||||||
PerfLogger.end('Loading site');
|
PerfLogger.end('Loading site');
|
||||||
|
|
||||||
// Apply user webpack config.
|
// Apply user webpack config.
|
||||||
const {outDir, plugins} = props;
|
const {
|
||||||
|
outDir,
|
||||||
|
plugins,
|
||||||
|
siteConfig: {router},
|
||||||
|
} = props;
|
||||||
|
|
||||||
// We can build the 2 configs in parallel
|
// We can build the 2 configs in parallel
|
||||||
PerfLogger.start('Creating webpack configs');
|
PerfLogger.start('Creating webpack configs');
|
||||||
|
|
@ -196,7 +208,11 @@ async function buildLocale({
|
||||||
|
|
||||||
// Run webpack to build JS bundle (client) and static html files (server).
|
// Run webpack to build JS bundle (client) and static html files (server).
|
||||||
PerfLogger.start('Bundling');
|
PerfLogger.start('Bundling');
|
||||||
await compile([clientConfig, serverConfig]);
|
if (router === 'hash') {
|
||||||
|
await compile([clientConfig]);
|
||||||
|
} else {
|
||||||
|
await compile([clientConfig, serverConfig]);
|
||||||
|
}
|
||||||
PerfLogger.end('Bundling');
|
PerfLogger.end('Bundling');
|
||||||
|
|
||||||
PerfLogger.start('Executing static site generation');
|
PerfLogger.start('Executing static site generation');
|
||||||
|
|
@ -204,6 +220,7 @@ async function buildLocale({
|
||||||
props,
|
props,
|
||||||
serverBundlePath,
|
serverBundlePath,
|
||||||
clientManifestPath,
|
clientManifestPath,
|
||||||
|
router,
|
||||||
});
|
});
|
||||||
PerfLogger.end('Executing static site generation');
|
PerfLogger.end('Executing static site generation');
|
||||||
|
|
||||||
|
|
@ -242,11 +259,13 @@ async function executeSSG({
|
||||||
props,
|
props,
|
||||||
serverBundlePath,
|
serverBundlePath,
|
||||||
clientManifestPath,
|
clientManifestPath,
|
||||||
|
router,
|
||||||
}: {
|
}: {
|
||||||
props: Props;
|
props: Props;
|
||||||
serverBundlePath: string;
|
serverBundlePath: string;
|
||||||
clientManifestPath: string;
|
clientManifestPath: string;
|
||||||
}) {
|
router: RouterType;
|
||||||
|
}): Promise<{collectedData: SiteCollectedData}> {
|
||||||
PerfLogger.start('Reading client manifest');
|
PerfLogger.start('Reading client manifest');
|
||||||
const manifest: Manifest = await fs.readJSON(clientManifestPath, 'utf-8');
|
const manifest: Manifest = await fs.readJSON(clientManifestPath, 'utf-8');
|
||||||
PerfLogger.end('Reading client manifest');
|
PerfLogger.end('Reading client manifest');
|
||||||
|
|
@ -257,6 +276,27 @@ async function executeSSG({
|
||||||
);
|
);
|
||||||
PerfLogger.end('Compiling SSR template');
|
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');
|
PerfLogger.start('Loading App renderer');
|
||||||
const renderer = await loadAppRenderer({
|
const renderer = await loadAppRenderer({
|
||||||
serverBundlePath,
|
serverBundlePath,
|
||||||
|
|
@ -267,18 +307,7 @@ async function executeSSG({
|
||||||
const ssgResult = await generateStaticFiles({
|
const ssgResult = await generateStaticFiles({
|
||||||
pathnames: props.routesPaths,
|
pathnames: props.routesPaths,
|
||||||
renderer,
|
renderer,
|
||||||
params: {
|
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,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
PerfLogger.end('Generate static files');
|
PerfLogger.end('Generate static files');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ exports[`loadSiteConfig website with .cjs siteConfig 1`] = `
|
||||||
"onDuplicateRoutes": "warn",
|
"onDuplicateRoutes": "warn",
|
||||||
"plugins": [],
|
"plugins": [],
|
||||||
"presets": [],
|
"presets": [],
|
||||||
|
"router": "browser",
|
||||||
"scripts": [],
|
"scripts": [],
|
||||||
"staticDirectories": [
|
"staticDirectories": [
|
||||||
"static",
|
"static",
|
||||||
|
|
@ -86,6 +87,7 @@ exports[`loadSiteConfig website with ts + js config 1`] = `
|
||||||
"onDuplicateRoutes": "warn",
|
"onDuplicateRoutes": "warn",
|
||||||
"plugins": [],
|
"plugins": [],
|
||||||
"presets": [],
|
"presets": [],
|
||||||
|
"router": "browser",
|
||||||
"scripts": [],
|
"scripts": [],
|
||||||
"staticDirectories": [
|
"staticDirectories": [
|
||||||
"static",
|
"static",
|
||||||
|
|
@ -137,6 +139,7 @@ exports[`loadSiteConfig website with valid JS CJS config 1`] = `
|
||||||
"onDuplicateRoutes": "warn",
|
"onDuplicateRoutes": "warn",
|
||||||
"plugins": [],
|
"plugins": [],
|
||||||
"presets": [],
|
"presets": [],
|
||||||
|
"router": "browser",
|
||||||
"scripts": [],
|
"scripts": [],
|
||||||
"staticDirectories": [
|
"staticDirectories": [
|
||||||
"static",
|
"static",
|
||||||
|
|
@ -188,6 +191,7 @@ exports[`loadSiteConfig website with valid JS ESM config 1`] = `
|
||||||
"onDuplicateRoutes": "warn",
|
"onDuplicateRoutes": "warn",
|
||||||
"plugins": [],
|
"plugins": [],
|
||||||
"presets": [],
|
"presets": [],
|
||||||
|
"router": "browser",
|
||||||
"scripts": [],
|
"scripts": [],
|
||||||
"staticDirectories": [
|
"staticDirectories": [
|
||||||
"static",
|
"static",
|
||||||
|
|
@ -239,6 +243,7 @@ exports[`loadSiteConfig website with valid TypeScript CJS config 1`] = `
|
||||||
"onDuplicateRoutes": "warn",
|
"onDuplicateRoutes": "warn",
|
||||||
"plugins": [],
|
"plugins": [],
|
||||||
"presets": [],
|
"presets": [],
|
||||||
|
"router": "browser",
|
||||||
"scripts": [],
|
"scripts": [],
|
||||||
"staticDirectories": [
|
"staticDirectories": [
|
||||||
"static",
|
"static",
|
||||||
|
|
@ -290,6 +295,7 @@ exports[`loadSiteConfig website with valid TypeScript ESM config 1`] = `
|
||||||
"onDuplicateRoutes": "warn",
|
"onDuplicateRoutes": "warn",
|
||||||
"plugins": [],
|
"plugins": [],
|
||||||
"presets": [],
|
"presets": [],
|
||||||
|
"router": "browser",
|
||||||
"scripts": [],
|
"scripts": [],
|
||||||
"staticDirectories": [
|
"staticDirectories": [
|
||||||
"static",
|
"static",
|
||||||
|
|
@ -343,6 +349,7 @@ exports[`loadSiteConfig website with valid async config 1`] = `
|
||||||
"plugins": [],
|
"plugins": [],
|
||||||
"presets": [],
|
"presets": [],
|
||||||
"projectName": "hello",
|
"projectName": "hello",
|
||||||
|
"router": "browser",
|
||||||
"scripts": [],
|
"scripts": [],
|
||||||
"staticDirectories": [
|
"staticDirectories": [
|
||||||
"static",
|
"static",
|
||||||
|
|
@ -396,6 +403,7 @@ exports[`loadSiteConfig website with valid async config creator function 1`] = `
|
||||||
"plugins": [],
|
"plugins": [],
|
||||||
"presets": [],
|
"presets": [],
|
||||||
"projectName": "hello",
|
"projectName": "hello",
|
||||||
|
"router": "browser",
|
||||||
"scripts": [],
|
"scripts": [],
|
||||||
"staticDirectories": [
|
"staticDirectories": [
|
||||||
"static",
|
"static",
|
||||||
|
|
@ -449,6 +457,7 @@ exports[`loadSiteConfig website with valid config creator function 1`] = `
|
||||||
"plugins": [],
|
"plugins": [],
|
||||||
"presets": [],
|
"presets": [],
|
||||||
"projectName": "hello",
|
"projectName": "hello",
|
||||||
|
"router": "browser",
|
||||||
"scripts": [],
|
"scripts": [],
|
||||||
"staticDirectories": [
|
"staticDirectories": [
|
||||||
"static",
|
"static",
|
||||||
|
|
@ -513,6 +522,7 @@ exports[`loadSiteConfig website with valid siteConfig 1`] = `
|
||||||
],
|
],
|
||||||
"presets": [],
|
"presets": [],
|
||||||
"projectName": "hello",
|
"projectName": "hello",
|
||||||
|
"router": "browser",
|
||||||
"scripts": [],
|
"scripts": [],
|
||||||
"staticDirectories": [
|
"staticDirectories": [
|
||||||
"static",
|
"static",
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,7 @@ exports[`load loads props for site with custom i18n path 1`] = `
|
||||||
"onDuplicateRoutes": "warn",
|
"onDuplicateRoutes": "warn",
|
||||||
"plugins": [],
|
"plugins": [],
|
||||||
"presets": [],
|
"presets": [],
|
||||||
|
"router": "browser",
|
||||||
"scripts": [],
|
"scripts": [],
|
||||||
"staticDirectories": [
|
"staticDirectories": [
|
||||||
"static",
|
"static",
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ export const DEFAULT_MARKDOWN_CONFIG: MarkdownConfig = {
|
||||||
export const DEFAULT_CONFIG: Pick<
|
export const DEFAULT_CONFIG: Pick<
|
||||||
DocusaurusConfig,
|
DocusaurusConfig,
|
||||||
| 'i18n'
|
| 'i18n'
|
||||||
|
| 'router'
|
||||||
| 'onBrokenLinks'
|
| 'onBrokenLinks'
|
||||||
| 'onBrokenAnchors'
|
| 'onBrokenAnchors'
|
||||||
| 'onBrokenMarkdownLinks'
|
| 'onBrokenMarkdownLinks'
|
||||||
|
|
@ -66,6 +67,7 @@ export const DEFAULT_CONFIG: Pick<
|
||||||
| 'markdown'
|
| 'markdown'
|
||||||
> = {
|
> = {
|
||||||
i18n: DEFAULT_I18N_CONFIG,
|
i18n: DEFAULT_I18N_CONFIG,
|
||||||
|
router: 'browser',
|
||||||
onBrokenLinks: 'throw',
|
onBrokenLinks: 'throw',
|
||||||
onBrokenAnchors: 'warn', // TODO Docusaurus v4: change to throw
|
onBrokenAnchors: 'warn', // TODO Docusaurus v4: change to throw
|
||||||
onBrokenMarkdownLinks: 'warn',
|
onBrokenMarkdownLinks: 'warn',
|
||||||
|
|
@ -208,6 +210,7 @@ export const ConfigSchema = Joi.object<DocusaurusConfig>({
|
||||||
favicon: Joi.string().optional(),
|
favicon: Joi.string().optional(),
|
||||||
title: Joi.string().required(),
|
title: Joi.string().required(),
|
||||||
url: SiteUrlSchema,
|
url: SiteUrlSchema,
|
||||||
|
router: Joi.string().equal('browser', 'hash').default(DEFAULT_CONFIG.router),
|
||||||
trailingSlash: Joi.boolean(), // No default value! undefined = retrocompatible legacy behavior!
|
trailingSlash: Joi.boolean(), // No default value! undefined = retrocompatible legacy behavior!
|
||||||
i18n: I18N_CONFIG_SCHEMA,
|
i18n: I18N_CONFIG_SCHEMA,
|
||||||
onBrokenLinks: Joi.string()
|
onBrokenLinks: Joi.string()
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
function htmlTagObjectToString(tag: unknown): string {
|
||||||
assertIsHtmlTagObject(tag);
|
assertIsHtmlTagObject(tag);
|
||||||
const isVoidTag = (voidHtmlTags as string[]).includes(tag.tagName);
|
const isVoidTag = (voidHtmlTags as string[]).includes(tag.tagName);
|
||||||
const tagAttributes = tag.attributes ?? {};
|
const tagAttributes = tag.attributes ?? {};
|
||||||
const attributes = Object.keys(tagAttributes)
|
const attributes = Object.keys(tagAttributes)
|
||||||
.map((attr) => {
|
.map((attr) => {
|
||||||
const value = tagAttributes[attr]!;
|
let value = tagAttributes[attr]!;
|
||||||
if (typeof value === 'boolean') {
|
if (typeof value === 'boolean') {
|
||||||
return value ? attr : undefined;
|
return value ? attr : undefined;
|
||||||
}
|
}
|
||||||
|
value = absoluteToRelativeTagAttribute(attr, value);
|
||||||
return `${attr}="${escapeHTML(value)}"`;
|
return `${attr}="${escapeHTML(value)}"`;
|
||||||
})
|
})
|
||||||
.filter((str): str is string => Boolean(str));
|
.filter((str): str is string => Boolean(str));
|
||||||
|
|
|
||||||
|
|
@ -232,6 +232,14 @@ ${Object.entries(registry)
|
||||||
JSON.stringify(siteMetadata, null, 2),
|
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([
|
await Promise.all([
|
||||||
genWarning,
|
genWarning,
|
||||||
genClientModules,
|
genClientModules,
|
||||||
|
|
@ -243,6 +251,7 @@ ${Object.entries(registry)
|
||||||
genSiteMetadata,
|
genSiteMetadata,
|
||||||
genI18n,
|
genI18n,
|
||||||
genCodeTranslations,
|
genCodeTranslations,
|
||||||
|
genRouter,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -226,6 +226,20 @@ It might also require to wrap your client code in ${logger.code(
|
||||||
return parts.join('\n');
|
return parts.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function generateHashRouterEntrypoint({
|
||||||
|
content,
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
content: string;
|
||||||
|
params: SSGParams;
|
||||||
|
}): Promise<void> {
|
||||||
|
await writeStaticFile({
|
||||||
|
pathname: '/',
|
||||||
|
content,
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function writeStaticFile({
|
async function writeStaticFile({
|
||||||
content,
|
content,
|
||||||
pathname,
|
pathname,
|
||||||
|
|
|
||||||
|
|
@ -113,3 +113,41 @@ export function renderSSRTemplate({
|
||||||
|
|
||||||
return ssrTemplate(data);
|
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);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,7 @@ export async function createBaseConfig({
|
||||||
chunkFilename: isProd
|
chunkFilename: isProd
|
||||||
? 'assets/js/[name].[contenthash:8].js'
|
? 'assets/js/[name].[contenthash:8].js'
|
||||||
: '[name].js',
|
: '[name].js',
|
||||||
publicPath: baseUrl,
|
publicPath: siteConfig.router === 'hash' ? 'auto' : baseUrl,
|
||||||
hashFunction: 'xxhash64',
|
hashFunction: 'xxhash64',
|
||||||
},
|
},
|
||||||
// Don't throw warning when asset created is over 250kb
|
// Don't throw warning when asset created is over 250kb
|
||||||
|
|
|
||||||
|
|
@ -129,7 +129,14 @@ export async function createBuildClientConfig({
|
||||||
bundleAnalyzer: boolean;
|
bundleAnalyzer: boolean;
|
||||||
}): Promise<{config: Configuration; clientManifestPath: string}> {
|
}): Promise<{config: Configuration; clientManifestPath: string}> {
|
||||||
// Apply user webpack config.
|
// 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(
|
const clientManifestPath = path.join(
|
||||||
generatedFilesDir,
|
generatedFilesDir,
|
||||||
|
|
@ -137,7 +144,7 @@ export async function createBuildClientConfig({
|
||||||
);
|
);
|
||||||
|
|
||||||
const config: Configuration = merge(
|
const config: Configuration = merge(
|
||||||
await createBaseClientConfig({props, minify, hydrate: true}),
|
await createBaseClientConfig({props, minify, hydrate}),
|
||||||
{
|
{
|
||||||
plugins: [
|
plugins: [
|
||||||
new ForceTerminatePlugin(),
|
new ForceTerminatePlugin(),
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,8 @@ function getNextVersionName() {
|
||||||
// Test with: DOCUSAURUS_CRASH_TEST=true yarn build:website:fast
|
// Test with: DOCUSAURUS_CRASH_TEST=true yarn build:website:fast
|
||||||
const crashTest = process.env.DOCUSAURUS_CRASH_TEST === 'true';
|
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 isDev = process.env.NODE_ENV === 'development';
|
||||||
|
|
||||||
const isDeployPreview =
|
const isDeployPreview =
|
||||||
|
|
@ -126,6 +128,7 @@ export default async function createConfigAsync() {
|
||||||
baseUrl,
|
baseUrl,
|
||||||
baseUrlIssueBanner: true,
|
baseUrlIssueBanner: true,
|
||||||
url: 'https://docusaurus.io',
|
url: 'https://docusaurus.io',
|
||||||
|
router,
|
||||||
// Dogfood both settings:
|
// Dogfood both settings:
|
||||||
// - force trailing slashes for deploy previews
|
// - force trailing slashes for deploy previews
|
||||||
// - avoid trailing slashes in prod
|
// - avoid trailing slashes in prod
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue