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>
```
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:

View file

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

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

@ -1,39 +1,40 @@
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>;
};
// number of entries per sitemap file
entryLimit?: number;
createLinkInHead?: boolean;
serialize?(item: SitemapItemLoose): SitemapItemLoose;
// 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;
@ -58,12 +59,10 @@ const createPlugin = (options?: SitemapOptions): AstroIntegration => {
'astro:build:done': async ({ dir, pages }) => {
const logger = new Logger(PKG_NAME);
const opts = withOptions(options || {});
try {
validateOpts(config.site, opts);
const opts = validateOptions(config.site, options);
const { filter, customPages, canonicalURL, serialize, createLinkInHead, entryLimit } = opts;
const { filter, customPages, canonicalURL, serialize, entryLimit } = opts;
let finalSiteUrl: URL;
if (canonicalURL) {
@ -72,7 +71,7 @@ const createPlugin = (options?: SitemapOptions): AstroIntegration => {
finalSiteUrl.pathname += '/'; // normalizes the final url since it's provided by user
}
} else {
// `validateOpts` forces to provide `canonicalURL` or `config.site` at least.
// `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);
}
@ -84,7 +83,7 @@ const createPlugin = (options?: SitemapOptions): AstroIntegration => {
try {
if (filter) {
pageUrls = pageUrls.filter((url) => filter(url));
pageUrls = pageUrls.filter(filter);
}
} catch (err) {
logger.error(`Error filtering pages\n${(err as any).toString()}`);
@ -102,11 +101,9 @@ const createPlugin = (options?: SitemapOptions): AstroIntegration => {
let urlData = generateSitemap(pageUrls, finalSiteUrl.href, opts);
let serializedUrls: SitemapItemLoose[];
if (serialize) {
serializedUrls = [];
try {
const serializedUrls: SitemapItemLoose[] = [];
for (const item of urlData) {
const serialized = await Promise.resolve(serialize(item));
serializedUrls.push(serialized);
@ -126,13 +123,6 @@ const createPlugin = (options?: SitemapOptions): AstroIntegration => {
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));

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

View file

@ -1,5 +1,16 @@
export const parseUrl = (url: string, defaultLocale: string, localeCodes: string[], base: string) => {
if (!url || !defaultLocale || localeCodes.length === 0 || localeCodes.some((key) => !key) || !base) {
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) {

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

21
pnpm-lock.yaml generated
View file

@ -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'