diff --git a/.changeset/late-bags-marry.md b/.changeset/late-bags-marry.md
new file mode 100644
index 000000000000..af6b4ba3ef5b
--- /dev/null
+++ b/.changeset/late-bags-marry.md
@@ -0,0 +1,5 @@
+---
+"@astrojs/sitemap": patch
+---
+
+Fixes an issue where the root url does not follow the `trailingSlash` config option
diff --git a/packages/integrations/sitemap/package.json b/packages/integrations/sitemap/package.json
index 9ccbf3d5bebc..cdff0a8aab99 100644
--- a/packages/integrations/sitemap/package.json
+++ b/packages/integrations/sitemap/package.json
@@ -34,6 +34,7 @@
},
"dependencies": {
"sitemap": "^7.1.1",
+ "stream-replace-string": "^2.0.0",
"zod": "^3.22.4"
},
"devDependencies": {
diff --git a/packages/integrations/sitemap/src/index.ts b/packages/integrations/sitemap/src/index.ts
index b0548d8f114c..a2fae5b5ade4 100644
--- a/packages/integrations/sitemap/src/index.ts
+++ b/packages/integrations/sitemap/src/index.ts
@@ -2,11 +2,11 @@ import path from 'node:path';
import { fileURLToPath } from 'node:url';
import type { AstroConfig, AstroIntegration } from 'astro';
import type { EnumChangefreq, LinkItem as LinkItemBase, SitemapItemLoose } from 'sitemap';
-import { simpleSitemapAndIndex } from 'sitemap';
import { ZodError } from 'zod';
-import { generateSitemap } from './generate-sitemap.js';
import { validateOptions } from './validate-options.js';
+import { generateSitemap } from './generate-sitemap.js';
+import { writeSitemap } from './write-sitemap.js';
export { EnumChangefreq as ChangeFreqEnum } from 'sitemap';
export type ChangeFreq = `${EnumChangefreq}`;
@@ -167,14 +167,13 @@ const createPlugin = (options?: SitemapOptions): AstroIntegration => {
}
}
const destDir = fileURLToPath(dir);
- await simpleSitemapAndIndex({
+ await writeSitemap({
hostname: finalSiteUrl.href,
destinationDir: destDir,
publicBasePath: config.base,
sourceData: urlData,
- limit: entryLimit,
- gzip: false,
- });
+ limit: entryLimit
+ }, config)
logger.info(`\`${OUTFILE}\` created at \`${path.relative(process.cwd(), destDir)}\``);
} catch (err) {
if (err instanceof ZodError) {
diff --git a/packages/integrations/sitemap/src/write-sitemap.ts b/packages/integrations/sitemap/src/write-sitemap.ts
new file mode 100644
index 000000000000..d55d4fc50b61
--- /dev/null
+++ b/packages/integrations/sitemap/src/write-sitemap.ts
@@ -0,0 +1,69 @@
+import { normalize, resolve } from 'path';
+import { createWriteStream, type WriteStream } from 'fs'
+import { mkdir } from 'fs/promises';
+import { promisify } from 'util';
+import { Readable, pipeline } from 'stream';
+import replace from 'stream-replace-string'
+
+import { SitemapAndIndexStream, SitemapStream } from 'sitemap';
+
+import type { AstroConfig } from 'astro';
+import type { SitemapItem } from "./index.js";
+
+type WriteSitemapConfig = {
+ hostname: string;
+ sitemapHostname?: string;
+ sourceData: SitemapItem[];
+ destinationDir: string;
+ publicBasePath?: string;
+ limit?: number;
+}
+
+// adapted from sitemap.js/sitemap-simple
+export async function writeSitemap({ hostname, sitemapHostname = hostname,
+sourceData, destinationDir, limit = 50000, publicBasePath = './', }: WriteSitemapConfig, astroConfig: AstroConfig) {
+
+ await mkdir(destinationDir, { recursive: true })
+
+ const sitemapAndIndexStream = new SitemapAndIndexStream({
+ limit,
+ getSitemapStream: (i) => {
+ const sitemapStream = new SitemapStream({
+ hostname,
+ });
+ const path = `./sitemap-${i}.xml`;
+ const writePath = resolve(destinationDir, path);
+ if (!publicBasePath.endsWith('/')) {
+ publicBasePath += '/';
+ }
+ const publicPath = normalize(publicBasePath + path);
+
+ let stream: WriteStream
+ if (astroConfig.trailingSlash === 'never' || astroConfig.build.format === 'file') {
+ // workaround for trailing slash issue in sitemap.js: https://github.com/ekalinin/sitemap.js/issues/403
+ const host = hostname.endsWith('/') ? hostname.slice(0, -1) : hostname
+ const searchStr = `${host}/`
+ const replaceStr = `${host}`
+ stream = sitemapStream.pipe(replace(searchStr, replaceStr)).pipe(createWriteStream(writePath))
+ } else {
+ stream = sitemapStream.pipe(createWriteStream(writePath))
+ }
+
+ return [
+ new URL(
+ publicPath,
+ sitemapHostname
+ ).toString(),
+ sitemapStream,
+ stream,
+ ];
+ },
+ });
+
+ let src = Readable.from(sourceData)
+ const indexPath = resolve(
+ destinationDir,
+ `./sitemap-index.xml`
+ );
+ return promisify(pipeline)(src, sitemapAndIndexStream, createWriteStream(indexPath));
+}
\ No newline at end of file
diff --git a/packages/integrations/sitemap/test/fixtures/trailing-slash/src/pages/index.astro b/packages/integrations/sitemap/test/fixtures/trailing-slash/src/pages/index.astro
new file mode 100644
index 000000000000..5a29cbdbe836
--- /dev/null
+++ b/packages/integrations/sitemap/test/fixtures/trailing-slash/src/pages/index.astro
@@ -0,0 +1,8 @@
+
+
+ Index
+
+
+ Index
+
+
\ No newline at end of file
diff --git a/packages/integrations/sitemap/test/trailing-slash.test.js b/packages/integrations/sitemap/test/trailing-slash.test.js
index e0a6158fbe4d..181f0def53d2 100644
--- a/packages/integrations/sitemap/test/trailing-slash.test.js
+++ b/packages/integrations/sitemap/test/trailing-slash.test.js
@@ -22,7 +22,10 @@ describe('Trailing slash', () => {
it('URLs end with trailing slash', async () => {
const data = await readXML(fixture.readFile('/sitemap-0.xml'));
const urls = data.urlset.url;
- assert.equal(urls[0].loc[0], 'http://example.com/one/');
+
+ assert.equal(urls[0].loc[0], 'http://example.com/');
+ assert.equal(urls[1].loc[0], 'http://example.com/one/');
+ assert.equal(urls[2].loc[0], 'http://example.com/two/');
});
});
@@ -41,7 +44,10 @@ describe('Trailing slash', () => {
it('URLs do not end with trailing slash', async () => {
const data = await readXML(fixture.readFile('/sitemap-0.xml'));
const urls = data.urlset.url;
- assert.equal(urls[0].loc[0], 'http://example.com/one');
+
+ assert.equal(urls[0].loc[0], 'http://example.com');
+ assert.equal(urls[1].loc[0], 'http://example.com/one');
+ assert.equal(urls[2].loc[0], 'http://example.com/two');
});
});
});
@@ -55,10 +61,13 @@ describe('Trailing slash', () => {
await fixture.build();
});
- it('URLs do no end with trailing slash', async () => {
+ it('URLs do not end with trailing slash', async () => {
const data = await readXML(fixture.readFile('/sitemap-0.xml'));
const urls = data.urlset.url;
- assert.equal(urls[0].loc[0], 'http://example.com/one');
+
+ assert.equal(urls[0].loc[0], 'http://example.com');
+ assert.equal(urls[1].loc[0], 'http://example.com/one');
+ assert.equal(urls[2].loc[0], 'http://example.com/two');
});
describe('with base path', () => {
before(async () => {
@@ -73,7 +82,9 @@ describe('Trailing slash', () => {
it('URLs do not end with trailing slash', async () => {
const data = await readXML(fixture.readFile('/sitemap-0.xml'));
const urls = data.urlset.url;
- assert.equal(urls[0].loc[0], 'http://example.com/base/one');
+ assert.equal(urls[0].loc[0], 'http://example.com/base');
+ assert.equal(urls[1].loc[0], 'http://example.com/base/one');
+ assert.equal(urls[2].loc[0], 'http://example.com/base/two');
});
});
});
@@ -90,7 +101,9 @@ describe('Trailing slash', () => {
it('URLs end with trailing slash', async () => {
const data = await readXML(fixture.readFile('/sitemap-0.xml'));
const urls = data.urlset.url;
- assert.equal(urls[0].loc[0], 'http://example.com/one/');
+ assert.equal(urls[0].loc[0], 'http://example.com/');
+ assert.equal(urls[1].loc[0], 'http://example.com/one/');
+ assert.equal(urls[2].loc[0], 'http://example.com/two/');
});
describe('with base path', () => {
before(async () => {
@@ -105,7 +118,9 @@ describe('Trailing slash', () => {
it('URLs end with trailing slash', async () => {
const data = await readXML(fixture.readFile('/sitemap-0.xml'));
const urls = data.urlset.url;
- assert.equal(urls[0].loc[0], 'http://example.com/base/one/');
+ assert.equal(urls[0].loc[0], 'http://example.com/base/');
+ assert.equal(urls[1].loc[0], 'http://example.com/base/one/');
+ assert.equal(urls[2].loc[0], 'http://example.com/base/two/');
});
});
});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 3b50248aaceb..8b81958b2d98 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -4888,6 +4888,9 @@ importers:
sitemap:
specifier: ^7.1.1
version: 7.1.1
+ stream-replace-string:
+ specifier: ^2.0.0
+ version: 2.0.0
zod:
specifier: ^3.22.4
version: 3.22.4
@@ -15642,6 +15645,10 @@ packages:
bl: 5.1.0
dev: false
+ /stream-replace-string@2.0.0:
+ resolution: {integrity: sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w==}
+ dev: false
+
/stream-transform@2.1.3:
resolution: {integrity: sha512-9GHUiM5hMiCi6Y03jD2ARC1ettBXkQBoQAe7nJsPknnI0ow10aXjTnew8QtYQmLjzn974BnmWEAJgCY6ZP1DeQ==}
dependencies: