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;
|
||||
}
|
||||
|
||||
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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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>",
|
||||
]
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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>"`,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
*
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
export {
|
||||
ReportingSeverity,
|
||||
RouterType,
|
||||
ThemeConfig,
|
||||
MarkdownConfig,
|
||||
DefaultParseFrontMatter,
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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('?');
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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!);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -109,6 +109,7 @@ exports[`load loads props for site with custom i18n path 1`] = `
|
|||
"onDuplicateRoutes": "warn",
|
||||
"plugins": [],
|
||||
"presets": [],
|
||||
"router": "browser",
|
||||
"scripts": [],
|
||||
"staticDirectories": [
|
||||
"static",
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue