diff --git a/.changeset/popular-cherries-float.md b/.changeset/popular-cherries-float.md
new file mode 100644
index 000000000000..f04ab3f3ce54
--- /dev/null
+++ b/.changeset/popular-cherries-float.md
@@ -0,0 +1,18 @@
+---
+'@astrojs/sitemap': minor
+---
+
+# Key features
+
+- Split up your large sitemap into multiple sitemaps by custom limit.
+- Ability to add sitemap specific attributes such as `lastmod` etc.
+- Final output customization via JS function.
+- Localization support.
+- Reliability: all config options are validated.
+
+## Important changes
+
+The integration always generates at least two files instead of one:
+
+- `sitemap-index.xml` - index file;
+- `sitemap-{i}.xml` - actual sitemap.
diff --git a/examples/integrations-playground/astro.config.mjs b/examples/integrations-playground/astro.config.mjs
index a7c3d4a0b7a2..939d228669c3 100644
--- a/examples/integrations-playground/astro.config.mjs
+++ b/examples/integrations-playground/astro.config.mjs
@@ -9,5 +9,6 @@ import solid from '@astrojs/solid-js';
// https://astro.build/config
export default defineConfig({
+ site: 'https://example.com',
integrations: [lit(), react(), tailwind(), turbolinks(), partytown(), sitemap(), solid()],
});
diff --git a/packages/integrations/sitemap/README.md b/packages/integrations/sitemap/README.md
index 9d84cae195d2..ab880a075f0a 100644
--- a/packages/integrations/sitemap/README.md
+++ b/packages/integrations/sitemap/README.md
@@ -64,7 +64,35 @@ export default {
}
```
-Now, [build your site for production](https://docs.astro.build/en/reference/cli-reference/#astro-build) via the `astro build` command. You should find your sitemap under `dist/sitemap.xml`!
+Now, [build your site for production](https://docs.astro.build/en/reference/cli-reference/#astro-build) via the `astro build` command. You should find your _sitemap_ under `dist/sitemap-index.xml` and `dist/sitemap-0.xml`!
+
+Generated sitemap content for two pages website:
+
+**sitemap-index.xml**
+
+```xml
+
+
+
+ https://stargazers.club/sitemap-0.xml
+
+
+```
+
+**sitemap-0.xml**
+
+
+```xml
+
+
+
+ https://stargazers.club/
+
+
+ https://stargazers.club/second-page/
+
+
+```
You can also check our [Astro Integration Documentation][astro-integration] for more on integrations.
@@ -111,5 +139,158 @@ export default {
}
```
+### entryLimit
+
+Non-negative `Number` of entries per sitemap file. Default value is 45000. A sitemap index and multiple sitemaps are created if you have more entries. See explanation on [Google](https://developers.google.com/search/docs/advanced/sitemaps/large-sitemaps).
+
+__astro.config.mjs__
+
+```js
+import sitemap from '@astrojs/sitemap';
+
+export default {
+ site: 'https://stargazers.club',
+ integrations: [
+ sitemap({
+ entryLimit: 10000,
+ }),
+ ],
+}
+```
+
+### changefreq, lastmod, priority
+
+`changefreq` - How frequently the page is likely to change. Available values: `always` \| `hourly` \| `daily` \| `weekly` \| `monthly` \| `yearly` \| `never`.
+
+`priority` - The priority of this URL relative to other URLs on your site. Valid values range from 0.0 to 1.0.
+
+`lastmod` - The date of page last modification.
+
+`changefreq` and `priority` are ignored by Google.
+
+See detailed explanation of sitemap specific options on [sitemap.org](https://www.sitemaps.org/protocol.html).
+
+
+:exclamation: This integration uses 'astro:build:done' hook. The hook exposes generated page paths only. So with present version of Astro the integration has no abilities to analyze a page source, frontmatter etc. The integration can add `changefreq`, `lastmod` and `priority` attributes only in a batch or nothing.
+
+__astro.config.mjs__
+
+```js
+import sitemap from '@astrojs/sitemap';
+
+export default {
+ site: 'https://stargazers.club',
+ integrations: [
+ sitemap({
+ changefreq: 'weekly',
+ priority: 0.7,
+ lastmod: new Date('2022-02-24'),
+ }),
+ ],
+}
+```
+
+### serialize
+
+Async or sync function called for each sitemap entry just before writing to a disk.
+
+It receives as parameter `SitemapItem` object which consists of `url` (required, absolute page URL) and optional `changefreq`, `lastmod`, `priority` and `links` properties.
+
+Optional `links` property contains a `LinkItem` list of alternate pages including a parent page.
+`LinkItem` type has two required fields: `url` (the fully-qualified URL for the version of this page for the specified language) and `hreflang` (a supported language code targeted by this version of the page).
+
+`serialize` function should return `SitemapItem`, touched or not.
+
+The example below shows the ability to add the sitemap specific properties individually.
+
+__astro.config.mjs__
+
+```js
+import sitemap from '@astrojs/sitemap';
+
+export default {
+ site: 'https://stargazers.club',
+ integrations: [
+ sitemap({
+ serialize(item) {
+ if (/your-special-page/.test(item.url)) {
+ item.changefreq = 'daily';
+ item.lastmod = new Date();
+ item.priority = 0.9;
+ }
+ return item;
+ },
+ }),
+ ],
+}
+```
+
+### i18n
+
+To localize a sitemap you should supply the integration config with the `i18n` option. The integration will check generated page paths on presence of locale keys in paths.
+
+`i18n` object has two required properties:
+
+- `defaultLocale`: `String`. Its value must exist as one of `locales` keys.
+- `locales`: `Record`, key/value - pairs. The key is used to look for a locale part in a page path. The value is a language attribute, only English alphabet and hyphen allowed. See more about language attribute on [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang).
+
+
+Read more about localization on Google in [Advanced SEO](https://developers.google.com/search/docs/advanced/crawling/localized-versions#all-method-guidelines).
+
+__astro.config.mjs__
+
+```js
+import sitemap from '@astrojs/sitemap';
+
+export default {
+ site: 'https://stargazers.club',
+ integrations: [
+ sitemap({
+ i18n: {
+ defaultLocale: 'en', // All urls that don't contain `es` or `fr` after `https://stargazers.club/` will be treated as default locale, i.e. `en`
+ locales: {
+ en: 'en-US', // The `defaultLocale` value must present in `locales` keys
+ es: 'es-ES',
+ fr: 'fr-CA',
+ },
+ },
+ }),
+ ],
+};
+...
+
+```
+
+The sitemap content will be:
+
+```xml
+...
+
+ https://stargazers.club/
+
+
+
+
+
+ https://stargazers.club/es/
+
+
+
+
+
+ https://stargazers.club/fr/
+
+
+
+
+
+ https://stargazers.club/es/second-page/
+
+
+
+
+...
+```
+
[astro-integration]: https://docs.astro.build/en/guides/integrations-guide/
[astro-ui-frameworks]: https://docs.astro.build/en/core-concepts/framework-components/#using-framework-components
diff --git a/packages/integrations/sitemap/package.json b/packages/integrations/sitemap/package.json
index 3fbfc2b94b5e..d61c608b3b54 100644
--- a/packages/integrations/sitemap/package.json
+++ b/packages/integrations/sitemap/package.json
@@ -13,7 +13,8 @@
},
"keywords": [
"astro-component",
- "seo"
+ "seo",
+ "sitemap"
],
"bugs": "https://github.com/withastro/astro/issues",
"homepage": "https://astro.build",
@@ -21,12 +22,18 @@
".": "./dist/index.js",
"./package.json": "./package.json"
},
+ "files": [
+ "dist"
+ ],
"scripts": {
"build": "astro-scripts build \"src/**/*.ts\" && tsc",
"build:ci": "astro-scripts build \"src/**/*.ts\"",
"dev": "astro-scripts dev \"src/**/*.ts\""
},
- "dependencies": {},
+ "dependencies": {
+ "sitemap": "^7.1.1",
+ "zod": "^3.17.3"
+ },
"devDependencies": {
"astro": "workspace:*",
"astro-scripts": "workspace:*"
diff --git a/packages/integrations/sitemap/src/config-defaults.ts b/packages/integrations/sitemap/src/config-defaults.ts
new file mode 100644
index 000000000000..22288fc119eb
--- /dev/null
+++ b/packages/integrations/sitemap/src/config-defaults.ts
@@ -0,0 +1,5 @@
+import type { SitemapOptions } from './index';
+
+export const SITEMAP_CONFIG_DEFAULTS: SitemapOptions & any = {
+ entryLimit: 45000,
+};
diff --git a/packages/integrations/sitemap/src/constants.ts b/packages/integrations/sitemap/src/constants.ts
new file mode 100644
index 000000000000..431cc5954e13
--- /dev/null
+++ b/packages/integrations/sitemap/src/constants.ts
@@ -0,0 +1,9 @@
+export const changefreqValues = [
+ 'always',
+ 'hourly',
+ 'daily',
+ 'weekly',
+ 'monthly',
+ 'yearly',
+ 'never',
+] as const;
diff --git a/packages/integrations/sitemap/src/generate-sitemap.ts b/packages/integrations/sitemap/src/generate-sitemap.ts
new file mode 100644
index 000000000000..3c39e1f7ec45
--- /dev/null
+++ b/packages/integrations/sitemap/src/generate-sitemap.ts
@@ -0,0 +1,55 @@
+import { SitemapItemLoose } from 'sitemap';
+
+import type { SitemapOptions } from './index';
+import { parseUrl } from './utils/parse-url';
+
+const STATUS_CODE_PAGE_REGEXP = /\/[0-9]{3}\/?$/;
+
+/** Construct sitemap.xml given a set of URLs */
+export function generateSitemap(pages: string[], finalSiteUrl: string, opts: SitemapOptions) {
+ const { changefreq, priority: prioritySrc, lastmod: lastmodSrc, i18n } = opts || {};
+ // TODO: find way to respect URLs here
+ const urls = [...pages].filter((url) => !STATUS_CODE_PAGE_REGEXP.test(url));
+ urls.sort((a, b) => a.localeCompare(b, 'en', { numeric: true })); // sort alphabetically so sitemap is same each time
+
+ const lastmod = lastmodSrc?.toISOString();
+ const priority = typeof prioritySrc === 'number' ? prioritySrc : undefined;
+
+ const { locales, defaultLocale } = i18n || {};
+ const localeCodes = Object.keys(locales || {});
+
+ const getPath = (url: string) => {
+ const result = parseUrl(url, i18n?.defaultLocale || '', localeCodes, finalSiteUrl);
+ return result?.path;
+ };
+ const getLocale = (url: string) => {
+ const result = parseUrl(url, i18n?.defaultLocale || '', localeCodes, finalSiteUrl);
+ return result?.locale;
+ };
+
+ const urlData = urls.map((url) => {
+ let links;
+ if (defaultLocale && locales) {
+ const currentPath = getPath(url);
+ if (currentPath) {
+ const filtered = urls.filter((subUrl) => getPath(subUrl) === currentPath);
+ if (filtered.length > 1) {
+ links = filtered.map((subUrl) => ({
+ url: subUrl,
+ lang: locales[getLocale(subUrl)!],
+ }));
+ }
+ }
+ }
+
+ return {
+ url,
+ links,
+ lastmod,
+ priority,
+ changefreq, // : changefreq as EnumChangefreq,
+ } as SitemapItemLoose;
+ });
+
+ return urlData;
+}
diff --git a/packages/integrations/sitemap/src/index.ts b/packages/integrations/sitemap/src/index.ts
index 169eeb788701..700f68876926 100644
--- a/packages/integrations/sitemap/src/index.ts
+++ b/packages/integrations/sitemap/src/index.ts
@@ -1,91 +1,138 @@
+import { fileURLToPath } from 'url';
import type { AstroConfig, AstroIntegration } from 'astro';
-import fs from 'node:fs';
-const STATUS_CODE_PAGE_REGEXP = /\/[0-9]{3}\/?$/;
+import { ZodError } from 'zod';
+import { LinkItem as LinkItemBase, SitemapItemLoose, simpleSitemapAndIndex } from 'sitemap';
-type SitemapOptions =
+import { Logger } from './utils/logger';
+import { changefreqValues } from './constants';
+import { validateOptions } from './validate-options';
+import { generateSitemap } from './generate-sitemap';
+
+export type ChangeFreq = typeof changefreqValues[number];
+export type SitemapItem = Pick<
+ SitemapItemLoose,
+ 'url' | 'lastmod' | 'changefreq' | 'priority' | 'links'
+>;
+export type LinkItem = LinkItemBase;
+
+export type SitemapOptions =
| {
- /**
- * All pages are included in your sitemap by default.
- * With this config option, you can filter included pages by URL.
- *
- * The `page` function parameter is the full URL of your rendered page, including your `site` domain.
- * Return `true` to include a page in your sitemap, and `false` to remove it.
- *
- * ```js
- * filter: (page) => page !== 'http://example.com/secret-page'
- * ```
- */
filter?(page: string): boolean;
-
- /**
- * If you have any URL, not rendered by Astro, that you want to include in your sitemap,
- * this config option will help you to include your array of custom pages in your sitemap.
- *
- * ```js
- * customPages: ['http://example.com/custom-page', 'http://example.com/custom-page2']
- * ```
- */
- customPages?: Array;
-
- /**
- * If present, we use the `site` config option as the base for all sitemap URLs
- * Use `canonicalURL` to override this
- */
+ customPages?: string[];
canonicalURL?: string;
+
+ i18n?: {
+ defaultLocale: string;
+ locales: Record;
+ };
+ // number of entries per sitemap file
+ entryLimit?: number;
+
+ // sitemap specific
+ changefreq?: ChangeFreq;
+ lastmod?: Date;
+ priority?: number;
+
+ // called for each sitemap item just before to save them on disk, sync or async
+ serialize?(item: SitemapItemLoose): SitemapItemLoose;
}
| undefined;
-/** Construct sitemap.xml given a set of URLs */
-function generateSitemap(pages: string[]) {
- // TODO: find way to respect URLs here
- const urls = [...pages].filter((url) => !STATUS_CODE_PAGE_REGEXP.test(url));
- urls.sort((a, b) => a.localeCompare(b, 'en', { numeric: true })); // sort alphabetically so sitemap is same each time
- let sitemap = ``;
- for (const url of urls) {
- sitemap += `${url}`;
- }
- sitemap += `\n`;
- return sitemap;
+function formatConfigErrorMessage(err: ZodError) {
+ const errorList = err.issues.map((issue) => ` ${issue.path.join('.')} ${issue.message + '.'}`);
+ return errorList.join('\n');
}
-export default function createPlugin({
- filter,
- customPages,
- canonicalURL,
-}: SitemapOptions = {}): AstroIntegration {
+const PKG_NAME = '@astrojs/sitemap';
+const OUTFILE = 'sitemap-index.xml';
+
+const createPlugin = (options?: SitemapOptions): AstroIntegration => {
let config: AstroConfig;
return {
- name: '@astrojs/sitemap',
+ name: PKG_NAME,
+
hooks: {
- 'astro:config:done': async ({ config: _config }) => {
- config = _config;
+ 'astro:config:done': async ({ config: cfg }) => {
+ config = cfg;
},
- 'astro:build:done': async ({ pages, dir }) => {
- let finalSiteUrl: URL;
- if (canonicalURL) {
- finalSiteUrl = new URL(canonicalURL);
- finalSiteUrl.pathname += finalSiteUrl.pathname.endsWith('/') ? '' : '/'; // normalizes the final url since it's provided by user
- } else if (config.site) {
- finalSiteUrl = new URL(config.base, config.site);
- } else {
- console.warn(
- 'The Sitemap integration requires either the `site` astro.config option or `canonicalURL` integration option. Skipping.'
- );
- return;
- }
- let pageUrls = pages.map((p) => {
- const path = finalSiteUrl.pathname + p.pathname;
- return new URL(path, finalSiteUrl).href;
- });
- if (filter) {
- pageUrls = pageUrls.filter((page: string) => filter(page));
- }
- if (customPages) {
- pageUrls = [...pageUrls, ...customPages];
+
+ 'astro:build:done': async ({ dir, pages }) => {
+ const logger = new Logger(PKG_NAME);
+
+ try {
+ const opts = validateOptions(config.site, options);
+
+ const { filter, customPages, canonicalURL, serialize, entryLimit } = opts;
+
+ let finalSiteUrl: URL;
+ if (canonicalURL) {
+ finalSiteUrl = new URL(canonicalURL);
+ if (!finalSiteUrl.pathname.endsWith('/')) {
+ finalSiteUrl.pathname += '/'; // normalizes the final url since it's provided by user
+ }
+ } else {
+ // `validateOptions` forces to provide `canonicalURL` or `config.site` at least.
+ // So step to check on empty values of `canonicalURL` and `config.site` is dropped.
+ finalSiteUrl = new URL(config.base, config.site);
+ }
+
+ let pageUrls = pages.map((p) => {
+ const path = finalSiteUrl.pathname + p.pathname;
+ return new URL(path, finalSiteUrl).href;
+ });
+
+ try {
+ if (filter) {
+ pageUrls = pageUrls.filter(filter);
+ }
+ } catch (err) {
+ logger.error(`Error filtering pages\n${(err as any).toString()}`);
+ return;
+ }
+
+ if (customPages) {
+ pageUrls = [...pageUrls, ...customPages];
+ }
+
+ if (pageUrls.length === 0) {
+ logger.warn(`No data for sitemap.\n\`${OUTFILE}\` is not created.`);
+ return;
+ }
+
+ let urlData = generateSitemap(pageUrls, finalSiteUrl.href, opts);
+
+ if (serialize) {
+ try {
+ const serializedUrls: SitemapItemLoose[] = [];
+ for (const item of urlData) {
+ const serialized = await Promise.resolve(serialize(item));
+ serializedUrls.push(serialized);
+ }
+ urlData = serializedUrls;
+ } catch (err) {
+ logger.error(`Error serializing pages\n${(err as any).toString()}`);
+ return;
+ }
+ }
+
+ await simpleSitemapAndIndex({
+ hostname: finalSiteUrl.href,
+ destinationDir: fileURLToPath(dir),
+ sourceData: urlData,
+ limit: entryLimit,
+ gzip: false,
+ });
+ logger.success(`\`${OUTFILE}\` is created.`);
+ } catch (err) {
+ if (err instanceof ZodError) {
+ logger.warn(formatConfigErrorMessage(err));
+ } else {
+ throw err;
+ }
}
- const sitemapContent = generateSitemap(pageUrls);
- fs.writeFileSync(new URL('sitemap.xml', dir), sitemapContent);
},
},
};
-}
+};
+
+export default createPlugin;
diff --git a/packages/integrations/sitemap/src/schema.ts b/packages/integrations/sitemap/src/schema.ts
new file mode 100644
index 000000000000..723f9ac58494
--- /dev/null
+++ b/packages/integrations/sitemap/src/schema.ts
@@ -0,0 +1,47 @@
+import { z } from 'zod';
+import { changefreqValues } from './constants';
+import { SITEMAP_CONFIG_DEFAULTS } from './config-defaults';
+
+const localeKeySchema = () => z.string().min(1);
+
+const isFunction = (fn: any) => fn instanceof Function;
+
+const fnSchema = () =>
+ z
+ .any()
+ .refine((val) => !val || isFunction(val), { message: 'Not a function' })
+ .optional();
+
+export const SitemapOptionsSchema = z
+ .object({
+ filter: fnSchema(),
+ customPages: z.string().url().array().optional(),
+ canonicalURL: z.string().url().optional(),
+
+ i18n: z
+ .object({
+ defaultLocale: localeKeySchema(),
+ locales: z.record(
+ localeKeySchema(),
+ z
+ .string()
+ .min(2)
+ .regex(/^[a-zA-Z\-]+$/gm, {
+ message: 'Only English alphabet symbols and hyphen allowed',
+ })
+ ),
+ })
+ .refine((val) => !val || val.locales[val.defaultLocale], {
+ message: '`defaultLocale` must exists in `locales` keys',
+ })
+ .optional(),
+
+ entryLimit: z.number().nonnegative().default(SITEMAP_CONFIG_DEFAULTS.entryLimit),
+ serialize: fnSchema(),
+
+ changefreq: z.enum(changefreqValues).optional(),
+ lastmod: z.date().optional(),
+ priority: z.number().min(0).max(1).optional(),
+ })
+ .strict()
+ .default(SITEMAP_CONFIG_DEFAULTS);
diff --git a/packages/integrations/sitemap/src/utils/is-object-empty.ts b/packages/integrations/sitemap/src/utils/is-object-empty.ts
new file mode 100644
index 000000000000..0d61810695a1
--- /dev/null
+++ b/packages/integrations/sitemap/src/utils/is-object-empty.ts
@@ -0,0 +1,10 @@
+// @internal
+export const isObjectEmpty = (o: any) => {
+ if (!o) {
+ return true;
+ }
+ if (Array.isArray(o)) {
+ return o.length === 0;
+ }
+ return Object.keys(o).length === 0 && Object.getPrototypeOf(o) === Object.prototype;
+};
diff --git a/packages/integrations/sitemap/src/utils/is-valid-url.ts b/packages/integrations/sitemap/src/utils/is-valid-url.ts
new file mode 100644
index 000000000000..b140623b090f
--- /dev/null
+++ b/packages/integrations/sitemap/src/utils/is-valid-url.ts
@@ -0,0 +1,13 @@
+// @internal
+export const isValidUrl = (s: any) => {
+ if (typeof s !== 'string' || !s) {
+ return false;
+ }
+ try {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const dummy = new URL(s);
+ return true;
+ } catch {
+ return false;
+ }
+};
diff --git a/packages/integrations/sitemap/src/utils/logger.ts b/packages/integrations/sitemap/src/utils/logger.ts
new file mode 100644
index 000000000000..203baeaa7a57
--- /dev/null
+++ b/packages/integrations/sitemap/src/utils/logger.ts
@@ -0,0 +1,46 @@
+// @internal
+export interface ILogger {
+ info(msg: string): void;
+ success(msg: string): void;
+ warn(msg: string): void;
+ error(msg: string): void;
+}
+
+// @internal
+export class Logger implements ILogger {
+ private colors = {
+ reset: '\x1b[0m',
+ fg: {
+ red: '\x1b[31m',
+ green: '\x1b[32m',
+ yellow: '\x1b[33m',
+ },
+ } as const;
+
+ private packageName: string;
+
+ constructor(packageName: string) {
+ this.packageName = packageName;
+ }
+
+ private log(msg: string, prefix: string = '') {
+ // eslint-disable-next-line no-console
+ console.log(`%s${this.packageName}:%s ${msg}\n`, prefix, prefix ? this.colors.reset : '');
+ }
+
+ info(msg: string) {
+ this.log(msg);
+ }
+
+ success(msg: string) {
+ this.log(msg, this.colors.fg.green);
+ }
+
+ warn(msg: string) {
+ this.log(`Skipped!\n${msg}`, this.colors.fg.yellow);
+ }
+
+ error(msg: string) {
+ this.log(`Failed!\n${msg}`, this.colors.fg.red);
+ }
+}
diff --git a/packages/integrations/sitemap/src/utils/parse-url.ts b/packages/integrations/sitemap/src/utils/parse-url.ts
new file mode 100644
index 000000000000..f9189cf7d60d
--- /dev/null
+++ b/packages/integrations/sitemap/src/utils/parse-url.ts
@@ -0,0 +1,39 @@
+export const parseUrl = (
+ url: string,
+ defaultLocale: string,
+ localeCodes: string[],
+ base: string
+) => {
+ if (
+ !url ||
+ !defaultLocale ||
+ localeCodes.length === 0 ||
+ localeCodes.some((key) => !key) ||
+ !base
+ ) {
+ throw new Error('parseUrl: some parameters are empty');
+ }
+ if (url.indexOf(base) !== 0) {
+ return undefined;
+ }
+ let s = url.replace(base, '');
+ if (!s || s === '/') {
+ return { locale: defaultLocale, path: '/' };
+ }
+ if (!s.startsWith('/')) {
+ s = '/' + s;
+ }
+ const a = s.split('/');
+ const locale = a[1];
+ if (localeCodes.some((key) => key === locale)) {
+ let path = a.slice(2).join('/');
+ if (path === '//') {
+ path = '/';
+ }
+ if (path !== '/' && !path.startsWith('/')) {
+ path = '/' + path;
+ }
+ return { locale, path };
+ }
+ return { locale: defaultLocale, path: s };
+};
diff --git a/packages/integrations/sitemap/src/validate-options.ts b/packages/integrations/sitemap/src/validate-options.ts
new file mode 100644
index 000000000000..f89582d82747
--- /dev/null
+++ b/packages/integrations/sitemap/src/validate-options.ts
@@ -0,0 +1,22 @@
+import { z } from 'zod';
+import type { SitemapOptions } from './index';
+import { SitemapOptionsSchema } from './schema';
+
+// @internal
+export const validateOptions = (site: string | undefined, opts: SitemapOptions) => {
+ const result = SitemapOptionsSchema.parse(opts);
+
+ z.object({
+ site: z.string().optional(), // Astro takes care of `site`: how to validate, transform and refine
+ canonicalURL: z.string().optional(), // `canonicalURL` is already validated in prev step
+ })
+ .refine(({ site, canonicalURL }) => site || canonicalURL, {
+ message: 'Required `site` astro.config option or `canonicalURL` integration option',
+ })
+ .parse({
+ site,
+ canonicalURL: result.canonicalURL,
+ });
+
+ return result;
+};
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index abb1384cfb26..7e862156e40b 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1824,6 +1824,11 @@ importers:
specifiers:
astro: workspace:*
astro-scripts: workspace:*
+ sitemap: ^7.1.1
+ zod: ^3.17.3
+ dependencies:
+ sitemap: 7.1.1
+ zod: 3.17.3
devDependencies:
astro: link:../../astro
astro-scripts: link:../../../scripts
@@ -6852,6 +6857,12 @@ packages:
'@types/node': 17.0.41
dev: false
+ /@types/sax/1.2.4:
+ resolution: {integrity: sha512-pSAff4IAxJjfAXUG6tFkO7dsSbTmf8CtUpfhhZ5VhkRpC4628tJhh3+V6H1E+/Gs9piSzYKT5yzHO5M4GG9jkw==}
+ dependencies:
+ '@types/node': 17.0.41
+ dev: false
+
/@types/scheduler/0.16.2:
resolution: {integrity: sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==}
@@ -8164,6 +8175,11 @@ packages:
/debug/3.2.7:
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
dependencies:
ms: 2.1.3
dev: false
@@ -11080,6 +11096,8 @@ packages:
debug: 3.2.7
iconv-lite: 0.4.24
sax: 1.2.4
+ transitivePeerDependencies:
+ - supports-color
dev: false
/netmask/2.0.2:
@@ -11163,6 +11181,8 @@ packages:
rimraf: 2.7.1
semver: 5.7.1
tar: 4.4.19
+ transitivePeerDependencies:
+ - supports-color
dev: false
/node-releases/2.0.5:
@@ -12535,6 +12555,17 @@ packages:
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
dev: false
+ /sitemap/7.1.1:
+ resolution: {integrity: sha512-mK3aFtjz4VdJN0igpIJrinf3EO8U8mxOPsTBzSsy06UtjZQJ3YY3o3Xa7zSc5nMqcMrRwlChHZ18Kxg0caiPBg==}
+ engines: {node: '>=12.0.0', npm: '>=5.6.0'}
+ hasBin: true
+ dependencies:
+ '@types/node': 17.0.41
+ '@types/sax': 1.2.4
+ arg: 5.0.2
+ sax: 1.2.4
+ dev: false
+
/slash/2.0.0:
resolution: {integrity: sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==}
engines: {node: '>=6'}