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>
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
## 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` - 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).
|
||||
|
||||
|
||||
: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__
|
||||
|
||||
|
@ -210,7 +184,7 @@ export default {
|
|||
sitemap({
|
||||
changefreq: 'weekly',
|
||||
priority: 0.7,
|
||||
lastmod: new Date('2022-05-28'),
|
||||
lastmod: new Date('2022-02-24'),
|
||||
}),
|
||||
],
|
||||
}
|
||||
|
@ -218,15 +192,17 @@ export default {
|
|||
|
||||
### 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.
|
||||
`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
|
||||
|
@ -237,7 +213,7 @@ export default {
|
|||
integrations: [
|
||||
sitemap({
|
||||
serialize(item) {
|
||||
if (/special-page/.test(item.url)) {
|
||||
if (/your-special-page/.test(item.url)) {
|
||||
item.changefreq = 'daily';
|
||||
item.lastmod = new Date();
|
||||
item.priority = 0.9;
|
||||
|
@ -251,7 +227,7 @@ export default {
|
|||
|
||||
### 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:
|
||||
|
||||
|
|
|
@ -31,9 +31,8 @@
|
|||
"dev": "astro-scripts dev \"src/**/*.ts\""
|
||||
},
|
||||
"dependencies": {
|
||||
"node-html-parser": "^5.3.3",
|
||||
"sitemap": "^7.1.1",
|
||||
"zod": "^3.17.3"
|
||||
"zod": "^3.17.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"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 */
|
||||
export function generateSitemap(pages: string[], finalSiteUrl: string, opts: SitemapOptions) {
|
||||
const { changefreq, priority: prioritySrc, lastmod: lastmodSrc, i18n } = opts || {};
|
||||
// TODO: find way to respect <link rel="canonical"> 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 { changefreq, priority: prioritySrc, lastmod: lastmodSrc, i18n } = opts || {};
|
||||
// TODO: find way to respect <link rel="canonical"> 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 lastmod = lastmodSrc?.toISOString();
|
||||
const priority = typeof prioritySrc === 'number' ? prioritySrc : undefined;
|
||||
|
||||
const { locales, defaultLocale } = i18n || {};
|
||||
const localeCodes = Object.keys(locales || {});
|
||||
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 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)!],
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
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 {
|
||||
url,
|
||||
links,
|
||||
lastmod,
|
||||
priority,
|
||||
changefreq, // : changefreq as EnumChangefreq,
|
||||
} as SitemapItemLoose;
|
||||
});
|
||||
|
||||
return urlData;
|
||||
return urlData;
|
||||
}
|
||||
|
|
|
@ -1,148 +1,138 @@
|
|||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import type { AstroConfig, AstroIntegration } from 'astro';
|
||||
import { ZodError } from 'zod';
|
||||
import { LinkItem as LinkItemBase, SitemapItemLoose, simpleSitemapAndIndex } from 'sitemap';
|
||||
|
||||
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 { processPages } from './process-pages';
|
||||
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 SitemapItem = Pick<
|
||||
SitemapItemLoose,
|
||||
'url' | 'lastmod' | 'changefreq' | 'priority' | 'links'
|
||||
>;
|
||||
export type LinkItem = LinkItemBase;
|
||||
|
||||
export type SitemapOptions =
|
||||
| {
|
||||
// the same with official
|
||||
filter?(page: string): boolean;
|
||||
customPages?: string[];
|
||||
canonicalURL?: string;
|
||||
// added
|
||||
i18n?: {
|
||||
defaultLocale: string;
|
||||
locales: Record<string, string>;
|
||||
};
|
||||
entryLimit?: number;
|
||||
| {
|
||||
filter?(page: string): boolean;
|
||||
customPages?: string[];
|
||||
canonicalURL?: string;
|
||||
|
||||
createLinkInHead?: boolean;
|
||||
serialize?(item: SitemapItemLoose): SitemapItemLoose;
|
||||
// sitemap specific
|
||||
changefreq?: ChangeFreq;
|
||||
lastmod?: Date;
|
||||
priority?: number;
|
||||
}
|
||||
| undefined;
|
||||
i18n?: {
|
||||
defaultLocale: string;
|
||||
locales: Record<string, string>;
|
||||
};
|
||||
// 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;
|
||||
|
||||
function formatConfigErrorMessage(err: ZodError) {
|
||||
const errorList = err.issues.map((issue) => ` ${issue.path.join('.')} ${issue.message + '.'}`);
|
||||
return errorList.join('\n');
|
||||
const errorList = err.issues.map((issue) => ` ${issue.path.join('.')} ${issue.message + '.'}`);
|
||||
return errorList.join('\n');
|
||||
}
|
||||
|
||||
const PKG_NAME = '@astrojs/sitemap';
|
||||
const OUTFILE = 'sitemap-index.xml';
|
||||
|
||||
const createPlugin = (options?: SitemapOptions): AstroIntegration => {
|
||||
let config: AstroConfig;
|
||||
return {
|
||||
name: PKG_NAME,
|
||||
let config: AstroConfig;
|
||||
return {
|
||||
name: PKG_NAME,
|
||||
|
||||
hooks: {
|
||||
'astro:config:done': async ({ config: cfg }) => {
|
||||
config = cfg;
|
||||
},
|
||||
hooks: {
|
||||
'astro:config:done': async ({ config: cfg }) => {
|
||||
config = cfg;
|
||||
},
|
||||
|
||||
'astro:build:done': async ({ dir, pages }) => {
|
||||
const logger = new Logger(PKG_NAME);
|
||||
'astro:build:done': async ({ dir, pages }) => {
|
||||
const logger = new Logger(PKG_NAME);
|
||||
|
||||
const opts = withOptions(options || {});
|
||||
try {
|
||||
const opts = validateOptions(config.site, options);
|
||||
|
||||
try {
|
||||
validateOpts(config.site, opts);
|
||||
const { filter, customPages, canonicalURL, serialize, entryLimit } = 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;
|
||||
if (canonicalURL) {
|
||||
finalSiteUrl = new URL(canonicalURL);
|
||||
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) => {
|
||||
const path = finalSiteUrl.pathname + p.pathname;
|
||||
return new URL(path, finalSiteUrl).href;
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
try {
|
||||
if (filter) {
|
||||
pageUrls = pageUrls.filter((url) => filter(url));
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(`Error filtering pages\n${(err as any).toString()}`);
|
||||
return;
|
||||
}
|
||||
if (customPages) {
|
||||
pageUrls = [...pageUrls, ...customPages];
|
||||
}
|
||||
|
||||
if (customPages) {
|
||||
pageUrls = [...pageUrls, ...customPages];
|
||||
}
|
||||
if (pageUrls.length === 0) {
|
||||
logger.warn(`No data for sitemap.\n\`${OUTFILE}\` is not created.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (pageUrls.length === 0) {
|
||||
logger.warn(`No data for sitemap.\n\`${OUTFILE}\` is not created.`);
|
||||
return;
|
||||
}
|
||||
let urlData = generateSitemap(pageUrls, finalSiteUrl.href, opts);
|
||||
|
||||
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[];
|
||||
|
||||
if (serialize) {
|
||||
serializedUrls = [];
|
||||
try {
|
||||
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.`);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
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;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
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 { isValidUrl } from './utils/is-valid-url';
|
||||
import { changefreqValues } from './constants';
|
||||
|
||||
const urlSchema = () =>
|
||||
z
|
||||
.string()
|
||||
.min(1)
|
||||
.refine((val) => !val || isValidUrl(val), 'Not valid url');
|
||||
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();
|
||||
const fnSchema = () =>
|
||||
z
|
||||
.any()
|
||||
.refine((val) => !val || isFunction(val), { message: 'Not a function' })
|
||||
.optional();
|
||||
|
||||
export const SitemapOptionsSchema = z.object({
|
||||
filter: fnSchema(),
|
||||
export const SitemapOptionsSchema = z
|
||||
.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
|
||||
.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(({ 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(),
|
||||
});
|
||||
changefreq: z.enum(changefreqValues).optional(),
|
||||
lastmod: z.date().optional(),
|
||||
priority: z.number().min(0).max(1).optional(),
|
||||
})
|
||||
.strict()
|
||||
.default(SITEMAP_CONFIG_DEFAULTS);
|
||||
|
|
|
@ -1,10 +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;
|
||||
if (!o) {
|
||||
return true;
|
||||
}
|
||||
if (Array.isArray(o)) {
|
||||
return o.length === 0;
|
||||
}
|
||||
return Object.keys(o).length === 0 && Object.getPrototypeOf(o) === Object.prototype;
|
||||
};
|
||||
|
|
|
@ -1,13 +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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,46 +1,46 @@
|
|||
// @internal
|
||||
export interface ILogger {
|
||||
info(msg: string): void;
|
||||
success(msg: string): void;
|
||||
warn(msg: string): void;
|
||||
error(msg: string): void;
|
||||
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 colors = {
|
||||
reset: '\x1b[0m',
|
||||
fg: {
|
||||
red: '\x1b[31m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
},
|
||||
} as const;
|
||||
|
||||
private packageName: string;
|
||||
private packageName: string;
|
||||
|
||||
constructor(packageName: string) {
|
||||
this.packageName = packageName;
|
||||
}
|
||||
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 : '');
|
||||
}
|
||||
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);
|
||||
}
|
||||
info(msg: string) {
|
||||
this.log(msg);
|
||||
}
|
||||
|
||||
success(msg: string) {
|
||||
this.log(msg, this.colors.fg.green);
|
||||
}
|
||||
success(msg: string) {
|
||||
this.log(msg, this.colors.fg.green);
|
||||
}
|
||||
|
||||
warn(msg: string) {
|
||||
this.log(`Skipped!\n${msg}`, this.colors.fg.yellow);
|
||||
}
|
||||
warn(msg: string) {
|
||||
this.log(`Skipped!\n${msg}`, this.colors.fg.yellow);
|
||||
}
|
||||
|
||||
error(msg: string) {
|
||||
this.log(`Failed!\n${msg}`, this.colors.fg.red);
|
||||
}
|
||||
error(msg: string) {
|
||||
this.log(`Failed!\n${msg}`, this.colors.fg.red);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,28 +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 };
|
||||
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 };
|
||||
};
|
||||
|
|
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:
|
||||
astro: workspace:*
|
||||
astro-scripts: workspace:*
|
||||
node-html-parser: ^5.3.3
|
||||
sitemap: ^7.1.1
|
||||
zod: ^3.17.3
|
||||
dependencies:
|
||||
node-html-parser: 5.3.3
|
||||
sitemap: 7.1.1
|
||||
zod: 3.17.3
|
||||
devDependencies:
|
||||
|
@ -8075,16 +8073,6 @@ packages:
|
|||
engines: {node: '>=8'}
|
||||
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:
|
||||
resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==}
|
||||
dependencies:
|
||||
|
@ -8101,6 +8089,7 @@ packages:
|
|||
/css-what/6.1.0:
|
||||
resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==}
|
||||
engines: {node: '>= 6'}
|
||||
dev: true
|
||||
|
||||
/cssesc/3.0.0:
|
||||
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
|
||||
|
@ -9612,6 +9601,7 @@ packages:
|
|||
/he/1.2.0:
|
||||
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/hosted-git-info/2.8.9:
|
||||
resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==}
|
||||
|
@ -11123,13 +11113,6 @@ packages:
|
|||
hasBin: true
|
||||
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:
|
||||
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'
|
||||
|
|
Loading…
Reference in a new issue