Skip to content

Commit

Permalink
fix(sitemap): Trailing slashes on root url (#10772)
Browse files Browse the repository at this point in the history
* add tests that reveal issue

* fix trailing slash root page issue

* add changeset
  • Loading branch information
gislerro authored Apr 18, 2024
1 parent 914daad commit 0e22462
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 13 deletions.
5 changes: 5 additions & 0 deletions .changeset/late-bags-marry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@astrojs/sitemap": patch
---

Fixes an issue where the root url does not follow the `trailingSlash` config option
1 change: 1 addition & 0 deletions packages/integrations/sitemap/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
},
"dependencies": {
"sitemap": "^7.1.1",
"stream-replace-string": "^2.0.0",
"zod": "^3.22.4"
},
"devDependencies": {
Expand Down
11 changes: 5 additions & 6 deletions packages/integrations/sitemap/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`;
Expand Down Expand Up @@ -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) {
Expand Down
69 changes: 69 additions & 0 deletions packages/integrations/sitemap/src/write-sitemap.ts
Original file line number Diff line number Diff line change
@@ -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 = `<loc>${host}/</loc>`
const replaceStr = `<loc>${host}</loc>`
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));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<html>
<head>
<title>Index</title>
</head>
<body>
<h1>Index</h1>
</body>
</html>
29 changes: 22 additions & 7 deletions packages/integrations/sitemap/test/trailing-slash.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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/');
});
});

Expand All @@ -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');
});
});
});
Expand All @@ -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 () => {
Expand All @@ -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');
});
});
});
Expand All @@ -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 () => {
Expand All @@ -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/');
});
});
});
Expand Down
7 changes: 7 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 0e22462

Please sign in to comment.