Skip to content

Commit 52d3f56

Browse files
fix(@astrojs/sitemap): preserve root path / in sitemap URLs when trailingSlash is 'never' (#17851)
Co-authored-by: factory[bot] <factory[bot]@users.noreply.github.com>
1 parent a51c533 commit 52d3f56

7 files changed

Lines changed: 72 additions & 105 deletions

File tree

.changeset/sour-poems-wave.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@astrojs/sitemap': patch
3+
---
4+
5+
Fixes the sitemap outputting a URL with an empty path for the homepage (e.g. `https://example.com` instead of `https://example.com/`) when `trailingSlash` is set to `"never"` or `build.format` is set to `"file"`

packages/integrations/sitemap/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
},
3434
"dependencies": {
3535
"sitemap": "^9.0.0",
36-
"stream-replace-string": "^2.0.0",
3736
"zod": "^4.3.6"
3837
},
3938
"devDependencies": {

packages/integrations/sitemap/src/index.ts

Lines changed: 25 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -231,44 +231,38 @@ const createPlugin = (options?: SitemapOptions): AstroIntegration => {
231231
(urlDataItem) => !groupedUrlCollection.includes(urlDataItem.url),
232232
);
233233
// Process each chunk here
234-
await writeSitemapChunk(
235-
{
236-
filenameBase,
237-
hostname: finalSiteUrl.href,
238-
sitemapHostname: finalSiteUrl.href,
239-
sourceData: chunksItem,
240-
destinationDir: destDir,
241-
publicBasePath: config.base,
242-
customSitemaps,
243-
limit: entryLimit,
244-
xslURL,
245-
lastmod,
246-
namespaces: opts.namespaces,
247-
},
248-
config,
249-
);
234+
await writeSitemapChunk({
235+
filenameBase,
236+
hostname: finalSiteUrl.href,
237+
sitemapHostname: finalSiteUrl.href,
238+
sourceData: chunksItem,
239+
destinationDir: destDir,
240+
publicBasePath: config.base,
241+
customSitemaps,
242+
limit: entryLimit,
243+
xslURL,
244+
lastmod,
245+
namespaces: opts.namespaces,
246+
});
250247
logger.info(`\`${outFile}\` created at \`${path.relative(process.cwd(), destDir)}\``);
251248
return;
252249
} catch (err) {
253250
logger.error(`Error chunking sitemaps\n${(err as any).toString()}`);
254251
return;
255252
}
256253
}
257-
await writeSitemap(
258-
{
259-
filenameBase: filenameBase,
260-
hostname: finalSiteUrl.href,
261-
destinationDir: destDir,
262-
publicBasePath: config.base,
263-
sourceData: urlData,
264-
limit: entryLimit,
265-
customSitemaps,
266-
xslURL: xslURL,
267-
lastmod,
268-
namespaces: opts.namespaces,
269-
},
270-
config,
271-
);
254+
await writeSitemap({
255+
filenameBase: filenameBase,
256+
hostname: finalSiteUrl.href,
257+
destinationDir: destDir,
258+
publicBasePath: config.base,
259+
sourceData: urlData,
260+
limit: entryLimit,
261+
customSitemaps,
262+
xslURL: xslURL,
263+
lastmod,
264+
namespaces: opts.namespaces,
265+
});
272266
logger.info(`\`${outFile}\` created at \`${path.relative(process.cwd(), destDir)}\``);
273267
} catch (err) {
274268
if (err instanceof ZodError) {

packages/integrations/sitemap/src/write-sitemap-chunk.ts

Lines changed: 15 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
import { createWriteStream, type WriteStream } from 'node:fs';
1+
import { createWriteStream } from 'node:fs';
22
import { mkdir } from 'node:fs/promises';
33
import { normalize, resolve } from 'node:path';
44
import { pipeline, Readable } from 'node:stream';
55
import { promisify } from 'node:util';
6-
import type { AstroConfig } from 'astro';
76
import { SitemapAndIndexStream, SitemapIndexStream, SitemapStream } from 'sitemap';
8-
import replace from 'stream-replace-string';
97
import type { SitemapItem } from './index.js';
108
import { getLatestLastmod } from './utils/lastmod.js';
119

@@ -29,22 +27,19 @@ type WriteSitemapChunkConfig = {
2927
};
3028

3129
// adapted from sitemap.js/sitemap-simple
32-
export async function writeSitemapChunk(
33-
{
34-
filenameBase,
35-
hostname,
36-
sitemapHostname = hostname,
37-
sourceData,
38-
destinationDir,
39-
limit = 50000,
40-
customSitemaps = [],
41-
publicBasePath = './',
42-
xslURL: xslUrl,
43-
lastmod,
44-
namespaces = { news: true, xhtml: true, image: true, video: true },
45-
}: WriteSitemapChunkConfig,
46-
astroConfig: AstroConfig,
47-
) {
30+
export async function writeSitemapChunk({
31+
filenameBase,
32+
hostname,
33+
sitemapHostname = hostname,
34+
sourceData,
35+
destinationDir,
36+
limit = 50000,
37+
customSitemaps = [],
38+
publicBasePath = './',
39+
xslURL: xslUrl,
40+
lastmod,
41+
namespaces = { news: true, xhtml: true, image: true, video: true },
42+
}: WriteSitemapChunkConfig) {
4843
await mkdir(destinationDir, { recursive: true });
4944

5045
// Normalize publicBasePath
@@ -78,18 +73,7 @@ export async function writeSitemapChunk(
7873
const writePath = resolve(destinationDir, path);
7974
const publicPath = normalize(normalizedPublicBasePath + path);
8075

81-
let stream: WriteStream;
82-
if (astroConfig.trailingSlash === 'never' || astroConfig.build.format === 'file') {
83-
// workaround for trailing slash issue in sitemap.js
84-
const host = hostname.endsWith('/') ? hostname.slice(0, -1) : hostname;
85-
const searchStr = `<loc>${host}/</loc>`;
86-
const replaceStr = `<loc>${host}</loc>`;
87-
stream = sitemapStream
88-
.pipe(replace(searchStr, replaceStr))
89-
.pipe(createWriteStream(writePath));
90-
} else {
91-
stream = sitemapStream.pipe(createWriteStream(writePath));
92-
}
76+
const stream = sitemapStream.pipe(createWriteStream(writePath));
9377

9478
const url = new URL(publicPath, sitemapHostname).toString();
9579
// Stamp this index entry with the freshest lastmod among the

packages/integrations/sitemap/src/write-sitemap.ts

Lines changed: 15 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
import { createWriteStream, type WriteStream } from 'node:fs';
1+
import { createWriteStream } from 'node:fs';
22
import { mkdir } from 'node:fs/promises';
33
import { normalize, resolve } from 'node:path';
44
import { pipeline, Readable } from 'node:stream';
55
import { promisify } from 'node:util';
6-
import type { AstroConfig } from 'astro';
76
import { SitemapAndIndexStream, SitemapIndexStream, SitemapStream } from 'sitemap';
8-
import replace from 'stream-replace-string';
97
import type { SitemapItem } from './index.js';
108
import { getLatestLastmod } from './utils/lastmod.js';
119

@@ -29,22 +27,19 @@ type WriteSitemapConfig = {
2927
};
3028

3129
// adapted from sitemap.js/sitemap-simple
32-
export async function writeSitemap(
33-
{
34-
filenameBase,
35-
hostname,
36-
sitemapHostname = hostname,
37-
sourceData,
38-
destinationDir,
39-
limit = 50000,
40-
customSitemaps = [],
41-
publicBasePath = './',
42-
xslURL: xslUrl,
43-
lastmod,
44-
namespaces = { news: true, xhtml: true, image: true, video: true },
45-
}: WriteSitemapConfig,
46-
astroConfig: AstroConfig,
47-
) {
30+
export async function writeSitemap({
31+
filenameBase,
32+
hostname,
33+
sitemapHostname = hostname,
34+
sourceData,
35+
destinationDir,
36+
limit = 50000,
37+
customSitemaps = [],
38+
publicBasePath = './',
39+
xslURL: xslUrl,
40+
lastmod,
41+
namespaces = { news: true, xhtml: true, image: true, video: true },
42+
}: WriteSitemapConfig) {
4843
await mkdir(destinationDir, { recursive: true });
4944

5045
const sitemapAndIndexStream = new SitemapAndIndexStream({
@@ -69,18 +64,7 @@ export async function writeSitemap(
6964
}
7065
const publicPath = normalize(publicBasePath + path);
7166

72-
let stream: WriteStream;
73-
if (astroConfig.trailingSlash === 'never' || astroConfig.build.format === 'file') {
74-
// workaround for trailing slash issue in sitemap.js: https://github.com/ekalinin/sitemap.js/issues/403
75-
const host = hostname.endsWith('/') ? hostname.slice(0, -1) : hostname;
76-
const searchStr = `<loc>${host}/</loc>`;
77-
const replaceStr = `<loc>${host}</loc>`;
78-
stream = sitemapStream
79-
.pipe(replace(searchStr, replaceStr))
80-
.pipe(createWriteStream(writePath));
81-
} else {
82-
stream = sitemapStream.pipe(createWriteStream(writePath));
83-
}
67+
const stream = sitemapStream.pipe(createWriteStream(writePath));
8468

8569
const url = new URL(publicPath, sitemapHostname).toString();
8670
// Stamp this index entry with the freshest lastmod among the URLs

packages/integrations/sitemap/test/trailing-slash.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ describe('Trailing slash', () => {
4444
const data = await readXML(fixture.readFile('/sitemap-0.xml'));
4545
const urls = data.urlset.url;
4646

47-
assert.equal(urls[0].loc[0], 'http://example.com');
47+
assert.equal(urls[0].loc[0], 'http://example.com/');
4848
assert.equal(urls[1].loc[0], 'http://example.com/one');
4949
assert.equal(urls[2].loc[0], 'http://example.com/two');
5050
});
@@ -64,7 +64,7 @@ describe('Trailing slash', () => {
6464
const data = await readXML(fixture.readFile('/sitemap-0.xml'));
6565
const urls = data.urlset.url;
6666

67-
assert.equal(urls[0].loc[0], 'http://example.com');
67+
assert.equal(urls[0].loc[0], 'http://example.com/');
6868
assert.equal(urls[1].loc[0], 'http://example.com/one');
6969
assert.equal(urls[2].loc[0], 'http://example.com/two');
7070
});
@@ -81,7 +81,7 @@ describe('Trailing slash', () => {
8181
it('URLs do not end with trailing slash', async () => {
8282
const data = await readXML(fixture.readFile('/sitemap-0.xml'));
8383
const urls = data.urlset.url;
84-
assert.equal(urls[0].loc[0], 'http://example.com/base');
84+
assert.equal(urls[0].loc[0], 'http://example.com/base/');
8585
assert.equal(urls[1].loc[0], 'http://example.com/base/one');
8686
assert.equal(urls[2].loc[0], 'http://example.com/base/two');
8787
});

pnpm-lock.yaml

Lines changed: 9 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)