Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 69 additions & 2 deletions core/app/sitemap.xml/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,77 @@ import { getChannelIdFromLocale } from '~/channels.config';
import { client } from '~/client';
import { defaultLocale } from '~/i18n/locales';

export const GET = async () => {
export const GET = async (request: Request) => {
const url = new URL(request.url);
const incomingHost = request.headers.get('host') ?? url.host;
const incomingProto = request.headers.get('x-forwarded-proto') ?? url.protocol.replace(':', '');

const type = url.searchParams.get('type');
const page = url.searchParams.get('page');

// If a specific sitemap within the index is requested, require both params
if (type !== null || page !== null) {
if (!type || !page) {
return new Response('Both "type" and "page" query params are required', {
status: 400,
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
});
}

const upstream = await client.fetchSitemapResponse(
{ type, page },
getChannelIdFromLocale(defaultLocale),
);

// Pass-through upstream status/body but enforce XML content-type
const body = await upstream.text();

return new Response(body, {
status: upstream.status,
statusText: upstream.statusText,
headers: { 'Content-Type': 'application/xml' },
});
}

// Otherwise, return the sitemap index with normalized internal links
const sitemapIndex = await client.fetchSitemapIndex(getChannelIdFromLocale(defaultLocale));

return new Response(sitemapIndex, {
const rewritten = sitemapIndex.replace(
/<loc>([^<]+)<\/loc>/g,
(match: string, locUrlStr: string) => {
try {
// Decode XML entities for '&' so URL parsing works
const decoded: string = locUrlStr.replace(/&amp;/g, '&');
const original = new URL(decoded);

if (!original.pathname.endsWith('/xmlsitemap.php')) {
return match;
}

const normalized = new URL(`${incomingProto}://${incomingHost}/sitemap.xml`);

const t = original.searchParams.get('type');
const p = original.searchParams.get('page');

// Only rewrite entries that include both type and page; otherwise leave untouched
if (!t || !p) {
return match;
}

normalized.searchParams.set('type', t);
normalized.searchParams.set('page', p);

// Re-encode '&' for XML output
const normalizedXml: string = normalized.toString().replace(/&/g, '&amp;');

return `<loc>${normalizedXml}</loc>`;
} catch {
return match;
}
},
);

return new Response(rewritten, {
headers: {
'Content-Type': 'application/xml',
},
Expand Down
37 changes: 37 additions & 0 deletions packages/client/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,43 @@ class Client<FetcherRequestInit extends RequestInit = RequestInit> {
return response.text();
}

async fetchSitemap(
params: { type?: string | null; page?: string | number | null },
channelId?: string,
): Promise<string> {
const response = await this.fetchSitemapResponse(params, channelId);

if (!response.ok) {
throw new Error(`Unable to get Sitemap: ${response.statusText}`);
}

return response.text();
}

async fetchSitemapResponse(
params: { type?: string | null; page?: string | number | null },
channelId?: string,
): Promise<Response> {
const baseUrl = new URL(`${await this.getCanonicalUrl(channelId)}/xmlsitemap.php`);

// Only forward well-known params
if (params.type) baseUrl.searchParams.set('type', String(params.type));
if (params.page !== undefined && params.page !== null)
baseUrl.searchParams.set('page', String(params.page));

const response = await fetch(baseUrl.toString(), {
method: 'GET',
headers: {
Accept: 'application/xml',
'Content-Type': 'application/xml',
'User-Agent': this.backendUserAgent,
...(this.trustedProxySecret && { 'X-BC-Trusted-Proxy-Secret': this.trustedProxySecret }),
},
});

return response;
}

private async getCanonicalUrl(channelId?: string) {
const resolvedChannelId = channelId ?? (await this.getChannelId(this.defaultChannelId));

Expand Down