mirror of
https://github.com/withastro/astro.git
synced 2024-12-30 22:03:56 -05:00
fix(sitemap): Trailing slashes on root url (#10772)
* add tests that reveal issue * fix trailing slash root page issue * add changeset
This commit is contained in:
parent
914daad18b
commit
0e22462d15
7 changed files with 117 additions and 13 deletions
5
.changeset/late-bags-marry.md
Normal file
5
.changeset/late-bags-marry.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
"@astrojs/sitemap": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fixes an issue where the root url does not follow the `trailingSlash` config option
|
|
@ -34,6 +34,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"sitemap": "^7.1.1",
|
"sitemap": "^7.1.1",
|
||||||
|
"stream-replace-string": "^2.0.0",
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -2,11 +2,11 @@ import path from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import type { AstroConfig, AstroIntegration } from 'astro';
|
import type { AstroConfig, AstroIntegration } from 'astro';
|
||||||
import type { EnumChangefreq, LinkItem as LinkItemBase, SitemapItemLoose } from 'sitemap';
|
import type { EnumChangefreq, LinkItem as LinkItemBase, SitemapItemLoose } from 'sitemap';
|
||||||
import { simpleSitemapAndIndex } from 'sitemap';
|
|
||||||
import { ZodError } from 'zod';
|
import { ZodError } from 'zod';
|
||||||
|
|
||||||
import { generateSitemap } from './generate-sitemap.js';
|
|
||||||
import { validateOptions } from './validate-options.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 { EnumChangefreq as ChangeFreqEnum } from 'sitemap';
|
||||||
export type ChangeFreq = `${EnumChangefreq}`;
|
export type ChangeFreq = `${EnumChangefreq}`;
|
||||||
|
@ -167,14 +167,13 @@ const createPlugin = (options?: SitemapOptions): AstroIntegration => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const destDir = fileURLToPath(dir);
|
const destDir = fileURLToPath(dir);
|
||||||
await simpleSitemapAndIndex({
|
await writeSitemap({
|
||||||
hostname: finalSiteUrl.href,
|
hostname: finalSiteUrl.href,
|
||||||
destinationDir: destDir,
|
destinationDir: destDir,
|
||||||
publicBasePath: config.base,
|
publicBasePath: config.base,
|
||||||
sourceData: urlData,
|
sourceData: urlData,
|
||||||
limit: entryLimit,
|
limit: entryLimit
|
||||||
gzip: false,
|
}, config)
|
||||||
});
|
|
||||||
logger.info(`\`${OUTFILE}\` created at \`${path.relative(process.cwd(), destDir)}\``);
|
logger.info(`\`${OUTFILE}\` created at \`${path.relative(process.cwd(), destDir)}\``);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof ZodError) {
|
if (err instanceof ZodError) {
|
||||||
|
|
69
packages/integrations/sitemap/src/write-sitemap.ts
Normal file
69
packages/integrations/sitemap/src/write-sitemap.ts
Normal file
|
@ -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));
|
||||||
|
}
|
8
packages/integrations/sitemap/test/fixtures/trailing-slash/src/pages/index.astro
vendored
Normal file
8
packages/integrations/sitemap/test/fixtures/trailing-slash/src/pages/index.astro
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Index</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Index</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -22,7 +22,10 @@ describe('Trailing slash', () => {
|
||||||
it('URLs end with trailing slash', async () => {
|
it('URLs end with trailing slash', async () => {
|
||||||
const data = await readXML(fixture.readFile('/sitemap-0.xml'));
|
const data = await readXML(fixture.readFile('/sitemap-0.xml'));
|
||||||
const urls = data.urlset.url;
|
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 () => {
|
it('URLs do not end with trailing slash', async () => {
|
||||||
const data = await readXML(fixture.readFile('/sitemap-0.xml'));
|
const data = await readXML(fixture.readFile('/sitemap-0.xml'));
|
||||||
const urls = data.urlset.url;
|
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();
|
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 data = await readXML(fixture.readFile('/sitemap-0.xml'));
|
||||||
const urls = data.urlset.url;
|
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', () => {
|
describe('with base path', () => {
|
||||||
before(async () => {
|
before(async () => {
|
||||||
|
@ -73,7 +82,9 @@ describe('Trailing slash', () => {
|
||||||
it('URLs do not end with trailing slash', async () => {
|
it('URLs do not end with trailing slash', async () => {
|
||||||
const data = await readXML(fixture.readFile('/sitemap-0.xml'));
|
const data = await readXML(fixture.readFile('/sitemap-0.xml'));
|
||||||
const urls = data.urlset.url;
|
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 () => {
|
it('URLs end with trailing slash', async () => {
|
||||||
const data = await readXML(fixture.readFile('/sitemap-0.xml'));
|
const data = await readXML(fixture.readFile('/sitemap-0.xml'));
|
||||||
const urls = data.urlset.url;
|
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', () => {
|
describe('with base path', () => {
|
||||||
before(async () => {
|
before(async () => {
|
||||||
|
@ -105,7 +118,9 @@ describe('Trailing slash', () => {
|
||||||
it('URLs end with trailing slash', async () => {
|
it('URLs end with trailing slash', async () => {
|
||||||
const data = await readXML(fixture.readFile('/sitemap-0.xml'));
|
const data = await readXML(fixture.readFile('/sitemap-0.xml'));
|
||||||
const urls = data.urlset.url;
|
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/');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -4888,6 +4888,9 @@ importers:
|
||||||
sitemap:
|
sitemap:
|
||||||
specifier: ^7.1.1
|
specifier: ^7.1.1
|
||||||
version: 7.1.1
|
version: 7.1.1
|
||||||
|
stream-replace-string:
|
||||||
|
specifier: ^2.0.0
|
||||||
|
version: 2.0.0
|
||||||
zod:
|
zod:
|
||||||
specifier: ^3.22.4
|
specifier: ^3.22.4
|
||||||
version: 3.22.4
|
version: 3.22.4
|
||||||
|
@ -15642,6 +15645,10 @@ packages:
|
||||||
bl: 5.1.0
|
bl: 5.1.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/stream-replace-string@2.0.0:
|
||||||
|
resolution: {integrity: sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/stream-transform@2.1.3:
|
/stream-transform@2.1.3:
|
||||||
resolution: {integrity: sha512-9GHUiM5hMiCi6Y03jD2ARC1ettBXkQBoQAe7nJsPknnI0ow10aXjTnew8QtYQmLjzn974BnmWEAJgCY6ZP1DeQ==}
|
resolution: {integrity: sha512-9GHUiM5hMiCi6Y03jD2ARC1ettBXkQBoQAe7nJsPknnI0ow10aXjTnew8QtYQmLjzn974BnmWEAJgCY6ZP1DeQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
Loading…
Reference in a new issue