From 81e34547c5e088cd3ce8c26bf1d9bdbd4fb30a2c Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Sat, 31 Oct 2026 23:51:37 -0600 Subject: [PATCH] fix(seo): include curated showcase detail pages in sitemap --- src/utils/showcase-sitemap.server.ts | 20 ++++++ src/utils/sitemap.ts | 13 ++-- tests/showcase-sitemap.integration.test.ts | 82 ++++++++++++++++++++++ 3 files changed, 111 insertions(+), 4 deletions(-) create mode 100644 src/utils/showcase-sitemap.server.ts create mode 100644 tests/showcase-sitemap.integration.test.ts diff --git a/src/utils/showcase-sitemap.server.ts b/src/utils/showcase-sitemap.server.ts new file mode 100644 index 000000000..93f328a5b --- /dev/null +++ b/src/utils/showcase-sitemap.server.ts @@ -0,0 +1,20 @@ +import { and, eq } from 'drizzle-orm' +import { db, isDatabaseConfigured } from '~/db/client' +import { showcases } from '~/db/schema' + +export async function getShowcaseSitemapEntries() { + if (process.env.NODE_ENV === 'development' && !(await isDatabaseConfigured())) + return [] + + const projects = await db + .select({ id: showcases.id }) + .from(showcases) + .where( + and( + eq(showcases.status, 'approved'), + eq(showcases.placement, 'showcase'), + ), + ) + + return projects.map(({ id }) => ({ path: `/showcase/${id}` })) +} diff --git a/src/utils/sitemap.ts b/src/utils/sitemap.ts index 4a2995e49..a62c88551 100644 --- a/src/utils/sitemap.ts +++ b/src/utils/sitemap.ts @@ -94,10 +94,14 @@ export function getSiteOrigin() { } export async function getSitemapEntries(): Promise> { - const [docsEntries, chartsCatalogEntries] = await Promise.all([ - Promise.all(libraries.map((library) => getLibraryDocsEntries(library))), - getPublishedChartsCatalogEntries(), - ]) + const [docsEntries, chartsCatalogEntries, showcaseEntries] = + await Promise.all([ + Promise.all(libraries.map((library) => getLibraryDocsEntries(library))), + getPublishedChartsCatalogEntries(), + import('./showcase-sitemap.server').then( + ({ getShowcaseSitemapEntries }) => getShowcaseSitemapEntries(), + ), + ]) const entries = [ ...HIGH_VALUE_NON_DOC_PAGES.map((path) => ({ path })), @@ -107,6 +111,7 @@ export async function getSitemapEntries(): Promise> { ...getBlogEntries(), ...getPartnerSitemapEntries(), ...chartsCatalogEntries, + ...showcaseEntries, ].filter( (entry) => entry.path !== '/intent/registry' && diff --git a/tests/showcase-sitemap.integration.test.ts b/tests/showcase-sitemap.integration.test.ts new file mode 100644 index 000000000..c17404040 --- /dev/null +++ b/tests/showcase-sitemap.integration.test.ts @@ -0,0 +1,82 @@ +import assert from 'node:assert/strict' +import test from 'node:test' +import { mkdtemp, mkdir, rm } from 'node:fs/promises' +import { resolve, join } from 'node:path' +import { pathToFileURL } from 'node:url' +import { randomUUID } from 'node:crypto' +import { build } from 'esbuild' +import postgres from 'postgres' +import { SHOWCASE_STATUSES, SHOWCASE_PLACEMENTS } from '../src/db/types' + +const databaseUrl = process.env.SHOWCASE_SITEMAP_TEST_DATABASE_URL + +test( + 'sitemap includes only approved curated showcases from the database', + { skip: !databaseUrl }, + async () => { + assert.ok(databaseUrl) + const url = new URL(databaseUrl) + assert.equal(url.hostname, '127.0.0.1') + assert.match(url.pathname, /^\/watch_sitemap_/) + const sql = postgres(databaseUrl, { max: 1 }) + await mkdir('node_modules/.cache', { recursive: true }) + const directory = await mkdtemp( + resolve('node_modules/.cache/sitemap-test-'), + ) + let createdTable = false + let closeDatabase: (() => Promise) | undefined + try { + await sql`CREATE TABLE showcases (id uuid PRIMARY KEY, status text NOT NULL, placement text NOT NULL)` + createdTable = true + let expectedId = '' + for (const status of SHOWCASE_STATUSES) { + for (const placement of SHOWCASE_PLACEMENTS) { + const id = randomUUID() + await sql`INSERT INTO showcases VALUES (${id}, ${status}, ${placement})` + if (status === 'approved' && placement === 'showcase') expectedId = id + } + } + const output = join(directory, 'query.mjs') + await build({ + stdin: { + contents: `export { getShowcaseSitemapEntries } from './src/utils/showcase-sitemap.server'; export { closeDatabase } from 'fixture:db';`, + resolveDir: process.cwd(), + loader: 'ts', + }, + outfile: output, + bundle: true, + platform: 'node', + format: 'esm', + packages: 'external', + plugins: [ + { + name: 'isolated-local-database', + setup(builder) { + builder.onResolve( + { filter: /^(~\/db\/client|fixture:db)$/ }, + () => ({ path: 'db', namespace: 'fixture' }), + ) + builder.onLoad({ filter: /.*/, namespace: 'fixture' }, () => ({ + contents: `import postgres from 'postgres'; import { drizzle } from 'drizzle-orm/postgres-js'; const client = postgres(process.env.SHOWCASE_SITEMAP_TEST_DATABASE_URL, { max: 1 }); export const db = drizzle(client); export const isDatabaseConfigured = async () => true; export const closeDatabase = () => client.end();`, + loader: 'js', + resolveDir: process.cwd(), + })) + }, + }, + ], + }) + const query = await import(pathToFileURL(output).href) + closeDatabase = query.closeDatabase + assert.deepEqual(await query.getShowcaseSitemapEntries(), [ + { path: `/showcase/${expectedId}` }, + ]) + await sql`UPDATE showcases SET placement = 'private' WHERE id = ${expectedId}` + assert.deepEqual(await query.getShowcaseSitemapEntries(), []) + } finally { + await closeDatabase?.() + if (createdTable) await sql`DROP TABLE showcases` + await sql.end() + await rm(directory, { recursive: true, force: true }) + } + }, +)