feat: createLinkInHead removed

This commit is contained in:
Alex Tim 2022-06-16 01:09:08 +03:00
parent 9ed2bd7c3a
commit 8ea53d0b58
16 changed files with 311 additions and 397 deletions

View file

@ -94,12 +94,6 @@ Generated sitemap content for two pages website:
</urlset> </urlset>
``` ```
All pages generated during build will contain in `<head>` section a link to sitemap:
```html
<link rel="sitemap" type="application/xml" href="/sitemap-index.xml">
```
You can also check our [Astro Integration Documentation][astro-integration] for more on integrations. You can also check our [Astro Integration Documentation][astro-integration] for more on integrations.
## Configuration ## Configuration
@ -164,26 +158,6 @@ export default {
} }
``` ```
### createLinkInHead
`Boolean`, default is `true`, create a link on sitemap in `<head>` section of generated pages.
__astro.config.mjs__
```js
import sitemap from '@astrojs/sitemap';
export default {
site: 'https://stargazers.club',
integrations: [
sitemap({
// disable create links to sitemap in <head>
createLinkInHead: false,
}),
],
}
```
### changefreq, lastmod, priority ### changefreq, lastmod, priority
`changefreq` - How frequently the page is likely to change. Available values: `always` \| `hourly` \| `daily` \| `weekly` \| `monthly` \| `yearly` \| `never`. `changefreq` - How frequently the page is likely to change. Available values: `always` \| `hourly` \| `daily` \| `weekly` \| `monthly` \| `yearly` \| `never`.
@ -197,7 +171,7 @@ export default {
See detailed explanation of sitemap specific options on [sitemap.org](https://www.sitemaps.org/protocol.html). 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 only generated page paths. 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. :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__ __astro.config.mjs__
@ -210,7 +184,7 @@ export default {
sitemap({ sitemap({
changefreq: 'weekly', changefreq: 'weekly',
priority: 0.7, priority: 0.7,
lastmod: new Date('2022-05-28'), lastmod: new Date('2022-02-24'),
}), }),
], ],
} }
@ -218,15 +192,17 @@ export default {
### serialize ### serialize
Async or sync function called for each sitemap entry just before writing to disk. 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 URL of page) and optional `changefreq`, `lastmod`, `priority` and `links` properties. 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. 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). `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. `serialize` function should return `SitemapItem`, touched or not.
The example below shows the ability to add the sitemap specific properties individually.
__astro.config.mjs__ __astro.config.mjs__
```js ```js
@ -237,7 +213,7 @@ export default {
integrations: [ integrations: [
sitemap({ sitemap({
serialize(item) { serialize(item) {
if (/special-page/.test(item.url)) { if (/your-special-page/.test(item.url)) {
item.changefreq = 'daily'; item.changefreq = 'daily';
item.lastmod = new Date(); item.lastmod = new Date();
item.priority = 0.9; item.priority = 0.9;
@ -251,7 +227,7 @@ export default {
### i18n ### i18n
To localize 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. 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: `i18n` object has two required properties:

View file

@ -31,9 +31,8 @@
"dev": "astro-scripts dev \"src/**/*.ts\"" "dev": "astro-scripts dev \"src/**/*.ts\""
}, },
"dependencies": { "dependencies": {
"node-html-parser": "^5.3.3",
"sitemap": "^7.1.1", "sitemap": "^7.1.1",
"zod": "^3.17.3" "zod": "^3.17.3"
}, },
"devDependencies": { "devDependencies": {
"astro": "workspace:*", "astro": "workspace:*",

View file

@ -0,0 +1,5 @@
import type { SitemapOptions } from './index';
export const SITEMAP_CONFIG_DEFAULTS: SitemapOptions & any = {
entryLimit: 45000,
};

View file

@ -1 +1,9 @@
export const changefreqValues = ['always', 'hourly', 'daily', 'weekly', 'monthly', 'yearly', 'never'] as const; export const changefreqValues = [
'always',
'hourly',
'daily',
'weekly',
'monthly',
'yearly',
'never',
] as const;

View file

@ -7,49 +7,49 @@ const STATUS_CODE_PAGE_REGEXP = /\/[0-9]{3}\/?$/;
/** Construct sitemap.xml given a set of URLs */ /** Construct sitemap.xml given a set of URLs */
export function generateSitemap(pages: string[], finalSiteUrl: string, opts: SitemapOptions) { export function generateSitemap(pages: string[], finalSiteUrl: string, opts: SitemapOptions) {
const { changefreq, priority: prioritySrc, lastmod: lastmodSrc, i18n } = opts || {}; const { changefreq, priority: prioritySrc, lastmod: lastmodSrc, i18n } = opts || {};
// TODO: find way to respect <link rel="canonical"> URLs here // TODO: find way to respect <link rel="canonical"> URLs here
const urls = [...pages].filter((url) => !STATUS_CODE_PAGE_REGEXP.test(url)); 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 urls.sort((a, b) => a.localeCompare(b, 'en', { numeric: true })); // sort alphabetically so sitemap is same each time
const lastmod = lastmodSrc?.toISOString(); const lastmod = lastmodSrc?.toISOString();
const priority = typeof prioritySrc === 'number' ? prioritySrc : undefined; const priority = typeof prioritySrc === 'number' ? prioritySrc : undefined;
const { locales, defaultLocale } = i18n || {}; const { locales, defaultLocale } = i18n || {};
const localeCodes = Object.keys(locales || {}); const localeCodes = Object.keys(locales || {});
const getPath = (url: string) => { const getPath = (url: string) => {
const result = parseUrl(url, i18n?.defaultLocale || '', localeCodes, finalSiteUrl); const result = parseUrl(url, i18n?.defaultLocale || '', localeCodes, finalSiteUrl);
return result?.path; return result?.path;
}; };
const getLocale = (url: string) => { const getLocale = (url: string) => {
const result = parseUrl(url, i18n?.defaultLocale || '', localeCodes, finalSiteUrl); const result = parseUrl(url, i18n?.defaultLocale || '', localeCodes, finalSiteUrl);
return result?.locale; return result?.locale;
}; };
const urlData = urls.map((url) => { const urlData = urls.map((url) => {
let links; let links;
if (defaultLocale && locales) { if (defaultLocale && locales) {
const currentPath = getPath(url); const currentPath = getPath(url);
if (currentPath) { if (currentPath) {
const filtered = urls.filter((subUrl) => getPath(subUrl) === currentPath); const filtered = urls.filter((subUrl) => getPath(subUrl) === currentPath);
if (filtered.length > 1) { if (filtered.length > 1) {
links = filtered.map((subUrl) => ({ links = filtered.map((subUrl) => ({
url: subUrl, url: subUrl,
lang: locales[getLocale(subUrl)!], lang: locales[getLocale(subUrl)!],
})); }));
} }
} }
} }
return { return {
url, url,
links, links,
lastmod, lastmod,
priority, priority,
changefreq, // : changefreq as EnumChangefreq, changefreq, // : changefreq as EnumChangefreq,
} as SitemapItemLoose; } as SitemapItemLoose;
}); });
return urlData; return urlData;
} }

View file

@ -1,148 +1,138 @@
import path from 'node:path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import type { AstroConfig, AstroIntegration } from 'astro'; import type { AstroConfig, AstroIntegration } from 'astro';
import { ZodError } from 'zod'; import { ZodError } from 'zod';
import { LinkItem as LinkItemBase, SitemapItemLoose, simpleSitemapAndIndex } from 'sitemap'; import { LinkItem as LinkItemBase, SitemapItemLoose, simpleSitemapAndIndex } from 'sitemap';
import { Logger } from './utils/logger'; import { Logger } from './utils/logger';
import { withOptions } from './with-options';
import { validateOpts } from './validate-opts';
import { generateSitemap } from './generate-sitemap';
import { changefreqValues } from './constants'; import { changefreqValues } from './constants';
import { processPages } from './process-pages'; import { validateOptions } from './validate-options';
import { generateSitemap } from './generate-sitemap';
export type ChangeFreq = typeof changefreqValues[number]; export type ChangeFreq = typeof changefreqValues[number];
export type SitemapItem = Pick<SitemapItemLoose, 'url' | 'lastmod' | 'changefreq' | 'priority' | 'links'>; export type SitemapItem = Pick<
SitemapItemLoose,
'url' | 'lastmod' | 'changefreq' | 'priority' | 'links'
>;
export type LinkItem = LinkItemBase; export type LinkItem = LinkItemBase;
export type SitemapOptions = export type SitemapOptions =
| { | {
// the same with official filter?(page: string): boolean;
filter?(page: string): boolean; customPages?: string[];
customPages?: string[]; canonicalURL?: string;
canonicalURL?: string;
// added
i18n?: {
defaultLocale: string;
locales: Record<string, string>;
};
entryLimit?: number;
createLinkInHead?: boolean; i18n?: {
serialize?(item: SitemapItemLoose): SitemapItemLoose; defaultLocale: string;
// sitemap specific locales: Record<string, string>;
changefreq?: ChangeFreq; };
lastmod?: Date; // number of entries per sitemap file
priority?: number; entryLimit?: number;
}
| undefined; // 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;
function formatConfigErrorMessage(err: ZodError) { function formatConfigErrorMessage(err: ZodError) {
const errorList = err.issues.map((issue) => ` ${issue.path.join('.')} ${issue.message + '.'}`); const errorList = err.issues.map((issue) => ` ${issue.path.join('.')} ${issue.message + '.'}`);
return errorList.join('\n'); return errorList.join('\n');
} }
const PKG_NAME = '@astrojs/sitemap'; const PKG_NAME = '@astrojs/sitemap';
const OUTFILE = 'sitemap-index.xml'; const OUTFILE = 'sitemap-index.xml';
const createPlugin = (options?: SitemapOptions): AstroIntegration => { const createPlugin = (options?: SitemapOptions): AstroIntegration => {
let config: AstroConfig; let config: AstroConfig;
return { return {
name: PKG_NAME, name: PKG_NAME,
hooks: { hooks: {
'astro:config:done': async ({ config: cfg }) => { 'astro:config:done': async ({ config: cfg }) => {
config = cfg; config = cfg;
}, },
'astro:build:done': async ({ dir, pages }) => { 'astro:build:done': async ({ dir, pages }) => {
const logger = new Logger(PKG_NAME); const logger = new Logger(PKG_NAME);
const opts = withOptions(options || {}); try {
const opts = validateOptions(config.site, options);
try { const { filter, customPages, canonicalURL, serialize, entryLimit } = opts;
validateOpts(config.site, opts);
const { filter, customPages, canonicalURL, serialize, createLinkInHead, 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 finalSiteUrl: URL; let pageUrls = pages.map((p) => {
if (canonicalURL) { const path = finalSiteUrl.pathname + p.pathname;
finalSiteUrl = new URL(canonicalURL); return new URL(path, finalSiteUrl).href;
if (!finalSiteUrl.pathname.endsWith('/')) { });
finalSiteUrl.pathname += '/'; // normalizes the final url since it's provided by user
}
} else {
// `validateOpts` 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) => { try {
const path = finalSiteUrl.pathname + p.pathname; if (filter) {
return new URL(path, finalSiteUrl).href; pageUrls = pageUrls.filter(filter);
}); }
} catch (err) {
logger.error(`Error filtering pages\n${(err as any).toString()}`);
return;
}
try { if (customPages) {
if (filter) { pageUrls = [...pageUrls, ...customPages];
pageUrls = pageUrls.filter((url) => filter(url)); }
}
} catch (err) {
logger.error(`Error filtering pages\n${(err as any).toString()}`);
return;
}
if (customPages) { if (pageUrls.length === 0) {
pageUrls = [...pageUrls, ...customPages]; logger.warn(`No data for sitemap.\n\`${OUTFILE}\` is not created.`);
} return;
}
if (pageUrls.length === 0) { let urlData = generateSitemap(pageUrls, finalSiteUrl.href, opts);
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;
}
}
let serializedUrls: SitemapItemLoose[]; await simpleSitemapAndIndex({
hostname: finalSiteUrl.href,
if (serialize) { destinationDir: fileURLToPath(dir),
serializedUrls = []; sourceData: urlData,
try { limit: entryLimit,
for (const item of urlData) { gzip: false,
const serialized = await Promise.resolve(serialize(item)); });
serializedUrls.push(serialized); logger.success(`\`${OUTFILE}\` is created.`);
} } catch (err) {
urlData = serializedUrls; if (err instanceof ZodError) {
} catch (err) { logger.warn(formatConfigErrorMessage(err));
logger.error(`Error serializing pages\n${(err as any).toString()}`); } else {
return; throw err;
} }
} }
},
await simpleSitemapAndIndex({ },
hostname: finalSiteUrl.href, };
destinationDir: fileURLToPath(dir),
sourceData: urlData,
limit: entryLimit,
gzip: false,
});
logger.success(`\`${OUTFILE}\` is created.`);
if (createLinkInHead) {
const sitemapHref = path.posix.join(config.base, OUTFILE);
const headHTML = `<link rel="sitemap" type="application/xml" href="${sitemapHref}">`;
await processPages(pages, dir, headHTML, config.build.format);
logger.success('Sitemap links are created in <head> section of generated pages.');
}
} catch (err) {
if (err instanceof ZodError) {
logger.warn(formatConfigErrorMessage(err));
} else {
throw err;
}
}
},
},
};
}; };
export default createPlugin; export default createPlugin;

View file

@ -1,39 +0,0 @@
import { promises as fs } from 'node:fs';
import { parse, HTMLElement } from 'node-html-parser';
const addTailSlash = (s: string) => (s.endsWith('/') ? s : s + '/');
const removeHeadingSlash = (s: string) => s.replace(/^\/+/, '');
const removeTrailingSlash = (s: string) => s.replace(/\/+$/, '');
const getFileDir = (pathname: string) => {
const name = addTailSlash(pathname);
const file = name === '404/' ? '404.html' : `${name}index.html`;
return removeHeadingSlash(file);
};
const getFileFile = (pathname: string) => (pathname ? `${removeTrailingSlash(pathname)}.html` : 'index.html');
export async function processPages(pages: { pathname: string }[], dir: URL, headHTML: string, buildFormat: string) {
if (pages.length === 0) {
return;
}
if (buildFormat !== 'directory' && buildFormat !== 'file') {
throw new Error(`Unsupported build.format: '${buildFormat}' in your astro.config`);
}
for (const page of pages) {
const fileUrl = new URL(buildFormat === 'directory' ? getFileDir(page.pathname) : getFileFile(page.pathname), dir);
const html = await fs.readFile(fileUrl, 'utf-8');
const root = parse(html);
let head = root.querySelector('head');
if (!head) {
head = new HTMLElement('head', {}, '', root);
root.appendChild(head);
console.warn(`No <head> found in \`${fileUrl.pathname}\`. <head> will be created.`);
}
head.innerHTML = head.innerHTML + headHTML;
const inlined = root.toString();
await fs.writeFile(fileUrl, inlined, 'utf-8');
}
}

View file

@ -1,52 +1,47 @@
import { z } from 'zod'; import { z } from 'zod';
import { isValidUrl } from './utils/is-valid-url';
import { changefreqValues } from './constants'; import { changefreqValues } from './constants';
import { SITEMAP_CONFIG_DEFAULTS } from './config-defaults';
const urlSchema = () =>
z
.string()
.min(1)
.refine((val) => !val || isValidUrl(val), 'Not valid url');
const localeKeySchema = () => z.string().min(1); const localeKeySchema = () => z.string().min(1);
const isFunction = (fn: any) => fn instanceof Function; const isFunction = (fn: any) => fn instanceof Function;
const fnSchema = () => z const fnSchema = () =>
.any() z
.refine((val) => !val || isFunction(val), { message: 'Not a function' }) .any()
.optional(); .refine((val) => !val || isFunction(val), { message: 'Not a function' })
.optional();
export const SitemapOptionsSchema = z.object({ export const SitemapOptionsSchema = z
filter: fnSchema(), .object({
filter: fnSchema(),
customPages: z.string().url().array().optional(),
canonicalURL: z.string().url().optional(),
customPages: urlSchema().array().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(),
canonicalURL: urlSchema().optional(), entryLimit: z.number().nonnegative().default(SITEMAP_CONFIG_DEFAULTS.entryLimit),
serialize: fnSchema(),
i18n: z changefreq: z.enum(changefreqValues).optional(),
.object({ lastmod: z.date().optional(),
defaultLocale: localeKeySchema(), priority: z.number().min(0).max(1).optional(),
locales: z.record( })
localeKeySchema(), .strict()
z .default(SITEMAP_CONFIG_DEFAULTS);
.string()
.min(2)
.regex(/^[a-zA-Z\-]+$/gm, { message: 'Only English alphabet symbols and hyphen allowed' }),
),
})
.refine(({ locales, defaultLocale }) => locales[defaultLocale], {
message: '`defaultLocale` must exists in `locales` keys',
})
.optional(),
createLinkInHead: z.boolean().optional(),
entryLimit: z.number().nonnegative().optional(),
serialize: fnSchema(),
changefreq: z.enum(changefreqValues).optional(),
lastmod: z.date().optional(),
priority: z.number().min(0).max(1).optional(),
});

View file

@ -1,10 +1,10 @@
// @internal // @internal
export const isObjectEmpty = (o: any) => { export const isObjectEmpty = (o: any) => {
if (!o) { if (!o) {
return true; return true;
} }
if (Array.isArray(o)) { if (Array.isArray(o)) {
return o.length === 0; return o.length === 0;
} }
return Object.keys(o).length === 0 && Object.getPrototypeOf(o) === Object.prototype; return Object.keys(o).length === 0 && Object.getPrototypeOf(o) === Object.prototype;
}; };

View file

@ -1,13 +1,13 @@
// @internal // @internal
export const isValidUrl = (s: any) => { export const isValidUrl = (s: any) => {
if (typeof s !== 'string' || !s) { if (typeof s !== 'string' || !s) {
return false; return false;
} }
try { try {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const dummy = new URL(s); const dummy = new URL(s);
return true; return true;
} catch { } catch {
return false; return false;
} }
}; };

View file

@ -1,46 +1,46 @@
// @internal // @internal
export interface ILogger { export interface ILogger {
info(msg: string): void; info(msg: string): void;
success(msg: string): void; success(msg: string): void;
warn(msg: string): void; warn(msg: string): void;
error(msg: string): void; error(msg: string): void;
} }
// @internal // @internal
export class Logger implements ILogger { export class Logger implements ILogger {
private colors = { private colors = {
reset: '\x1b[0m', reset: '\x1b[0m',
fg: { fg: {
red: '\x1b[31m', red: '\x1b[31m',
green: '\x1b[32m', green: '\x1b[32m',
yellow: '\x1b[33m', yellow: '\x1b[33m',
}, },
} as const; } as const;
private packageName: string; private packageName: string;
constructor(packageName: string) { constructor(packageName: string) {
this.packageName = packageName; this.packageName = packageName;
} }
private log(msg: string, prefix: string = '') { private log(msg: string, prefix: string = '') {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(`%s${this.packageName}:%s ${msg}\n`, prefix, prefix ? this.colors.reset : ''); console.log(`%s${this.packageName}:%s ${msg}\n`, prefix, prefix ? this.colors.reset : '');
} }
info(msg: string) { info(msg: string) {
this.log(msg); this.log(msg);
} }
success(msg: string) { success(msg: string) {
this.log(msg, this.colors.fg.green); this.log(msg, this.colors.fg.green);
} }
warn(msg: string) { warn(msg: string) {
this.log(`Skipped!\n${msg}`, this.colors.fg.yellow); this.log(`Skipped!\n${msg}`, this.colors.fg.yellow);
} }
error(msg: string) { error(msg: string) {
this.log(`Failed!\n${msg}`, this.colors.fg.red); this.log(`Failed!\n${msg}`, this.colors.fg.red);
} }
} }

View file

@ -1,28 +1,39 @@
export const parseUrl = (url: string, defaultLocale: string, localeCodes: string[], base: string) => { export const parseUrl = (
if (!url || !defaultLocale || localeCodes.length === 0 || localeCodes.some((key) => !key) || !base) { url: string,
throw new Error('parseUrl: some parameters are empty'); defaultLocale: string,
} localeCodes: string[],
if (url.indexOf(base) !== 0) { base: string
return undefined; ) => {
} if (
let s = url.replace(base, ''); !url ||
if (!s || s === '/') { !defaultLocale ||
return { locale: defaultLocale, path: '/' }; localeCodes.length === 0 ||
} localeCodes.some((key) => !key) ||
if (!s.startsWith('/')) { !base
s = '/' + s; ) {
} throw new Error('parseUrl: some parameters are empty');
const a = s.split('/'); }
const locale = a[1]; if (url.indexOf(base) !== 0) {
if (localeCodes.some((key) => key === locale)) { return undefined;
let path = a.slice(2).join('/'); }
if (path === '//') { let s = url.replace(base, '');
path = '/'; if (!s || s === '/') {
} return { locale: defaultLocale, path: '/' };
if (path !== '/' && !path.startsWith('/')) { }
path = '/' + path; if (!s.startsWith('/')) {
} s = '/' + s;
return { locale, path }; }
} const a = s.split('/');
return { locale: defaultLocale, path: s }; 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 };
}; };

View file

@ -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;
};

View file

@ -1,16 +0,0 @@
import { z } from 'zod';
import type { SitemapOptions } from './index';
import { SitemapOptionsSchema } from './schema';
// @internal
export const validateOpts = (site: string | undefined, opts: SitemapOptions) => {
const schema = SitemapOptionsSchema.extend({
site: z.string().optional(),
})
.strict()
.refine(({ site, canonicalURL }) => site || canonicalURL, {
message: 'Required `site` astro.config option or `canonicalURL` integration option',
});
schema.parse({ site: site || '', ...(opts || {}) });
};

View file

@ -1,20 +0,0 @@
import { isObjectEmpty } from './utils/is-object-empty';
import type { SitemapOptions } from './index';
const defaultOptions: Readonly<SitemapOptions> = {
createLinkInHead: true,
entryLimit: 45000,
};
// @internal
export const withOptions = (pluginOptions: SitemapOptions) => {
if (isObjectEmpty(pluginOptions)) {
return defaultOptions;
}
const options: SitemapOptions = {
...pluginOptions,
createLinkInHead: pluginOptions?.createLinkInHead ?? defaultOptions.createLinkInHead,
entryLimit: pluginOptions?.entryLimit || defaultOptions.entryLimit,
};
return options;
};

View file

@ -1800,11 +1800,9 @@ importers:
specifiers: specifiers:
astro: workspace:* astro: workspace:*
astro-scripts: workspace:* astro-scripts: workspace:*
node-html-parser: ^5.3.3
sitemap: ^7.1.1 sitemap: ^7.1.1
zod: ^3.17.3 zod: ^3.17.3
dependencies: dependencies:
node-html-parser: 5.3.3
sitemap: 7.1.1 sitemap: 7.1.1
zod: 3.17.3 zod: 3.17.3
devDependencies: devDependencies:
@ -8075,16 +8073,6 @@ packages:
engines: {node: '>=8'} engines: {node: '>=8'}
dev: true dev: true
/css-select/4.3.0:
resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==}
dependencies:
boolbase: 1.0.0
css-what: 6.1.0
domhandler: 4.3.1
domutils: 2.8.0
nth-check: 2.1.1
dev: false
/css-select/5.1.0: /css-select/5.1.0:
resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==}
dependencies: dependencies:
@ -8101,6 +8089,7 @@ packages:
/css-what/6.1.0: /css-what/6.1.0:
resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
dev: true
/cssesc/3.0.0: /cssesc/3.0.0:
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
@ -9612,6 +9601,7 @@ packages:
/he/1.2.0: /he/1.2.0:
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
hasBin: true hasBin: true
dev: true
/hosted-git-info/2.8.9: /hosted-git-info/2.8.9:
resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==}
@ -11123,13 +11113,6 @@ packages:
hasBin: true hasBin: true
dev: false dev: false
/node-html-parser/5.3.3:
resolution: {integrity: sha512-ncg1033CaX9UexbyA7e1N0aAoAYRDiV8jkTvzEnfd1GDvzFdrsXLzR4p4ik8mwLgnaKP/jyUFWDy9q3jvRT2Jw==}
dependencies:
css-select: 4.3.0
he: 1.2.0
dev: false
/node-pre-gyp/0.13.0: /node-pre-gyp/0.13.0:
resolution: {integrity: sha512-Md1D3xnEne8b/HGVQkZZwV27WUi1ZRuZBij24TNaZwUPU3ZAFtvT6xxJGaUVillfmMKnn5oD1HoGsp2Ftik7SQ==} resolution: {integrity: sha512-Md1D3xnEne8b/HGVQkZZwV27WUi1ZRuZBij24TNaZwUPU3ZAFtvT6xxJGaUVillfmMKnn5oD1HoGsp2Ftik7SQ==}
deprecated: 'Please upgrade to @mapbox/node-pre-gyp: the non-scoped node-pre-gyp package is deprecated and only the @mapbox scoped package will recieve updates in the future' deprecated: 'Please upgrade to @mapbox/node-pre-gyp: the non-scoped node-pre-gyp package is deprecated and only the @mapbox scoped package will recieve updates in the future'