fix(seo): correct sitemap routes
This commit is contained in:
210
tests/int/sitemap-routes.int.spec.ts
Normal file
210
tests/int/sitemap-routes.int.spec.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
import { createRequire } from 'node:module'
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { getSitemapBaseURL, normalizeSitemapBaseURL } from '@/utilities/sitemapBaseURL.cjs'
|
||||
|
||||
type SitemapCollection = 'pages' | 'posts' | 'events' | 'preistraeger'
|
||||
|
||||
type SitemapDocument = {
|
||||
_status: 'draft' | 'published'
|
||||
slug?: string
|
||||
spaPath?: string
|
||||
updatedAt?: string
|
||||
}
|
||||
|
||||
type SitemapFindQuery = {
|
||||
collection: SitemapCollection
|
||||
draft?: boolean
|
||||
overrideAccess?: boolean
|
||||
where?: {
|
||||
_status?: {
|
||||
equals?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const routeState = vi.hoisted(() => ({
|
||||
docs: {
|
||||
events: [],
|
||||
pages: [],
|
||||
posts: [],
|
||||
preistraeger: [],
|
||||
} as Record<SitemapCollection, SitemapDocument[]>,
|
||||
queries: [] as SitemapFindQuery[],
|
||||
}))
|
||||
|
||||
vi.mock('@payload-config', () => ({ default: {} }))
|
||||
|
||||
vi.mock('next/cache', () => ({
|
||||
unstable_cache: (callback: () => unknown) => callback,
|
||||
}))
|
||||
|
||||
vi.mock('payload', () => ({
|
||||
getPayload: async () => ({
|
||||
find: async (query: SitemapFindQuery) => {
|
||||
routeState.queries.push(query)
|
||||
|
||||
const isPublishedPublicQuery =
|
||||
query.draft === false &&
|
||||
query.overrideAccess === false &&
|
||||
query.where?._status?.equals === 'published'
|
||||
|
||||
return {
|
||||
docs: isPublishedPublicQuery
|
||||
? routeState.docs[query.collection].filter((document) => document._status === 'published')
|
||||
: routeState.docs[query.collection],
|
||||
}
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
import { GET as getPagesSitemap } from '@/app/(frontend)/(sitemaps)/pages-sitemap.xml/route'
|
||||
import { GET as getPostsSitemap } from '@/app/(frontend)/(sitemaps)/posts-sitemap.xml/route'
|
||||
|
||||
const productionURL = 'https://www.bayerischer-mittelstandspreis.de'
|
||||
|
||||
async function sitemapLocations(response: Response): Promise<string[]> {
|
||||
const xml = await response.text()
|
||||
return [...xml.matchAll(/<loc>([^<]+)<\/loc>/g)].map((match) => match[1])
|
||||
}
|
||||
|
||||
describe('sitemap route generation', () => {
|
||||
beforeEach(() => {
|
||||
process.env.NEXT_PUBLIC_SERVER_URL = `${productionURL}/`
|
||||
process.env.VERCEL_PROJECT_PRODUCTION_URL = ''
|
||||
|
||||
for (const collection of Object.keys(routeState.docs) as SitemapCollection[]) {
|
||||
routeState.docs[collection] = []
|
||||
}
|
||||
routeState.queries = []
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it.each([
|
||||
{
|
||||
label: 'stored homepage path',
|
||||
document: { _status: 'published', slug: 'startseite', spaPath: '/' },
|
||||
expectedPath: '/',
|
||||
},
|
||||
{
|
||||
label: 'home slug fallback',
|
||||
document: { _status: 'published', slug: 'home' },
|
||||
expectedPath: '/',
|
||||
},
|
||||
{
|
||||
label: 'startseite slug fallback',
|
||||
document: { _status: 'published', slug: 'startseite' },
|
||||
expectedPath: '/',
|
||||
},
|
||||
{
|
||||
label: 'normal stored page path',
|
||||
document: { _status: 'published', slug: 'legacy-contact', spaPath: '/kontakt' },
|
||||
expectedPath: '/kontakt',
|
||||
},
|
||||
] as const)('emits the $label', async ({ document, expectedPath }) => {
|
||||
routeState.docs.pages = [document]
|
||||
|
||||
await expect(sitemapLocations(await getPagesSitemap())).resolves.toEqual([
|
||||
`${productionURL}${expectedPath}`,
|
||||
])
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ collection: 'posts', prefix: '/presse/blog', slug: 'presseartikel' },
|
||||
{ collection: 'events', prefix: '/presse/events', slug: 'preisverleihung' },
|
||||
{ collection: 'preistraeger', prefix: '/preistraeger', slug: 'muster-gmbh' },
|
||||
] as const)(
|
||||
'emits $collection detail routes under $prefix',
|
||||
async ({ collection, prefix, slug }) => {
|
||||
routeState.docs[collection] = [
|
||||
{
|
||||
_status: 'published',
|
||||
slug,
|
||||
spaPath: `${prefix}/${slug}`,
|
||||
},
|
||||
]
|
||||
|
||||
await expect(sitemapLocations(await getPostsSitemap())).resolves.toContain(
|
||||
`${productionURL}${prefix}/${slug}`,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
it('excludes unpublished documents through the established anonymous public query contract', async () => {
|
||||
routeState.docs.pages = [
|
||||
{ _status: 'published', slug: 'sichtbar', spaPath: '/sichtbar' },
|
||||
{ _status: 'draft', slug: 'entwurf', spaPath: '/entwurf' },
|
||||
]
|
||||
routeState.docs.posts = [
|
||||
{ _status: 'published', slug: 'sichtbar', spaPath: '/presse/blog/sichtbar' },
|
||||
{ _status: 'draft', slug: 'entwurf', spaPath: '/presse/blog/entwurf' },
|
||||
]
|
||||
|
||||
const locations = [
|
||||
...(await sitemapLocations(await getPagesSitemap())),
|
||||
...(await sitemapLocations(await getPostsSitemap())),
|
||||
]
|
||||
|
||||
expect(locations).toContain(`${productionURL}/sichtbar`)
|
||||
expect(locations).toContain(`${productionURL}/presse/blog/sichtbar`)
|
||||
expect(locations.some((location) => location.includes('entwurf'))).toBe(false)
|
||||
expect(routeState.queries).toHaveLength(4)
|
||||
expect(routeState.queries).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
draft: false,
|
||||
overrideAccess: false,
|
||||
where: { _status: { equals: 'published' } },
|
||||
}),
|
||||
]),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('sitemap production base URL', () => {
|
||||
it.each([
|
||||
[
|
||||
'trailing path and slash',
|
||||
'https://www.bayerischer-mittelstandspreis.de/path///',
|
||||
productionURL,
|
||||
],
|
||||
['bare deployment hostname', 'bmp-production.vercel.app', 'https://bmp-production.vercel.app'],
|
||||
])('normalizes a %s', (_label, configuredURL, expectedURL) => {
|
||||
expect(normalizeSitemapBaseURL(configuredURL)).toBe(expectedURL)
|
||||
})
|
||||
|
||||
it('rejects a localhost production origin', () => {
|
||||
expect(() => normalizeSitemapBaseURL('http://localhost:3000', { production: true })).toThrow(
|
||||
'Production sitemap base URL must be a public HTTPS origin.',
|
||||
)
|
||||
})
|
||||
|
||||
it('fails instead of emitting a fallback when no public URL is configured', () => {
|
||||
expect(() => getSitemapBaseURL({ NODE_ENV: 'production' })).toThrow(
|
||||
'Sitemap base URL is not configured.',
|
||||
)
|
||||
})
|
||||
|
||||
it('uses the normalized production origin for the robots sitemap URLs', () => {
|
||||
const require = createRequire(import.meta.url)
|
||||
const configPath = require.resolve('../../next-sitemap.config.cjs')
|
||||
|
||||
delete require.cache[configPath]
|
||||
process.env.NEXT_PUBLIC_SERVER_URL = `${productionURL}/deployment/path/`
|
||||
|
||||
const config = require(configPath) as {
|
||||
robotsTxtOptions: { additionalSitemaps: string[] }
|
||||
siteUrl: string
|
||||
}
|
||||
|
||||
expect(config.siteUrl).toBe(productionURL)
|
||||
expect(config.robotsTxtOptions.additionalSitemaps).toEqual([
|
||||
`${productionURL}/pages-sitemap.xml`,
|
||||
`${productionURL}/posts-sitemap.xml`,
|
||||
])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user