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": {
|
||||
"sitemap": "^7.1.1",
|
||||
"stream-replace-string": "^2.0.0",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -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) {
|
||||
|
|
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 () => {
|
||||
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/');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue