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
137 lines
4.7 KiB
TypeScript
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
|
|
}
|