pounce/frontend/src/app/sitemap.ts
Yves Gugger bb7ce97330
Some checks failed
CI / Frontend Lint & Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Backend Lint (push) Has been cancelled
CI / Backend Tests (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
Deploy / Build & Push Images (push) Has been cancelled
Deploy / Deploy to Server (push) Has been cancelled
Deploy / Notify (push) Has been cancelled
Deploy: referral rewards antifraud + legal contact updates
2025-12-15 13:56:43 +01:00

137 lines
4.7 KiB
TypeScript

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<T>(url: string): Promise<T> {
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<MetadataRoute.Sitemap> {
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<TldListResponse>(`${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<BlogListResponse>(`${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<ListingPublic[]>(
`${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
}