feat: createLinkInHead removed
This commit is contained in:
parent
9ed2bd7c3a
commit
8ea53d0b58
16 changed files with 311 additions and 397 deletions
|
@ -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:
|
||||||
|
|
||||||
|
|
|
@ -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:*",
|
||||||
|
|
5
packages/integrations/sitemap/src/config-defaults.ts
Normal file
5
packages/integrations/sitemap/src/config-defaults.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import type { SitemapOptions } from './index';
|
||||||
|
|
||||||
|
export const SITEMAP_CONFIG_DEFAULTS: SitemapOptions & any = {
|
||||||
|
entryLimit: 45000,
|
||||||
|
};
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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');
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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(),
|
|
||||||
});
|
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 };
|
||||||
};
|
};
|
||||||
|
|
22
packages/integrations/sitemap/src/validate-options.ts
Normal file
22
packages/integrations/sitemap/src/validate-options.ts
Normal 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;
|
||||||
|
};
|
|
@ -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 || {}) });
|
|
||||||
};
|
|
|
@ -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;
|
|
||||||
};
|
|
|
@ -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'
|
||||||
|
|
Loading…
Reference in a new issue