mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-29 17:07:08 +02:00
feat(sitemap): add support for "lastmod" (#9954)
This commit is contained in:
parent
465cf4d82c
commit
9017fb9b1d
41 changed files with 1449 additions and 359 deletions
6
.eslintrc.js
vendored
6
.eslintrc.js
vendored
|
@ -85,6 +85,7 @@ module.exports = {
|
||||||
ignorePattern: '(eslint-disable|@)',
|
ignorePattern: '(eslint-disable|@)',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
'arrow-body-style': OFF,
|
||||||
'no-await-in-loop': OFF,
|
'no-await-in-loop': OFF,
|
||||||
'no-case-declarations': WARNING,
|
'no-case-declarations': WARNING,
|
||||||
'no-console': OFF,
|
'no-console': OFF,
|
||||||
|
@ -347,10 +348,7 @@ module.exports = {
|
||||||
ERROR,
|
ERROR,
|
||||||
{'ts-expect-error': 'allow-with-description'},
|
{'ts-expect-error': 'allow-with-description'},
|
||||||
],
|
],
|
||||||
'@typescript-eslint/consistent-indexed-object-style': [
|
'@typescript-eslint/consistent-indexed-object-style': OFF,
|
||||||
WARNING,
|
|
||||||
'index-signature',
|
|
||||||
],
|
|
||||||
'@typescript-eslint/consistent-type-imports': [
|
'@typescript-eslint/consistent-type-imports': [
|
||||||
WARNING,
|
WARNING,
|
||||||
{disallowTypeAnnotations: false},
|
{disallowTypeAnnotations: false},
|
||||||
|
|
|
@ -11,8 +11,7 @@ import {normalizePluginOptions} from '@docusaurus/utils-validation';
|
||||||
import {
|
import {
|
||||||
posixPath,
|
posixPath,
|
||||||
getFileCommitDate,
|
getFileCommitDate,
|
||||||
GIT_FALLBACK_LAST_UPDATE_DATE,
|
LAST_UPDATE_FALLBACK,
|
||||||
GIT_FALLBACK_LAST_UPDATE_AUTHOR,
|
|
||||||
} from '@docusaurus/utils';
|
} from '@docusaurus/utils';
|
||||||
import pluginContentBlog from '../index';
|
import pluginContentBlog from '../index';
|
||||||
import {validateOptions} from '../options';
|
import {validateOptions} from '../options';
|
||||||
|
@ -554,14 +553,14 @@ describe('last update', () => {
|
||||||
|
|
||||||
expect(blogPosts[0]?.metadata.lastUpdatedBy).toBe('seb');
|
expect(blogPosts[0]?.metadata.lastUpdatedBy).toBe('seb');
|
||||||
expect(blogPosts[0]?.metadata.lastUpdatedAt).toBe(
|
expect(blogPosts[0]?.metadata.lastUpdatedAt).toBe(
|
||||||
GIT_FALLBACK_LAST_UPDATE_DATE,
|
LAST_UPDATE_FALLBACK.lastUpdatedAt,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(blogPosts[1]?.metadata.lastUpdatedBy).toBe(
|
expect(blogPosts[1]?.metadata.lastUpdatedBy).toBe(
|
||||||
GIT_FALLBACK_LAST_UPDATE_AUTHOR,
|
LAST_UPDATE_FALLBACK.lastUpdatedBy,
|
||||||
);
|
);
|
||||||
expect(blogPosts[1]?.metadata.lastUpdatedAt).toBe(
|
expect(blogPosts[1]?.metadata.lastUpdatedAt).toBe(
|
||||||
GIT_FALLBACK_LAST_UPDATE_DATE,
|
LAST_UPDATE_FALLBACK.lastUpdatedAt,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(blogPosts[2]?.metadata.lastUpdatedBy).toBe('seb');
|
expect(blogPosts[2]?.metadata.lastUpdatedBy).toBe('seb');
|
||||||
|
@ -570,7 +569,7 @@ describe('last update', () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(blogPosts[3]?.metadata.lastUpdatedBy).toBe(
|
expect(blogPosts[3]?.metadata.lastUpdatedBy).toBe(
|
||||||
GIT_FALLBACK_LAST_UPDATE_AUTHOR,
|
LAST_UPDATE_FALLBACK.lastUpdatedBy,
|
||||||
);
|
);
|
||||||
expect(blogPosts[3]?.metadata.lastUpdatedAt).toBe(
|
expect(blogPosts[3]?.metadata.lastUpdatedAt).toBe(
|
||||||
lastUpdateFor('2021-01-01'),
|
lastUpdateFor('2021-01-01'),
|
||||||
|
@ -591,13 +590,13 @@ describe('last update', () => {
|
||||||
expect(blogPosts[0]?.metadata.title).toBe('Author');
|
expect(blogPosts[0]?.metadata.title).toBe('Author');
|
||||||
expect(blogPosts[0]?.metadata.lastUpdatedBy).toBeUndefined();
|
expect(blogPosts[0]?.metadata.lastUpdatedBy).toBeUndefined();
|
||||||
expect(blogPosts[0]?.metadata.lastUpdatedAt).toBe(
|
expect(blogPosts[0]?.metadata.lastUpdatedAt).toBe(
|
||||||
GIT_FALLBACK_LAST_UPDATE_DATE,
|
LAST_UPDATE_FALLBACK.lastUpdatedAt,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(blogPosts[1]?.metadata.title).toBe('Nothing');
|
expect(blogPosts[1]?.metadata.title).toBe('Nothing');
|
||||||
expect(blogPosts[1]?.metadata.lastUpdatedBy).toBeUndefined();
|
expect(blogPosts[1]?.metadata.lastUpdatedBy).toBeUndefined();
|
||||||
expect(blogPosts[1]?.metadata.lastUpdatedAt).toBe(
|
expect(blogPosts[1]?.metadata.lastUpdatedAt).toBe(
|
||||||
GIT_FALLBACK_LAST_UPDATE_DATE,
|
LAST_UPDATE_FALLBACK.lastUpdatedAt,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(blogPosts[2]?.metadata.title).toBe('Both');
|
expect(blogPosts[2]?.metadata.title).toBe('Both');
|
||||||
|
@ -628,7 +627,7 @@ describe('last update', () => {
|
||||||
expect(blogPosts[0]?.metadata.lastUpdatedAt).toBeUndefined();
|
expect(blogPosts[0]?.metadata.lastUpdatedAt).toBeUndefined();
|
||||||
|
|
||||||
expect(blogPosts[1]?.metadata.lastUpdatedBy).toBe(
|
expect(blogPosts[1]?.metadata.lastUpdatedBy).toBe(
|
||||||
GIT_FALLBACK_LAST_UPDATE_AUTHOR,
|
LAST_UPDATE_FALLBACK.lastUpdatedBy,
|
||||||
);
|
);
|
||||||
expect(blogPosts[1]?.metadata.lastUpdatedAt).toBeUndefined();
|
expect(blogPosts[1]?.metadata.lastUpdatedAt).toBeUndefined();
|
||||||
|
|
||||||
|
@ -636,7 +635,7 @@ describe('last update', () => {
|
||||||
expect(blogPosts[2]?.metadata.lastUpdatedAt).toBeUndefined();
|
expect(blogPosts[2]?.metadata.lastUpdatedAt).toBeUndefined();
|
||||||
|
|
||||||
expect(blogPosts[3]?.metadata.lastUpdatedBy).toBe(
|
expect(blogPosts[3]?.metadata.lastUpdatedBy).toBe(
|
||||||
GIT_FALLBACK_LAST_UPDATE_AUTHOR,
|
LAST_UPDATE_FALLBACK.lastUpdatedBy,
|
||||||
);
|
);
|
||||||
expect(blogPosts[3]?.metadata.lastUpdatedAt).toBeUndefined();
|
expect(blogPosts[3]?.metadata.lastUpdatedAt).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
normalizeUrl,
|
normalizeUrl,
|
||||||
docuHash,
|
docuHash,
|
||||||
aliasedSitePath,
|
aliasedSitePath,
|
||||||
|
aliasedSitePathToRelativePath,
|
||||||
getPluginI18nPath,
|
getPluginI18nPath,
|
||||||
posixPath,
|
posixPath,
|
||||||
addTrailingPathSeparator,
|
addTrailingPathSeparator,
|
||||||
|
@ -33,7 +34,12 @@ import {createBlogFeedFiles} from './feed';
|
||||||
|
|
||||||
import {toTagProp, toTagsProp} from './props';
|
import {toTagProp, toTagsProp} from './props';
|
||||||
import type {BlogContentPaths, BlogMarkdownLoaderOptions} from './types';
|
import type {BlogContentPaths, BlogMarkdownLoaderOptions} from './types';
|
||||||
import type {LoadContext, Plugin, HtmlTags} from '@docusaurus/types';
|
import type {
|
||||||
|
LoadContext,
|
||||||
|
Plugin,
|
||||||
|
HtmlTags,
|
||||||
|
RouteMetadata,
|
||||||
|
} from '@docusaurus/types';
|
||||||
import type {
|
import type {
|
||||||
PluginOptions,
|
PluginOptions,
|
||||||
BlogPostFrontMatter,
|
BlogPostFrontMatter,
|
||||||
|
@ -273,6 +279,15 @@ export default async function pluginContentBlog(
|
||||||
JSON.stringify(blogMetadata, null, 2),
|
JSON.stringify(blogMetadata, null, 2),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function createBlogPostRouteMetadata(
|
||||||
|
blogPostMeta: BlogPostMetadata,
|
||||||
|
): RouteMetadata {
|
||||||
|
return {
|
||||||
|
sourceFilePath: aliasedSitePathToRelativePath(blogPostMeta.source),
|
||||||
|
lastUpdatedAt: blogPostMeta.lastUpdatedAt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Create routes for blog entries.
|
// Create routes for blog entries.
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
blogPosts.map(async (blogPost) => {
|
blogPosts.map(async (blogPost) => {
|
||||||
|
@ -292,6 +307,7 @@ export default async function pluginContentBlog(
|
||||||
sidebar: aliasedSource(sidebarProp),
|
sidebar: aliasedSource(sidebarProp),
|
||||||
content: metadata.source,
|
content: metadata.source,
|
||||||
},
|
},
|
||||||
|
metadata: createBlogPostRouteMetadata(metadata),
|
||||||
context: {
|
context: {
|
||||||
blogMetadata: aliasedSource(blogMetadataPath),
|
blogMetadata: aliasedSource(blogMetadataPath),
|
||||||
},
|
},
|
||||||
|
|
|
@ -1482,6 +1482,10 @@ exports[`simple website content: route config 1`] = `
|
||||||
{
|
{
|
||||||
"component": "@theme/DocItem",
|
"component": "@theme/DocItem",
|
||||||
"exact": true,
|
"exact": true,
|
||||||
|
"metadata": {
|
||||||
|
"lastUpdatedAt": undefined,
|
||||||
|
"sourceFilePath": "docs/hello.md",
|
||||||
|
},
|
||||||
"modules": {
|
"modules": {
|
||||||
"content": "@site/docs/hello.md",
|
"content": "@site/docs/hello.md",
|
||||||
},
|
},
|
||||||
|
@ -1491,6 +1495,10 @@ exports[`simple website content: route config 1`] = `
|
||||||
{
|
{
|
||||||
"component": "@theme/DocItem",
|
"component": "@theme/DocItem",
|
||||||
"exact": true,
|
"exact": true,
|
||||||
|
"metadata": {
|
||||||
|
"lastUpdatedAt": undefined,
|
||||||
|
"sourceFilePath": "docs/slugs/absoluteSlug.md",
|
||||||
|
},
|
||||||
"modules": {
|
"modules": {
|
||||||
"content": "@site/docs/slugs/absoluteSlug.md",
|
"content": "@site/docs/slugs/absoluteSlug.md",
|
||||||
},
|
},
|
||||||
|
@ -1508,6 +1516,10 @@ exports[`simple website content: route config 1`] = `
|
||||||
{
|
{
|
||||||
"component": "@theme/DocItem",
|
"component": "@theme/DocItem",
|
||||||
"exact": true,
|
"exact": true,
|
||||||
|
"metadata": {
|
||||||
|
"lastUpdatedAt": undefined,
|
||||||
|
"sourceFilePath": "docs/customLastUpdate.md",
|
||||||
|
},
|
||||||
"modules": {
|
"modules": {
|
||||||
"content": "@site/docs/customLastUpdate.md",
|
"content": "@site/docs/customLastUpdate.md",
|
||||||
},
|
},
|
||||||
|
@ -1516,6 +1528,10 @@ exports[`simple website content: route config 1`] = `
|
||||||
{
|
{
|
||||||
"component": "@theme/DocItem",
|
"component": "@theme/DocItem",
|
||||||
"exact": true,
|
"exact": true,
|
||||||
|
"metadata": {
|
||||||
|
"lastUpdatedAt": undefined,
|
||||||
|
"sourceFilePath": "docs/doc with space.md",
|
||||||
|
},
|
||||||
"modules": {
|
"modules": {
|
||||||
"content": "@site/docs/doc with space.md",
|
"content": "@site/docs/doc with space.md",
|
||||||
},
|
},
|
||||||
|
@ -1524,6 +1540,10 @@ exports[`simple website content: route config 1`] = `
|
||||||
{
|
{
|
||||||
"component": "@theme/DocItem",
|
"component": "@theme/DocItem",
|
||||||
"exact": true,
|
"exact": true,
|
||||||
|
"metadata": {
|
||||||
|
"lastUpdatedAt": undefined,
|
||||||
|
"sourceFilePath": "docs/doc-draft.md",
|
||||||
|
},
|
||||||
"modules": {
|
"modules": {
|
||||||
"content": "@site/docs/doc-draft.md",
|
"content": "@site/docs/doc-draft.md",
|
||||||
},
|
},
|
||||||
|
@ -1532,6 +1552,10 @@ exports[`simple website content: route config 1`] = `
|
||||||
{
|
{
|
||||||
"component": "@theme/DocItem",
|
"component": "@theme/DocItem",
|
||||||
"exact": true,
|
"exact": true,
|
||||||
|
"metadata": {
|
||||||
|
"lastUpdatedAt": undefined,
|
||||||
|
"sourceFilePath": "docs/doc-unlisted.md",
|
||||||
|
},
|
||||||
"modules": {
|
"modules": {
|
||||||
"content": "@site/docs/doc-unlisted.md",
|
"content": "@site/docs/doc-unlisted.md",
|
||||||
},
|
},
|
||||||
|
@ -1541,6 +1565,10 @@ exports[`simple website content: route config 1`] = `
|
||||||
{
|
{
|
||||||
"component": "@theme/DocItem",
|
"component": "@theme/DocItem",
|
||||||
"exact": true,
|
"exact": true,
|
||||||
|
"metadata": {
|
||||||
|
"lastUpdatedAt": undefined,
|
||||||
|
"sourceFilePath": "docs/foo/bar.md",
|
||||||
|
},
|
||||||
"modules": {
|
"modules": {
|
||||||
"content": "@site/docs/foo/bar.md",
|
"content": "@site/docs/foo/bar.md",
|
||||||
},
|
},
|
||||||
|
@ -1550,6 +1578,10 @@ exports[`simple website content: route config 1`] = `
|
||||||
{
|
{
|
||||||
"component": "@theme/DocItem",
|
"component": "@theme/DocItem",
|
||||||
"exact": true,
|
"exact": true,
|
||||||
|
"metadata": {
|
||||||
|
"lastUpdatedAt": undefined,
|
||||||
|
"sourceFilePath": "docs/foo/baz.md",
|
||||||
|
},
|
||||||
"modules": {
|
"modules": {
|
||||||
"content": "@site/docs/foo/baz.md",
|
"content": "@site/docs/foo/baz.md",
|
||||||
},
|
},
|
||||||
|
@ -1559,6 +1591,10 @@ exports[`simple website content: route config 1`] = `
|
||||||
{
|
{
|
||||||
"component": "@theme/DocItem",
|
"component": "@theme/DocItem",
|
||||||
"exact": true,
|
"exact": true,
|
||||||
|
"metadata": {
|
||||||
|
"lastUpdatedAt": undefined,
|
||||||
|
"sourceFilePath": "docs/headingAsTitle.md",
|
||||||
|
},
|
||||||
"modules": {
|
"modules": {
|
||||||
"content": "@site/docs/headingAsTitle.md",
|
"content": "@site/docs/headingAsTitle.md",
|
||||||
},
|
},
|
||||||
|
@ -1568,6 +1604,10 @@ exports[`simple website content: route config 1`] = `
|
||||||
{
|
{
|
||||||
"component": "@theme/DocItem",
|
"component": "@theme/DocItem",
|
||||||
"exact": true,
|
"exact": true,
|
||||||
|
"metadata": {
|
||||||
|
"lastUpdatedAt": undefined,
|
||||||
|
"sourceFilePath": "docs/rootResolvedSlug.md",
|
||||||
|
},
|
||||||
"modules": {
|
"modules": {
|
||||||
"content": "@site/docs/rootResolvedSlug.md",
|
"content": "@site/docs/rootResolvedSlug.md",
|
||||||
},
|
},
|
||||||
|
@ -1577,6 +1617,10 @@ exports[`simple website content: route config 1`] = `
|
||||||
{
|
{
|
||||||
"component": "@theme/DocItem",
|
"component": "@theme/DocItem",
|
||||||
"exact": true,
|
"exact": true,
|
||||||
|
"metadata": {
|
||||||
|
"lastUpdatedAt": undefined,
|
||||||
|
"sourceFilePath": "docs/ipsum.md",
|
||||||
|
},
|
||||||
"modules": {
|
"modules": {
|
||||||
"content": "@site/docs/ipsum.md",
|
"content": "@site/docs/ipsum.md",
|
||||||
},
|
},
|
||||||
|
@ -1585,6 +1629,10 @@ exports[`simple website content: route config 1`] = `
|
||||||
{
|
{
|
||||||
"component": "@theme/DocItem",
|
"component": "@theme/DocItem",
|
||||||
"exact": true,
|
"exact": true,
|
||||||
|
"metadata": {
|
||||||
|
"lastUpdatedAt": undefined,
|
||||||
|
"sourceFilePath": "docs/lastUpdateAuthorOnly.md",
|
||||||
|
},
|
||||||
"modules": {
|
"modules": {
|
||||||
"content": "@site/docs/lastUpdateAuthorOnly.md",
|
"content": "@site/docs/lastUpdateAuthorOnly.md",
|
||||||
},
|
},
|
||||||
|
@ -1593,6 +1641,10 @@ exports[`simple website content: route config 1`] = `
|
||||||
{
|
{
|
||||||
"component": "@theme/DocItem",
|
"component": "@theme/DocItem",
|
||||||
"exact": true,
|
"exact": true,
|
||||||
|
"metadata": {
|
||||||
|
"lastUpdatedAt": undefined,
|
||||||
|
"sourceFilePath": "docs/lastUpdateDateOnly.md",
|
||||||
|
},
|
||||||
"modules": {
|
"modules": {
|
||||||
"content": "@site/docs/lastUpdateDateOnly.md",
|
"content": "@site/docs/lastUpdateDateOnly.md",
|
||||||
},
|
},
|
||||||
|
@ -1601,6 +1653,10 @@ exports[`simple website content: route config 1`] = `
|
||||||
{
|
{
|
||||||
"component": "@theme/DocItem",
|
"component": "@theme/DocItem",
|
||||||
"exact": true,
|
"exact": true,
|
||||||
|
"metadata": {
|
||||||
|
"lastUpdatedAt": undefined,
|
||||||
|
"sourceFilePath": "docs/lorem.md",
|
||||||
|
},
|
||||||
"modules": {
|
"modules": {
|
||||||
"content": "@site/docs/lorem.md",
|
"content": "@site/docs/lorem.md",
|
||||||
},
|
},
|
||||||
|
@ -1609,6 +1665,10 @@ exports[`simple website content: route config 1`] = `
|
||||||
{
|
{
|
||||||
"component": "@theme/DocItem",
|
"component": "@theme/DocItem",
|
||||||
"exact": true,
|
"exact": true,
|
||||||
|
"metadata": {
|
||||||
|
"lastUpdatedAt": undefined,
|
||||||
|
"sourceFilePath": "docs/rootAbsoluteSlug.md",
|
||||||
|
},
|
||||||
"modules": {
|
"modules": {
|
||||||
"content": "@site/docs/rootAbsoluteSlug.md",
|
"content": "@site/docs/rootAbsoluteSlug.md",
|
||||||
},
|
},
|
||||||
|
@ -1618,6 +1678,10 @@ exports[`simple website content: route config 1`] = `
|
||||||
{
|
{
|
||||||
"component": "@theme/DocItem",
|
"component": "@theme/DocItem",
|
||||||
"exact": true,
|
"exact": true,
|
||||||
|
"metadata": {
|
||||||
|
"lastUpdatedAt": undefined,
|
||||||
|
"sourceFilePath": "docs/rootRelativeSlug.md",
|
||||||
|
},
|
||||||
"modules": {
|
"modules": {
|
||||||
"content": "@site/docs/rootRelativeSlug.md",
|
"content": "@site/docs/rootRelativeSlug.md",
|
||||||
},
|
},
|
||||||
|
@ -1627,6 +1691,10 @@ exports[`simple website content: route config 1`] = `
|
||||||
{
|
{
|
||||||
"component": "@theme/DocItem",
|
"component": "@theme/DocItem",
|
||||||
"exact": true,
|
"exact": true,
|
||||||
|
"metadata": {
|
||||||
|
"lastUpdatedAt": undefined,
|
||||||
|
"sourceFilePath": "docs/rootTryToEscapeSlug.md",
|
||||||
|
},
|
||||||
"modules": {
|
"modules": {
|
||||||
"content": "@site/docs/rootTryToEscapeSlug.md",
|
"content": "@site/docs/rootTryToEscapeSlug.md",
|
||||||
},
|
},
|
||||||
|
@ -1636,6 +1704,10 @@ exports[`simple website content: route config 1`] = `
|
||||||
{
|
{
|
||||||
"component": "@theme/DocItem",
|
"component": "@theme/DocItem",
|
||||||
"exact": true,
|
"exact": true,
|
||||||
|
"metadata": {
|
||||||
|
"lastUpdatedAt": undefined,
|
||||||
|
"sourceFilePath": "docs/slugs/resolvedSlug.md",
|
||||||
|
},
|
||||||
"modules": {
|
"modules": {
|
||||||
"content": "@site/docs/slugs/resolvedSlug.md",
|
"content": "@site/docs/slugs/resolvedSlug.md",
|
||||||
},
|
},
|
||||||
|
@ -1644,6 +1716,10 @@ exports[`simple website content: route config 1`] = `
|
||||||
{
|
{
|
||||||
"component": "@theme/DocItem",
|
"component": "@theme/DocItem",
|
||||||
"exact": true,
|
"exact": true,
|
||||||
|
"metadata": {
|
||||||
|
"lastUpdatedAt": undefined,
|
||||||
|
"sourceFilePath": "docs/slugs/relativeSlug.md",
|
||||||
|
},
|
||||||
"modules": {
|
"modules": {
|
||||||
"content": "@site/docs/slugs/relativeSlug.md",
|
"content": "@site/docs/slugs/relativeSlug.md",
|
||||||
},
|
},
|
||||||
|
@ -1652,6 +1728,10 @@ exports[`simple website content: route config 1`] = `
|
||||||
{
|
{
|
||||||
"component": "@theme/DocItem",
|
"component": "@theme/DocItem",
|
||||||
"exact": true,
|
"exact": true,
|
||||||
|
"metadata": {
|
||||||
|
"lastUpdatedAt": undefined,
|
||||||
|
"sourceFilePath": "docs/slugs/tryToEscapeSlug.md",
|
||||||
|
},
|
||||||
"modules": {
|
"modules": {
|
||||||
"content": "@site/docs/slugs/tryToEscapeSlug.md",
|
"content": "@site/docs/slugs/tryToEscapeSlug.md",
|
||||||
},
|
},
|
||||||
|
@ -1660,6 +1740,10 @@ exports[`simple website content: route config 1`] = `
|
||||||
{
|
{
|
||||||
"component": "@theme/DocItem",
|
"component": "@theme/DocItem",
|
||||||
"exact": true,
|
"exact": true,
|
||||||
|
"metadata": {
|
||||||
|
"lastUpdatedAt": undefined,
|
||||||
|
"sourceFilePath": "docs/unlisted-category/index.md",
|
||||||
|
},
|
||||||
"modules": {
|
"modules": {
|
||||||
"content": "@site/docs/unlisted-category/index.md",
|
"content": "@site/docs/unlisted-category/index.md",
|
||||||
},
|
},
|
||||||
|
@ -1669,6 +1753,10 @@ exports[`simple website content: route config 1`] = `
|
||||||
{
|
{
|
||||||
"component": "@theme/DocItem",
|
"component": "@theme/DocItem",
|
||||||
"exact": true,
|
"exact": true,
|
||||||
|
"metadata": {
|
||||||
|
"lastUpdatedAt": undefined,
|
||||||
|
"sourceFilePath": "docs/unlisted-category/unlisted-category-doc.md",
|
||||||
|
},
|
||||||
"modules": {
|
"modules": {
|
||||||
"content": "@site/docs/unlisted-category/unlisted-category-doc.md",
|
"content": "@site/docs/unlisted-category/unlisted-category-doc.md",
|
||||||
},
|
},
|
||||||
|
@ -2940,6 +3028,10 @@ exports[`versioned website (community) content: route config 1`] = `
|
||||||
{
|
{
|
||||||
"component": "@theme/DocItem",
|
"component": "@theme/DocItem",
|
||||||
"exact": true,
|
"exact": true,
|
||||||
|
"metadata": {
|
||||||
|
"lastUpdatedAt": undefined,
|
||||||
|
"sourceFilePath": "i18n/en/docusaurus-plugin-content-docs-community/current/team.md",
|
||||||
|
},
|
||||||
"modules": {
|
"modules": {
|
||||||
"content": "@site/i18n/en/docusaurus-plugin-content-docs-community/current/team.md",
|
"content": "@site/i18n/en/docusaurus-plugin-content-docs-community/current/team.md",
|
||||||
},
|
},
|
||||||
|
@ -2967,6 +3059,10 @@ exports[`versioned website (community) content: route config 1`] = `
|
||||||
{
|
{
|
||||||
"component": "@theme/DocItem",
|
"component": "@theme/DocItem",
|
||||||
"exact": true,
|
"exact": true,
|
||||||
|
"metadata": {
|
||||||
|
"lastUpdatedAt": undefined,
|
||||||
|
"sourceFilePath": "community_versioned_docs/version-1.0.0/team.md",
|
||||||
|
},
|
||||||
"modules": {
|
"modules": {
|
||||||
"content": "@site/community_versioned_docs/version-1.0.0/team.md",
|
"content": "@site/community_versioned_docs/version-1.0.0/team.md",
|
||||||
},
|
},
|
||||||
|
@ -4174,6 +4270,10 @@ exports[`versioned website content: route config 1`] = `
|
||||||
{
|
{
|
||||||
"component": "@theme/DocItem",
|
"component": "@theme/DocItem",
|
||||||
"exact": true,
|
"exact": true,
|
||||||
|
"metadata": {
|
||||||
|
"lastUpdatedAt": undefined,
|
||||||
|
"sourceFilePath": "i18n/en/docusaurus-plugin-content-docs/version-1.0.0/hello.md",
|
||||||
|
},
|
||||||
"modules": {
|
"modules": {
|
||||||
"content": "@site/i18n/en/docusaurus-plugin-content-docs/version-1.0.0/hello.md",
|
"content": "@site/i18n/en/docusaurus-plugin-content-docs/version-1.0.0/hello.md",
|
||||||
},
|
},
|
||||||
|
@ -4183,6 +4283,10 @@ exports[`versioned website content: route config 1`] = `
|
||||||
{
|
{
|
||||||
"component": "@theme/DocItem",
|
"component": "@theme/DocItem",
|
||||||
"exact": true,
|
"exact": true,
|
||||||
|
"metadata": {
|
||||||
|
"lastUpdatedAt": undefined,
|
||||||
|
"sourceFilePath": "versioned_docs/version-1.0.0/foo/bar.md",
|
||||||
|
},
|
||||||
"modules": {
|
"modules": {
|
||||||
"content": "@site/versioned_docs/version-1.0.0/foo/bar.md",
|
"content": "@site/versioned_docs/version-1.0.0/foo/bar.md",
|
||||||
},
|
},
|
||||||
|
@ -4192,6 +4296,10 @@ exports[`versioned website content: route config 1`] = `
|
||||||
{
|
{
|
||||||
"component": "@theme/DocItem",
|
"component": "@theme/DocItem",
|
||||||
"exact": true,
|
"exact": true,
|
||||||
|
"metadata": {
|
||||||
|
"lastUpdatedAt": undefined,
|
||||||
|
"sourceFilePath": "versioned_docs/version-1.0.0/foo/baz.md",
|
||||||
|
},
|
||||||
"modules": {
|
"modules": {
|
||||||
"content": "@site/versioned_docs/version-1.0.0/foo/baz.md",
|
"content": "@site/versioned_docs/version-1.0.0/foo/baz.md",
|
||||||
},
|
},
|
||||||
|
@ -4251,6 +4359,10 @@ exports[`versioned website content: route config 1`] = `
|
||||||
{
|
{
|
||||||
"component": "@theme/DocItem",
|
"component": "@theme/DocItem",
|
||||||
"exact": true,
|
"exact": true,
|
||||||
|
"metadata": {
|
||||||
|
"lastUpdatedAt": undefined,
|
||||||
|
"sourceFilePath": "docs/hello.md",
|
||||||
|
},
|
||||||
"modules": {
|
"modules": {
|
||||||
"content": "@site/docs/hello.md",
|
"content": "@site/docs/hello.md",
|
||||||
},
|
},
|
||||||
|
@ -4260,6 +4372,10 @@ exports[`versioned website content: route config 1`] = `
|
||||||
{
|
{
|
||||||
"component": "@theme/DocItem",
|
"component": "@theme/DocItem",
|
||||||
"exact": true,
|
"exact": true,
|
||||||
|
"metadata": {
|
||||||
|
"lastUpdatedAt": undefined,
|
||||||
|
"sourceFilePath": "docs/slugs/absoluteSlug.md",
|
||||||
|
},
|
||||||
"modules": {
|
"modules": {
|
||||||
"content": "@site/docs/slugs/absoluteSlug.md",
|
"content": "@site/docs/slugs/absoluteSlug.md",
|
||||||
},
|
},
|
||||||
|
@ -4268,6 +4384,10 @@ exports[`versioned website content: route config 1`] = `
|
||||||
{
|
{
|
||||||
"component": "@theme/DocItem",
|
"component": "@theme/DocItem",
|
||||||
"exact": true,
|
"exact": true,
|
||||||
|
"metadata": {
|
||||||
|
"lastUpdatedAt": undefined,
|
||||||
|
"sourceFilePath": "docs/foo/bar.md",
|
||||||
|
},
|
||||||
"modules": {
|
"modules": {
|
||||||
"content": "@site/docs/foo/bar.md",
|
"content": "@site/docs/foo/bar.md",
|
||||||
},
|
},
|
||||||
|
@ -4277,6 +4397,10 @@ exports[`versioned website content: route config 1`] = `
|
||||||
{
|
{
|
||||||
"component": "@theme/DocItem",
|
"component": "@theme/DocItem",
|
||||||
"exact": true,
|
"exact": true,
|
||||||
|
"metadata": {
|
||||||
|
"lastUpdatedAt": undefined,
|
||||||
|
"sourceFilePath": "docs/slugs/resolvedSlug.md",
|
||||||
|
},
|
||||||
"modules": {
|
"modules": {
|
||||||
"content": "@site/docs/slugs/resolvedSlug.md",
|
"content": "@site/docs/slugs/resolvedSlug.md",
|
||||||
},
|
},
|
||||||
|
@ -4285,6 +4409,10 @@ exports[`versioned website content: route config 1`] = `
|
||||||
{
|
{
|
||||||
"component": "@theme/DocItem",
|
"component": "@theme/DocItem",
|
||||||
"exact": true,
|
"exact": true,
|
||||||
|
"metadata": {
|
||||||
|
"lastUpdatedAt": undefined,
|
||||||
|
"sourceFilePath": "docs/slugs/relativeSlug.md",
|
||||||
|
},
|
||||||
"modules": {
|
"modules": {
|
||||||
"content": "@site/docs/slugs/relativeSlug.md",
|
"content": "@site/docs/slugs/relativeSlug.md",
|
||||||
},
|
},
|
||||||
|
@ -4293,6 +4421,10 @@ exports[`versioned website content: route config 1`] = `
|
||||||
{
|
{
|
||||||
"component": "@theme/DocItem",
|
"component": "@theme/DocItem",
|
||||||
"exact": true,
|
"exact": true,
|
||||||
|
"metadata": {
|
||||||
|
"lastUpdatedAt": undefined,
|
||||||
|
"sourceFilePath": "docs/slugs/tryToEscapeSlug.md",
|
||||||
|
},
|
||||||
"modules": {
|
"modules": {
|
||||||
"content": "@site/docs/slugs/tryToEscapeSlug.md",
|
"content": "@site/docs/slugs/tryToEscapeSlug.md",
|
||||||
},
|
},
|
||||||
|
@ -4319,6 +4451,10 @@ exports[`versioned website content: route config 1`] = `
|
||||||
{
|
{
|
||||||
"component": "@theme/DocItem",
|
"component": "@theme/DocItem",
|
||||||
"exact": true,
|
"exact": true,
|
||||||
|
"metadata": {
|
||||||
|
"lastUpdatedAt": undefined,
|
||||||
|
"sourceFilePath": "versioned_docs/version-withSlugs/slugs/absoluteSlug.md",
|
||||||
|
},
|
||||||
"modules": {
|
"modules": {
|
||||||
"content": "@site/versioned_docs/version-withSlugs/slugs/absoluteSlug.md",
|
"content": "@site/versioned_docs/version-withSlugs/slugs/absoluteSlug.md",
|
||||||
},
|
},
|
||||||
|
@ -4327,6 +4463,10 @@ exports[`versioned website content: route config 1`] = `
|
||||||
{
|
{
|
||||||
"component": "@theme/DocItem",
|
"component": "@theme/DocItem",
|
||||||
"exact": true,
|
"exact": true,
|
||||||
|
"metadata": {
|
||||||
|
"lastUpdatedAt": undefined,
|
||||||
|
"sourceFilePath": "versioned_docs/version-withSlugs/rootResolvedSlug.md",
|
||||||
|
},
|
||||||
"modules": {
|
"modules": {
|
||||||
"content": "@site/versioned_docs/version-withSlugs/rootResolvedSlug.md",
|
"content": "@site/versioned_docs/version-withSlugs/rootResolvedSlug.md",
|
||||||
},
|
},
|
||||||
|
@ -4335,6 +4475,10 @@ exports[`versioned website content: route config 1`] = `
|
||||||
{
|
{
|
||||||
"component": "@theme/DocItem",
|
"component": "@theme/DocItem",
|
||||||
"exact": true,
|
"exact": true,
|
||||||
|
"metadata": {
|
||||||
|
"lastUpdatedAt": undefined,
|
||||||
|
"sourceFilePath": "versioned_docs/version-withSlugs/rootAbsoluteSlug.md",
|
||||||
|
},
|
||||||
"modules": {
|
"modules": {
|
||||||
"content": "@site/versioned_docs/version-withSlugs/rootAbsoluteSlug.md",
|
"content": "@site/versioned_docs/version-withSlugs/rootAbsoluteSlug.md",
|
||||||
},
|
},
|
||||||
|
@ -4344,6 +4488,10 @@ exports[`versioned website content: route config 1`] = `
|
||||||
{
|
{
|
||||||
"component": "@theme/DocItem",
|
"component": "@theme/DocItem",
|
||||||
"exact": true,
|
"exact": true,
|
||||||
|
"metadata": {
|
||||||
|
"lastUpdatedAt": undefined,
|
||||||
|
"sourceFilePath": "versioned_docs/version-withSlugs/rootRelativeSlug.md",
|
||||||
|
},
|
||||||
"modules": {
|
"modules": {
|
||||||
"content": "@site/versioned_docs/version-withSlugs/rootRelativeSlug.md",
|
"content": "@site/versioned_docs/version-withSlugs/rootRelativeSlug.md",
|
||||||
},
|
},
|
||||||
|
@ -4352,6 +4500,10 @@ exports[`versioned website content: route config 1`] = `
|
||||||
{
|
{
|
||||||
"component": "@theme/DocItem",
|
"component": "@theme/DocItem",
|
||||||
"exact": true,
|
"exact": true,
|
||||||
|
"metadata": {
|
||||||
|
"lastUpdatedAt": undefined,
|
||||||
|
"sourceFilePath": "versioned_docs/version-withSlugs/rootTryToEscapeSlug.md",
|
||||||
|
},
|
||||||
"modules": {
|
"modules": {
|
||||||
"content": "@site/versioned_docs/version-withSlugs/rootTryToEscapeSlug.md",
|
"content": "@site/versioned_docs/version-withSlugs/rootTryToEscapeSlug.md",
|
||||||
},
|
},
|
||||||
|
@ -4360,6 +4512,10 @@ exports[`versioned website content: route config 1`] = `
|
||||||
{
|
{
|
||||||
"component": "@theme/DocItem",
|
"component": "@theme/DocItem",
|
||||||
"exact": true,
|
"exact": true,
|
||||||
|
"metadata": {
|
||||||
|
"lastUpdatedAt": undefined,
|
||||||
|
"sourceFilePath": "versioned_docs/version-withSlugs/slugs/resolvedSlug.md",
|
||||||
|
},
|
||||||
"modules": {
|
"modules": {
|
||||||
"content": "@site/versioned_docs/version-withSlugs/slugs/resolvedSlug.md",
|
"content": "@site/versioned_docs/version-withSlugs/slugs/resolvedSlug.md",
|
||||||
},
|
},
|
||||||
|
@ -4368,6 +4524,10 @@ exports[`versioned website content: route config 1`] = `
|
||||||
{
|
{
|
||||||
"component": "@theme/DocItem",
|
"component": "@theme/DocItem",
|
||||||
"exact": true,
|
"exact": true,
|
||||||
|
"metadata": {
|
||||||
|
"lastUpdatedAt": undefined,
|
||||||
|
"sourceFilePath": "versioned_docs/version-withSlugs/slugs/relativeSlug.md",
|
||||||
|
},
|
||||||
"modules": {
|
"modules": {
|
||||||
"content": "@site/versioned_docs/version-withSlugs/slugs/relativeSlug.md",
|
"content": "@site/versioned_docs/version-withSlugs/slugs/relativeSlug.md",
|
||||||
},
|
},
|
||||||
|
@ -4376,6 +4536,10 @@ exports[`versioned website content: route config 1`] = `
|
||||||
{
|
{
|
||||||
"component": "@theme/DocItem",
|
"component": "@theme/DocItem",
|
||||||
"exact": true,
|
"exact": true,
|
||||||
|
"metadata": {
|
||||||
|
"lastUpdatedAt": undefined,
|
||||||
|
"sourceFilePath": "versioned_docs/version-withSlugs/slugs/tryToEscapeSlug.md",
|
||||||
|
},
|
||||||
"modules": {
|
"modules": {
|
||||||
"content": "@site/versioned_docs/version-withSlugs/slugs/tryToEscapeSlug.md",
|
"content": "@site/versioned_docs/version-withSlugs/slugs/tryToEscapeSlug.md",
|
||||||
},
|
},
|
||||||
|
@ -4402,6 +4566,10 @@ exports[`versioned website content: route config 1`] = `
|
||||||
{
|
{
|
||||||
"component": "@theme/DocItem",
|
"component": "@theme/DocItem",
|
||||||
"exact": true,
|
"exact": true,
|
||||||
|
"metadata": {
|
||||||
|
"lastUpdatedAt": undefined,
|
||||||
|
"sourceFilePath": "versioned_docs/version-1.0.1/hello.md",
|
||||||
|
},
|
||||||
"modules": {
|
"modules": {
|
||||||
"content": "@site/versioned_docs/version-1.0.1/hello.md",
|
"content": "@site/versioned_docs/version-1.0.1/hello.md",
|
||||||
},
|
},
|
||||||
|
@ -4411,6 +4579,10 @@ exports[`versioned website content: route config 1`] = `
|
||||||
{
|
{
|
||||||
"component": "@theme/DocItem",
|
"component": "@theme/DocItem",
|
||||||
"exact": true,
|
"exact": true,
|
||||||
|
"metadata": {
|
||||||
|
"lastUpdatedAt": undefined,
|
||||||
|
"sourceFilePath": "versioned_docs/version-1.0.1/foo/bar.md",
|
||||||
|
},
|
||||||
"modules": {
|
"modules": {
|
||||||
"content": "@site/versioned_docs/version-1.0.1/foo/bar.md",
|
"content": "@site/versioned_docs/version-1.0.1/foo/bar.md",
|
||||||
},
|
},
|
||||||
|
|
|
@ -12,7 +12,7 @@ import {
|
||||||
createSlugger,
|
createSlugger,
|
||||||
posixPath,
|
posixPath,
|
||||||
DEFAULT_PLUGIN_ID,
|
DEFAULT_PLUGIN_ID,
|
||||||
GIT_FALLBACK_LAST_UPDATE_DATE,
|
LAST_UPDATE_FALLBACK,
|
||||||
} from '@docusaurus/utils';
|
} from '@docusaurus/utils';
|
||||||
import {createSidebarsUtils} from '../sidebars/utils';
|
import {createSidebarsUtils} from '../sidebars/utils';
|
||||||
import {
|
import {
|
||||||
|
@ -479,8 +479,8 @@ describe('simple site', () => {
|
||||||
custom_edit_url: 'https://github.com/customUrl/docs/lorem.md',
|
custom_edit_url: 'https://github.com/customUrl/docs/lorem.md',
|
||||||
unrelated_front_matter: "won't be part of metadata",
|
unrelated_front_matter: "won't be part of metadata",
|
||||||
},
|
},
|
||||||
lastUpdatedAt: GIT_FALLBACK_LAST_UPDATE_DATE,
|
lastUpdatedAt: LAST_UPDATE_FALLBACK.lastUpdatedAt,
|
||||||
lastUpdatedBy: 'Author',
|
lastUpdatedBy: LAST_UPDATE_FALLBACK.lastUpdatedBy,
|
||||||
tags: [],
|
tags: [],
|
||||||
unlisted: false,
|
unlisted: false,
|
||||||
});
|
});
|
||||||
|
@ -614,7 +614,7 @@ describe('simple site', () => {
|
||||||
},
|
},
|
||||||
title: 'Last Update Author Only',
|
title: 'Last Update Author Only',
|
||||||
},
|
},
|
||||||
lastUpdatedAt: GIT_FALLBACK_LAST_UPDATE_DATE,
|
lastUpdatedAt: LAST_UPDATE_FALLBACK.lastUpdatedAt,
|
||||||
lastUpdatedBy: 'Custom Author (processed by parseFrontMatter)',
|
lastUpdatedBy: 'Custom Author (processed by parseFrontMatter)',
|
||||||
sidebarPosition: undefined,
|
sidebarPosition: undefined,
|
||||||
tags: [],
|
tags: [],
|
||||||
|
|
|
@ -17,6 +17,7 @@ declare module '@docusaurus/plugin-content-docs' {
|
||||||
TagModule,
|
TagModule,
|
||||||
Tag,
|
Tag,
|
||||||
FrontMatterLastUpdate,
|
FrontMatterLastUpdate,
|
||||||
|
LastUpdateData,
|
||||||
} from '@docusaurus/utils';
|
} from '@docusaurus/utils';
|
||||||
import type {Plugin, LoadContext} from '@docusaurus/types';
|
import type {Plugin, LoadContext} from '@docusaurus/types';
|
||||||
import type {Overwrite, Required} from 'utility-types';
|
import type {Overwrite, Required} from 'utility-types';
|
||||||
|
@ -397,13 +398,6 @@ declare module '@docusaurus/plugin-content-docs' {
|
||||||
last_update?: FrontMatterLastUpdate;
|
last_update?: FrontMatterLastUpdate;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LastUpdateData = {
|
|
||||||
/** A timestamp in **seconds**, directly acquired from `git log`. */
|
|
||||||
lastUpdatedAt?: number;
|
|
||||||
/** The author's name directly acquired from `git log`. */
|
|
||||||
lastUpdatedBy?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type DocMetadataBase = LastUpdateData & {
|
export type DocMetadataBase = LastUpdateData & {
|
||||||
/**
|
/**
|
||||||
* The document id.
|
* The document id.
|
||||||
|
|
|
@ -7,21 +7,38 @@
|
||||||
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import logger from '@docusaurus/logger';
|
import logger from '@docusaurus/logger';
|
||||||
import {docuHash, createSlugger, normalizeUrl} from '@docusaurus/utils';
|
import {
|
||||||
|
docuHash,
|
||||||
|
createSlugger,
|
||||||
|
normalizeUrl,
|
||||||
|
aliasedSitePathToRelativePath,
|
||||||
|
} from '@docusaurus/utils';
|
||||||
import {
|
import {
|
||||||
toTagDocListProp,
|
toTagDocListProp,
|
||||||
toTagsListTagsProp,
|
toTagsListTagsProp,
|
||||||
toVersionMetadataProp,
|
toVersionMetadataProp,
|
||||||
} from './props';
|
} from './props';
|
||||||
import {getVersionTags} from './tags';
|
import {getVersionTags} from './tags';
|
||||||
import type {PluginContentLoadedActions, RouteConfig} from '@docusaurus/types';
|
import type {
|
||||||
|
PluginContentLoadedActions,
|
||||||
|
RouteConfig,
|
||||||
|
RouteMetadata,
|
||||||
|
} from '@docusaurus/types';
|
||||||
import type {FullVersion, VersionTag} from './types';
|
import type {FullVersion, VersionTag} from './types';
|
||||||
import type {
|
import type {
|
||||||
CategoryGeneratedIndexMetadata,
|
CategoryGeneratedIndexMetadata,
|
||||||
|
DocMetadata,
|
||||||
PluginOptions,
|
PluginOptions,
|
||||||
PropTagsListPage,
|
PropTagsListPage,
|
||||||
} from '@docusaurus/plugin-content-docs';
|
} from '@docusaurus/plugin-content-docs';
|
||||||
|
|
||||||
|
function createDocRouteMetadata(docMeta: DocMetadata): RouteMetadata {
|
||||||
|
return {
|
||||||
|
sourceFilePath: aliasedSitePathToRelativePath(docMeta.source),
|
||||||
|
lastUpdatedAt: docMeta.lastUpdatedAt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function buildVersionCategoryGeneratedIndexRoutes({
|
async function buildVersionCategoryGeneratedIndexRoutes({
|
||||||
version,
|
version,
|
||||||
actions,
|
actions,
|
||||||
|
@ -68,26 +85,27 @@ async function buildVersionDocRoutes({
|
||||||
options,
|
options,
|
||||||
}: BuildVersionRoutesParam): Promise<RouteConfig[]> {
|
}: BuildVersionRoutesParam): Promise<RouteConfig[]> {
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
version.docs.map(async (metadataItem) => {
|
version.docs.map(async (doc) => {
|
||||||
await actions.createData(
|
await actions.createData(
|
||||||
// Note that this created data path must be in sync with
|
// Note that this created data path must be in sync with
|
||||||
// metadataPath provided to mdx-loader.
|
// metadataPath provided to mdx-loader.
|
||||||
`${docuHash(metadataItem.source)}.json`,
|
`${docuHash(doc.source)}.json`,
|
||||||
JSON.stringify(metadataItem, null, 2),
|
JSON.stringify(doc, null, 2),
|
||||||
);
|
);
|
||||||
|
|
||||||
const docRoute: RouteConfig = {
|
const docRoute: RouteConfig = {
|
||||||
path: metadataItem.permalink,
|
path: doc.permalink,
|
||||||
component: options.docItemComponent,
|
component: options.docItemComponent,
|
||||||
exact: true,
|
exact: true,
|
||||||
modules: {
|
modules: {
|
||||||
content: metadataItem.source,
|
content: doc.source,
|
||||||
},
|
},
|
||||||
|
metadata: createDocRouteMetadata(doc),
|
||||||
// Because the parent (DocRoot) comp need to access it easily
|
// Because the parent (DocRoot) comp need to access it easily
|
||||||
// This permits to render the sidebar once without unmount/remount when
|
// This permits to render the sidebar once without unmount/remount when
|
||||||
// navigating (and preserve sidebar state)
|
// navigating (and preserve sidebar state)
|
||||||
...(metadataItem.sidebar && {
|
...(doc.sidebar && {
|
||||||
sidebar: metadataItem.sidebar,
|
sidebar: doc.sidebar,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
encodePath,
|
encodePath,
|
||||||
fileToPath,
|
fileToPath,
|
||||||
aliasedSitePath,
|
aliasedSitePath,
|
||||||
|
aliasedSitePathToRelativePath,
|
||||||
docuHash,
|
docuHash,
|
||||||
getPluginI18nPath,
|
getPluginI18nPath,
|
||||||
getFolderContainingFile,
|
getFolderContainingFile,
|
||||||
|
@ -24,8 +25,7 @@ import {
|
||||||
isDraft,
|
isDraft,
|
||||||
} from '@docusaurus/utils';
|
} from '@docusaurus/utils';
|
||||||
import {validatePageFrontMatter} from './frontMatter';
|
import {validatePageFrontMatter} from './frontMatter';
|
||||||
|
import type {LoadContext, Plugin, RouteMetadata} from '@docusaurus/types';
|
||||||
import type {LoadContext, Plugin} from '@docusaurus/types';
|
|
||||||
import type {PagesContentPaths} from './types';
|
import type {PagesContentPaths} from './types';
|
||||||
import type {
|
import type {
|
||||||
PluginOptions,
|
PluginOptions,
|
||||||
|
@ -159,9 +159,20 @@ export default function pluginContentPages(
|
||||||
|
|
||||||
const {addRoute, createData} = actions;
|
const {addRoute, createData} = actions;
|
||||||
|
|
||||||
|
function createPageRouteMetadata(metadata: Metadata): RouteMetadata {
|
||||||
|
return {
|
||||||
|
sourceFilePath: aliasedSitePathToRelativePath(metadata.source),
|
||||||
|
// TODO add support for last updated date in the page plugin
|
||||||
|
// at least for Markdown files
|
||||||
|
// lastUpdatedAt: metadata.lastUpdatedAt,
|
||||||
|
lastUpdatedAt: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
content.map(async (metadata) => {
|
content.map(async (metadata) => {
|
||||||
const {permalink, source} = metadata;
|
const {permalink, source} = metadata;
|
||||||
|
const routeMetadata = createPageRouteMetadata(metadata);
|
||||||
if (metadata.type === 'mdx') {
|
if (metadata.type === 'mdx') {
|
||||||
await createData(
|
await createData(
|
||||||
// Note that this created data path must be in sync with
|
// Note that this created data path must be in sync with
|
||||||
|
@ -173,6 +184,7 @@ export default function pluginContentPages(
|
||||||
path: permalink,
|
path: permalink,
|
||||||
component: options.mdxPageComponent,
|
component: options.mdxPageComponent,
|
||||||
exact: true,
|
exact: true,
|
||||||
|
metadata: routeMetadata,
|
||||||
modules: {
|
modules: {
|
||||||
content: source,
|
content: source,
|
||||||
},
|
},
|
||||||
|
@ -182,6 +194,7 @@ export default function pluginContentPages(
|
||||||
path: permalink,
|
path: permalink,
|
||||||
component: source,
|
component: source,
|
||||||
exact: true,
|
exact: true,
|
||||||
|
metadata: routeMetadata,
|
||||||
modules: {
|
modules: {
|
||||||
config: `@generated/docusaurus.config`,
|
config: `@generated/docusaurus.config`,
|
||||||
},
|
},
|
||||||
|
|
|
@ -28,6 +28,9 @@
|
||||||
"sitemap": "^7.1.1",
|
"sitemap": "^7.1.1",
|
||||||
"tslib": "^2.6.0"
|
"tslib": "^2.6.0"
|
||||||
},
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@total-typescript/shoehorn": "^0.1.2"
|
||||||
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^18.0.0",
|
"react": "^18.0.0",
|
||||||
"react-dom": "^18.0.0"
|
"react-dom": "^18.0.0"
|
||||||
|
|
|
@ -6,95 +6,91 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {EnumChangefreq} from 'sitemap';
|
import {fromPartial} from '@total-typescript/shoehorn';
|
||||||
import createSitemap from '../createSitemap';
|
import createSitemap from '../createSitemap';
|
||||||
import type {PluginOptions} from '../options';
|
import type {PluginOptions} from '../options';
|
||||||
import type {DocusaurusConfig} from '@docusaurus/types';
|
import type {DocusaurusConfig, RouteConfig} from '@docusaurus/types';
|
||||||
|
|
||||||
|
const siteConfig: DocusaurusConfig = fromPartial({
|
||||||
|
url: 'https://example.com',
|
||||||
|
});
|
||||||
|
|
||||||
|
const options: PluginOptions = {
|
||||||
|
changefreq: 'daily',
|
||||||
|
priority: 0.7,
|
||||||
|
ignorePatterns: [],
|
||||||
|
filename: 'sitemap.xml',
|
||||||
|
lastmod: 'datetime',
|
||||||
|
};
|
||||||
|
|
||||||
|
const route = (routePath: string, routePaths?: string[]): RouteConfig => {
|
||||||
|
return fromPartial({
|
||||||
|
path: routePath,
|
||||||
|
routes: routePaths?.map((p) => route(p)),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const routes = (routePaths: string[]): RouteConfig[] => {
|
||||||
|
return routePaths.map((p) => route(p));
|
||||||
|
};
|
||||||
|
|
||||||
describe('createSitemap', () => {
|
describe('createSitemap', () => {
|
||||||
it('simple site', async () => {
|
it('simple site', async () => {
|
||||||
const sitemap = await createSitemap(
|
const sitemap = await createSitemap({
|
||||||
{
|
siteConfig,
|
||||||
url: 'https://example.com',
|
routes: routes(['/', '/test']),
|
||||||
} as DocusaurusConfig,
|
head: {},
|
||||||
['/', '/test'],
|
options,
|
||||||
{},
|
});
|
||||||
{
|
|
||||||
changefreq: EnumChangefreq.DAILY,
|
|
||||||
priority: 0.7,
|
|
||||||
ignorePatterns: [],
|
|
||||||
filename: 'sitemap.xml',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
expect(sitemap).toContain(
|
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">`,
|
`<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">`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('empty site', () =>
|
it('site with no routes', async () => {
|
||||||
expect(async () => {
|
const sitemap = await createSitemap({
|
||||||
// @ts-expect-error: test
|
siteConfig,
|
||||||
await createSitemap({}, [], {}, {} as PluginOptions);
|
routes: routes([]),
|
||||||
}).rejects.toThrow(
|
head: {},
|
||||||
'URL in docusaurus.config.js cannot be empty/undefined.',
|
options,
|
||||||
));
|
});
|
||||||
|
expect(sitemap).toBeNull();
|
||||||
it('exclusion of 404 page', async () => {
|
|
||||||
const sitemap = await createSitemap(
|
|
||||||
{
|
|
||||||
url: 'https://example.com',
|
|
||||||
} as DocusaurusConfig,
|
|
||||||
['/', '/404.html', '/my-page'],
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
changefreq: EnumChangefreq.DAILY,
|
|
||||||
priority: 0.7,
|
|
||||||
ignorePatterns: [],
|
|
||||||
filename: 'sitemap.xml',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
expect(sitemap).not.toContain('404');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('excludes patterns configured to be ignored', async () => {
|
it('excludes patterns configured to be ignored', async () => {
|
||||||
const sitemap = await createSitemap(
|
const sitemap = await createSitemap({
|
||||||
{
|
siteConfig,
|
||||||
url: 'https://example.com',
|
routes: routes([
|
||||||
} as DocusaurusConfig,
|
'/',
|
||||||
['/', '/search/', '/tags/', '/search/foo', '/tags/foo/bar'],
|
'/search/',
|
||||||
{},
|
'/tags/',
|
||||||
{
|
'/search/foo',
|
||||||
changefreq: EnumChangefreq.DAILY,
|
'/tags/foo/bar',
|
||||||
priority: 0.7,
|
]),
|
||||||
|
head: {},
|
||||||
|
options: {
|
||||||
|
...options,
|
||||||
ignorePatterns: [
|
ignorePatterns: [
|
||||||
// Shallow ignore
|
// Shallow ignore
|
||||||
'/search/',
|
'/search/',
|
||||||
// Deep ignore
|
// Deep ignore
|
||||||
'/tags/**',
|
'/tags/**',
|
||||||
],
|
],
|
||||||
filename: 'sitemap.xml',
|
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
|
|
||||||
expect(sitemap).not.toContain('/search/</loc>');
|
expect(sitemap).not.toContain('/search/</loc>');
|
||||||
expect(sitemap).toContain('/search/foo');
|
expect(sitemap).toContain('/search/foo');
|
||||||
expect(sitemap).not.toContain('/tags');
|
expect(sitemap).not.toContain('/tags');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keep trailing slash unchanged', async () => {
|
it('keep trailing slash unchanged', async () => {
|
||||||
const sitemap = await createSitemap(
|
const sitemap = await createSitemap({
|
||||||
{
|
siteConfig,
|
||||||
url: 'https://example.com',
|
routes: routes(['/', '/test', '/nested/test', '/nested/test2/']),
|
||||||
trailingSlash: undefined,
|
head: {},
|
||||||
} as DocusaurusConfig,
|
options,
|
||||||
['/', '/test', '/nested/test', '/nested/test2/'],
|
});
|
||||||
{},
|
|
||||||
{
|
|
||||||
changefreq: EnumChangefreq.DAILY,
|
|
||||||
priority: 0.7,
|
|
||||||
ignorePatterns: [],
|
|
||||||
filename: 'sitemap.xml',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(sitemap).toContain('<loc>https://example.com/</loc>');
|
expect(sitemap).toContain('<loc>https://example.com/</loc>');
|
||||||
expect(sitemap).toContain('<loc>https://example.com/test</loc>');
|
expect(sitemap).toContain('<loc>https://example.com/test</loc>');
|
||||||
|
@ -103,20 +99,12 @@ describe('createSitemap', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('add trailing slash', async () => {
|
it('add trailing slash', async () => {
|
||||||
const sitemap = await createSitemap(
|
const sitemap = await createSitemap({
|
||||||
{
|
siteConfig: {...siteConfig, trailingSlash: true},
|
||||||
url: 'https://example.com',
|
routes: routes(['/', '/test', '/nested/test', '/nested/test2/']),
|
||||||
trailingSlash: true,
|
head: {},
|
||||||
} as DocusaurusConfig,
|
options,
|
||||||
['/', '/test', '/nested/test', '/nested/test2/'],
|
});
|
||||||
{},
|
|
||||||
{
|
|
||||||
changefreq: EnumChangefreq.DAILY,
|
|
||||||
priority: 0.7,
|
|
||||||
ignorePatterns: [],
|
|
||||||
filename: 'sitemap.xml',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(sitemap).toContain('<loc>https://example.com/</loc>');
|
expect(sitemap).toContain('<loc>https://example.com/</loc>');
|
||||||
expect(sitemap).toContain('<loc>https://example.com/test/</loc>');
|
expect(sitemap).toContain('<loc>https://example.com/test/</loc>');
|
||||||
|
@ -125,20 +113,16 @@ describe('createSitemap', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('remove trailing slash', async () => {
|
it('remove trailing slash', async () => {
|
||||||
const sitemap = await createSitemap(
|
const sitemap = await createSitemap({
|
||||||
{
|
siteConfig: {
|
||||||
|
...siteConfig,
|
||||||
url: 'https://example.com',
|
url: 'https://example.com',
|
||||||
trailingSlash: false,
|
trailingSlash: false,
|
||||||
} as DocusaurusConfig,
|
|
||||||
['/', '/test', '/nested/test', '/nested/test2/'],
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
changefreq: EnumChangefreq.DAILY,
|
|
||||||
priority: 0.7,
|
|
||||||
ignorePatterns: [],
|
|
||||||
filename: 'sitemap.xml',
|
|
||||||
},
|
},
|
||||||
);
|
routes: routes(['/', '/test', '/nested/test', '/nested/test2/']),
|
||||||
|
head: {},
|
||||||
|
options,
|
||||||
|
});
|
||||||
|
|
||||||
expect(sitemap).toContain('<loc>https://example.com/</loc>');
|
expect(sitemap).toContain('<loc>https://example.com/</loc>');
|
||||||
expect(sitemap).toContain('<loc>https://example.com/test</loc>');
|
expect(sitemap).toContain('<loc>https://example.com/test</loc>');
|
||||||
|
@ -147,13 +131,11 @@ describe('createSitemap', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('filters pages with noindex', async () => {
|
it('filters pages with noindex', async () => {
|
||||||
const sitemap = await createSitemap(
|
const sitemap = await createSitemap({
|
||||||
{
|
siteConfig,
|
||||||
url: 'https://example.com',
|
routesPaths: ['/', '/noindex', '/nested/test', '/nested/test2/'],
|
||||||
trailingSlash: false,
|
routes: routes(['/', '/noindex', '/nested/test', '/nested/test2/']),
|
||||||
} as DocusaurusConfig,
|
head: {
|
||||||
['/', '/noindex', '/nested/test', '/nested/test2/'],
|
|
||||||
{
|
|
||||||
'/noindex': {
|
'/noindex': {
|
||||||
meta: {
|
meta: {
|
||||||
// @ts-expect-error: bad lib def
|
// @ts-expect-error: bad lib def
|
||||||
|
@ -166,24 +148,18 @@ describe('createSitemap', () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
options,
|
||||||
changefreq: EnumChangefreq.DAILY,
|
});
|
||||||
priority: 0.7,
|
|
||||||
ignorePatterns: [],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(sitemap).not.toContain('/noindex');
|
expect(sitemap).not.toContain('/noindex');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not generate anything for all pages with noindex', async () => {
|
it('does not generate anything for all pages with noindex', async () => {
|
||||||
const sitemap = await createSitemap(
|
const sitemap = await createSitemap({
|
||||||
{
|
siteConfig,
|
||||||
url: 'https://example.com',
|
routesPaths: ['/', '/noindex'],
|
||||||
trailingSlash: false,
|
routes: routes(['/', '/noindex']),
|
||||||
} as DocusaurusConfig,
|
head: {
|
||||||
['/', '/noindex'],
|
|
||||||
{
|
|
||||||
'/': {
|
'/': {
|
||||||
meta: {
|
meta: {
|
||||||
// @ts-expect-error: bad lib def
|
// @ts-expect-error: bad lib def
|
||||||
|
@ -201,12 +177,8 @@ describe('createSitemap', () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
options,
|
||||||
changefreq: EnumChangefreq.DAILY,
|
});
|
||||||
priority: 0.7,
|
|
||||||
ignorePatterns: [],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(sitemap).toBeNull();
|
expect(sitemap).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,229 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {fromPartial} from '@total-typescript/shoehorn';
|
||||||
|
import {createSitemapItem} from '../createSitemapItem';
|
||||||
|
import {DEFAULT_OPTIONS} from '../options';
|
||||||
|
import type {PluginOptions} from '../options';
|
||||||
|
import type {DocusaurusConfig, RouteConfig} from '@docusaurus/types';
|
||||||
|
|
||||||
|
const siteConfig: DocusaurusConfig = fromPartial({
|
||||||
|
url: 'https://example.com',
|
||||||
|
});
|
||||||
|
|
||||||
|
function test(params: {
|
||||||
|
route: Partial<RouteConfig>;
|
||||||
|
siteConfig?: Partial<DocusaurusConfig>;
|
||||||
|
options?: Partial<PluginOptions>;
|
||||||
|
}) {
|
||||||
|
return createSitemapItem({
|
||||||
|
route: params.route as unknown as RouteConfig,
|
||||||
|
siteConfig: {...siteConfig, ...params.siteConfig},
|
||||||
|
options: {...DEFAULT_OPTIONS, ...params.options},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function testRoute(route: Partial<RouteConfig>) {
|
||||||
|
return test({
|
||||||
|
route,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('createSitemapItem', () => {
|
||||||
|
it('simple item', async () => {
|
||||||
|
await expect(testRoute({path: '/routePath'})).resolves
|
||||||
|
.toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"changefreq": "weekly",
|
||||||
|
"lastmod": null,
|
||||||
|
"priority": 0.5,
|
||||||
|
"url": "https://example.com/routePath",
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('lastmod', () => {
|
||||||
|
const date = new Date('2024/01/01');
|
||||||
|
|
||||||
|
describe('read from route metadata', () => {
|
||||||
|
const route = {
|
||||||
|
path: '/routePath',
|
||||||
|
metadata: {lastUpdatedAt: date.getTime()},
|
||||||
|
};
|
||||||
|
|
||||||
|
it('lastmod default option', async () => {
|
||||||
|
await expect(
|
||||||
|
test({
|
||||||
|
route,
|
||||||
|
}),
|
||||||
|
).resolves.toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"changefreq": "weekly",
|
||||||
|
"lastmod": null,
|
||||||
|
"priority": 0.5,
|
||||||
|
"url": "https://example.com/routePath",
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lastmod date option', async () => {
|
||||||
|
await expect(
|
||||||
|
test({
|
||||||
|
route,
|
||||||
|
options: {
|
||||||
|
lastmod: 'date',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"changefreq": "weekly",
|
||||||
|
"lastmod": "2024-01-01",
|
||||||
|
"priority": 0.5,
|
||||||
|
"url": "https://example.com/routePath",
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lastmod datetime option', async () => {
|
||||||
|
await expect(
|
||||||
|
test({
|
||||||
|
route,
|
||||||
|
options: {
|
||||||
|
lastmod: 'datetime',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"changefreq": "weekly",
|
||||||
|
"lastmod": "2024-01-01T00:00:00.000Z",
|
||||||
|
"priority": 0.5,
|
||||||
|
"url": "https://example.com/routePath",
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('read from git', () => {
|
||||||
|
const route = {
|
||||||
|
path: '/routePath',
|
||||||
|
metadata: {sourceFilePath: 'route/file.md'},
|
||||||
|
};
|
||||||
|
|
||||||
|
it('lastmod default option', async () => {
|
||||||
|
await expect(
|
||||||
|
test({
|
||||||
|
route,
|
||||||
|
}),
|
||||||
|
).resolves.toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"changefreq": "weekly",
|
||||||
|
"lastmod": null,
|
||||||
|
"priority": 0.5,
|
||||||
|
"url": "https://example.com/routePath",
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lastmod date option', async () => {
|
||||||
|
await expect(
|
||||||
|
test({
|
||||||
|
route,
|
||||||
|
options: {
|
||||||
|
lastmod: 'date',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"changefreq": "weekly",
|
||||||
|
"lastmod": "2018-10-14",
|
||||||
|
"priority": 0.5,
|
||||||
|
"url": "https://example.com/routePath",
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lastmod datetime option', async () => {
|
||||||
|
await expect(
|
||||||
|
test({
|
||||||
|
route,
|
||||||
|
options: {
|
||||||
|
lastmod: 'datetime',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"changefreq": "weekly",
|
||||||
|
"lastmod": "2018-10-14T07:27:35.000Z",
|
||||||
|
"priority": 0.5,
|
||||||
|
"url": "https://example.com/routePath",
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('read from both - route metadata takes precedence', () => {
|
||||||
|
const route = {
|
||||||
|
path: '/routePath',
|
||||||
|
metadata: {
|
||||||
|
sourceFilePath: 'route/file.md',
|
||||||
|
lastUpdatedAt: date.getTime(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
it('lastmod default option', async () => {
|
||||||
|
await expect(
|
||||||
|
test({
|
||||||
|
route,
|
||||||
|
}),
|
||||||
|
).resolves.toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"changefreq": "weekly",
|
||||||
|
"lastmod": null,
|
||||||
|
"priority": 0.5,
|
||||||
|
"url": "https://example.com/routePath",
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lastmod date option', async () => {
|
||||||
|
await expect(
|
||||||
|
test({
|
||||||
|
route,
|
||||||
|
options: {
|
||||||
|
lastmod: 'date',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"changefreq": "weekly",
|
||||||
|
"lastmod": "2024-01-01",
|
||||||
|
"priority": 0.5,
|
||||||
|
"url": "https://example.com/routePath",
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lastmod datetime option', async () => {
|
||||||
|
await expect(
|
||||||
|
test({
|
||||||
|
route,
|
||||||
|
options: {
|
||||||
|
lastmod: 'datetime',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"changefreq": "weekly",
|
||||||
|
"lastmod": "2024-01-01T00:00:00.000Z",
|
||||||
|
"priority": 0.5,
|
||||||
|
"url": "https://example.com/routePath",
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -12,7 +12,6 @@ import {
|
||||||
type Options,
|
type Options,
|
||||||
type PluginOptions,
|
type PluginOptions,
|
||||||
} from '../options';
|
} from '../options';
|
||||||
import type {EnumChangefreq} from 'sitemap';
|
|
||||||
import type {Validate} from '@docusaurus/types';
|
import type {Validate} from '@docusaurus/types';
|
||||||
|
|
||||||
function testValidate(options: Options) {
|
function testValidate(options: Options) {
|
||||||
|
@ -34,9 +33,10 @@ describe('validateOptions', () => {
|
||||||
|
|
||||||
it('accepts correctly defined user options', () => {
|
it('accepts correctly defined user options', () => {
|
||||||
const userOptions: Options = {
|
const userOptions: Options = {
|
||||||
changefreq: 'yearly' as EnumChangefreq,
|
changefreq: 'yearly',
|
||||||
priority: 0.9,
|
priority: 0.9,
|
||||||
ignorePatterns: ['/search/**'],
|
ignorePatterns: ['/search/**'],
|
||||||
|
lastmod: 'datetime',
|
||||||
};
|
};
|
||||||
expect(testValidate(userOptions)).toEqual({
|
expect(testValidate(userOptions)).toEqual({
|
||||||
...defaultOptions,
|
...defaultOptions,
|
||||||
|
@ -44,32 +44,209 @@ describe('validateOptions', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('rejects out-of-range priority inputs', () => {
|
describe('lastmod', () => {
|
||||||
expect(() =>
|
it('accepts lastmod undefined', () => {
|
||||||
testValidate({priority: 2}),
|
const userOptions: Options = {
|
||||||
).toThrowErrorMatchingInlineSnapshot(
|
lastmod: undefined,
|
||||||
`""priority" must be less than or equal to 1"`,
|
};
|
||||||
);
|
expect(testValidate(userOptions)).toEqual(defaultOptions);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts lastmod null', () => {
|
||||||
|
const userOptions: Options = {
|
||||||
|
lastmod: null,
|
||||||
|
};
|
||||||
|
expect(testValidate(userOptions)).toEqual({
|
||||||
|
...defaultOptions,
|
||||||
|
...userOptions,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts lastmod datetime', () => {
|
||||||
|
const userOptions: Options = {
|
||||||
|
lastmod: 'datetime',
|
||||||
|
};
|
||||||
|
expect(testValidate(userOptions)).toEqual({
|
||||||
|
...defaultOptions,
|
||||||
|
...userOptions,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects lastmod bad input', () => {
|
||||||
|
const userOptions: Options = {
|
||||||
|
// @ts-expect-error: bad value on purpose
|
||||||
|
lastmod: 'dateTimeZone',
|
||||||
|
};
|
||||||
|
expect(() =>
|
||||||
|
testValidate(userOptions),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`""lastmod" must be one of [null, date, datetime]"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('rejects bad changefreq inputs', () => {
|
describe('priority', () => {
|
||||||
expect(() =>
|
it('accepts priority undefined', () => {
|
||||||
testValidate({changefreq: 'annually' as EnumChangefreq}),
|
const userOptions: Options = {
|
||||||
).toThrowErrorMatchingInlineSnapshot(
|
priority: undefined,
|
||||||
`""changefreq" must be one of [daily, monthly, always, hourly, weekly, yearly, never]"`,
|
};
|
||||||
);
|
expect(testValidate(userOptions)).toEqual(defaultOptions);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts priority null', () => {
|
||||||
|
const userOptions: Options = {
|
||||||
|
priority: null,
|
||||||
|
};
|
||||||
|
expect(testValidate(userOptions)).toEqual({
|
||||||
|
...defaultOptions,
|
||||||
|
...userOptions,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts priority 0', () => {
|
||||||
|
const userOptions: Options = {
|
||||||
|
priority: 0,
|
||||||
|
};
|
||||||
|
expect(testValidate(userOptions)).toEqual({
|
||||||
|
...defaultOptions,
|
||||||
|
...userOptions,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts priority 0.4', () => {
|
||||||
|
const userOptions: Options = {
|
||||||
|
priority: 0.4,
|
||||||
|
};
|
||||||
|
expect(testValidate(userOptions)).toEqual({
|
||||||
|
...defaultOptions,
|
||||||
|
...userOptions,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts priority 1', () => {
|
||||||
|
const userOptions: Options = {
|
||||||
|
priority: 1,
|
||||||
|
};
|
||||||
|
expect(testValidate(userOptions)).toEqual({
|
||||||
|
...defaultOptions,
|
||||||
|
...userOptions,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects priority > 1', () => {
|
||||||
|
const userOptions: Options = {
|
||||||
|
priority: 2,
|
||||||
|
};
|
||||||
|
expect(() =>
|
||||||
|
testValidate(userOptions),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`""priority" must be less than or equal to 1"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects priority < 0', () => {
|
||||||
|
const userOptions: Options = {
|
||||||
|
priority: -3,
|
||||||
|
};
|
||||||
|
expect(() =>
|
||||||
|
testValidate(userOptions),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`""priority" must be greater than or equal to 0"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('rejects bad ignorePatterns inputs', () => {
|
describe('changefreq', () => {
|
||||||
expect(() =>
|
it('accepts changefreq undefined', () => {
|
||||||
// @ts-expect-error: test
|
const userOptions: Options = {
|
||||||
testValidate({ignorePatterns: '/search'}),
|
changefreq: undefined,
|
||||||
).toThrowErrorMatchingInlineSnapshot(`""ignorePatterns" must be an array"`);
|
};
|
||||||
expect(() =>
|
expect(testValidate(userOptions)).toEqual(defaultOptions);
|
||||||
// @ts-expect-error: test
|
});
|
||||||
testValidate({ignorePatterns: [/^\/search/]}),
|
|
||||||
).toThrowErrorMatchingInlineSnapshot(
|
it('accepts changefreq null', () => {
|
||||||
`""ignorePatterns[0]" must be a string"`,
|
const userOptions: Options = {
|
||||||
);
|
changefreq: null,
|
||||||
|
};
|
||||||
|
expect(testValidate(userOptions)).toEqual({
|
||||||
|
...defaultOptions,
|
||||||
|
...userOptions,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts changefreq always', () => {
|
||||||
|
const userOptions: Options = {
|
||||||
|
changefreq: 'always',
|
||||||
|
};
|
||||||
|
expect(testValidate(userOptions)).toEqual({
|
||||||
|
...defaultOptions,
|
||||||
|
...userOptions,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects changefreq bad inputs', () => {
|
||||||
|
const userOptions: Options = {
|
||||||
|
// @ts-expect-error: bad value on purpose
|
||||||
|
changefreq: 'annually',
|
||||||
|
};
|
||||||
|
expect(() =>
|
||||||
|
testValidate(userOptions),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`""changefreq" must be one of [null, hourly, daily, weekly, monthly, yearly, always, never]"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ignorePatterns', () => {
|
||||||
|
it('accept ignorePatterns undefined', () => {
|
||||||
|
const userOptions: Options = {
|
||||||
|
ignorePatterns: undefined,
|
||||||
|
};
|
||||||
|
expect(testValidate(userOptions)).toEqual(defaultOptions);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accept ignorePatterns empty', () => {
|
||||||
|
const userOptions: Options = {
|
||||||
|
ignorePatterns: [],
|
||||||
|
};
|
||||||
|
expect(testValidate(userOptions)).toEqual({
|
||||||
|
...defaultOptions,
|
||||||
|
...userOptions,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accept ignorePatterns valid', () => {
|
||||||
|
const userOptions: Options = {
|
||||||
|
ignorePatterns: ['/tags/**'],
|
||||||
|
};
|
||||||
|
expect(testValidate(userOptions)).toEqual({
|
||||||
|
...defaultOptions,
|
||||||
|
...userOptions,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects ignorePatterns bad input array', () => {
|
||||||
|
const userOptions: Options = {
|
||||||
|
// @ts-expect-error: test
|
||||||
|
ignorePatterns: '/search',
|
||||||
|
};
|
||||||
|
expect(() =>
|
||||||
|
testValidate(userOptions),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`""ignorePatterns" must be an array"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects ignorePatterns bad input item string', () => {
|
||||||
|
const userOptions: Options = {
|
||||||
|
// @ts-expect-error: test
|
||||||
|
ignorePatterns: [/^\/search/],
|
||||||
|
};
|
||||||
|
expect(() =>
|
||||||
|
testValidate(userOptions),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`""ignorePatterns[0]" must be a string"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
67
packages/docusaurus-plugin-sitemap/src/__tests__/xml.test.ts
Normal file
67
packages/docusaurus-plugin-sitemap/src/__tests__/xml.test.ts
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {sitemapItemsToXmlString} from '../xml';
|
||||||
|
import type {SitemapItem} from '../types';
|
||||||
|
|
||||||
|
const options = {lastmod: 'datetime'} as const;
|
||||||
|
|
||||||
|
describe('createSitemap', () => {
|
||||||
|
it('no items', async () => {
|
||||||
|
const items: SitemapItem[] = [];
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sitemapItemsToXmlString(items, options),
|
||||||
|
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"Can't generate a sitemap with no items"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('simple item', async () => {
|
||||||
|
const items: SitemapItem[] = [{url: 'https://docusaurus.io/docs/doc1'}];
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sitemapItemsToXmlString(items, options),
|
||||||
|
).resolves.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://docusaurus.io/docs/doc1</loc></url></urlset>"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('complex item', async () => {
|
||||||
|
const items: SitemapItem[] = [
|
||||||
|
{
|
||||||
|
url: 'https://docusaurus.io/docs/doc1',
|
||||||
|
changefreq: 'always',
|
||||||
|
priority: 1,
|
||||||
|
lastmod: new Date('01/01/2024').toISOString(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sitemapItemsToXmlString(items, options),
|
||||||
|
).resolves.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://docusaurus.io/docs/doc1</loc><lastmod>2024-01-01T00:00:00.000Z</lastmod><changefreq>always</changefreq><priority>1.0</priority></url></urlset>"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('date only lastmod', async () => {
|
||||||
|
const items: SitemapItem[] = [
|
||||||
|
{
|
||||||
|
url: 'https://docusaurus.io/docs/doc1',
|
||||||
|
changefreq: 'always',
|
||||||
|
priority: 1,
|
||||||
|
lastmod: new Date('01/01/2024').toISOString(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sitemapItemsToXmlString(items, {lastmod: 'date'}),
|
||||||
|
).resolves.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://docusaurus.io/docs/doc1</loc><lastmod>2024-01-01</lastmod><changefreq>always</changefreq><priority>1.0</priority></url></urlset>"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -6,13 +6,23 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {ReactElement} from 'react';
|
import type {ReactElement} from 'react';
|
||||||
import {SitemapStream, streamToPromise} from 'sitemap';
|
import {createMatcher, flattenRoutes} from '@docusaurus/utils';
|
||||||
import {applyTrailingSlash} from '@docusaurus/utils-common';
|
import {sitemapItemsToXmlString} from './xml';
|
||||||
import {createMatcher} from '@docusaurus/utils';
|
import {createSitemapItem} from './createSitemapItem';
|
||||||
import type {DocusaurusConfig} from '@docusaurus/types';
|
import type {SitemapItem} from './types';
|
||||||
|
import type {DocusaurusConfig, RouteConfig} 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';
|
||||||
|
|
||||||
|
type CreateSitemapParams = {
|
||||||
|
siteConfig: DocusaurusConfig;
|
||||||
|
routes: RouteConfig[];
|
||||||
|
head: {[location: string]: HelmetServerState};
|
||||||
|
options: PluginOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Maybe we want to add a routeConfig.metadata.noIndex instead?
|
||||||
|
// But using Helmet is more reliable for third-party plugins...
|
||||||
function isNoIndexMetaRoute({
|
function isNoIndexMetaRoute({
|
||||||
head,
|
head,
|
||||||
route,
|
route,
|
||||||
|
@ -47,50 +57,51 @@ function isNoIndexMetaRoute({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function createSitemap(
|
// Not all routes should appear in the sitemap, and we should filter:
|
||||||
siteConfig: DocusaurusConfig,
|
// - parent routes, used for layouts
|
||||||
routesPaths: string[],
|
// - routes matching options.ignorePatterns
|
||||||
head: {[location: string]: HelmetServerState},
|
// - routes with no index metadata
|
||||||
options: PluginOptions,
|
function getSitemapRoutes({routes, head, options}: CreateSitemapParams) {
|
||||||
): Promise<string | null> {
|
const {ignorePatterns} = options;
|
||||||
const {url: hostname} = siteConfig;
|
|
||||||
if (!hostname) {
|
|
||||||
throw new Error('URL in docusaurus.config.js cannot be empty/undefined.');
|
|
||||||
}
|
|
||||||
const {changefreq, priority, ignorePatterns} = options;
|
|
||||||
|
|
||||||
const ignoreMatcher = createMatcher(ignorePatterns);
|
const ignoreMatcher = createMatcher(ignorePatterns);
|
||||||
|
|
||||||
function isRouteExcluded(route: string) {
|
function isRouteExcluded(route: RouteConfig) {
|
||||||
return (
|
return (
|
||||||
route.endsWith('404.html') ||
|
ignoreMatcher(route.path) || isNoIndexMetaRoute({head, route: route.path})
|
||||||
ignoreMatcher(route) ||
|
|
||||||
isNoIndexMetaRoute({head, route})
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const includedRoutes = routesPaths.filter((route) => !isRouteExcluded(route));
|
return flattenRoutes(routes).filter((route) => !isRouteExcluded(route));
|
||||||
|
}
|
||||||
|
|
||||||
if (includedRoutes.length === 0) {
|
async function createSitemapItems(
|
||||||
|
params: CreateSitemapParams,
|
||||||
|
): Promise<SitemapItem[]> {
|
||||||
|
const sitemapRoutes = getSitemapRoutes(params);
|
||||||
|
if (sitemapRoutes.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return Promise.all(
|
||||||
|
sitemapRoutes.map((route) =>
|
||||||
|
createSitemapItem({
|
||||||
|
route,
|
||||||
|
siteConfig: params.siteConfig,
|
||||||
|
options: params.options,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function createSitemap(
|
||||||
|
params: CreateSitemapParams,
|
||||||
|
): Promise<string | null> {
|
||||||
|
const items = await createSitemapItems(params);
|
||||||
|
if (items.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
const xmlString = await sitemapItemsToXmlString(items, {
|
||||||
const sitemapStream = new SitemapStream({hostname});
|
lastmod: params.options.lastmod,
|
||||||
|
});
|
||||||
includedRoutes.forEach((routePath) =>
|
return xmlString;
|
||||||
sitemapStream.write({
|
|
||||||
url: applyTrailingSlash(routePath, {
|
|
||||||
trailingSlash: siteConfig.trailingSlash,
|
|
||||||
baseUrl: siteConfig.baseUrl,
|
|
||||||
}),
|
|
||||||
changefreq,
|
|
||||||
priority,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
sitemapStream.end();
|
|
||||||
|
|
||||||
const generatedSitemap = (await streamToPromise(sitemapStream)).toString();
|
|
||||||
|
|
||||||
return generatedSitemap;
|
|
||||||
}
|
}
|
||||||
|
|
76
packages/docusaurus-plugin-sitemap/src/createSitemapItem.ts
Normal file
76
packages/docusaurus-plugin-sitemap/src/createSitemapItem.ts
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {applyTrailingSlash} from '@docusaurus/utils-common';
|
||||||
|
import {getLastUpdate, normalizeUrl} from '@docusaurus/utils';
|
||||||
|
import type {LastModOption, SitemapItem} from './types';
|
||||||
|
import type {DocusaurusConfig, RouteConfig} from '@docusaurus/types';
|
||||||
|
import type {PluginOptions} from './options';
|
||||||
|
|
||||||
|
async function getRouteLastUpdatedAt(
|
||||||
|
route: RouteConfig,
|
||||||
|
): Promise<number | undefined> {
|
||||||
|
if (route.metadata?.lastUpdatedAt) {
|
||||||
|
return route.metadata?.lastUpdatedAt;
|
||||||
|
}
|
||||||
|
if (route.metadata?.sourceFilePath) {
|
||||||
|
const lastUpdate = await getLastUpdate(route.metadata?.sourceFilePath);
|
||||||
|
return lastUpdate?.lastUpdatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
type LastModFormatter = (timestamp: number) => string;
|
||||||
|
|
||||||
|
const LastmodFormatters: Record<LastModOption, LastModFormatter> = {
|
||||||
|
date: (timestamp) => new Date(timestamp).toISOString().split('T')[0]!,
|
||||||
|
datetime: (timestamp) => new Date(timestamp).toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
function formatLastmod(timestamp: number, lastmodOption: LastModOption) {
|
||||||
|
const format = LastmodFormatters[lastmodOption];
|
||||||
|
return format(timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getRouteLastmod({
|
||||||
|
route,
|
||||||
|
lastmod,
|
||||||
|
}: {
|
||||||
|
route: RouteConfig;
|
||||||
|
lastmod: LastModOption | null;
|
||||||
|
}): Promise<string | null> {
|
||||||
|
if (lastmod === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const lastUpdatedAt = (await getRouteLastUpdatedAt(route)) ?? null;
|
||||||
|
return lastUpdatedAt ? formatLastmod(lastUpdatedAt, lastmod) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createSitemapItem({
|
||||||
|
route,
|
||||||
|
siteConfig,
|
||||||
|
options,
|
||||||
|
}: {
|
||||||
|
route: RouteConfig;
|
||||||
|
siteConfig: DocusaurusConfig;
|
||||||
|
options: PluginOptions;
|
||||||
|
}): Promise<SitemapItem> {
|
||||||
|
const {changefreq, priority, lastmod} = options;
|
||||||
|
return {
|
||||||
|
url: normalizeUrl([
|
||||||
|
siteConfig.url,
|
||||||
|
applyTrailingSlash(route.path, {
|
||||||
|
trailingSlash: siteConfig.trailingSlash,
|
||||||
|
baseUrl: siteConfig.baseUrl,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
changefreq,
|
||||||
|
priority,
|
||||||
|
lastmod: await getRouteLastmod({route, lastmod}),
|
||||||
|
};
|
||||||
|
}
|
|
@ -19,17 +19,17 @@ export default function pluginSitemap(
|
||||||
return {
|
return {
|
||||||
name: 'docusaurus-plugin-sitemap',
|
name: 'docusaurus-plugin-sitemap',
|
||||||
|
|
||||||
async postBuild({siteConfig, routesPaths, outDir, head}) {
|
async postBuild({siteConfig, routes, outDir, head}) {
|
||||||
if (siteConfig.noIndex) {
|
if (siteConfig.noIndex) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Generate sitemap.
|
// Generate sitemap.
|
||||||
const generatedSitemap = await createSitemap(
|
const generatedSitemap = await createSitemap({
|
||||||
siteConfig,
|
siteConfig,
|
||||||
routesPaths,
|
routes,
|
||||||
head,
|
head,
|
||||||
options,
|
options,
|
||||||
);
|
});
|
||||||
if (!generatedSitemap) {
|
if (!generatedSitemap) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,33 +6,60 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {Joi} from '@docusaurus/utils-validation';
|
import {Joi} from '@docusaurus/utils-validation';
|
||||||
import {EnumChangefreq} from 'sitemap';
|
import {ChangeFreqList, LastModOptionList} from './types';
|
||||||
import type {OptionValidationContext} from '@docusaurus/types';
|
import type {OptionValidationContext} from '@docusaurus/types';
|
||||||
|
import type {ChangeFreq, LastModOption} from './types';
|
||||||
|
|
||||||
export type PluginOptions = {
|
export type PluginOptions = {
|
||||||
/** @see https://www.sitemaps.org/protocol.html#xmlTagDefinitions */
|
|
||||||
changefreq: EnumChangefreq;
|
|
||||||
/** @see https://www.sitemaps.org/protocol.html#xmlTagDefinitions */
|
|
||||||
priority: number;
|
|
||||||
/**
|
|
||||||
* A list of glob patterns; matching route paths will be filtered from the
|
|
||||||
* sitemap. Note that you may need to include the base URL in here.
|
|
||||||
*/
|
|
||||||
ignorePatterns: string[];
|
|
||||||
/**
|
/**
|
||||||
* The path to the created sitemap file, relative to the output directory.
|
* The path to the created sitemap file, relative to the output directory.
|
||||||
* Useful if you have two plugin instances outputting two files.
|
* Useful if you have two plugin instances outputting two files.
|
||||||
*/
|
*/
|
||||||
filename: string;
|
filename: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of glob patterns; matching route paths will be filtered from the
|
||||||
|
* sitemap. Note that you may need to include the base URL in here.
|
||||||
|
*/
|
||||||
|
ignorePatterns: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the format of the "lastmod" sitemap item entry, between:
|
||||||
|
* - null: do not compute/add a "lastmod" sitemap entry
|
||||||
|
* - "date": add a "lastmod" sitemap entry without time (YYYY-MM-DD)
|
||||||
|
* - "datetime": add a "lastmod" sitemap entry with time (ISO 8601 datetime)
|
||||||
|
* @see https://www.sitemaps.org/protocol.html#xmlTagDefinitions
|
||||||
|
* @see https://www.w3.org/TR/NOTE-datetime
|
||||||
|
*/
|
||||||
|
lastmod: LastModOption | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO Docusaurus v4 breaking change: remove useless option
|
||||||
|
* @see https://www.sitemaps.org/protocol.html#xmlTagDefinitions
|
||||||
|
*/
|
||||||
|
changefreq: ChangeFreq | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO Docusaurus v4 breaking change: remove useless option
|
||||||
|
* @see https://www.sitemaps.org/protocol.html#xmlTagDefinitions
|
||||||
|
*/
|
||||||
|
priority: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Options = Partial<PluginOptions>;
|
export type Options = Partial<PluginOptions>;
|
||||||
|
|
||||||
export const DEFAULT_OPTIONS: PluginOptions = {
|
export const DEFAULT_OPTIONS: PluginOptions = {
|
||||||
changefreq: EnumChangefreq.WEEKLY,
|
|
||||||
priority: 0.5,
|
|
||||||
ignorePatterns: [],
|
|
||||||
filename: 'sitemap.xml',
|
filename: 'sitemap.xml',
|
||||||
|
ignorePatterns: [],
|
||||||
|
|
||||||
|
// TODO Docusaurus v4 breaking change
|
||||||
|
// change default to "date" if no bug or perf issue reported
|
||||||
|
lastmod: null,
|
||||||
|
|
||||||
|
// TODO Docusaurus v4 breaking change
|
||||||
|
// those options are useless and should be removed
|
||||||
|
changefreq: 'weekly',
|
||||||
|
priority: 0.5,
|
||||||
};
|
};
|
||||||
|
|
||||||
const PluginOptionSchema = Joi.object<PluginOptions>({
|
const PluginOptionSchema = Joi.object<PluginOptions>({
|
||||||
|
@ -41,10 +68,28 @@ const PluginOptionSchema = Joi.object<PluginOptions>({
|
||||||
'any.unknown':
|
'any.unknown':
|
||||||
'Option `cacheTime` in sitemap config is deprecated. Please remove it.',
|
'Option `cacheTime` in sitemap config is deprecated. Please remove it.',
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
// TODO remove for Docusaurus v4 breaking changes?
|
||||||
|
// This is not even used by Google crawlers
|
||||||
|
// See also https://github.com/facebook/docusaurus/issues/2604
|
||||||
changefreq: Joi.string()
|
changefreq: Joi.string()
|
||||||
.valid(...Object.values(EnumChangefreq))
|
.valid(null, ...ChangeFreqList)
|
||||||
.default(DEFAULT_OPTIONS.changefreq),
|
.default(DEFAULT_OPTIONS.changefreq),
|
||||||
priority: Joi.number().min(0).max(1).default(DEFAULT_OPTIONS.priority),
|
|
||||||
|
// TODO remove for Docusaurus v4 breaking changes?
|
||||||
|
// This is not even used by Google crawlers
|
||||||
|
// The priority is "relative", and using the same priority for all routes
|
||||||
|
// does not make sense according to the spec
|
||||||
|
// See also https://github.com/facebook/docusaurus/issues/2604
|
||||||
|
// See also https://www.sitemaps.org/protocol.html
|
||||||
|
priority: Joi.alternatives()
|
||||||
|
.try(Joi.valid(null), Joi.number().min(0).max(1))
|
||||||
|
.default(DEFAULT_OPTIONS.priority),
|
||||||
|
|
||||||
|
lastmod: Joi.string()
|
||||||
|
.valid(null, ...LastModOptionList)
|
||||||
|
.default(DEFAULT_OPTIONS.lastmod),
|
||||||
|
|
||||||
ignorePatterns: Joi.array()
|
ignorePatterns: Joi.array()
|
||||||
.items(Joi.string())
|
.items(Joi.string())
|
||||||
.default(DEFAULT_OPTIONS.ignorePatterns),
|
.default(DEFAULT_OPTIONS.ignorePatterns),
|
||||||
|
|
67
packages/docusaurus-plugin-sitemap/src/types.ts
Normal file
67
packages/docusaurus-plugin-sitemap/src/types.ts
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const LastModOptionList = ['date', 'datetime'] as const;
|
||||||
|
|
||||||
|
export type LastModOption = (typeof LastModOptionList)[number];
|
||||||
|
|
||||||
|
// types are according to the sitemap spec:
|
||||||
|
// see also https://www.sitemaps.org/protocol.html
|
||||||
|
|
||||||
|
export const ChangeFreqList = [
|
||||||
|
'hourly',
|
||||||
|
'daily',
|
||||||
|
'weekly',
|
||||||
|
'monthly',
|
||||||
|
'yearly',
|
||||||
|
'always',
|
||||||
|
'never',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type ChangeFreq = (typeof ChangeFreqList)[number];
|
||||||
|
|
||||||
|
// We re-recreate our own type because the "sitemap" lib types are not good
|
||||||
|
export type SitemapItem = {
|
||||||
|
/**
|
||||||
|
* URL of the page.
|
||||||
|
* This URL must begin with the protocol (such as http).
|
||||||
|
* It should eventually end with a trailing slash.
|
||||||
|
* It should be less than 2,048 characters.
|
||||||
|
*/
|
||||||
|
url: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ISO 8601 date string.
|
||||||
|
* See also https://www.w3.org/TR/NOTE-datetime
|
||||||
|
*
|
||||||
|
* It is recommended to use one of:
|
||||||
|
* - date.toISOString()
|
||||||
|
* - YYYY-MM-DD
|
||||||
|
*
|
||||||
|
* Note: as of 2024, Google uses this value for crawling priority.
|
||||||
|
* See also https://github.com/facebook/docusaurus/issues/2604
|
||||||
|
*/
|
||||||
|
lastmod?: string | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One of the specified enum values
|
||||||
|
*
|
||||||
|
* Note: as of 2024, Google ignores this value.
|
||||||
|
* See also https://github.com/facebook/docusaurus/issues/2604
|
||||||
|
*/
|
||||||
|
changefreq?: ChangeFreq | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The priority of this URL relative to other URLs on your site.
|
||||||
|
* Valid values range from 0.0 to 1.0.
|
||||||
|
* The default priority of a page is 0.5.
|
||||||
|
*
|
||||||
|
* Note: as of 2024, Google ignores this value.
|
||||||
|
* See also https://github.com/facebook/docusaurus/issues/2604
|
||||||
|
*/
|
||||||
|
priority?: number | null;
|
||||||
|
};
|
35
packages/docusaurus-plugin-sitemap/src/xml.ts
Normal file
35
packages/docusaurus-plugin-sitemap/src/xml.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {SitemapStream, streamToPromise} from 'sitemap';
|
||||||
|
import type {LastModOption, SitemapItem} from './types';
|
||||||
|
|
||||||
|
export async function sitemapItemsToXmlString(
|
||||||
|
items: SitemapItem[],
|
||||||
|
options: {lastmod: LastModOption | null},
|
||||||
|
): Promise<string> {
|
||||||
|
if (items.length === 0) {
|
||||||
|
// Note: technically we could, but there is a bug in the lib code
|
||||||
|
// and the code below would never resolve, so it's better to fail fast
|
||||||
|
throw new Error("Can't generate a sitemap with no items");
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO remove sitemap lib dependency?
|
||||||
|
// https://github.com/ekalinin/sitemap.js
|
||||||
|
// it looks like an outdated confusion super old lib
|
||||||
|
// we might as well achieve the same result with a pure xml lib
|
||||||
|
const sitemapStream = new SitemapStream({
|
||||||
|
// WTF is this lib reformatting the string YYYY-MM-DD to datetime...
|
||||||
|
lastmodDateOnly: options?.lastmod === 'date',
|
||||||
|
});
|
||||||
|
|
||||||
|
items.forEach((item) => sitemapStream.write(item));
|
||||||
|
sitemapStream.end();
|
||||||
|
|
||||||
|
const buffer = await streamToPromise(sitemapStream);
|
||||||
|
return buffer.toString();
|
||||||
|
}
|
2
packages/docusaurus-types/src/index.d.ts
vendored
2
packages/docusaurus-types/src/index.d.ts
vendored
|
@ -45,6 +45,7 @@ export {
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Plugin,
|
Plugin,
|
||||||
|
PluginIdentifier,
|
||||||
InitializedPlugin,
|
InitializedPlugin,
|
||||||
LoadedPlugin,
|
LoadedPlugin,
|
||||||
PluginModule,
|
PluginModule,
|
||||||
|
@ -69,6 +70,7 @@ export {
|
||||||
|
|
||||||
export {
|
export {
|
||||||
RouteConfig,
|
RouteConfig,
|
||||||
|
RouteMetadata,
|
||||||
RouteContext,
|
RouteContext,
|
||||||
PluginRouteContext,
|
PluginRouteContext,
|
||||||
Registry,
|
Registry,
|
||||||
|
|
58
packages/docusaurus-types/src/routing.d.ts
vendored
58
packages/docusaurus-types/src/routing.d.ts
vendored
|
@ -11,7 +11,7 @@ import type {ParsedUrlQueryInput} from 'querystring';
|
||||||
* A "module" represents a unit of serialized data emitted from the plugin. It
|
* A "module" represents a unit of serialized data emitted from the plugin. It
|
||||||
* will be imported on client-side and passed as props, context, etc.
|
* will be imported on client-side and passed as props, context, etc.
|
||||||
*
|
*
|
||||||
* If it's a string, it's a file path that Webpack can `require`; if it's
|
* If it's a string, it's a file path that the bundler can `require`; if it's
|
||||||
* an object, it can also contain `query` or other metadata.
|
* an object, it can also contain `query` or other metadata.
|
||||||
*/
|
*/
|
||||||
export type Module =
|
export type Module =
|
||||||
|
@ -36,14 +36,45 @@ export type RouteModules = {
|
||||||
[propName: string]: Module | RouteModules | RouteModules[];
|
[propName: string]: Module | RouteModules | RouteModules[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin authors can assign extra metadata to the created routes
|
||||||
|
* It is only available on the Node.js side, and not sent to the browser
|
||||||
|
* Optional: plugin authors are encouraged but not required to provide it
|
||||||
|
*
|
||||||
|
* Some plugins might use this data to provide additional features.
|
||||||
|
* This is the case of the sitemap plugin to provide support for "lastmod".
|
||||||
|
* See also: https://github.com/facebook/docusaurus/pull/9954
|
||||||
|
*/
|
||||||
|
export type RouteMetadata = {
|
||||||
|
/**
|
||||||
|
* The source code file path that led to the creation of the current route
|
||||||
|
* In official content plugins, this is usually a Markdown or React file
|
||||||
|
* This path is expected to be relative to the site directory
|
||||||
|
*/
|
||||||
|
sourceFilePath?: string;
|
||||||
|
/**
|
||||||
|
* The last updated date of this route
|
||||||
|
* This is generally read from the Git history of the sourceFilePath
|
||||||
|
* but can also be provided through other means (usually front matter)
|
||||||
|
*
|
||||||
|
* This has notably been introduced for adding "lastmod" support to the
|
||||||
|
* sitemap plugin, see https://github.com/facebook/docusaurus/pull/9954
|
||||||
|
*/
|
||||||
|
lastUpdatedAt?: number;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a "slice" of the final route structure returned from the plugin
|
* Represents a "slice" of the final route structure returned from the plugin
|
||||||
* `addRoute` action.
|
* `addRoute` action.
|
||||||
*/
|
*/
|
||||||
export type RouteConfig = {
|
export type RouteConfig = {
|
||||||
/** With leading slash. Trailing slash will be normalized by config. */
|
/**
|
||||||
|
* With leading slash. Trailing slash will be normalized by config.
|
||||||
|
*/
|
||||||
path: string;
|
path: string;
|
||||||
/** Component used to render this route, a path that Webpack can `require`. */
|
/**
|
||||||
|
* Component used to render this route, a path that the bundler can `require`.
|
||||||
|
*/
|
||||||
component: string;
|
component: string;
|
||||||
/**
|
/**
|
||||||
* Props. Each entry should be `[propName]: pathToPropModule` (created with
|
* Props. Each entry should be `[propName]: pathToPropModule` (created with
|
||||||
|
@ -56,18 +87,31 @@ export type RouteConfig = {
|
||||||
* here will be namespaced under {@link RouteContext.data}.
|
* here will be namespaced under {@link RouteContext.data}.
|
||||||
*/
|
*/
|
||||||
context?: RouteModules;
|
context?: RouteModules;
|
||||||
/** Nested routes config. */
|
/**
|
||||||
|
* Nested routes config, useful for "layout routes" having subroutes.
|
||||||
|
*/
|
||||||
routes?: RouteConfig[];
|
routes?: RouteConfig[];
|
||||||
/** React router config option: `exact` routes would not match subroutes. */
|
/**
|
||||||
|
* React router config option: `exact` routes would not match subroutes.
|
||||||
|
*/
|
||||||
exact?: boolean;
|
exact?: boolean;
|
||||||
/**
|
/**
|
||||||
* React router config option: `strict` routes are sensitive to the presence
|
* React router config option: `strict` routes are sensitive to the presence
|
||||||
* of a trailing slash.
|
* of a trailing slash.
|
||||||
*/
|
*/
|
||||||
strict?: boolean;
|
strict?: boolean;
|
||||||
/** Used to sort routes. Higher-priority routes will be placed first. */
|
/**
|
||||||
|
* Used to sort routes.
|
||||||
|
* Higher-priority routes will be matched first.
|
||||||
|
*/
|
||||||
priority?: number;
|
priority?: number;
|
||||||
/** Extra props; will be copied to routes.js. */
|
/**
|
||||||
|
* Optional route metadata
|
||||||
|
*/
|
||||||
|
metadata?: RouteMetadata;
|
||||||
|
/**
|
||||||
|
* Extra props; will be available on the client side.
|
||||||
|
*/
|
||||||
[propName: string]: unknown;
|
[propName: string]: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ import fs from 'fs-extra';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import {createTempRepo} from '@testing-utils/git';
|
import {createTempRepo} from '@testing-utils/git';
|
||||||
import {FileNotTrackedError, getFileCommitDate} from '../gitUtils';
|
import {FileNotTrackedError, getFileCommitDate} from '../gitUtils';
|
||||||
import {getFileLastUpdate} from '../lastUpdateUtils';
|
import {getGitLastUpdate} from '../lastUpdateUtils';
|
||||||
|
|
||||||
/* eslint-disable no-restricted-properties */
|
/* eslint-disable no-restricted-properties */
|
||||||
function initializeTempRepo() {
|
function initializeTempRepo() {
|
||||||
|
@ -146,8 +146,9 @@ describe('getFileCommitDate', () => {
|
||||||
const tempFilePath2 = path.join(repoDir, 'file2.md');
|
const tempFilePath2 = path.join(repoDir, 'file2.md');
|
||||||
await fs.writeFile(tempFilePath1, 'Lorem ipsum :)');
|
await fs.writeFile(tempFilePath1, 'Lorem ipsum :)');
|
||||||
await fs.writeFile(tempFilePath2, 'Lorem ipsum :)');
|
await fs.writeFile(tempFilePath2, 'Lorem ipsum :)');
|
||||||
await expect(getFileLastUpdate(tempFilePath1)).resolves.toBeNull();
|
// TODO this is not the correct place to test "getGitLastUpdate"
|
||||||
await expect(getFileLastUpdate(tempFilePath2)).resolves.toBeNull();
|
await expect(getGitLastUpdate(tempFilePath1)).resolves.toBeNull();
|
||||||
|
await expect(getGitLastUpdate(tempFilePath2)).resolves.toBeNull();
|
||||||
expect(consoleMock).toHaveBeenCalledTimes(1);
|
expect(consoleMock).toHaveBeenCalledTimes(1);
|
||||||
expect(consoleMock).toHaveBeenLastCalledWith(
|
expect(consoleMock).toHaveBeenLastCalledWith(
|
||||||
expect.stringMatching(/not tracked by git./),
|
expect.stringMatching(/not tracked by git./),
|
||||||
|
|
|
@ -11,13 +11,12 @@ import path from 'path';
|
||||||
import {createTempRepo} from '@testing-utils/git';
|
import {createTempRepo} from '@testing-utils/git';
|
||||||
import shell from 'shelljs';
|
import shell from 'shelljs';
|
||||||
import {
|
import {
|
||||||
getFileLastUpdate,
|
getGitLastUpdate,
|
||||||
GIT_FALLBACK_LAST_UPDATE_AUTHOR,
|
LAST_UPDATE_FALLBACK,
|
||||||
GIT_FALLBACK_LAST_UPDATE_DATE,
|
|
||||||
readLastUpdateData,
|
readLastUpdateData,
|
||||||
} from '@docusaurus/utils';
|
} from '@docusaurus/utils';
|
||||||
|
|
||||||
describe('getFileLastUpdate', () => {
|
describe('getGitLastUpdate', () => {
|
||||||
const {repoDir} = createTempRepo();
|
const {repoDir} = createTempRepo();
|
||||||
|
|
||||||
const existingFilePath = path.join(
|
const existingFilePath = path.join(
|
||||||
|
@ -25,15 +24,15 @@ describe('getFileLastUpdate', () => {
|
||||||
'__fixtures__/simple-site/hello.md',
|
'__fixtures__/simple-site/hello.md',
|
||||||
);
|
);
|
||||||
it('existing test file in repository with Git timestamp', async () => {
|
it('existing test file in repository with Git timestamp', async () => {
|
||||||
const lastUpdateData = await getFileLastUpdate(existingFilePath);
|
const lastUpdateData = await getGitLastUpdate(existingFilePath);
|
||||||
expect(lastUpdateData).not.toBeNull();
|
expect(lastUpdateData).not.toBeNull();
|
||||||
|
|
||||||
const {author, timestamp} = lastUpdateData!;
|
const {lastUpdatedAt, lastUpdatedBy} = lastUpdateData!;
|
||||||
expect(author).not.toBeNull();
|
expect(lastUpdatedBy).not.toBeNull();
|
||||||
expect(typeof author).toBe('string');
|
expect(typeof lastUpdatedBy).toBe('string');
|
||||||
|
|
||||||
expect(timestamp).not.toBeNull();
|
expect(lastUpdatedAt).not.toBeNull();
|
||||||
expect(typeof timestamp).toBe('number');
|
expect(typeof lastUpdatedAt).toBe('number');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('existing test file with spaces in path', async () => {
|
it('existing test file with spaces in path', async () => {
|
||||||
|
@ -41,15 +40,15 @@ describe('getFileLastUpdate', () => {
|
||||||
__dirname,
|
__dirname,
|
||||||
'__fixtures__/simple-site/doc with space.md',
|
'__fixtures__/simple-site/doc with space.md',
|
||||||
);
|
);
|
||||||
const lastUpdateData = await getFileLastUpdate(filePathWithSpace);
|
const lastUpdateData = await getGitLastUpdate(filePathWithSpace);
|
||||||
expect(lastUpdateData).not.toBeNull();
|
expect(lastUpdateData).not.toBeNull();
|
||||||
|
|
||||||
const {author, timestamp} = lastUpdateData!;
|
const {lastUpdatedBy, lastUpdatedAt} = lastUpdateData!;
|
||||||
expect(author).not.toBeNull();
|
expect(lastUpdatedBy).not.toBeNull();
|
||||||
expect(typeof author).toBe('string');
|
expect(typeof lastUpdatedBy).toBe('string');
|
||||||
|
|
||||||
expect(timestamp).not.toBeNull();
|
expect(lastUpdatedAt).not.toBeNull();
|
||||||
expect(typeof timestamp).toBe('number');
|
expect(typeof lastUpdatedAt).toBe('number');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('non-existing file', async () => {
|
it('non-existing file', async () => {
|
||||||
|
@ -62,7 +61,7 @@ describe('getFileLastUpdate', () => {
|
||||||
'__fixtures__',
|
'__fixtures__',
|
||||||
nonExistingFileName,
|
nonExistingFileName,
|
||||||
);
|
);
|
||||||
await expect(getFileLastUpdate(nonExistingFilePath)).rejects.toThrow(
|
await expect(getGitLastUpdate(nonExistingFilePath)).rejects.toThrow(
|
||||||
/An error occurred when trying to get the last update date/,
|
/An error occurred when trying to get the last update date/,
|
||||||
);
|
);
|
||||||
expect(consoleMock).toHaveBeenCalledTimes(0);
|
expect(consoleMock).toHaveBeenCalledTimes(0);
|
||||||
|
@ -74,7 +73,7 @@ describe('getFileLastUpdate', () => {
|
||||||
const consoleMock = jest
|
const consoleMock = jest
|
||||||
.spyOn(console, 'warn')
|
.spyOn(console, 'warn')
|
||||||
.mockImplementation(() => {});
|
.mockImplementation(() => {});
|
||||||
const lastUpdateData = await getFileLastUpdate(existingFilePath);
|
const lastUpdateData = await getGitLastUpdate(existingFilePath);
|
||||||
expect(lastUpdateData).toBeNull();
|
expect(lastUpdateData).toBeNull();
|
||||||
expect(consoleMock).toHaveBeenLastCalledWith(
|
expect(consoleMock).toHaveBeenLastCalledWith(
|
||||||
expect.stringMatching(
|
expect.stringMatching(
|
||||||
|
@ -92,7 +91,7 @@ describe('getFileLastUpdate', () => {
|
||||||
.mockImplementation(() => {});
|
.mockImplementation(() => {});
|
||||||
const tempFilePath = path.join(repoDir, 'file.md');
|
const tempFilePath = path.join(repoDir, 'file.md');
|
||||||
await fs.writeFile(tempFilePath, 'Lorem ipsum :)');
|
await fs.writeFile(tempFilePath, 'Lorem ipsum :)');
|
||||||
await expect(getFileLastUpdate(tempFilePath)).resolves.toBeNull();
|
await expect(getGitLastUpdate(tempFilePath)).resolves.toBeNull();
|
||||||
expect(consoleMock).toHaveBeenCalledTimes(1);
|
expect(consoleMock).toHaveBeenCalledTimes(1);
|
||||||
expect(consoleMock).toHaveBeenLastCalledWith(
|
expect(consoleMock).toHaveBeenLastCalledWith(
|
||||||
expect.stringMatching(/not tracked by git./),
|
expect.stringMatching(/not tracked by git./),
|
||||||
|
@ -113,7 +112,7 @@ describe('readLastUpdateData', () => {
|
||||||
{date: testDate},
|
{date: testDate},
|
||||||
);
|
);
|
||||||
expect(lastUpdatedAt).toEqual(testTimestamp);
|
expect(lastUpdatedAt).toEqual(testTimestamp);
|
||||||
expect(lastUpdatedBy).toBe(GIT_FALLBACK_LAST_UPDATE_AUTHOR);
|
expect(lastUpdatedBy).toBe(LAST_UPDATE_FALLBACK.lastUpdatedBy);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('read last author show author time', async () => {
|
it('read last author show author time', async () => {
|
||||||
|
@ -123,7 +122,7 @@ describe('readLastUpdateData', () => {
|
||||||
{author: testAuthor},
|
{author: testAuthor},
|
||||||
);
|
);
|
||||||
expect(lastUpdatedBy).toEqual(testAuthor);
|
expect(lastUpdatedBy).toEqual(testAuthor);
|
||||||
expect(lastUpdatedAt).toBe(GIT_FALLBACK_LAST_UPDATE_DATE);
|
expect(lastUpdatedAt).toBe(LAST_UPDATE_FALLBACK.lastUpdatedAt);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('read last all show author time', async () => {
|
it('read last all show author time', async () => {
|
||||||
|
@ -160,7 +159,7 @@ describe('readLastUpdateData', () => {
|
||||||
{showLastUpdateAuthor: true, showLastUpdateTime: false},
|
{showLastUpdateAuthor: true, showLastUpdateTime: false},
|
||||||
{date: testDate},
|
{date: testDate},
|
||||||
);
|
);
|
||||||
expect(lastUpdatedBy).toBe(GIT_FALLBACK_LAST_UPDATE_AUTHOR);
|
expect(lastUpdatedBy).toBe(LAST_UPDATE_FALLBACK.lastUpdatedBy);
|
||||||
expect(lastUpdatedAt).toBeUndefined();
|
expect(lastUpdatedAt).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -180,7 +179,7 @@ describe('readLastUpdateData', () => {
|
||||||
{showLastUpdateAuthor: true, showLastUpdateTime: false},
|
{showLastUpdateAuthor: true, showLastUpdateTime: false},
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
expect(lastUpdatedBy).toBe(GIT_FALLBACK_LAST_UPDATE_AUTHOR);
|
expect(lastUpdatedBy).toBe(LAST_UPDATE_FALLBACK.lastUpdatedBy);
|
||||||
expect(lastUpdatedAt).toBeUndefined();
|
expect(lastUpdatedAt).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -201,7 +200,7 @@ describe('readLastUpdateData', () => {
|
||||||
{author: testAuthor},
|
{author: testAuthor},
|
||||||
);
|
);
|
||||||
expect(lastUpdatedBy).toBeUndefined();
|
expect(lastUpdatedBy).toBeUndefined();
|
||||||
expect(lastUpdatedAt).toEqual(GIT_FALLBACK_LAST_UPDATE_DATE);
|
expect(lastUpdatedAt).toEqual(LAST_UPDATE_FALLBACK.lastUpdatedAt);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('read last author show time only - both front matter', async () => {
|
it('read last author show time only - both front matter', async () => {
|
||||||
|
|
|
@ -15,6 +15,7 @@ import {
|
||||||
aliasedSitePath,
|
aliasedSitePath,
|
||||||
toMessageRelativeFilePath,
|
toMessageRelativeFilePath,
|
||||||
addTrailingPathSeparator,
|
addTrailingPathSeparator,
|
||||||
|
aliasedSitePathToRelativePath,
|
||||||
} from '../pathUtils';
|
} from '../pathUtils';
|
||||||
|
|
||||||
describe('isNameTooLong', () => {
|
describe('isNameTooLong', () => {
|
||||||
|
@ -185,6 +186,20 @@ describe('aliasedSitePath', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('aliasedSitePathToRelativePath', () => {
|
||||||
|
it('works', () => {
|
||||||
|
expect(aliasedSitePathToRelativePath('@site/site/relative/path')).toBe(
|
||||||
|
'site/relative/path',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is fail-fast', () => {
|
||||||
|
expect(() => aliasedSitePathToRelativePath('/site/relative/path')).toThrow(
|
||||||
|
/Unexpected, filePath is not site-aliased: \/site\/relative\/path/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('addTrailingPathSeparator', () => {
|
describe('addTrailingPathSeparator', () => {
|
||||||
it('works', () => {
|
it('works', () => {
|
||||||
expect(addTrailingPathSeparator('foo')).toEqual(
|
expect(addTrailingPathSeparator('foo')).toEqual(
|
||||||
|
|
33
packages/docusaurus-utils/src/__tests__/routeUtils.test.ts
Normal file
33
packages/docusaurus-utils/src/__tests__/routeUtils.test.ts
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {flattenRoutes} from '../routeUtils';
|
||||||
|
import type {RouteConfig} from '@docusaurus/types';
|
||||||
|
|
||||||
|
describe('flattenRoutes', () => {
|
||||||
|
it('returns flattened routes without parents', () => {
|
||||||
|
const routes: RouteConfig[] = [
|
||||||
|
{
|
||||||
|
path: '/docs',
|
||||||
|
component: '',
|
||||||
|
routes: [
|
||||||
|
{path: '/docs/someDoc', component: ''},
|
||||||
|
{path: '/docs/someOtherDoc', component: ''},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/community',
|
||||||
|
component: '',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
expect(flattenRoutes(routes)).toEqual([
|
||||||
|
routes[0]!.routes![0],
|
||||||
|
routes[0]!.routes![1],
|
||||||
|
routes[1],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
|
@ -91,6 +91,7 @@ export {
|
||||||
posixPath,
|
posixPath,
|
||||||
toMessageRelativeFilePath,
|
toMessageRelativeFilePath,
|
||||||
aliasedSitePath,
|
aliasedSitePath,
|
||||||
|
aliasedSitePathToRelativePath,
|
||||||
escapePath,
|
escapePath,
|
||||||
addTrailingPathSeparator,
|
addTrailingPathSeparator,
|
||||||
} from './pathUtils';
|
} from './pathUtils';
|
||||||
|
@ -118,12 +119,13 @@ export {
|
||||||
export {isDraft, isUnlisted} from './contentVisibilityUtils';
|
export {isDraft, isUnlisted} from './contentVisibilityUtils';
|
||||||
export {escapeRegexp} from './regExpUtils';
|
export {escapeRegexp} from './regExpUtils';
|
||||||
export {askPreferredLanguage} from './cliUtils';
|
export {askPreferredLanguage} from './cliUtils';
|
||||||
|
export {flattenRoutes} from './routeUtils';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getFileLastUpdate,
|
getGitLastUpdate,
|
||||||
|
getLastUpdate,
|
||||||
|
readLastUpdateData,
|
||||||
|
LAST_UPDATE_FALLBACK,
|
||||||
type LastUpdateData,
|
type LastUpdateData,
|
||||||
type FrontMatterLastUpdate,
|
type FrontMatterLastUpdate,
|
||||||
readLastUpdateData,
|
|
||||||
GIT_FALLBACK_LAST_UPDATE_AUTHOR,
|
|
||||||
GIT_FALLBACK_LAST_UPDATE_DATE,
|
|
||||||
} from './lastUpdateUtils';
|
} from './lastUpdateUtils';
|
||||||
|
|
|
@ -14,22 +14,6 @@ import {
|
||||||
} from './gitUtils';
|
} from './gitUtils';
|
||||||
import type {PluginOptions} from '@docusaurus/types';
|
import type {PluginOptions} from '@docusaurus/types';
|
||||||
|
|
||||||
export const GIT_FALLBACK_LAST_UPDATE_DATE = 1539502055000;
|
|
||||||
|
|
||||||
export const GIT_FALLBACK_LAST_UPDATE_AUTHOR = 'Author';
|
|
||||||
|
|
||||||
async function getGitLastUpdate(filePath: string): Promise<LastUpdateData> {
|
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
|
||||||
// Use fake data in dev/test for faster development.
|
|
||||||
return {
|
|
||||||
lastUpdatedBy: GIT_FALLBACK_LAST_UPDATE_AUTHOR,
|
|
||||||
lastUpdatedAt: GIT_FALLBACK_LAST_UPDATE_DATE,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const {author, timestamp} = (await getFileLastUpdate(filePath)) ?? {};
|
|
||||||
return {lastUpdatedBy: author, lastUpdatedAt: timestamp};
|
|
||||||
}
|
|
||||||
|
|
||||||
export type LastUpdateData = {
|
export type LastUpdateData = {
|
||||||
/** A timestamp in **milliseconds**, usually read from `git log` */
|
/** A timestamp in **milliseconds**, usually read from `git log` */
|
||||||
lastUpdatedAt?: number;
|
lastUpdatedAt?: number;
|
||||||
|
@ -37,20 +21,12 @@ export type LastUpdateData = {
|
||||||
lastUpdatedBy?: string;
|
lastUpdatedBy?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FrontMatterLastUpdate = {
|
|
||||||
author?: string;
|
|
||||||
/** Date can be any
|
|
||||||
* [parsable date string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse).
|
|
||||||
*/
|
|
||||||
date?: Date | string;
|
|
||||||
};
|
|
||||||
|
|
||||||
let showedGitRequirementError = false;
|
let showedGitRequirementError = false;
|
||||||
let showedFileNotTrackedError = false;
|
let showedFileNotTrackedError = false;
|
||||||
|
|
||||||
export async function getFileLastUpdate(
|
export async function getGitLastUpdate(
|
||||||
filePath: string,
|
filePath: string,
|
||||||
): Promise<{timestamp: number; author: string} | null> {
|
): Promise<LastUpdateData | null> {
|
||||||
if (!filePath) {
|
if (!filePath) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -63,7 +39,7 @@ export async function getFileLastUpdate(
|
||||||
includeAuthor: true,
|
includeAuthor: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {timestamp: result.timestamp, author: result.author};
|
return {lastUpdatedAt: result.timestamp, lastUpdatedBy: result.author};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof GitNotFoundError) {
|
if (err instanceof GitNotFoundError) {
|
||||||
if (!showedGitRequirementError) {
|
if (!showedGitRequirementError) {
|
||||||
|
@ -87,11 +63,35 @@ export async function getFileLastUpdate(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const LAST_UPDATE_FALLBACK: LastUpdateData = {
|
||||||
|
lastUpdatedAt: 1539502055000,
|
||||||
|
lastUpdatedBy: 'Author',
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getLastUpdate(
|
||||||
|
filePath: string,
|
||||||
|
): Promise<LastUpdateData | null> {
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
// Use fake data in dev/test for faster development.
|
||||||
|
return LAST_UPDATE_FALLBACK;
|
||||||
|
}
|
||||||
|
return getGitLastUpdate(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
type LastUpdateOptions = Pick<
|
type LastUpdateOptions = Pick<
|
||||||
PluginOptions,
|
PluginOptions,
|
||||||
'showLastUpdateAuthor' | 'showLastUpdateTime'
|
'showLastUpdateAuthor' | 'showLastUpdateTime'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
export type FrontMatterLastUpdate = {
|
||||||
|
author?: string;
|
||||||
|
/**
|
||||||
|
* Date can be any
|
||||||
|
* [parsable date string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse).
|
||||||
|
*/
|
||||||
|
date?: Date | string;
|
||||||
|
};
|
||||||
|
|
||||||
export async function readLastUpdateData(
|
export async function readLastUpdateData(
|
||||||
filePath: string,
|
filePath: string,
|
||||||
options: LastUpdateOptions,
|
options: LastUpdateOptions,
|
||||||
|
@ -111,18 +111,18 @@ export async function readLastUpdateData(
|
||||||
// We try to minimize git last update calls
|
// We try to minimize git last update calls
|
||||||
// We call it at most once
|
// We call it at most once
|
||||||
// If all the data is provided as front matter, we do not call it
|
// If all the data is provided as front matter, we do not call it
|
||||||
const getGitLastUpdateMemoized = _.memoize(() => getGitLastUpdate(filePath));
|
const getLastUpdateMemoized = _.memoize(() => getLastUpdate(filePath));
|
||||||
const getGitLastUpdateBy = () =>
|
const getLastUpdateBy = () =>
|
||||||
getGitLastUpdateMemoized().then((update) => update.lastUpdatedBy);
|
getLastUpdateMemoized().then((update) => update?.lastUpdatedBy);
|
||||||
const getGitLastUpdateAt = () =>
|
const getLastUpdateAt = () =>
|
||||||
getGitLastUpdateMemoized().then((update) => update.lastUpdatedAt);
|
getLastUpdateMemoized().then((update) => update?.lastUpdatedAt);
|
||||||
|
|
||||||
const lastUpdatedBy = showLastUpdateAuthor
|
const lastUpdatedBy = showLastUpdateAuthor
|
||||||
? frontMatterAuthor ?? (await getGitLastUpdateBy())
|
? frontMatterAuthor ?? (await getLastUpdateBy())
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const lastUpdatedAt = showLastUpdateTime
|
const lastUpdatedAt = showLastUpdateTime
|
||||||
? frontMatterTimestamp ?? (await getGitLastUpdateAt())
|
? frontMatterTimestamp ?? (await getLastUpdateAt())
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -92,6 +92,20 @@ export function aliasedSitePath(filePath: string, siteDir: string): string {
|
||||||
return `@site/${relativePath}`;
|
return `@site/${relativePath}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts back the aliased site path (starting with "@site/...") to a relative path
|
||||||
|
*
|
||||||
|
* TODO method this is a workaround, we shouldn't need to alias/un-alias paths
|
||||||
|
* we should refactor the codebase to not have aliased site paths everywhere
|
||||||
|
* We probably only need aliasing for client-only paths required by Webpack
|
||||||
|
*/
|
||||||
|
export function aliasedSitePathToRelativePath(filePath: string): string {
|
||||||
|
if (filePath.startsWith('@site/')) {
|
||||||
|
return filePath.replace('@site/', '');
|
||||||
|
}
|
||||||
|
throw new Error(`Unexpected, filePath is not site-aliased: ${filePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When you have a path like C:\X\Y
|
* When you have a path like C:\X\Y
|
||||||
* It is not safe to use directly when generating code
|
* It is not safe to use directly when generating code
|
||||||
|
|
19
packages/docusaurus-utils/src/routeUtils.ts
Normal file
19
packages/docusaurus-utils/src/routeUtils.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type {RouteConfig} from '@docusaurus/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively flatten routes and only return the "leaf routes"
|
||||||
|
* Parent routes are filtered out
|
||||||
|
*/
|
||||||
|
export function flattenRoutes(routeConfig: RouteConfig[]): RouteConfig[] {
|
||||||
|
function flatten(route: RouteConfig): RouteConfig[] {
|
||||||
|
return route.routes ? route.routes.flatMap(flatten) : [route];
|
||||||
|
}
|
||||||
|
return routeConfig.flatMap(flatten);
|
||||||
|
}
|
|
@ -6,33 +6,9 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {jest} from '@jest/globals';
|
import {jest} from '@jest/globals';
|
||||||
import {getAllFinalRoutes, handleDuplicateRoutes} from '../routes';
|
import {handleDuplicateRoutes} from '../routes';
|
||||||
import type {RouteConfig} from '@docusaurus/types';
|
import type {RouteConfig} from '@docusaurus/types';
|
||||||
|
|
||||||
describe('getAllFinalRoutes', () => {
|
|
||||||
it('gets final routes correctly', () => {
|
|
||||||
const routes: RouteConfig[] = [
|
|
||||||
{
|
|
||||||
path: '/docs',
|
|
||||||
component: '',
|
|
||||||
routes: [
|
|
||||||
{path: '/docs/someDoc', component: ''},
|
|
||||||
{path: '/docs/someOtherDoc', component: ''},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/community',
|
|
||||||
component: '',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
expect(getAllFinalRoutes(routes)).toEqual([
|
|
||||||
routes[0]!.routes![0],
|
|
||||||
routes[0]!.routes![1],
|
|
||||||
routes[1],
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('handleDuplicateRoutes', () => {
|
describe('handleDuplicateRoutes', () => {
|
||||||
const routes: RouteConfig[] = [
|
const routes: RouteConfig[] = [
|
||||||
{
|
{
|
||||||
|
|
|
@ -13,9 +13,9 @@ import {
|
||||||
parseURLPath,
|
parseURLPath,
|
||||||
removeTrailingSlash,
|
removeTrailingSlash,
|
||||||
serializeURLPath,
|
serializeURLPath,
|
||||||
|
flattenRoutes,
|
||||||
type URLPath,
|
type URLPath,
|
||||||
} from '@docusaurus/utils';
|
} from '@docusaurus/utils';
|
||||||
import {getAllFinalRoutes} from './routes';
|
|
||||||
import type {RouteConfig, ReportingSeverity} from '@docusaurus/types';
|
import type {RouteConfig, ReportingSeverity} from '@docusaurus/types';
|
||||||
|
|
||||||
function matchRoutes(routeConfig: RouteConfig[], pathname: string) {
|
function matchRoutes(routeConfig: RouteConfig[], pathname: string) {
|
||||||
|
@ -188,7 +188,7 @@ function getBrokenLinksForPage({
|
||||||
*/
|
*/
|
||||||
function filterIntermediateRoutes(routesInput: RouteConfig[]): RouteConfig[] {
|
function filterIntermediateRoutes(routesInput: RouteConfig[]): RouteConfig[] {
|
||||||
const routesWithout404 = routesInput.filter((route) => route.path !== '*');
|
const routesWithout404 = routesInput.filter((route) => route.path !== '*');
|
||||||
return getAllFinalRoutes(routesWithout404);
|
return flattenRoutes(routesWithout404);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBrokenLinks({
|
function getBrokenLinks({
|
||||||
|
|
|
@ -9,7 +9,7 @@ exports[`loadRoutes loads flat route config 1`] = `
|
||||||
"metadata---blog-0-b-6-74c": "blog-2018-12-14-happy-first-birthday-slash-d2c.json",
|
"metadata---blog-0-b-6-74c": "blog-2018-12-14-happy-first-birthday-slash-d2c.json",
|
||||||
},
|
},
|
||||||
"routesChunkNames": {
|
"routesChunkNames": {
|
||||||
"/blog-599": {
|
"/blog-030": {
|
||||||
"__comp": "__comp---theme-blog-list-pagea-6-a-7ba",
|
"__comp": "__comp---theme-blog-list-pagea-6-a-7ba",
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
|
@ -31,7 +31,7 @@ import ComponentCreator from '@docusaurus/ComponentCreator';
|
||||||
export default [
|
export default [
|
||||||
{
|
{
|
||||||
path: '/blog',
|
path: '/blog',
|
||||||
component: ComponentCreator('/blog', '599'),
|
component: ComponentCreator('/blog', '030'),
|
||||||
exact: true
|
exact: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -56,7 +56,7 @@ exports[`loadRoutes loads nested route config 1`] = `
|
||||||
"plugin---docs-hello-665-3ca": "pluginRouteContextModule-100.json",
|
"plugin---docs-hello-665-3ca": "pluginRouteContextModule-100.json",
|
||||||
},
|
},
|
||||||
"routesChunkNames": {
|
"routesChunkNames": {
|
||||||
"/docs/hello-fcc": {
|
"/docs/hello-ab4": {
|
||||||
"__comp": "__comp---theme-doc-item-178-a40",
|
"__comp": "__comp---theme-doc-item-178-a40",
|
||||||
"__context": {
|
"__context": {
|
||||||
"plugin": "plugin---docs-hello-665-3ca",
|
"plugin": "plugin---docs-hello-665-3ca",
|
||||||
|
@ -64,11 +64,11 @@ exports[`loadRoutes loads nested route config 1`] = `
|
||||||
"content": "content---docs-helloaff-811",
|
"content": "content---docs-helloaff-811",
|
||||||
"metadata": "metadata---docs-hello-956-741",
|
"metadata": "metadata---docs-hello-956-741",
|
||||||
},
|
},
|
||||||
"/docs:route-9d0": {
|
"/docs:route-001": {
|
||||||
"__comp": "__comp---theme-doc-roota-94-67a",
|
"__comp": "__comp---theme-doc-roota-94-67a",
|
||||||
"docsMetadata": "docsMetadata---docs-routef-34-881",
|
"docsMetadata": "docsMetadata---docs-routef-34-881",
|
||||||
},
|
},
|
||||||
"docs/foo/baz-eb2": {
|
"docs/foo/baz-125": {
|
||||||
"__comp": "__comp---theme-doc-item-178-a40",
|
"__comp": "__comp---theme-doc-item-178-a40",
|
||||||
"__context": {
|
"__context": {
|
||||||
"plugin": "plugin---docs-hello-665-3ca",
|
"plugin": "plugin---docs-hello-665-3ca",
|
||||||
|
@ -83,17 +83,17 @@ import ComponentCreator from '@docusaurus/ComponentCreator';
|
||||||
export default [
|
export default [
|
||||||
{
|
{
|
||||||
path: '/docs:route',
|
path: '/docs:route',
|
||||||
component: ComponentCreator('/docs:route', '9d0'),
|
component: ComponentCreator('/docs:route', '001'),
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: '/docs/hello',
|
path: '/docs/hello',
|
||||||
component: ComponentCreator('/docs/hello', 'fcc'),
|
component: ComponentCreator('/docs/hello', 'ab4'),
|
||||||
exact: true,
|
exact: true,
|
||||||
sidebar: "main"
|
sidebar: "main"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'docs/foo/baz',
|
path: 'docs/foo/baz',
|
||||||
component: ComponentCreator('docs/foo/baz', 'eb2'),
|
component: ComponentCreator('docs/foo/baz', '125'),
|
||||||
sidebar: "secondary",
|
sidebar: "secondary",
|
||||||
"key:a": "containing colon",
|
"key:a": "containing colon",
|
||||||
"key'b": "containing quote",
|
"key'b": "containing quote",
|
||||||
|
|
|
@ -108,6 +108,10 @@ describe('loadRoutes', () => {
|
||||||
content: 'docs/hello.md',
|
content: 'docs/hello.md',
|
||||||
metadata: 'docs-hello-da2.json',
|
metadata: 'docs-hello-da2.json',
|
||||||
},
|
},
|
||||||
|
metadata: {
|
||||||
|
sourceFilePath: 'docs/hello.md',
|
||||||
|
lastUpdatedAt: 1710842708527,
|
||||||
|
},
|
||||||
context: {
|
context: {
|
||||||
plugin: 'pluginRouteContextModule-100.json',
|
plugin: 'pluginRouteContextModule-100.json',
|
||||||
},
|
},
|
||||||
|
@ -120,6 +124,10 @@ describe('loadRoutes', () => {
|
||||||
content: 'docs/foo/baz.md',
|
content: 'docs/foo/baz.md',
|
||||||
metadata: 'docs-foo-baz-dd9.json',
|
metadata: 'docs-foo-baz-dd9.json',
|
||||||
},
|
},
|
||||||
|
metadata: {
|
||||||
|
sourceFilePath: 'docs/foo/baz.md',
|
||||||
|
lastUpdatedAt: 1710842708527,
|
||||||
|
},
|
||||||
context: {
|
context: {
|
||||||
plugin: 'pluginRouteContextModule-100.json',
|
plugin: 'pluginRouteContextModule-100.json',
|
||||||
},
|
},
|
||||||
|
@ -142,6 +150,9 @@ describe('loadRoutes', () => {
|
||||||
path: '/blog',
|
path: '/blog',
|
||||||
component: '@theme/BlogListPage',
|
component: '@theme/BlogListPage',
|
||||||
exact: true,
|
exact: true,
|
||||||
|
metadata: {
|
||||||
|
lastUpdatedAt: 1710842708527,
|
||||||
|
},
|
||||||
modules: {
|
modules: {
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
|
|
|
@ -200,6 +200,7 @@ function genRouteCode(routeConfig: RouteConfig, res: RoutesCode): string {
|
||||||
routes: subroutes,
|
routes: subroutes,
|
||||||
priority,
|
priority,
|
||||||
exact,
|
exact,
|
||||||
|
metadata,
|
||||||
...props
|
...props
|
||||||
} = routeConfig;
|
} = routeConfig;
|
||||||
|
|
||||||
|
|
|
@ -18,10 +18,10 @@ import type {
|
||||||
RouteConfig,
|
RouteConfig,
|
||||||
AllContent,
|
AllContent,
|
||||||
GlobalData,
|
GlobalData,
|
||||||
|
PluginIdentifier,
|
||||||
LoadedPlugin,
|
LoadedPlugin,
|
||||||
InitializedPlugin,
|
InitializedPlugin,
|
||||||
} from '@docusaurus/types';
|
} from '@docusaurus/types';
|
||||||
import type {PluginIdentifier} from '@docusaurus/types/src/plugin';
|
|
||||||
|
|
||||||
async function translatePlugin({
|
async function translatePlugin({
|
||||||
plugin,
|
plugin,
|
||||||
|
|
|
@ -6,17 +6,9 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import logger from '@docusaurus/logger';
|
import logger from '@docusaurus/logger';
|
||||||
import {normalizeUrl} from '@docusaurus/utils';
|
import {normalizeUrl, flattenRoutes} from '@docusaurus/utils';
|
||||||
import type {RouteConfig, ReportingSeverity} from '@docusaurus/types';
|
import type {RouteConfig, ReportingSeverity} from '@docusaurus/types';
|
||||||
|
|
||||||
// Recursively get the final routes (routes with no subroutes)
|
|
||||||
export function getAllFinalRoutes(routeConfig: RouteConfig[]): RouteConfig[] {
|
|
||||||
function getFinalRoutes(route: RouteConfig): RouteConfig[] {
|
|
||||||
return route.routes ? route.routes.flatMap(getFinalRoutes) : [route];
|
|
||||||
}
|
|
||||||
return routeConfig.flatMap(getFinalRoutes);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function handleDuplicateRoutes(
|
export function handleDuplicateRoutes(
|
||||||
routes: RouteConfig[],
|
routes: RouteConfig[],
|
||||||
onDuplicateRoutes: ReportingSeverity,
|
onDuplicateRoutes: ReportingSeverity,
|
||||||
|
@ -24,7 +16,7 @@ export function handleDuplicateRoutes(
|
||||||
if (onDuplicateRoutes === 'ignore') {
|
if (onDuplicateRoutes === 'ignore') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const allRoutes: string[] = getAllFinalRoutes(routes).map(
|
const allRoutes: string[] = flattenRoutes(routes).map(
|
||||||
(routeConfig) => routeConfig.path,
|
(routeConfig) => routeConfig.path,
|
||||||
);
|
);
|
||||||
const seenRoutes = new Set<string>();
|
const seenRoutes = new Set<string>();
|
||||||
|
@ -52,6 +44,13 @@ This could lead to non-deterministic routing behavior.`;
|
||||||
* This is rendered through the catch-all ComponentCreator("*") route
|
* This is rendered through the catch-all ComponentCreator("*") route
|
||||||
* Note CDNs only understand the 404.html file by convention
|
* Note CDNs only understand the 404.html file by convention
|
||||||
* The extension probably permits to avoid emitting "/404/index.html"
|
* The extension probably permits to avoid emitting "/404/index.html"
|
||||||
|
*
|
||||||
|
* TODO we should probably deprecate/remove "postBuild({routesPaths})
|
||||||
|
* The 404 generation handling can be moved to the SSG code
|
||||||
|
* We only need getAllFinalRoutes() utils IMHO
|
||||||
|
* This would be a plugin lifecycle breaking change :/
|
||||||
|
* Although not many plugins probably use this
|
||||||
|
*
|
||||||
*/
|
*/
|
||||||
const NotFoundRoutePath = '/404.html';
|
const NotFoundRoutePath = '/404.html';
|
||||||
|
|
||||||
|
@ -61,6 +60,6 @@ export function getRoutesPaths(
|
||||||
): string[] {
|
): string[] {
|
||||||
return [
|
return [
|
||||||
normalizeUrl([baseUrl, NotFoundRoutePath]),
|
normalizeUrl([baseUrl, NotFoundRoutePath]),
|
||||||
...getAllFinalRoutes(routeConfigs).map((r) => r.path),
|
...flattenRoutes(routeConfigs).map((r) => r.path),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,8 +31,8 @@ import type {
|
||||||
GlobalData,
|
GlobalData,
|
||||||
LoadContext,
|
LoadContext,
|
||||||
Props,
|
Props,
|
||||||
|
PluginIdentifier,
|
||||||
} from '@docusaurus/types';
|
} from '@docusaurus/types';
|
||||||
import type {PluginIdentifier} from '@docusaurus/types/src/plugin';
|
|
||||||
|
|
||||||
export type LoadContextParams = {
|
export type LoadContextParams = {
|
||||||
/** Usually the CWD; can be overridden with command argument. */
|
/** Usually the CWD; can be overridden with command argument. */
|
||||||
|
|
|
@ -42,7 +42,6 @@ cdabcdab
|
||||||
cdpath
|
cdpath
|
||||||
Cena
|
Cena
|
||||||
cena
|
cena
|
||||||
Changefreq
|
|
||||||
changefreq
|
changefreq
|
||||||
Chedeau
|
Chedeau
|
||||||
chedeau
|
chedeau
|
||||||
|
@ -157,6 +156,8 @@ Knapen
|
||||||
Koyeb
|
Koyeb
|
||||||
Koyeb's
|
Koyeb's
|
||||||
Lamana
|
Lamana
|
||||||
|
Lastmod
|
||||||
|
lastmod
|
||||||
Lifecycles
|
Lifecycles
|
||||||
lifecycles
|
lifecycles
|
||||||
Linkify
|
Linkify
|
||||||
|
|
|
@ -43,17 +43,85 @@ The data that was loaded in `loadContent` will be consumed in `contentLoaded`. I
|
||||||
Create a route to add to the website.
|
Create a route to add to the website.
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
type RouteConfig = {
|
export type RouteConfig = {
|
||||||
|
/**
|
||||||
|
* With leading slash. Trailing slash will be normalized by config.
|
||||||
|
*/
|
||||||
path: string;
|
path: string;
|
||||||
|
/**
|
||||||
|
* Component used to render this route, a path that the bundler can `require`.
|
||||||
|
*/
|
||||||
component: string;
|
component: string;
|
||||||
|
/**
|
||||||
|
* Props. Each entry should be `[propName]: pathToPropModule` (created with
|
||||||
|
* `createData`)
|
||||||
|
*/
|
||||||
modules?: RouteModules;
|
modules?: RouteModules;
|
||||||
|
/**
|
||||||
|
* The route context will wrap the `component`. Use `useRouteContext` to
|
||||||
|
* retrieve what's declared here. Note that all custom route context declared
|
||||||
|
* here will be namespaced under {@link RouteContext.data}.
|
||||||
|
*/
|
||||||
|
context?: RouteModules;
|
||||||
|
/**
|
||||||
|
* Nested routes config, useful for "layout routes" having subroutes.
|
||||||
|
*/
|
||||||
routes?: RouteConfig[];
|
routes?: RouteConfig[];
|
||||||
|
/**
|
||||||
|
* React router config option: `exact` routes would not match subroutes.
|
||||||
|
*/
|
||||||
exact?: boolean;
|
exact?: boolean;
|
||||||
|
/**
|
||||||
|
* React router config option: `strict` routes are sensitive to the presence
|
||||||
|
* of a trailing slash.
|
||||||
|
*/
|
||||||
|
strict?: boolean;
|
||||||
|
/**
|
||||||
|
* Used to sort routes.
|
||||||
|
* Higher-priority routes will be matched first.
|
||||||
|
*/
|
||||||
priority?: number;
|
priority?: number;
|
||||||
|
/**
|
||||||
|
* Optional route metadata
|
||||||
|
*/
|
||||||
|
metadata?: RouteMetadata;
|
||||||
|
/**
|
||||||
|
* Extra props; will be available on the client side.
|
||||||
|
*/
|
||||||
|
[propName: string]: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin authors can assign extra metadata to the created routes
|
||||||
|
* It is only available on the Node.js side, and not sent to the browser
|
||||||
|
* Optional: plugin authors are encouraged but not required to provide it
|
||||||
|
*
|
||||||
|
* Some plugins might use this data to provide additional features.
|
||||||
|
* This is the case of the sitemap plugin to provide support for "lastmod".
|
||||||
|
* See also: https://github.com/facebook/docusaurus/pull/9954
|
||||||
|
*/
|
||||||
|
export type RouteMetadata = {
|
||||||
|
/**
|
||||||
|
* The source code file path that led to the creation of the current route
|
||||||
|
* In official content plugins, this is usually a Markdown or React file
|
||||||
|
* This path is expected to be relative to the site directory
|
||||||
|
*/
|
||||||
|
sourceFilePath?: string;
|
||||||
|
/**
|
||||||
|
* The last updated date of this route
|
||||||
|
* This is generally read from the Git history of the sourceFilePath
|
||||||
|
* but can also be provided through other means (usually front matter)
|
||||||
|
*
|
||||||
|
* This has notably been introduced for adding "lastmod" support to the
|
||||||
|
* sitemap plugin, see https://github.com/facebook/docusaurus/pull/9954
|
||||||
|
*/
|
||||||
|
lastUpdatedAt?: number;
|
||||||
|
};
|
||||||
|
|
||||||
type RouteModules = {
|
type RouteModules = {
|
||||||
[module: string]: Module | RouteModules | RouteModules[];
|
[module: string]: Module | RouteModules | RouteModules[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type Module =
|
type Module =
|
||||||
| {
|
| {
|
||||||
path: string;
|
path: string;
|
||||||
|
|
|
@ -39,8 +39,9 @@ Accepted fields:
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `changefreq` | `string` | `'weekly'` | See [sitemap docs](https://www.sitemaps.org/protocol.html#xmlTagDefinitions) |
|
| `lastmod` | `'date' \| 'datetime' \| null` | `null` | `date` is YYYY-MM-DD. `datetime` is a ISO 8601 datetime. `null` is disabled. See [sitemap docs](https://www.sitemaps.org/protocol.html#xmlTagDefinitions). |
|
||||||
| `priority` | `number` | `0.5` | See [sitemap docs](https://www.sitemaps.org/protocol.html#xmlTagDefinitions) |
|
| `changefreq` | `string \| null` | `'weekly'` | See [sitemap docs](https://www.sitemaps.org/protocol.html#xmlTagDefinitions) |
|
||||||
|
| `priority` | `number \| null` | `0.5` | See [sitemap docs](https://www.sitemaps.org/protocol.html#xmlTagDefinitions) |
|
||||||
| `ignorePatterns` | `string[]` | `[]` | A list of glob patterns; matching route paths will be filtered from the sitemap. Note that you may need to include the base URL in here. |
|
| `ignorePatterns` | `string[]` | `[]` | A list of glob patterns; matching route paths will be filtered from the sitemap. Note that you may need to include the base URL in here. |
|
||||||
| `filename` | `string` | `sitemap.xml` | The path to the created sitemap file, relative to the output directory. Useful if you have two plugin instances outputting two files. |
|
| `filename` | `string` | `sitemap.xml` | The path to the created sitemap file, relative to the output directory. Useful if you have two plugin instances outputting two files. |
|
||||||
|
|
||||||
|
@ -57,6 +58,14 @@ This plugin also respects some site config:
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
:::note About `lastmod`
|
||||||
|
|
||||||
|
The `lastmod` option will only output a sitemap `<lastmod>` tag if plugins provide [route metadata](../plugin-methods/lifecycle-apis.mdx#addRoute) attributes `sourceFilePath` and/or `lastUpdatedAt`.
|
||||||
|
|
||||||
|
All the official content plugins provide the metadata for routes backed by a content file (Markdown, MDX or React page components), but it is possible third-party plugin authors do not provide this information, and the plugin will not be able to output a `<lastmod>` tag for their routes.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
### Example configuration {#ex-config}
|
### Example configuration {#ex-config}
|
||||||
|
|
||||||
You can configure this plugin through preset options or plugin options.
|
You can configure this plugin through preset options or plugin options.
|
||||||
|
@ -72,6 +81,7 @@ Most Docusaurus users configure this plugin through the preset options.
|
||||||
// Plugin Options: @docusaurus/plugin-sitemap
|
// Plugin Options: @docusaurus/plugin-sitemap
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
|
lastmod: 'date',
|
||||||
changefreq: 'weekly',
|
changefreq: 'weekly',
|
||||||
priority: 0.5,
|
priority: 0.5,
|
||||||
ignorePatterns: ['/tags/**'],
|
ignorePatterns: ['/tags/**'],
|
||||||
|
|
|
@ -480,6 +480,9 @@ export default async function createConfigAsync() {
|
||||||
sitemap: {
|
sitemap: {
|
||||||
// Note: /tests/docs already has noIndex: true
|
// Note: /tests/docs already has noIndex: true
|
||||||
ignorePatterns: ['/tests/{blog,pages}/**'],
|
ignorePatterns: ['/tests/{blog,pages}/**'],
|
||||||
|
lastmod: 'date',
|
||||||
|
priority: null,
|
||||||
|
changefreq: null,
|
||||||
},
|
},
|
||||||
} satisfies Preset.Options,
|
} satisfies Preset.Options,
|
||||||
],
|
],
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue