@docusaurus/plugin-sitemap 3.1.0 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/createSitemap.d.ts +11 -4
- package/lib/createSitemap.js +31 -25
- package/lib/createSitemapItem.d.ts +14 -0
- package/lib/createSitemapItem.js +52 -0
- package/lib/index.js +7 -2
- package/lib/options.d.ts +23 -8
- package/lib/options.js +25 -6
- package/lib/types.d.ts +47 -0
- package/lib/types.js +21 -0
- package/lib/xml.d.ts +10 -0
- package/lib/xml.js +30 -0
- package/package.json +11 -8
- package/src/createSitemap.ts +49 -38
- package/src/createSitemapItem.ts +76 -0
- package/src/index.ts +4 -4
- package/src/options.ts +58 -13
- package/src/types.ts +67 -0
- package/src/xml.ts +35 -0
package/lib/createSitemap.d.ts
CHANGED
|
@@ -4,9 +4,16 @@
|
|
|
4
4
|
* This source code is licensed under the MIT license found in the
|
|
5
5
|
* LICENSE file in the root directory of this source tree.
|
|
6
6
|
*/
|
|
7
|
-
import type { DocusaurusConfig } from '@docusaurus/types';
|
|
7
|
+
import type { DocusaurusConfig, RouteConfig } from '@docusaurus/types';
|
|
8
8
|
import type { HelmetServerState } from 'react-helmet-async';
|
|
9
9
|
import type { PluginOptions } from './options';
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
type CreateSitemapParams = {
|
|
11
|
+
siteConfig: DocusaurusConfig;
|
|
12
|
+
routes: RouteConfig[];
|
|
13
|
+
head: {
|
|
14
|
+
[location: string]: HelmetServerState;
|
|
15
|
+
};
|
|
16
|
+
options: PluginOptions;
|
|
17
|
+
};
|
|
18
|
+
export default function createSitemap(params: CreateSitemapParams): Promise<string | null>;
|
|
19
|
+
export {};
|
package/lib/createSitemap.js
CHANGED
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
-
const sitemap_1 = require("sitemap");
|
|
10
|
-
const utils_common_1 = require("@docusaurus/utils-common");
|
|
11
9
|
const utils_1 = require("@docusaurus/utils");
|
|
10
|
+
const xml_1 = require("./xml");
|
|
11
|
+
const createSitemapItem_1 = require("./createSitemapItem");
|
|
12
|
+
// Maybe we want to add a routeConfig.metadata.noIndex instead?
|
|
13
|
+
// But using Helmet is more reliable for third-party plugins...
|
|
12
14
|
function isNoIndexMetaRoute({ head, route, }) {
|
|
13
15
|
const isNoIndexMetaTag = ({ name, content, }) => {
|
|
14
16
|
if (!name || !content) {
|
|
@@ -24,33 +26,37 @@ function isNoIndexMetaRoute({ head, route, }) {
|
|
|
24
26
|
const meta = head[route]?.meta.toComponent();
|
|
25
27
|
return meta?.some((tag) => isNoIndexMetaTag({ name: tag.props.name, content: tag.props.content }));
|
|
26
28
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const {
|
|
29
|
+
// Not all routes should appear in the sitemap, and we should filter:
|
|
30
|
+
// - parent routes, used for layouts
|
|
31
|
+
// - routes matching options.ignorePatterns
|
|
32
|
+
// - routes with no index metadata
|
|
33
|
+
function getSitemapRoutes({ routes, head, options }) {
|
|
34
|
+
const { ignorePatterns } = options;
|
|
33
35
|
const ignoreMatcher = (0, utils_1.createMatcher)(ignorePatterns);
|
|
34
36
|
function isRouteExcluded(route) {
|
|
35
|
-
return (route.
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
return (ignoreMatcher(route.path) || isNoIndexMetaRoute({ head, route: route.path }));
|
|
38
|
+
}
|
|
39
|
+
return (0, utils_1.flattenRoutes)(routes).filter((route) => !isRouteExcluded(route));
|
|
40
|
+
}
|
|
41
|
+
async function createSitemapItems(params) {
|
|
42
|
+
const sitemapRoutes = getSitemapRoutes(params);
|
|
43
|
+
if (sitemapRoutes.length === 0) {
|
|
44
|
+
return [];
|
|
38
45
|
}
|
|
39
|
-
|
|
40
|
-
|
|
46
|
+
return Promise.all(sitemapRoutes.map((route) => (0, createSitemapItem_1.createSitemapItem)({
|
|
47
|
+
route,
|
|
48
|
+
siteConfig: params.siteConfig,
|
|
49
|
+
options: params.options,
|
|
50
|
+
})));
|
|
51
|
+
}
|
|
52
|
+
async function createSitemap(params) {
|
|
53
|
+
const items = await createSitemapItems(params);
|
|
54
|
+
if (items.length === 0) {
|
|
41
55
|
return null;
|
|
42
56
|
}
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
baseUrl: siteConfig.baseUrl,
|
|
48
|
-
}),
|
|
49
|
-
changefreq,
|
|
50
|
-
priority,
|
|
51
|
-
}));
|
|
52
|
-
sitemapStream.end();
|
|
53
|
-
const generatedSitemap = (await (0, sitemap_1.streamToPromise)(sitemapStream)).toString();
|
|
54
|
-
return generatedSitemap;
|
|
57
|
+
const xmlString = await (0, xml_1.sitemapItemsToXmlString)(items, {
|
|
58
|
+
lastmod: params.options.lastmod,
|
|
59
|
+
});
|
|
60
|
+
return xmlString;
|
|
55
61
|
}
|
|
56
62
|
exports.default = createSitemap;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
import type { SitemapItem } from './types';
|
|
8
|
+
import type { DocusaurusConfig, RouteConfig } from '@docusaurus/types';
|
|
9
|
+
import type { PluginOptions } from './options';
|
|
10
|
+
export declare function createSitemapItem({ route, siteConfig, options, }: {
|
|
11
|
+
route: RouteConfig;
|
|
12
|
+
siteConfig: DocusaurusConfig;
|
|
13
|
+
options: PluginOptions;
|
|
14
|
+
}): Promise<SitemapItem>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.createSitemapItem = void 0;
|
|
10
|
+
const utils_common_1 = require("@docusaurus/utils-common");
|
|
11
|
+
const utils_1 = require("@docusaurus/utils");
|
|
12
|
+
async function getRouteLastUpdatedAt(route) {
|
|
13
|
+
if (route.metadata?.lastUpdatedAt) {
|
|
14
|
+
return route.metadata?.lastUpdatedAt;
|
|
15
|
+
}
|
|
16
|
+
if (route.metadata?.sourceFilePath) {
|
|
17
|
+
const lastUpdate = await (0, utils_1.getLastUpdate)(route.metadata?.sourceFilePath);
|
|
18
|
+
return lastUpdate?.lastUpdatedAt;
|
|
19
|
+
}
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
const LastmodFormatters = {
|
|
23
|
+
date: (timestamp) => new Date(timestamp).toISOString().split('T')[0],
|
|
24
|
+
datetime: (timestamp) => new Date(timestamp).toISOString(),
|
|
25
|
+
};
|
|
26
|
+
function formatLastmod(timestamp, lastmodOption) {
|
|
27
|
+
const format = LastmodFormatters[lastmodOption];
|
|
28
|
+
return format(timestamp);
|
|
29
|
+
}
|
|
30
|
+
async function getRouteLastmod({ route, lastmod, }) {
|
|
31
|
+
if (lastmod === null) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const lastUpdatedAt = (await getRouteLastUpdatedAt(route)) ?? null;
|
|
35
|
+
return lastUpdatedAt ? formatLastmod(lastUpdatedAt, lastmod) : null;
|
|
36
|
+
}
|
|
37
|
+
async function createSitemapItem({ route, siteConfig, options, }) {
|
|
38
|
+
const { changefreq, priority, lastmod } = options;
|
|
39
|
+
return {
|
|
40
|
+
url: (0, utils_1.normalizeUrl)([
|
|
41
|
+
siteConfig.url,
|
|
42
|
+
(0, utils_common_1.applyTrailingSlash)(route.path, {
|
|
43
|
+
trailingSlash: siteConfig.trailingSlash,
|
|
44
|
+
baseUrl: siteConfig.baseUrl,
|
|
45
|
+
}),
|
|
46
|
+
]),
|
|
47
|
+
changefreq,
|
|
48
|
+
priority,
|
|
49
|
+
lastmod: await getRouteLastmod({ route, lastmod }),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
exports.createSitemapItem = createSitemapItem;
|
package/lib/index.js
CHANGED
|
@@ -15,12 +15,17 @@ const createSitemap_1 = tslib_1.__importDefault(require("./createSitemap"));
|
|
|
15
15
|
function pluginSitemap(context, options) {
|
|
16
16
|
return {
|
|
17
17
|
name: 'docusaurus-plugin-sitemap',
|
|
18
|
-
async postBuild({ siteConfig,
|
|
18
|
+
async postBuild({ siteConfig, routes, outDir, head }) {
|
|
19
19
|
if (siteConfig.noIndex) {
|
|
20
20
|
return;
|
|
21
21
|
}
|
|
22
22
|
// Generate sitemap.
|
|
23
|
-
const generatedSitemap = await (0, createSitemap_1.default)(
|
|
23
|
+
const generatedSitemap = await (0, createSitemap_1.default)({
|
|
24
|
+
siteConfig,
|
|
25
|
+
routes,
|
|
26
|
+
head,
|
|
27
|
+
options,
|
|
28
|
+
});
|
|
24
29
|
if (!generatedSitemap) {
|
|
25
30
|
return;
|
|
26
31
|
}
|
package/lib/options.d.ts
CHANGED
|
@@ -4,23 +4,38 @@
|
|
|
4
4
|
* This source code is licensed under the MIT license found in the
|
|
5
5
|
* LICENSE file in the root directory of this source tree.
|
|
6
6
|
*/
|
|
7
|
-
import { EnumChangefreq } from 'sitemap';
|
|
8
7
|
import type { OptionValidationContext } from '@docusaurus/types';
|
|
8
|
+
import type { ChangeFreq, LastModOption } from './types';
|
|
9
9
|
export type PluginOptions = {
|
|
10
|
-
/**
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
/**
|
|
11
|
+
* The path to the created sitemap file, relative to the output directory.
|
|
12
|
+
* Useful if you have two plugin instances outputting two files.
|
|
13
|
+
*/
|
|
14
|
+
filename: string;
|
|
14
15
|
/**
|
|
15
16
|
* A list of glob patterns; matching route paths will be filtered from the
|
|
16
17
|
* sitemap. Note that you may need to include the base URL in here.
|
|
17
18
|
*/
|
|
18
19
|
ignorePatterns: string[];
|
|
19
20
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
21
|
+
* Defines the format of the "lastmod" sitemap item entry, between:
|
|
22
|
+
* - null: do not compute/add a "lastmod" sitemap entry
|
|
23
|
+
* - "date": add a "lastmod" sitemap entry without time (YYYY-MM-DD)
|
|
24
|
+
* - "datetime": add a "lastmod" sitemap entry with time (ISO 8601 datetime)
|
|
25
|
+
* @see https://www.sitemaps.org/protocol.html#xmlTagDefinitions
|
|
26
|
+
* @see https://www.w3.org/TR/NOTE-datetime
|
|
22
27
|
*/
|
|
23
|
-
|
|
28
|
+
lastmod: LastModOption | null;
|
|
29
|
+
/**
|
|
30
|
+
* TODO Docusaurus v4 breaking change: remove useless option
|
|
31
|
+
* @see https://www.sitemaps.org/protocol.html#xmlTagDefinitions
|
|
32
|
+
*/
|
|
33
|
+
changefreq: ChangeFreq | null;
|
|
34
|
+
/**
|
|
35
|
+
* TODO Docusaurus v4 breaking change: remove useless option
|
|
36
|
+
* @see https://www.sitemaps.org/protocol.html#xmlTagDefinitions
|
|
37
|
+
*/
|
|
38
|
+
priority: number | null;
|
|
24
39
|
};
|
|
25
40
|
export type Options = Partial<PluginOptions>;
|
|
26
41
|
export declare const DEFAULT_OPTIONS: PluginOptions;
|
package/lib/options.js
CHANGED
|
@@ -8,22 +8,41 @@
|
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
9
|
exports.validateOptions = exports.DEFAULT_OPTIONS = void 0;
|
|
10
10
|
const utils_validation_1 = require("@docusaurus/utils-validation");
|
|
11
|
-
const
|
|
11
|
+
const types_1 = require("./types");
|
|
12
12
|
exports.DEFAULT_OPTIONS = {
|
|
13
|
-
changefreq: sitemap_1.EnumChangefreq.WEEKLY,
|
|
14
|
-
priority: 0.5,
|
|
15
|
-
ignorePatterns: [],
|
|
16
13
|
filename: 'sitemap.xml',
|
|
14
|
+
ignorePatterns: [],
|
|
15
|
+
// TODO Docusaurus v4 breaking change
|
|
16
|
+
// change default to "date" if no bug or perf issue reported
|
|
17
|
+
lastmod: null,
|
|
18
|
+
// TODO Docusaurus v4 breaking change
|
|
19
|
+
// those options are useless and should be removed
|
|
20
|
+
changefreq: 'weekly',
|
|
21
|
+
priority: 0.5,
|
|
17
22
|
};
|
|
18
23
|
const PluginOptionSchema = utils_validation_1.Joi.object({
|
|
19
24
|
// @ts-expect-error: forbidden
|
|
20
25
|
cacheTime: utils_validation_1.Joi.forbidden().messages({
|
|
21
26
|
'any.unknown': 'Option `cacheTime` in sitemap config is deprecated. Please remove it.',
|
|
22
27
|
}),
|
|
28
|
+
// TODO remove for Docusaurus v4 breaking changes?
|
|
29
|
+
// This is not even used by Google crawlers
|
|
30
|
+
// See also https://github.com/facebook/docusaurus/issues/2604
|
|
23
31
|
changefreq: utils_validation_1.Joi.string()
|
|
24
|
-
.valid(...
|
|
32
|
+
.valid(null, ...types_1.ChangeFreqList)
|
|
25
33
|
.default(exports.DEFAULT_OPTIONS.changefreq),
|
|
26
|
-
|
|
34
|
+
// TODO remove for Docusaurus v4 breaking changes?
|
|
35
|
+
// This is not even used by Google crawlers
|
|
36
|
+
// The priority is "relative", and using the same priority for all routes
|
|
37
|
+
// does not make sense according to the spec
|
|
38
|
+
// See also https://github.com/facebook/docusaurus/issues/2604
|
|
39
|
+
// See also https://www.sitemaps.org/protocol.html
|
|
40
|
+
priority: utils_validation_1.Joi.alternatives()
|
|
41
|
+
.try(utils_validation_1.Joi.valid(null), utils_validation_1.Joi.number().min(0).max(1))
|
|
42
|
+
.default(exports.DEFAULT_OPTIONS.priority),
|
|
43
|
+
lastmod: utils_validation_1.Joi.string()
|
|
44
|
+
.valid(null, ...types_1.LastModOptionList)
|
|
45
|
+
.default(exports.DEFAULT_OPTIONS.lastmod),
|
|
27
46
|
ignorePatterns: utils_validation_1.Joi.array()
|
|
28
47
|
.items(utils_validation_1.Joi.string())
|
|
29
48
|
.default(exports.DEFAULT_OPTIONS.ignorePatterns),
|
package/lib/types.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
export declare const LastModOptionList: readonly ["date", "datetime"];
|
|
8
|
+
export type LastModOption = (typeof LastModOptionList)[number];
|
|
9
|
+
export declare const ChangeFreqList: readonly ["hourly", "daily", "weekly", "monthly", "yearly", "always", "never"];
|
|
10
|
+
export type ChangeFreq = (typeof ChangeFreqList)[number];
|
|
11
|
+
export type SitemapItem = {
|
|
12
|
+
/**
|
|
13
|
+
* URL of the page.
|
|
14
|
+
* This URL must begin with the protocol (such as http).
|
|
15
|
+
* It should eventually end with a trailing slash.
|
|
16
|
+
* It should be less than 2,048 characters.
|
|
17
|
+
*/
|
|
18
|
+
url: string;
|
|
19
|
+
/**
|
|
20
|
+
* ISO 8601 date string.
|
|
21
|
+
* See also https://www.w3.org/TR/NOTE-datetime
|
|
22
|
+
*
|
|
23
|
+
* It is recommended to use one of:
|
|
24
|
+
* - date.toISOString()
|
|
25
|
+
* - YYYY-MM-DD
|
|
26
|
+
*
|
|
27
|
+
* Note: as of 2024, Google uses this value for crawling priority.
|
|
28
|
+
* See also https://github.com/facebook/docusaurus/issues/2604
|
|
29
|
+
*/
|
|
30
|
+
lastmod?: string | null;
|
|
31
|
+
/**
|
|
32
|
+
* One of the specified enum values
|
|
33
|
+
*
|
|
34
|
+
* Note: as of 2024, Google ignores this value.
|
|
35
|
+
* See also https://github.com/facebook/docusaurus/issues/2604
|
|
36
|
+
*/
|
|
37
|
+
changefreq?: ChangeFreq | null;
|
|
38
|
+
/**
|
|
39
|
+
* The priority of this URL relative to other URLs on your site.
|
|
40
|
+
* Valid values range from 0.0 to 1.0.
|
|
41
|
+
* The default priority of a page is 0.5.
|
|
42
|
+
*
|
|
43
|
+
* Note: as of 2024, Google ignores this value.
|
|
44
|
+
* See also https://github.com/facebook/docusaurus/issues/2604
|
|
45
|
+
*/
|
|
46
|
+
priority?: number | null;
|
|
47
|
+
};
|
package/lib/types.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.ChangeFreqList = exports.LastModOptionList = void 0;
|
|
10
|
+
exports.LastModOptionList = ['date', 'datetime'];
|
|
11
|
+
// types are according to the sitemap spec:
|
|
12
|
+
// see also https://www.sitemaps.org/protocol.html
|
|
13
|
+
exports.ChangeFreqList = [
|
|
14
|
+
'hourly',
|
|
15
|
+
'daily',
|
|
16
|
+
'weekly',
|
|
17
|
+
'monthly',
|
|
18
|
+
'yearly',
|
|
19
|
+
'always',
|
|
20
|
+
'never',
|
|
21
|
+
];
|
package/lib/xml.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
import type { LastModOption, SitemapItem } from './types';
|
|
8
|
+
export declare function sitemapItemsToXmlString(items: SitemapItem[], options: {
|
|
9
|
+
lastmod: LastModOption | null;
|
|
10
|
+
}): Promise<string>;
|
package/lib/xml.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.sitemapItemsToXmlString = void 0;
|
|
10
|
+
const sitemap_1 = require("sitemap");
|
|
11
|
+
async function sitemapItemsToXmlString(items, options) {
|
|
12
|
+
if (items.length === 0) {
|
|
13
|
+
// Note: technically we could, but there is a bug in the lib code
|
|
14
|
+
// and the code below would never resolve, so it's better to fail fast
|
|
15
|
+
throw new Error("Can't generate a sitemap with no items");
|
|
16
|
+
}
|
|
17
|
+
// TODO remove sitemap lib dependency?
|
|
18
|
+
// https://github.com/ekalinin/sitemap.js
|
|
19
|
+
// it looks like an outdated confusion super old lib
|
|
20
|
+
// we might as well achieve the same result with a pure xml lib
|
|
21
|
+
const sitemapStream = new sitemap_1.SitemapStream({
|
|
22
|
+
// WTF is this lib reformatting the string YYYY-MM-DD to datetime...
|
|
23
|
+
lastmodDateOnly: options?.lastmod === 'date',
|
|
24
|
+
});
|
|
25
|
+
items.forEach((item) => sitemapStream.write(item));
|
|
26
|
+
sitemapStream.end();
|
|
27
|
+
const buffer = await (0, sitemap_1.streamToPromise)(sitemapStream);
|
|
28
|
+
return buffer.toString();
|
|
29
|
+
}
|
|
30
|
+
exports.sitemapItemsToXmlString = sitemapItemsToXmlString;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@docusaurus/plugin-sitemap",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "Simple sitemap generation plugin for Docusaurus.",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -18,16 +18,19 @@
|
|
|
18
18
|
},
|
|
19
19
|
"license": "MIT",
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@docusaurus/core": "3.
|
|
22
|
-
"@docusaurus/logger": "3.
|
|
23
|
-
"@docusaurus/types": "3.
|
|
24
|
-
"@docusaurus/utils": "3.
|
|
25
|
-
"@docusaurus/utils-common": "3.
|
|
26
|
-
"@docusaurus/utils-validation": "3.
|
|
21
|
+
"@docusaurus/core": "3.2.0",
|
|
22
|
+
"@docusaurus/logger": "3.2.0",
|
|
23
|
+
"@docusaurus/types": "3.2.0",
|
|
24
|
+
"@docusaurus/utils": "3.2.0",
|
|
25
|
+
"@docusaurus/utils-common": "3.2.0",
|
|
26
|
+
"@docusaurus/utils-validation": "3.2.0",
|
|
27
27
|
"fs-extra": "^11.1.1",
|
|
28
28
|
"sitemap": "^7.1.1",
|
|
29
29
|
"tslib": "^2.6.0"
|
|
30
30
|
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@total-typescript/shoehorn": "^0.1.2"
|
|
33
|
+
},
|
|
31
34
|
"peerDependencies": {
|
|
32
35
|
"react": "^18.0.0",
|
|
33
36
|
"react-dom": "^18.0.0"
|
|
@@ -35,5 +38,5 @@
|
|
|
35
38
|
"engines": {
|
|
36
39
|
"node": ">=18.0"
|
|
37
40
|
},
|
|
38
|
-
"gitHead": "
|
|
41
|
+
"gitHead": "5af143651b26b39761361acd96e9c5be7ba0cb25"
|
|
39
42
|
}
|
package/src/createSitemap.ts
CHANGED
|
@@ -6,13 +6,23 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type {ReactElement} from 'react';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import type {
|
|
9
|
+
import {createMatcher, flattenRoutes} from '@docusaurus/utils';
|
|
10
|
+
import {sitemapItemsToXmlString} from './xml';
|
|
11
|
+
import {createSitemapItem} from './createSitemapItem';
|
|
12
|
+
import type {SitemapItem} from './types';
|
|
13
|
+
import type {DocusaurusConfig, RouteConfig} from '@docusaurus/types';
|
|
13
14
|
import type {HelmetServerState} from 'react-helmet-async';
|
|
14
15
|
import type {PluginOptions} from './options';
|
|
15
16
|
|
|
17
|
+
type CreateSitemapParams = {
|
|
18
|
+
siteConfig: DocusaurusConfig;
|
|
19
|
+
routes: RouteConfig[];
|
|
20
|
+
head: {[location: string]: HelmetServerState};
|
|
21
|
+
options: PluginOptions;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Maybe we want to add a routeConfig.metadata.noIndex instead?
|
|
25
|
+
// But using Helmet is more reliable for third-party plugins...
|
|
16
26
|
function isNoIndexMetaRoute({
|
|
17
27
|
head,
|
|
18
28
|
route,
|
|
@@ -47,50 +57,51 @@ function isNoIndexMetaRoute({
|
|
|
47
57
|
);
|
|
48
58
|
}
|
|
49
59
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const {url: hostname} = siteConfig;
|
|
57
|
-
if (!hostname) {
|
|
58
|
-
throw new Error('URL in docusaurus.config.js cannot be empty/undefined.');
|
|
59
|
-
}
|
|
60
|
-
const {changefreq, priority, ignorePatterns} = options;
|
|
60
|
+
// Not all routes should appear in the sitemap, and we should filter:
|
|
61
|
+
// - parent routes, used for layouts
|
|
62
|
+
// - routes matching options.ignorePatterns
|
|
63
|
+
// - routes with no index metadata
|
|
64
|
+
function getSitemapRoutes({routes, head, options}: CreateSitemapParams) {
|
|
65
|
+
const {ignorePatterns} = options;
|
|
61
66
|
|
|
62
67
|
const ignoreMatcher = createMatcher(ignorePatterns);
|
|
63
68
|
|
|
64
|
-
function isRouteExcluded(route:
|
|
69
|
+
function isRouteExcluded(route: RouteConfig) {
|
|
65
70
|
return (
|
|
66
|
-
route.
|
|
67
|
-
ignoreMatcher(route) ||
|
|
68
|
-
isNoIndexMetaRoute({head, route})
|
|
71
|
+
ignoreMatcher(route.path) || isNoIndexMetaRoute({head, route: route.path})
|
|
69
72
|
);
|
|
70
73
|
}
|
|
71
74
|
|
|
72
|
-
|
|
75
|
+
return flattenRoutes(routes).filter((route) => !isRouteExcluded(route));
|
|
76
|
+
}
|
|
73
77
|
|
|
74
|
-
|
|
75
|
-
|
|
78
|
+
async function createSitemapItems(
|
|
79
|
+
params: CreateSitemapParams,
|
|
80
|
+
): Promise<SitemapItem[]> {
|
|
81
|
+
const sitemapRoutes = getSitemapRoutes(params);
|
|
82
|
+
if (sitemapRoutes.length === 0) {
|
|
83
|
+
return [];
|
|
76
84
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
trailingSlash: siteConfig.trailingSlash,
|
|
84
|
-
baseUrl: siteConfig.baseUrl,
|
|
85
|
+
return Promise.all(
|
|
86
|
+
sitemapRoutes.map((route) =>
|
|
87
|
+
createSitemapItem({
|
|
88
|
+
route,
|
|
89
|
+
siteConfig: params.siteConfig,
|
|
90
|
+
options: params.options,
|
|
85
91
|
}),
|
|
86
|
-
|
|
87
|
-
priority,
|
|
88
|
-
}),
|
|
92
|
+
),
|
|
89
93
|
);
|
|
94
|
+
}
|
|
90
95
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
+
export default async function createSitemap(
|
|
97
|
+
params: CreateSitemapParams,
|
|
98
|
+
): Promise<string | null> {
|
|
99
|
+
const items = await createSitemapItems(params);
|
|
100
|
+
if (items.length === 0) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
const xmlString = await sitemapItemsToXmlString(items, {
|
|
104
|
+
lastmod: params.options.lastmod,
|
|
105
|
+
});
|
|
106
|
+
return xmlString;
|
|
96
107
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {applyTrailingSlash} from '@docusaurus/utils-common';
|
|
9
|
+
import {getLastUpdate, normalizeUrl} from '@docusaurus/utils';
|
|
10
|
+
import type {LastModOption, SitemapItem} from './types';
|
|
11
|
+
import type {DocusaurusConfig, RouteConfig} from '@docusaurus/types';
|
|
12
|
+
import type {PluginOptions} from './options';
|
|
13
|
+
|
|
14
|
+
async function getRouteLastUpdatedAt(
|
|
15
|
+
route: RouteConfig,
|
|
16
|
+
): Promise<number | undefined> {
|
|
17
|
+
if (route.metadata?.lastUpdatedAt) {
|
|
18
|
+
return route.metadata?.lastUpdatedAt;
|
|
19
|
+
}
|
|
20
|
+
if (route.metadata?.sourceFilePath) {
|
|
21
|
+
const lastUpdate = await getLastUpdate(route.metadata?.sourceFilePath);
|
|
22
|
+
return lastUpdate?.lastUpdatedAt;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type LastModFormatter = (timestamp: number) => string;
|
|
29
|
+
|
|
30
|
+
const LastmodFormatters: Record<LastModOption, LastModFormatter> = {
|
|
31
|
+
date: (timestamp) => new Date(timestamp).toISOString().split('T')[0]!,
|
|
32
|
+
datetime: (timestamp) => new Date(timestamp).toISOString(),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function formatLastmod(timestamp: number, lastmodOption: LastModOption) {
|
|
36
|
+
const format = LastmodFormatters[lastmodOption];
|
|
37
|
+
return format(timestamp);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function getRouteLastmod({
|
|
41
|
+
route,
|
|
42
|
+
lastmod,
|
|
43
|
+
}: {
|
|
44
|
+
route: RouteConfig;
|
|
45
|
+
lastmod: LastModOption | null;
|
|
46
|
+
}): Promise<string | null> {
|
|
47
|
+
if (lastmod === null) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
const lastUpdatedAt = (await getRouteLastUpdatedAt(route)) ?? null;
|
|
51
|
+
return lastUpdatedAt ? formatLastmod(lastUpdatedAt, lastmod) : null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function createSitemapItem({
|
|
55
|
+
route,
|
|
56
|
+
siteConfig,
|
|
57
|
+
options,
|
|
58
|
+
}: {
|
|
59
|
+
route: RouteConfig;
|
|
60
|
+
siteConfig: DocusaurusConfig;
|
|
61
|
+
options: PluginOptions;
|
|
62
|
+
}): Promise<SitemapItem> {
|
|
63
|
+
const {changefreq, priority, lastmod} = options;
|
|
64
|
+
return {
|
|
65
|
+
url: normalizeUrl([
|
|
66
|
+
siteConfig.url,
|
|
67
|
+
applyTrailingSlash(route.path, {
|
|
68
|
+
trailingSlash: siteConfig.trailingSlash,
|
|
69
|
+
baseUrl: siteConfig.baseUrl,
|
|
70
|
+
}),
|
|
71
|
+
]),
|
|
72
|
+
changefreq,
|
|
73
|
+
priority,
|
|
74
|
+
lastmod: await getRouteLastmod({route, lastmod}),
|
|
75
|
+
};
|
|
76
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -19,17 +19,17 @@ export default function pluginSitemap(
|
|
|
19
19
|
return {
|
|
20
20
|
name: 'docusaurus-plugin-sitemap',
|
|
21
21
|
|
|
22
|
-
async postBuild({siteConfig,
|
|
22
|
+
async postBuild({siteConfig, routes, outDir, head}) {
|
|
23
23
|
if (siteConfig.noIndex) {
|
|
24
24
|
return;
|
|
25
25
|
}
|
|
26
26
|
// Generate sitemap.
|
|
27
|
-
const generatedSitemap = await createSitemap(
|
|
27
|
+
const generatedSitemap = await createSitemap({
|
|
28
28
|
siteConfig,
|
|
29
|
-
|
|
29
|
+
routes,
|
|
30
30
|
head,
|
|
31
31
|
options,
|
|
32
|
-
);
|
|
32
|
+
});
|
|
33
33
|
if (!generatedSitemap) {
|
|
34
34
|
return;
|
|
35
35
|
}
|
package/src/options.ts
CHANGED
|
@@ -6,33 +6,60 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import {Joi} from '@docusaurus/utils-validation';
|
|
9
|
-
import {
|
|
9
|
+
import {ChangeFreqList, LastModOptionList} from './types';
|
|
10
10
|
import type {OptionValidationContext} from '@docusaurus/types';
|
|
11
|
+
import type {ChangeFreq, LastModOption} from './types';
|
|
11
12
|
|
|
12
13
|
export type PluginOptions = {
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
/**
|
|
15
|
+
* The path to the created sitemap file, relative to the output directory.
|
|
16
|
+
* Useful if you have two plugin instances outputting two files.
|
|
17
|
+
*/
|
|
18
|
+
filename: string;
|
|
19
|
+
|
|
17
20
|
/**
|
|
18
21
|
* A list of glob patterns; matching route paths will be filtered from the
|
|
19
22
|
* sitemap. Note that you may need to include the base URL in here.
|
|
20
23
|
*/
|
|
21
24
|
ignorePatterns: string[];
|
|
25
|
+
|
|
22
26
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
27
|
+
* Defines the format of the "lastmod" sitemap item entry, between:
|
|
28
|
+
* - null: do not compute/add a "lastmod" sitemap entry
|
|
29
|
+
* - "date": add a "lastmod" sitemap entry without time (YYYY-MM-DD)
|
|
30
|
+
* - "datetime": add a "lastmod" sitemap entry with time (ISO 8601 datetime)
|
|
31
|
+
* @see https://www.sitemaps.org/protocol.html#xmlTagDefinitions
|
|
32
|
+
* @see https://www.w3.org/TR/NOTE-datetime
|
|
25
33
|
*/
|
|
26
|
-
|
|
34
|
+
lastmod: LastModOption | null;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* TODO Docusaurus v4 breaking change: remove useless option
|
|
38
|
+
* @see https://www.sitemaps.org/protocol.html#xmlTagDefinitions
|
|
39
|
+
*/
|
|
40
|
+
changefreq: ChangeFreq | null;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* TODO Docusaurus v4 breaking change: remove useless option
|
|
44
|
+
* @see https://www.sitemaps.org/protocol.html#xmlTagDefinitions
|
|
45
|
+
*/
|
|
46
|
+
priority: number | null;
|
|
27
47
|
};
|
|
28
48
|
|
|
29
49
|
export type Options = Partial<PluginOptions>;
|
|
30
50
|
|
|
31
51
|
export const DEFAULT_OPTIONS: PluginOptions = {
|
|
32
|
-
changefreq: EnumChangefreq.WEEKLY,
|
|
33
|
-
priority: 0.5,
|
|
34
|
-
ignorePatterns: [],
|
|
35
52
|
filename: 'sitemap.xml',
|
|
53
|
+
ignorePatterns: [],
|
|
54
|
+
|
|
55
|
+
// TODO Docusaurus v4 breaking change
|
|
56
|
+
// change default to "date" if no bug or perf issue reported
|
|
57
|
+
lastmod: null,
|
|
58
|
+
|
|
59
|
+
// TODO Docusaurus v4 breaking change
|
|
60
|
+
// those options are useless and should be removed
|
|
61
|
+
changefreq: 'weekly',
|
|
62
|
+
priority: 0.5,
|
|
36
63
|
};
|
|
37
64
|
|
|
38
65
|
const PluginOptionSchema = Joi.object<PluginOptions>({
|
|
@@ -41,10 +68,28 @@ const PluginOptionSchema = Joi.object<PluginOptions>({
|
|
|
41
68
|
'any.unknown':
|
|
42
69
|
'Option `cacheTime` in sitemap config is deprecated. Please remove it.',
|
|
43
70
|
}),
|
|
71
|
+
|
|
72
|
+
// TODO remove for Docusaurus v4 breaking changes?
|
|
73
|
+
// This is not even used by Google crawlers
|
|
74
|
+
// See also https://github.com/facebook/docusaurus/issues/2604
|
|
44
75
|
changefreq: Joi.string()
|
|
45
|
-
.valid(...
|
|
76
|
+
.valid(null, ...ChangeFreqList)
|
|
46
77
|
.default(DEFAULT_OPTIONS.changefreq),
|
|
47
|
-
|
|
78
|
+
|
|
79
|
+
// TODO remove for Docusaurus v4 breaking changes?
|
|
80
|
+
// This is not even used by Google crawlers
|
|
81
|
+
// The priority is "relative", and using the same priority for all routes
|
|
82
|
+
// does not make sense according to the spec
|
|
83
|
+
// See also https://github.com/facebook/docusaurus/issues/2604
|
|
84
|
+
// See also https://www.sitemaps.org/protocol.html
|
|
85
|
+
priority: Joi.alternatives()
|
|
86
|
+
.try(Joi.valid(null), Joi.number().min(0).max(1))
|
|
87
|
+
.default(DEFAULT_OPTIONS.priority),
|
|
88
|
+
|
|
89
|
+
lastmod: Joi.string()
|
|
90
|
+
.valid(null, ...LastModOptionList)
|
|
91
|
+
.default(DEFAULT_OPTIONS.lastmod),
|
|
92
|
+
|
|
48
93
|
ignorePatterns: Joi.array()
|
|
49
94
|
.items(Joi.string())
|
|
50
95
|
.default(DEFAULT_OPTIONS.ignorePatterns),
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const LastModOptionList = ['date', 'datetime'] as const;
|
|
9
|
+
|
|
10
|
+
export type LastModOption = (typeof LastModOptionList)[number];
|
|
11
|
+
|
|
12
|
+
// types are according to the sitemap spec:
|
|
13
|
+
// see also https://www.sitemaps.org/protocol.html
|
|
14
|
+
|
|
15
|
+
export const ChangeFreqList = [
|
|
16
|
+
'hourly',
|
|
17
|
+
'daily',
|
|
18
|
+
'weekly',
|
|
19
|
+
'monthly',
|
|
20
|
+
'yearly',
|
|
21
|
+
'always',
|
|
22
|
+
'never',
|
|
23
|
+
] as const;
|
|
24
|
+
|
|
25
|
+
export type ChangeFreq = (typeof ChangeFreqList)[number];
|
|
26
|
+
|
|
27
|
+
// We re-recreate our own type because the "sitemap" lib types are not good
|
|
28
|
+
export type SitemapItem = {
|
|
29
|
+
/**
|
|
30
|
+
* URL of the page.
|
|
31
|
+
* This URL must begin with the protocol (such as http).
|
|
32
|
+
* It should eventually end with a trailing slash.
|
|
33
|
+
* It should be less than 2,048 characters.
|
|
34
|
+
*/
|
|
35
|
+
url: string;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* ISO 8601 date string.
|
|
39
|
+
* See also https://www.w3.org/TR/NOTE-datetime
|
|
40
|
+
*
|
|
41
|
+
* It is recommended to use one of:
|
|
42
|
+
* - date.toISOString()
|
|
43
|
+
* - YYYY-MM-DD
|
|
44
|
+
*
|
|
45
|
+
* Note: as of 2024, Google uses this value for crawling priority.
|
|
46
|
+
* See also https://github.com/facebook/docusaurus/issues/2604
|
|
47
|
+
*/
|
|
48
|
+
lastmod?: string | null;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* One of the specified enum values
|
|
52
|
+
*
|
|
53
|
+
* Note: as of 2024, Google ignores this value.
|
|
54
|
+
* See also https://github.com/facebook/docusaurus/issues/2604
|
|
55
|
+
*/
|
|
56
|
+
changefreq?: ChangeFreq | null;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The priority of this URL relative to other URLs on your site.
|
|
60
|
+
* Valid values range from 0.0 to 1.0.
|
|
61
|
+
* The default priority of a page is 0.5.
|
|
62
|
+
*
|
|
63
|
+
* Note: as of 2024, Google ignores this value.
|
|
64
|
+
* See also https://github.com/facebook/docusaurus/issues/2604
|
|
65
|
+
*/
|
|
66
|
+
priority?: number | null;
|
|
67
|
+
};
|
package/src/xml.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {SitemapStream, streamToPromise} from 'sitemap';
|
|
9
|
+
import type {LastModOption, SitemapItem} from './types';
|
|
10
|
+
|
|
11
|
+
export async function sitemapItemsToXmlString(
|
|
12
|
+
items: SitemapItem[],
|
|
13
|
+
options: {lastmod: LastModOption | null},
|
|
14
|
+
): Promise<string> {
|
|
15
|
+
if (items.length === 0) {
|
|
16
|
+
// Note: technically we could, but there is a bug in the lib code
|
|
17
|
+
// and the code below would never resolve, so it's better to fail fast
|
|
18
|
+
throw new Error("Can't generate a sitemap with no items");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// TODO remove sitemap lib dependency?
|
|
22
|
+
// https://github.com/ekalinin/sitemap.js
|
|
23
|
+
// it looks like an outdated confusion super old lib
|
|
24
|
+
// we might as well achieve the same result with a pure xml lib
|
|
25
|
+
const sitemapStream = new SitemapStream({
|
|
26
|
+
// WTF is this lib reformatting the string YYYY-MM-DD to datetime...
|
|
27
|
+
lastmodDateOnly: options?.lastmod === 'date',
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
items.forEach((item) => sitemapStream.write(item));
|
|
31
|
+
sitemapStream.end();
|
|
32
|
+
|
|
33
|
+
const buffer = await streamToPromise(sitemapStream);
|
|
34
|
+
return buffer.toString();
|
|
35
|
+
}
|