Skip to content

Commit 9b78c99

Browse files
ktym4aematipicosarah11918
authored
Add option to prefix sitemap (#9846)
* Add option to prefix sitemap * Fix call resolve twice * let to const * Apply suggestions from code review Co-authored-by: Emanuele Stoppa <my.burning@gmail.com> * change changeset patch to minor * use node:test * Update changeset * Add regex validation for prefix * Update .changeset/eighty-falcons-tease.md Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> * Update prefix regex in SitemapOptionsSchema --------- Co-authored-by: Emanuele Stoppa <my.burning@gmail.com> Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
1 parent 3c73441 commit 9b78c99

4 files changed

Lines changed: 125 additions & 9 deletions

File tree

.changeset/eighty-falcons-tease.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
"@astrojs/sitemap": minor
3+
---
4+
5+
Adds a new configuration option `prefix` that allows you to change the default `sitemap-*.xml` file name.
6+
7+
By default, running `astro build` creates both `sitemap-index.xml` and `sitemap-0.xml` in your output directory.
8+
9+
To change the names of these files (e.g. to `astrosite-index.xml` and `astrosite-0.xml`), set the `prefix` option in your `sitemap` integration configuration:
10+
11+
```
12+
import { defineConfig } from 'astro/config';
13+
import sitemap from '@astrojs/sitemap';
14+
export default defineConfig({
15+
site: 'https://example.com',
16+
integrations: [
17+
sitemap({
18+
prefix: 'astrosite-',
19+
}),
20+
],
21+
});
22+
```
23+
24+
This option is useful when Google Search Console is unable to fetch your default sitemap files, but can read renamed files.

packages/integrations/sitemap/src/index.ts

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import type { AstroConfig, AstroIntegration } from 'astro';
2-
import path from 'node:path';
2+
import path, { resolve } from 'node:path';
33
import { fileURLToPath } from 'node:url';
44
import type { EnumChangefreq, LinkItem as LinkItemBase, SitemapItemLoose } from 'sitemap';
5-
import { simpleSitemapAndIndex } from 'sitemap';
5+
import { SitemapAndIndexStream, SitemapStream, streamToPromise } from 'sitemap';
66
import { ZodError } from 'zod';
77

88
import { generateSitemap } from './generate-sitemap.js';
99
import { validateOptions } from './validate-options.js';
10+
import { createWriteStream } from 'node:fs';
11+
import { Readable } from 'node:stream';
1012

1113
export { EnumChangefreq as ChangeFreqEnum } from 'sitemap';
1214
export type ChangeFreq = `${EnumChangefreq}`;
@@ -33,6 +35,8 @@ export type SitemapOptions =
3335
lastmod?: Date;
3436
priority?: number;
3537

38+
prefix?: string;
39+
3640
// called for each sitemap item just before to save them on disk, sync or async
3741
serialize?(item: SitemapItem): SitemapItem | Promise<SitemapItem | undefined> | undefined;
3842
}
@@ -44,7 +48,6 @@ function formatConfigErrorMessage(err: ZodError) {
4448
}
4549

4650
const PKG_NAME = '@astrojs/sitemap';
47-
const OUTFILE = 'sitemap-index.xml';
4851
const STATUS_CODE_PAGES = new Set(['404', '500']);
4952

5053
function isStatusCodePage(pathname: string): boolean {
@@ -77,7 +80,8 @@ const createPlugin = (options?: SitemapOptions): AstroIntegration => {
7780

7881
const opts = validateOptions(config.site, options);
7982

80-
const { filter, customPages, serialize, entryLimit } = opts;
83+
const { filter, customPages, serialize, entryLimit, prefix = 'sitemap-' } = opts;
84+
const OUTFILE = `${prefix}index.xml`;
8185

8286
let finalSiteUrl: URL;
8387
if (config.site) {
@@ -166,13 +170,22 @@ const createPlugin = (options?: SitemapOptions): AstroIntegration => {
166170
}
167171
}
168172
const destDir = fileURLToPath(dir);
169-
await simpleSitemapAndIndex({
170-
hostname: finalSiteUrl.href,
171-
destinationDir: destDir,
172-
sourceData: urlData,
173+
174+
const sms = new SitemapAndIndexStream({
173175
limit: entryLimit,
174-
gzip: false,
176+
getSitemapStream: (i) => {
177+
const sitemapStream = new SitemapStream({ hostname: finalSiteUrl.href });
178+
const fileName = `${prefix}${i}.xml`;
179+
180+
const ws = sitemapStream.pipe(createWriteStream(resolve(destDir + fileName)));
181+
182+
return [new URL(fileName, finalSiteUrl.href).toString(), sitemapStream, ws];
183+
},
175184
});
185+
186+
sms.pipe(createWriteStream(resolve(destDir + OUTFILE)));
187+
await streamToPromise(Readable.from(urlData).pipe(sms));
188+
sms.end();
176189
logger.info(`\`${OUTFILE}\` created at \`${path.relative(process.cwd(), destDir)}\``);
177190
} catch (err) {
178191
if (err instanceof ZodError) {

packages/integrations/sitemap/src/schema.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@ export const SitemapOptionsSchema = z
3434
changefreq: z.nativeEnum(ChangeFreq).optional(),
3535
lastmod: z.date().optional(),
3636
priority: z.number().min(0).max(1).optional(),
37+
38+
prefix: z
39+
.string()
40+
.regex(/^[a-zA-Z\-_]+$/gm, {
41+
message: 'Only English alphabet symbols, hyphen and underscore allowed',
42+
})
43+
.optional(),
3744
})
3845
.strict()
3946
.default(SITEMAP_CONFIG_DEFAULTS);
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { loadFixture, readXML } from './test-utils.js';
2+
import { sitemap } from './fixtures/static/deps.mjs';
3+
import assert from 'node:assert/strict';
4+
import { before, describe, it } from 'node:test';
5+
6+
describe('Prefix support', () => {
7+
/** @type {import('./test-utils.js').Fixture} */
8+
let fixture;
9+
const prefix = 'test-';
10+
11+
describe('Static', () => {
12+
before(async () => {
13+
fixture = await loadFixture({
14+
root: './fixtures/static/',
15+
integrations: [
16+
sitemap(),
17+
sitemap({
18+
prefix,
19+
}),
20+
],
21+
});
22+
await fixture.build();
23+
});
24+
25+
it('Content is same', async () => {
26+
const data = await readXML(fixture.readFile('/sitemap-0.xml'));
27+
const prefixData = await readXML(fixture.readFile(`/${prefix}0.xml`));
28+
assert.deepEqual(prefixData, data);
29+
});
30+
31+
it('Index file load correct sitemap', async () => {
32+
const data = await readXML(fixture.readFile('/sitemap-index.xml'));
33+
const sitemapUrl = data.sitemapindex.sitemap[0].loc[0];
34+
assert.strictEqual(sitemapUrl, 'http://example.com/sitemap-0.xml');
35+
36+
const prefixData = await readXML(fixture.readFile(`/${prefix}index.xml`));
37+
const prefixSitemapUrl = prefixData.sitemapindex.sitemap[0].loc[0];
38+
assert.strictEqual(prefixSitemapUrl, `http://example.com/${prefix}0.xml`);
39+
});
40+
});
41+
42+
describe('SSR', () => {
43+
before(async () => {
44+
fixture = await loadFixture({
45+
root: './fixtures/ssr/',
46+
integrations: [
47+
sitemap(),
48+
sitemap({
49+
prefix,
50+
}),
51+
],
52+
});
53+
await fixture.build();
54+
});
55+
56+
it('Content is same', async () => {
57+
const data = await readXML(fixture.readFile('/client/sitemap-0.xml'));
58+
const prefixData = await readXML(fixture.readFile(`/client/${prefix}0.xml`));
59+
assert.deepEqual(prefixData, data);
60+
});
61+
62+
it('Index file load correct sitemap', async () => {
63+
const data = await readXML(fixture.readFile('/client/sitemap-index.xml'));
64+
const sitemapUrl = data.sitemapindex.sitemap[0].loc[0];
65+
assert.strictEqual(sitemapUrl, 'http://example.com/sitemap-0.xml');
66+
67+
const prefixData = await readXML(fixture.readFile(`/client/${prefix}index.xml`));
68+
const prefixSitemapUrl = prefixData.sitemapindex.sitemap[0].loc[0];
69+
assert.strictEqual(prefixSitemapUrl, `http://example.com/${prefix}0.xml`);
70+
});
71+
});
72+
});

0 commit comments

Comments
 (0)