import { MetadataRoute } from 'next' import { SITE_URL } from '@/lib/seo' type TldListResponse = { tlds: string[] latest_recorded_at: string | null } type BlogListResponse = { posts: Array<{ slug: string; published_at?: string | null; updated_at?: string | null }> total?: number limit?: number offset?: number } type ListingPublic = { slug: string } async function fetchJson(url: string): Promise { const res = await fetch(url, { // Cache to protect the backend. Sitemaps don't need to be real-time. next: { revalidate: 3600 }, }) if (!res.ok) { throw new Error(`Failed to fetch ${url}: ${res.status}`) } return (await res.json()) as T } export default async function sitemap(): Promise { const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || SITE_URL const now = new Date() const staticPages: MetadataRoute.Sitemap = [ { url: baseUrl, lastModified: now, changeFrequency: 'daily', priority: 1.0 }, { url: `${baseUrl}/discover`, lastModified: now, changeFrequency: 'daily', priority: 0.9 }, { url: `${baseUrl}/acquire`, lastModified: now, changeFrequency: 'hourly', priority: 0.9 }, { url: `${baseUrl}/buy`, lastModified: now, changeFrequency: 'hourly', priority: 0.8 }, { url: `${baseUrl}/auctions`, lastModified: now, changeFrequency: 'hourly', priority: 0.8 }, { url: `${baseUrl}/yield`, lastModified: now, changeFrequency: 'weekly', priority: 0.8 }, { url: `${baseUrl}/pricing`, lastModified: now, changeFrequency: 'weekly', priority: 0.8 }, { url: `${baseUrl}/about`, lastModified: now, changeFrequency: 'monthly', priority: 0.5 }, { url: `${baseUrl}/blog`, lastModified: now, changeFrequency: 'weekly', priority: 0.7 }, { url: `${baseUrl}/contact`, lastModified: now, changeFrequency: 'monthly', priority: 0.4 }, { url: `${baseUrl}/privacy`, lastModified: now, changeFrequency: 'yearly', priority: 0.2 }, { url: `${baseUrl}/terms`, lastModified: now, changeFrequency: 'yearly', priority: 0.2 }, { url: `${baseUrl}/imprint`, lastModified: now, changeFrequency: 'yearly', priority: 0.2 }, { url: `${baseUrl}/cookies`, lastModified: now, changeFrequency: 'yearly', priority: 0.2 }, // We intentionally do not include /login and /register in the sitemap. ] const out: MetadataRoute.Sitemap = [...staticPages] // ---------------------------- // Discover (TLD) pages (DB-driven) // ---------------------------- try { const tlds = await fetchJson(`${baseUrl}/api/v1/tld-prices/tlds?limit=20000`) const lastMod = tlds.latest_recorded_at ? new Date(tlds.latest_recorded_at) : now for (const tld of tlds.tlds || []) { if (!tld) continue out.push({ url: `${baseUrl}/discover/${encodeURIComponent(tld)}`, lastModified: lastMod, changeFrequency: 'daily', priority: 0.7, }) } } catch { // If DB isn't ready yet, we still serve a valid sitemap for static pages. } // ---------------------------- // Blog post pages // ---------------------------- try { const limit = 200 const maxPages = 25 // hard safety cap let offset = 0 let page = 0 while (page < maxPages) { const blog = await fetchJson(`${baseUrl}/api/v1/blog/posts?limit=${limit}&offset=${offset}`) const posts = blog.posts || [] for (const post of posts) { if (!post?.slug) continue const ts = post.updated_at || post.published_at out.push({ url: `${baseUrl}/blog/${encodeURIComponent(post.slug)}`, lastModified: ts ? new Date(ts) : now, changeFrequency: 'weekly', priority: 0.6, }) } // Stop when API reports total or when a page is not full. if (typeof blog.total === 'number' && offset + limit >= blog.total) break if (posts.length < limit) break offset += limit page += 1 } } catch { // ignore } // ---------------------------- // Public listing pages (Pounce Direct) // ---------------------------- try { const limit = 50 const maxPages = 30 // 1500 listings max in sitemap for (let page = 0; page < maxPages; page += 1) { const offset = page * limit const listings = await fetchJson( `${baseUrl}/api/v1/listings?limit=${limit}&offset=${offset}&clean_only=true&sort_by=newest` ) const rows = listings || [] for (const l of rows) { if (!l?.slug) continue out.push({ url: `${baseUrl}/buy/${encodeURIComponent(l.slug)}`, lastModified: now, changeFrequency: 'daily', priority: 0.6, }) } if (rows.length < limit) break } } catch { // ignore } return out }