@azerion/bunosaurus-plugin-sitemap 0.0.0-stage → 4.0.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/README.md CHANGED
@@ -1,3 +1,7 @@
1
- # Temporary Holding Version
1
+ # `@docusaurus/plugin-sitemap`
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Simple sitemap generation plugin for Docusaurus.
4
+
5
+ ## Usage
6
+
7
+ See [plugin-sitemap documentation](https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-sitemap).
@@ -0,0 +1,17 @@
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 { CreateSitemapItemsParams } from './types';
8
+ import type { RouteBuildMetadata } from '@docusaurus/types';
9
+ import type { PluginOptions } from './options';
10
+ type CreateSitemapParams = CreateSitemapItemsParams & {
11
+ routesBuildMetadata: {
12
+ [location: string]: RouteBuildMetadata;
13
+ };
14
+ options: PluginOptions;
15
+ };
16
+ export default function createSitemap(params: CreateSitemapParams): Promise<string | null>;
17
+ export {};
@@ -0,0 +1,62 @@
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.default = createSitemap;
10
+ const utils_1 = require("@docusaurus/utils");
11
+ const glob_1 = require("@docusaurus/glob");
12
+ const xml_1 = require("./xml");
13
+ const createSitemapItem_1 = require("./createSitemapItem");
14
+ const head_1 = require("./head");
15
+ // Not all routes should appear in the sitemap, and we should filter:
16
+ // - parent routes, used for layouts
17
+ // - routes matching options.ignorePatterns
18
+ // - routes with no index metadata
19
+ function getSitemapRoutes({ routes, routesBuildMetadata, options, }) {
20
+ const { ignorePatterns } = options;
21
+ const ignoreMatcher = (0, glob_1.createMatcher)(ignorePatterns);
22
+ function isRouteExcluded(route) {
23
+ return (ignoreMatcher(route.path) ||
24
+ (0, head_1.isNoIndexMetaRoute)({ routesBuildMetadata, route: route.path }));
25
+ }
26
+ return (0, utils_1.flattenRoutes)(routes).filter((route) => !isRouteExcluded(route));
27
+ }
28
+ // Our default implementation receives some additional parameters on purpose
29
+ function createDefaultCreateSitemapItems(internalParams) {
30
+ return async (params) => {
31
+ const sitemapRoutes = getSitemapRoutes({ ...params, ...internalParams });
32
+ if (sitemapRoutes.length === 0) {
33
+ return [];
34
+ }
35
+ return Promise.all(sitemapRoutes.map((route) => (0, createSitemapItem_1.createSitemapItem)({
36
+ route,
37
+ siteConfig: params.siteConfig,
38
+ options: internalParams.options,
39
+ })));
40
+ };
41
+ }
42
+ async function createSitemap(params) {
43
+ const { routesBuildMetadata, options, routes, siteConfig } = params;
44
+ const defaultCreateSitemapItems = createDefaultCreateSitemapItems({ routesBuildMetadata, options });
45
+ const sitemapItems = params.options.createSitemapItems
46
+ ? await params.options.createSitemapItems({
47
+ routes,
48
+ siteConfig,
49
+ defaultCreateSitemapItems,
50
+ })
51
+ : await defaultCreateSitemapItems({
52
+ routes,
53
+ siteConfig,
54
+ });
55
+ if (sitemapItems.length === 0) {
56
+ return null;
57
+ }
58
+ const xmlString = await (0, xml_1.sitemapItemsToXmlString)(sitemapItems, {
59
+ lastmod: params.options.lastmod,
60
+ });
61
+ return xmlString;
62
+ }
@@ -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,61 @@
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 = createSitemapItem;
10
+ const utils_common_1 = require("@docusaurus/utils-common");
11
+ const utils_1 = require("@docusaurus/utils");
12
+ async function getRouteLastUpdatedAt(route, vcs) {
13
+ // Important to bail-out early here
14
+ // This can lead to duplicated VCS calls and performance problems
15
+ // See https://github.com/facebook/docusaurus/pull/11211
16
+ if (route.metadata?.lastUpdatedAt === null) {
17
+ return null;
18
+ }
19
+ if (route.metadata?.lastUpdatedAt != null) {
20
+ return route.metadata?.lastUpdatedAt;
21
+ }
22
+ if (route.metadata?.sourceFilePath) {
23
+ const lastUpdateInfo = await vcs.getFileLastUpdateInfo(route.metadata?.sourceFilePath);
24
+ return lastUpdateInfo?.timestamp ?? null;
25
+ }
26
+ return undefined;
27
+ }
28
+ const LastmodFormatters = {
29
+ date: (timestamp) => new Date(timestamp).toISOString().split('T')[0],
30
+ datetime: (timestamp) => new Date(timestamp).toISOString(),
31
+ };
32
+ function formatLastmod(timestamp, lastmodOption) {
33
+ const format = LastmodFormatters[lastmodOption];
34
+ return format(timestamp);
35
+ }
36
+ async function getRouteLastmod({ route, lastmod, vcs, }) {
37
+ if (lastmod === null) {
38
+ return null;
39
+ }
40
+ const lastUpdatedAt = (await getRouteLastUpdatedAt(route, vcs)) ?? null;
41
+ return lastUpdatedAt != null ? formatLastmod(lastUpdatedAt, lastmod) : null;
42
+ }
43
+ async function createSitemapItem({ route, siteConfig, options, }) {
44
+ const { changefreq, priority, lastmod } = options;
45
+ return {
46
+ url: (0, utils_1.normalizeUrl)([
47
+ siteConfig.url,
48
+ (0, utils_common_1.applyTrailingSlash)(route.path, {
49
+ trailingSlash: siteConfig.trailingSlash,
50
+ baseUrl: siteConfig.baseUrl,
51
+ }),
52
+ ]),
53
+ changefreq,
54
+ priority,
55
+ lastmod: await getRouteLastmod({
56
+ route,
57
+ lastmod,
58
+ vcs: siteConfig.vcs,
59
+ }),
60
+ };
61
+ }
package/lib/head.d.ts ADDED
@@ -0,0 +1,13 @@
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 { RouteBuildMetadata } from '@docusaurus/types';
8
+ export declare function isNoIndexMetaRoute({ routesBuildMetadata, route, }: {
9
+ routesBuildMetadata: {
10
+ [location: string]: RouteBuildMetadata;
11
+ };
12
+ route: string;
13
+ }): boolean;
package/lib/head.js ADDED
@@ -0,0 +1,18 @@
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.isNoIndexMetaRoute = isNoIndexMetaRoute;
10
+ // Maybe we want to add a routeConfig.metadata.noIndex instead?
11
+ // But using Helmet is more reliable for third-party plugins...
12
+ function isNoIndexMetaRoute({ routesBuildMetadata, route, }) {
13
+ const routeBuildMetadata = routesBuildMetadata[route];
14
+ if (routeBuildMetadata) {
15
+ return routeBuildMetadata.noIndex;
16
+ }
17
+ return false;
18
+ }
package/lib/index.d.ts ADDED
@@ -0,0 +1,11 @@
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 { PluginOptions, Options } from './options';
8
+ import type { LoadContext, Plugin } from '@docusaurus/types';
9
+ export default function pluginSitemap(context: LoadContext, options: PluginOptions): Plugin<void> | null;
10
+ export { validateOptions } from './options';
11
+ export type { PluginOptions, Options };
package/lib/index.js ADDED
@@ -0,0 +1,51 @@
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.validateOptions = void 0;
10
+ exports.default = pluginSitemap;
11
+ const tslib_1 = require("tslib");
12
+ const node_path_1 = tslib_1.__importDefault(require("node:path"));
13
+ const logger_1 = tslib_1.__importDefault(require("@docusaurus/logger"));
14
+ const fs_1 = require("@docusaurus/fs");
15
+ const createSitemap_1 = tslib_1.__importDefault(require("./createSitemap"));
16
+ const PluginName = 'docusaurus-plugin-sitemap';
17
+ function pluginSitemap(context, options) {
18
+ if (context.siteConfig.future.experimental_router === 'hash') {
19
+ logger_1.default.warn(`${PluginName} does not support the Hash Router and will be disabled.`);
20
+ return null;
21
+ }
22
+ return {
23
+ name: PluginName,
24
+ async postBuild({ siteConfig, routes, outDir, routesBuildMetadata }) {
25
+ if (siteConfig.noIndex) {
26
+ return;
27
+ }
28
+ // Generate sitemap.
29
+ const generatedSitemap = await (0, createSitemap_1.default)({
30
+ siteConfig,
31
+ routes,
32
+ routesBuildMetadata,
33
+ options,
34
+ });
35
+ if (!generatedSitemap) {
36
+ return;
37
+ }
38
+ // Write sitemap file.
39
+ const sitemapPath = node_path_1.default.join(outDir, options.filename);
40
+ try {
41
+ await (0, fs_1.outputFile)(sitemapPath, generatedSitemap);
42
+ }
43
+ catch (err) {
44
+ logger_1.default.error('Writing sitemap failed.');
45
+ throw err;
46
+ }
47
+ },
48
+ };
49
+ }
50
+ var options_1 = require("./options");
51
+ Object.defineProperty(exports, "validateOptions", { enumerable: true, get: function () { return options_1.validateOptions; } });
@@ -0,0 +1,48 @@
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 { OptionValidationContext } from '@docusaurus/types';
8
+ import type { ChangeFreq, LastModOption, SitemapItem, CreateSitemapItemsFn, CreateSitemapItemsParams } from './types';
9
+ export type PluginOptions = {
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;
15
+ /**
16
+ * A list of glob patterns; matching route paths will be filtered from the
17
+ * sitemap. Note that you may need to include the base URL in here.
18
+ */
19
+ ignorePatterns: string[];
20
+ /**
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
27
+ */
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;
39
+ /** Allow control over the construction of SitemapItems */
40
+ createSitemapItems?: CreateSitemapItemsOption;
41
+ };
42
+ type CreateSitemapItemsOption = (params: CreateSitemapItemsParams & {
43
+ defaultCreateSitemapItems: CreateSitemapItemsFn;
44
+ }) => Promise<SitemapItem[]>;
45
+ export type Options = Partial<PluginOptions>;
46
+ export declare const DEFAULT_OPTIONS: PluginOptions;
47
+ export declare function validateOptions({ validate, options, }: OptionValidationContext<Options, PluginOptions>): PluginOptions;
48
+ export {};
package/lib/options.js ADDED
@@ -0,0 +1,59 @@
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.DEFAULT_OPTIONS = void 0;
10
+ exports.validateOptions = validateOptions;
11
+ const utils_validation_1 = require("@docusaurus/utils-validation");
12
+ const types_1 = require("./types");
13
+ exports.DEFAULT_OPTIONS = {
14
+ filename: 'sitemap.xml',
15
+ ignorePatterns: [],
16
+ // TODO Docusaurus v4 breaking change
17
+ // change default to "date" if no bug or perf issue reported
18
+ lastmod: null,
19
+ // TODO Docusaurus v4 breaking change
20
+ // those options are useless and should be removed
21
+ changefreq: 'weekly',
22
+ priority: 0.5,
23
+ };
24
+ const PluginOptionSchema = utils_validation_1.Joi.object({
25
+ // @ts-expect-error: forbidden
26
+ cacheTime: utils_validation_1.Joi.forbidden().messages({
27
+ 'any.unknown': 'Option `cacheTime` in sitemap config is deprecated. Please remove it.',
28
+ }),
29
+ // TODO remove for Docusaurus v4 breaking changes?
30
+ // This is not even used by Google crawlers
31
+ // See also https://github.com/facebook/docusaurus/issues/2604
32
+ changefreq: utils_validation_1.Joi.string()
33
+ .valid(null, ...types_1.ChangeFreqList)
34
+ .default(exports.DEFAULT_OPTIONS.changefreq),
35
+ // TODO remove for Docusaurus v4 breaking changes?
36
+ // This is not even used by Google crawlers
37
+ // The priority is "relative", and using the same priority for all routes
38
+ // does not make sense according to the spec
39
+ // See also https://github.com/facebook/docusaurus/issues/2604
40
+ // See also https://www.sitemaps.org/protocol.html
41
+ priority: utils_validation_1.Joi.alternatives()
42
+ .try(utils_validation_1.Joi.valid(null), utils_validation_1.Joi.number().min(0).max(1))
43
+ .default(exports.DEFAULT_OPTIONS.priority),
44
+ lastmod: utils_validation_1.Joi.string()
45
+ .valid(null, ...types_1.LastModOptionList)
46
+ .default(exports.DEFAULT_OPTIONS.lastmod),
47
+ createSitemapItems: utils_validation_1.Joi.function(),
48
+ ignorePatterns: utils_validation_1.Joi.array()
49
+ .items(utils_validation_1.Joi.string())
50
+ .default(exports.DEFAULT_OPTIONS.ignorePatterns),
51
+ trailingSlash: utils_validation_1.Joi.forbidden().messages({
52
+ 'any.unknown': 'Please use the new Docusaurus global trailingSlash config instead, and the sitemaps plugin will use it.',
53
+ }),
54
+ filename: utils_validation_1.Joi.string().default(exports.DEFAULT_OPTIONS.filename),
55
+ });
56
+ function validateOptions({ validate, options, }) {
57
+ const validatedOptions = validate(PluginOptionSchema, options);
58
+ return validatedOptions;
59
+ }
package/lib/types.d.ts ADDED
@@ -0,0 +1,53 @@
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 { DocusaurusConfig, RouteConfig } from '@docusaurus/types';
8
+ export declare const LastModOptionList: readonly ['date', 'datetime'];
9
+ export type LastModOption = (typeof LastModOptionList)[number];
10
+ export declare const ChangeFreqList: readonly ['hourly', 'daily', 'weekly', 'monthly', 'yearly', 'always', 'never'];
11
+ export type ChangeFreq = (typeof ChangeFreqList)[number];
12
+ export type SitemapItem = {
13
+ /**
14
+ * URL of the page.
15
+ * This URL must begin with the protocol (such as http).
16
+ * It should eventually end with a trailing slash.
17
+ * It should be less than 2,048 characters.
18
+ */
19
+ url: string;
20
+ /**
21
+ * ISO 8601 date string.
22
+ * See also https://www.w3.org/TR/NOTE-datetime
23
+ *
24
+ * It is recommended to use one of:
25
+ * - date.toISOString()
26
+ * - YYYY-MM-DD
27
+ *
28
+ * Note: as of 2024, Google uses this value for crawling priority.
29
+ * See also https://github.com/facebook/docusaurus/issues/2604
30
+ */
31
+ lastmod?: string | null;
32
+ /**
33
+ * One of the specified enum values
34
+ *
35
+ * Note: as of 2024, Google ignores this value.
36
+ * See also https://github.com/facebook/docusaurus/issues/2604
37
+ */
38
+ changefreq?: ChangeFreq | null;
39
+ /**
40
+ * The priority of this URL relative to other URLs on your site.
41
+ * Valid values range from 0.0 to 1.0.
42
+ * The default priority of a page is 0.5.
43
+ *
44
+ * Note: as of 2024, Google ignores this value.
45
+ * See also https://github.com/facebook/docusaurus/issues/2604
46
+ */
47
+ priority?: number | null;
48
+ };
49
+ export type CreateSitemapItemsParams = {
50
+ siteConfig: DocusaurusConfig;
51
+ routes: RouteConfig[];
52
+ };
53
+ export type CreateSitemapItemsFn = (params: CreateSitemapItemsParams) => Promise<SitemapItem[]>;
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,28 @@
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 = sitemapItemsToXmlString;
10
+ const sitemap_1 = require("sitemap");
11
+ async function sitemapItemsToXmlString(items, options) {
12
+ if (items.length === 0) {
13
+ // Fail fast with a clearer error than the lib's EmptySitemap error
14
+ throw new Error("Can't generate a sitemap with no items");
15
+ }
16
+ // We could generate the XML ourselves, but we keep using the lib on purpose
17
+ // Users can return undocumented fields from createSitemapItems()
18
+ // (links, img, video, news...) that the lib serializes
19
+ // See https://github.com/ekalinin/sitemap.js
20
+ const sitemapStream = new sitemap_1.SitemapStream({
21
+ // WTF is this lib reformatting the string YYYY-MM-DD to datetime...
22
+ lastmodDateOnly: options?.lastmod === 'date',
23
+ });
24
+ items.forEach((item) => sitemapStream.write(item));
25
+ sitemapStream.end();
26
+ const buffer = await (0, sitemap_1.streamToPromise)(sitemapStream);
27
+ return buffer.toString();
28
+ }
package/package.json CHANGED
@@ -1,6 +1,42 @@
1
1
  {
2
2
  "name": "@azerion/bunosaurus-plugin-sitemap",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "4.0.0",
4
+ "description": "Simple sitemap generation plugin for Docusaurus.",
5
+ "main": "lib/index.js",
6
+ "types": "lib/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "watch": "tsc --watch"
10
+ },
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/azerion/bunosaurus.git",
17
+ "directory": "packages/docusaurus-plugin-sitemap"
18
+ },
19
+ "license": "MIT",
20
+ "dependencies": {
21
+ "@docusaurus/core": "npm:@azerion/bunosaurus-core@4.0.0",
22
+ "@docusaurus/fs": "npm:@azerion/bunosaurus-fs@4.0.0",
23
+ "@docusaurus/glob": "npm:@azerion/bunosaurus-glob@4.0.0",
24
+ "@docusaurus/logger": "npm:@azerion/bunosaurus-logger@4.0.0",
25
+ "@docusaurus/types": "npm:@azerion/bunosaurus-types@4.0.0",
26
+ "@docusaurus/utils": "npm:@azerion/bunosaurus-utils@4.0.0",
27
+ "@docusaurus/utils-common": "npm:@azerion/bunosaurus-utils-common@4.0.0",
28
+ "@docusaurus/utils-validation": "npm:@azerion/bunosaurus-utils-validation@4.0.0",
29
+ "sitemap": "^9.0.1",
30
+ "tslib": "^2.8.1"
31
+ },
32
+ "devDependencies": {
33
+ "@total-typescript/shoehorn": "^0.1.2"
34
+ },
35
+ "peerDependencies": {
36
+ "react": "^19.3.0",
37
+ "react-dom": "^19.3.0"
38
+ },
39
+ "engines": {
40
+ "bun": ">=1.4.1"
41
+ }
42
+ }
@@ -0,0 +1,93 @@
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 {flattenRoutes} from '@docusaurus/utils';
9
+ import {createMatcher} from '@docusaurus/glob';
10
+ import {sitemapItemsToXmlString} from './xml';
11
+ import {createSitemapItem} from './createSitemapItem';
12
+ import {isNoIndexMetaRoute} from './head';
13
+ import type {CreateSitemapItemsFn, CreateSitemapItemsParams} from './types';
14
+ import type {RouteConfig, RouteBuildMetadata} from '@docusaurus/types';
15
+ import type {PluginOptions} from './options';
16
+
17
+ // Not all routes should appear in the sitemap, and we should filter:
18
+ // - parent routes, used for layouts
19
+ // - routes matching options.ignorePatterns
20
+ // - routes with no index metadata
21
+ function getSitemapRoutes({
22
+ routes,
23
+ routesBuildMetadata,
24
+ options,
25
+ }: CreateSitemapParams) {
26
+ const {ignorePatterns} = options;
27
+
28
+ const ignoreMatcher = createMatcher(ignorePatterns);
29
+
30
+ function isRouteExcluded(route: RouteConfig) {
31
+ return (
32
+ ignoreMatcher(route.path) ||
33
+ isNoIndexMetaRoute({routesBuildMetadata, route: route.path})
34
+ );
35
+ }
36
+
37
+ return flattenRoutes(routes).filter((route) => !isRouteExcluded(route));
38
+ }
39
+
40
+ // Our default implementation receives some additional parameters on purpose
41
+ function createDefaultCreateSitemapItems(
42
+ internalParams: Pick<CreateSitemapParams, 'routesBuildMetadata' | 'options'>,
43
+ ): CreateSitemapItemsFn {
44
+ return async (params) => {
45
+ const sitemapRoutes = getSitemapRoutes({...params, ...internalParams});
46
+ if (sitemapRoutes.length === 0) {
47
+ return [];
48
+ }
49
+ return Promise.all(
50
+ sitemapRoutes.map((route) =>
51
+ createSitemapItem({
52
+ route,
53
+ siteConfig: params.siteConfig,
54
+ options: internalParams.options,
55
+ }),
56
+ ),
57
+ );
58
+ };
59
+ }
60
+
61
+ type CreateSitemapParams = CreateSitemapItemsParams & {
62
+ routesBuildMetadata: {[location: string]: RouteBuildMetadata};
63
+ options: PluginOptions;
64
+ };
65
+
66
+ export default async function createSitemap(
67
+ params: CreateSitemapParams,
68
+ ): Promise<string | null> {
69
+ const {routesBuildMetadata, options, routes, siteConfig} = params;
70
+
71
+ const defaultCreateSitemapItems: CreateSitemapItemsFn =
72
+ createDefaultCreateSitemapItems({routesBuildMetadata, options});
73
+
74
+ const sitemapItems = params.options.createSitemapItems
75
+ ? await params.options.createSitemapItems({
76
+ routes,
77
+ siteConfig,
78
+ defaultCreateSitemapItems,
79
+ })
80
+ : await defaultCreateSitemapItems({
81
+ routes,
82
+ siteConfig,
83
+ });
84
+
85
+ if (sitemapItems.length === 0) {
86
+ return null;
87
+ }
88
+
89
+ const xmlString = await sitemapItemsToXmlString(sitemapItems, {
90
+ lastmod: params.options.lastmod,
91
+ });
92
+ return xmlString;
93
+ }
@@ -0,0 +1,91 @@
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 {normalizeUrl} from '@docusaurus/utils';
10
+ import type {LastModOption, SitemapItem} from './types';
11
+ import type {DocusaurusConfig, RouteConfig, VcsConfig} from '@docusaurus/types';
12
+ import type {PluginOptions} from './options';
13
+
14
+ async function getRouteLastUpdatedAt(
15
+ route: RouteConfig,
16
+ vcs: Pick<VcsConfig, 'getFileLastUpdateInfo'>,
17
+ ): Promise<number | null | undefined> {
18
+ // Important to bail-out early here
19
+ // This can lead to duplicated VCS calls and performance problems
20
+ // See https://github.com/facebook/docusaurus/pull/11211
21
+ if (route.metadata?.lastUpdatedAt === null) {
22
+ return null;
23
+ }
24
+ if (route.metadata?.lastUpdatedAt != null) {
25
+ return route.metadata?.lastUpdatedAt;
26
+ }
27
+ if (route.metadata?.sourceFilePath) {
28
+ const lastUpdateInfo = await vcs.getFileLastUpdateInfo(
29
+ route.metadata?.sourceFilePath,
30
+ );
31
+ return lastUpdateInfo?.timestamp ?? null;
32
+ }
33
+
34
+ return undefined;
35
+ }
36
+
37
+ type LastModFormatter = (timestamp: number) => string;
38
+
39
+ const LastmodFormatters: Record<LastModOption, LastModFormatter> = {
40
+ date: (timestamp) => new Date(timestamp).toISOString().split('T')[0]!,
41
+ datetime: (timestamp) => new Date(timestamp).toISOString(),
42
+ };
43
+
44
+ function formatLastmod(timestamp: number, lastmodOption: LastModOption) {
45
+ const format = LastmodFormatters[lastmodOption];
46
+ return format(timestamp);
47
+ }
48
+
49
+ async function getRouteLastmod({
50
+ route,
51
+ lastmod,
52
+ vcs,
53
+ }: {
54
+ route: RouteConfig;
55
+ lastmod: LastModOption | null;
56
+ vcs: Pick<VcsConfig, 'getFileLastUpdateInfo'>;
57
+ }): Promise<string | null> {
58
+ if (lastmod === null) {
59
+ return null;
60
+ }
61
+ const lastUpdatedAt = (await getRouteLastUpdatedAt(route, vcs)) ?? null;
62
+ return lastUpdatedAt != null ? formatLastmod(lastUpdatedAt, lastmod) : null;
63
+ }
64
+
65
+ export async function createSitemapItem({
66
+ route,
67
+ siteConfig,
68
+ options,
69
+ }: {
70
+ route: RouteConfig;
71
+ siteConfig: DocusaurusConfig;
72
+ options: PluginOptions;
73
+ }): Promise<SitemapItem> {
74
+ const {changefreq, priority, lastmod} = options;
75
+ return {
76
+ url: normalizeUrl([
77
+ siteConfig.url,
78
+ applyTrailingSlash(route.path, {
79
+ trailingSlash: siteConfig.trailingSlash,
80
+ baseUrl: siteConfig.baseUrl,
81
+ }),
82
+ ]),
83
+ changefreq,
84
+ priority,
85
+ lastmod: await getRouteLastmod({
86
+ route,
87
+ lastmod,
88
+ vcs: siteConfig.vcs,
89
+ }),
90
+ };
91
+ }
package/src/head.ts ADDED
@@ -0,0 +1,25 @@
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 type {RouteBuildMetadata} from '@docusaurus/types';
9
+
10
+ // Maybe we want to add a routeConfig.metadata.noIndex instead?
11
+ // But using Helmet is more reliable for third-party plugins...
12
+ export function isNoIndexMetaRoute({
13
+ routesBuildMetadata,
14
+ route,
15
+ }: {
16
+ routesBuildMetadata: {[location: string]: RouteBuildMetadata};
17
+ route: string;
18
+ }): boolean {
19
+ const routeBuildMetadata = routesBuildMetadata[route];
20
+
21
+ if (routeBuildMetadata) {
22
+ return routeBuildMetadata.noIndex;
23
+ }
24
+ return false;
25
+ }
package/src/index.ts ADDED
@@ -0,0 +1,59 @@
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 path from 'node:path';
9
+ import logger from '@docusaurus/logger';
10
+ import {outputFile} from '@docusaurus/fs';
11
+ import createSitemap from './createSitemap';
12
+ import type {PluginOptions, Options} from './options';
13
+ import type {LoadContext, Plugin} from '@docusaurus/types';
14
+
15
+ const PluginName = 'docusaurus-plugin-sitemap';
16
+
17
+ export default function pluginSitemap(
18
+ context: LoadContext,
19
+ options: PluginOptions,
20
+ ): Plugin<void> | null {
21
+ if (context.siteConfig.future.experimental_router === 'hash') {
22
+ logger.warn(
23
+ `${PluginName} does not support the Hash Router and will be disabled.`,
24
+ );
25
+ return null;
26
+ }
27
+
28
+ return {
29
+ name: PluginName,
30
+
31
+ async postBuild({siteConfig, routes, outDir, routesBuildMetadata}) {
32
+ if (siteConfig.noIndex) {
33
+ return;
34
+ }
35
+ // Generate sitemap.
36
+ const generatedSitemap = await createSitemap({
37
+ siteConfig,
38
+ routes,
39
+ routesBuildMetadata,
40
+ options,
41
+ });
42
+ if (!generatedSitemap) {
43
+ return;
44
+ }
45
+
46
+ // Write sitemap file.
47
+ const sitemapPath = path.join(outDir, options.filename);
48
+ try {
49
+ await outputFile(sitemapPath, generatedSitemap);
50
+ } catch (err) {
51
+ logger.error('Writing sitemap failed.');
52
+ throw err;
53
+ }
54
+ },
55
+ };
56
+ }
57
+
58
+ export {validateOptions} from './options';
59
+ export type {PluginOptions, Options};
package/src/options.ts ADDED
@@ -0,0 +1,126 @@
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 {Joi} from '@docusaurus/utils-validation';
9
+ import {ChangeFreqList, LastModOptionList} from './types';
10
+ import type {OptionValidationContext} from '@docusaurus/types';
11
+ import type {
12
+ ChangeFreq,
13
+ LastModOption,
14
+ SitemapItem,
15
+ CreateSitemapItemsFn,
16
+ CreateSitemapItemsParams,
17
+ } from './types';
18
+
19
+ export type PluginOptions = {
20
+ /**
21
+ * The path to the created sitemap file, relative to the output directory.
22
+ * Useful if you have two plugin instances outputting two files.
23
+ */
24
+ filename: string;
25
+
26
+ /**
27
+ * A list of glob patterns; matching route paths will be filtered from the
28
+ * sitemap. Note that you may need to include the base URL in here.
29
+ */
30
+ ignorePatterns: string[];
31
+
32
+ /**
33
+ * Defines the format of the "lastmod" sitemap item entry, between:
34
+ * - null: do not compute/add a "lastmod" sitemap entry
35
+ * - "date": add a "lastmod" sitemap entry without time (YYYY-MM-DD)
36
+ * - "datetime": add a "lastmod" sitemap entry with time (ISO 8601 datetime)
37
+ * @see https://www.sitemaps.org/protocol.html#xmlTagDefinitions
38
+ * @see https://www.w3.org/TR/NOTE-datetime
39
+ */
40
+ lastmod: LastModOption | null;
41
+
42
+ /**
43
+ * TODO Docusaurus v4 breaking change: remove useless option
44
+ * @see https://www.sitemaps.org/protocol.html#xmlTagDefinitions
45
+ */
46
+ changefreq: ChangeFreq | null;
47
+
48
+ /**
49
+ * TODO Docusaurus v4 breaking change: remove useless option
50
+ * @see https://www.sitemaps.org/protocol.html#xmlTagDefinitions
51
+ */
52
+ priority: number | null;
53
+
54
+ /** Allow control over the construction of SitemapItems */
55
+ createSitemapItems?: CreateSitemapItemsOption;
56
+ };
57
+
58
+ type CreateSitemapItemsOption = (
59
+ params: CreateSitemapItemsParams & {
60
+ defaultCreateSitemapItems: CreateSitemapItemsFn;
61
+ },
62
+ ) => Promise<SitemapItem[]>;
63
+
64
+ export type Options = Partial<PluginOptions>;
65
+
66
+ export const DEFAULT_OPTIONS: PluginOptions = {
67
+ filename: 'sitemap.xml',
68
+ ignorePatterns: [],
69
+
70
+ // TODO Docusaurus v4 breaking change
71
+ // change default to "date" if no bug or perf issue reported
72
+ lastmod: null,
73
+
74
+ // TODO Docusaurus v4 breaking change
75
+ // those options are useless and should be removed
76
+ changefreq: 'weekly',
77
+ priority: 0.5,
78
+ };
79
+
80
+ const PluginOptionSchema = Joi.object<PluginOptions>({
81
+ // @ts-expect-error: forbidden
82
+ cacheTime: Joi.forbidden().messages({
83
+ 'any.unknown':
84
+ 'Option `cacheTime` in sitemap config is deprecated. Please remove it.',
85
+ }),
86
+
87
+ // TODO remove for Docusaurus v4 breaking changes?
88
+ // This is not even used by Google crawlers
89
+ // See also https://github.com/facebook/docusaurus/issues/2604
90
+ changefreq: Joi.string()
91
+ .valid(null, ...ChangeFreqList)
92
+ .default(DEFAULT_OPTIONS.changefreq),
93
+
94
+ // TODO remove for Docusaurus v4 breaking changes?
95
+ // This is not even used by Google crawlers
96
+ // The priority is "relative", and using the same priority for all routes
97
+ // does not make sense according to the spec
98
+ // See also https://github.com/facebook/docusaurus/issues/2604
99
+ // See also https://www.sitemaps.org/protocol.html
100
+ priority: Joi.alternatives()
101
+ .try(Joi.valid(null), Joi.number().min(0).max(1))
102
+ .default(DEFAULT_OPTIONS.priority),
103
+
104
+ lastmod: Joi.string()
105
+ .valid(null, ...LastModOptionList)
106
+ .default(DEFAULT_OPTIONS.lastmod),
107
+
108
+ createSitemapItems: Joi.function(),
109
+
110
+ ignorePatterns: Joi.array()
111
+ .items(Joi.string())
112
+ .default(DEFAULT_OPTIONS.ignorePatterns),
113
+ trailingSlash: Joi.forbidden().messages({
114
+ 'any.unknown':
115
+ 'Please use the new Docusaurus global trailingSlash config instead, and the sitemaps plugin will use it.',
116
+ }),
117
+ filename: Joi.string().default(DEFAULT_OPTIONS.filename),
118
+ });
119
+
120
+ export function validateOptions({
121
+ validate,
122
+ options,
123
+ }: OptionValidationContext<Options, PluginOptions>): PluginOptions {
124
+ const validatedOptions = validate(PluginOptionSchema, options);
125
+ return validatedOptions;
126
+ }
package/src/types.ts ADDED
@@ -0,0 +1,78 @@
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 type {DocusaurusConfig, RouteConfig} from '@docusaurus/types';
9
+
10
+ export const LastModOptionList = ['date', 'datetime'] as const;
11
+
12
+ export type LastModOption = (typeof LastModOptionList)[number];
13
+
14
+ // types are according to the sitemap spec:
15
+ // see also https://www.sitemaps.org/protocol.html
16
+
17
+ export const ChangeFreqList = [
18
+ 'hourly',
19
+ 'daily',
20
+ 'weekly',
21
+ 'monthly',
22
+ 'yearly',
23
+ 'always',
24
+ 'never',
25
+ ] as const;
26
+
27
+ export type ChangeFreq = (typeof ChangeFreqList)[number];
28
+
29
+ // We re-recreate our own type because the "sitemap" lib types are not good
30
+ export type SitemapItem = {
31
+ /**
32
+ * URL of the page.
33
+ * This URL must begin with the protocol (such as http).
34
+ * It should eventually end with a trailing slash.
35
+ * It should be less than 2,048 characters.
36
+ */
37
+ url: string;
38
+
39
+ /**
40
+ * ISO 8601 date string.
41
+ * See also https://www.w3.org/TR/NOTE-datetime
42
+ *
43
+ * It is recommended to use one of:
44
+ * - date.toISOString()
45
+ * - YYYY-MM-DD
46
+ *
47
+ * Note: as of 2024, Google uses this value for crawling priority.
48
+ * See also https://github.com/facebook/docusaurus/issues/2604
49
+ */
50
+ lastmod?: string | null;
51
+
52
+ /**
53
+ * One of the specified enum values
54
+ *
55
+ * Note: as of 2024, Google ignores this value.
56
+ * See also https://github.com/facebook/docusaurus/issues/2604
57
+ */
58
+ changefreq?: ChangeFreq | null;
59
+
60
+ /**
61
+ * The priority of this URL relative to other URLs on your site.
62
+ * Valid values range from 0.0 to 1.0.
63
+ * The default priority of a page is 0.5.
64
+ *
65
+ * Note: as of 2024, Google ignores this value.
66
+ * See also https://github.com/facebook/docusaurus/issues/2604
67
+ */
68
+ priority?: number | null;
69
+ };
70
+
71
+ export type CreateSitemapItemsParams = {
72
+ siteConfig: DocusaurusConfig;
73
+ routes: RouteConfig[];
74
+ };
75
+
76
+ export type CreateSitemapItemsFn = (
77
+ params: CreateSitemapItemsParams,
78
+ ) => Promise<SitemapItem[]>;
package/src/xml.ts ADDED
@@ -0,0 +1,34 @@
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
+ // Fail fast with a clearer error than the lib's EmptySitemap error
17
+ throw new Error("Can't generate a sitemap with no items");
18
+ }
19
+
20
+ // We could generate the XML ourselves, but we keep using the lib on purpose
21
+ // Users can return undocumented fields from createSitemapItems()
22
+ // (links, img, video, news...) that the lib serializes
23
+ // See https://github.com/ekalinin/sitemap.js
24
+ const sitemapStream = new SitemapStream({
25
+ // WTF is this lib reformatting the string YYYY-MM-DD to datetime...
26
+ lastmodDateOnly: options?.lastmod === 'date',
27
+ });
28
+
29
+ items.forEach((item) => sitemapStream.write(item));
30
+ sitemapStream.end();
31
+
32
+ const buffer = await streamToPromise(sitemapStream);
33
+ return buffer.toString();
34
+ }