import fs from "fs"
import path from "path"
import { db } from "@/lib/db/index"
import { games, hardware } from "@/lib/db/schema"
import { or, ne, isNull } from "drizzle-orm"
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://deckyvault.xyz"
const MAX_URLS_PER_SITEMAP = 45000 // Leave buffer below 50k limit
interface SitemapEntry {
loc: string
lastmod?: string
changefreq?: string
priority?: number
"image:image"?: {
"image:loc": string
}
}
function escapeXml(str: string): string {
return str
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """)
.replace(/'/g, "'")
}
function buildStaticEntries(): SitemapEntry[] {
return [
{ loc: BASE_URL, changefreq: "weekly", priority: 1.0 },
{ loc: `${BASE_URL}/games`, changefreq: "daily", priority: 0.8 },
{ loc: `${BASE_URL}/dashboard`, changefreq: "daily", priority: 0.7 },
{ loc: `${BASE_URL}/devices`, changefreq: "monthly", priority: 0.6 },
{ loc: `${BASE_URL}/updates`, changefreq: "weekly", priority: 0.5 },
{ loc: `${BASE_URL}/contact`, changefreq: "yearly", priority: 0.3 },
]
}
function renderSitemap(entries: SitemapEntry[]): string {
const urls = entries.map((entry) => {
let xml = ` \n ${escapeXml(entry.loc)}\n`
if (entry.lastmod) xml += ` ${entry.lastmod}\n`
if (entry.changefreq) xml += ` ${entry.changefreq}\n`
if (entry.priority) xml += ` ${entry.priority}\n`
if (entry["image:image"]) {
xml += ` \n ${escapeXml(entry["image:image"]["image:loc"])}\n \n`
}
xml += ` `
return xml
})
return `\n\n${urls.join("\n")}\n`
}
function renderSitemapIndex(sitemaps: { loc: string; lastmod: string }[]): string {
const entries = sitemaps.map((s) => {
return ` \n ${escapeXml(s.loc)}\n ${s.lastmod}\n `
})
return `\n\n${entries.join("\n")}\n`
}
async function generateSitemaps() {
console.log("Generating static sitemaps...")
// Fetch games
const gameRows = await db
.select({
id: games.id,
updatedAt: games.updatedAt,
capsuleImage: games.capsuleImage,
})
.from(games)
.where(or(ne(games.syncStatus, "failed"), isNull(games.syncStatus)))
// Fetch devices
const deviceRows = await db
.select({
slug: hardware.slug,
createdAt: hardware.createdAt,
})
.from(hardware)
console.log(`Found ${gameRows.length} games and ${deviceRows.length} devices`)
// Build game entries
const gameEntries: SitemapEntry[] = gameRows.map((row) => {
const image =
row.capsuleImage &&
typeof row.capsuleImage === "string" &&
row.capsuleImage.trim().startsWith("https://") &&
row.capsuleImage.trim().length <= 2048
? row.capsuleImage.trim()
: undefined
return {
loc: `${BASE_URL}/game/${row.id}`,
lastmod: row.updatedAt ? new Date(row.updatedAt).toISOString() : undefined,
changefreq: "weekly",
priority: 0.7,
...(image ? { "image:image": { "image:loc": image } } : {}),
}
})
// Build device entries
const deviceEntries: SitemapEntry[] = deviceRows.map((row) => ({
loc: `${BASE_URL}/devices/${row.slug}`,
lastmod: row.createdAt ? new Date(row.createdAt).toISOString() : undefined,
changefreq: "monthly",
priority: 0.5,
}))
// Static entries
const staticEntries = buildStaticEntries()
const now = new Date().toISOString()
const publicDir = path.join(process.cwd(), "public")
// Ensure public dir exists
if (!fs.existsSync(publicDir)) {
fs.mkdirSync(publicDir, { recursive: true })
}
// Decide: single file or sitemap index
const allDynamicEntries = [...gameEntries, ...deviceEntries]
const needsIndex = (staticEntries.length + allDynamicEntries.length) > MAX_URLS_PER_SITEMAP
if (needsIndex) {
// Write static sitemap
const staticXml = renderSitemap(staticEntries)
fs.writeFileSync(path.join(publicDir, "sitemap-static.xml"), staticXml)
// Write device sitemap
const deviceXml = renderSitemap(deviceEntries)
fs.writeFileSync(path.join(publicDir, "sitemap-devices.xml"), deviceXml)
// Split game entries into chunks
const chunks: SitemapEntry[][] = []
for (let i = 0; i < gameEntries.length; i += MAX_URLS_PER_SITEMAP) {
chunks.push(gameEntries.slice(i, i + MAX_URLS_PER_SITEMAP))
}
const sitemaps: { loc: string; lastmod: string }[] = [
{ loc: `${BASE_URL}/sitemap-static.xml`, lastmod: now },
{ loc: `${BASE_URL}/sitemap-devices.xml`, lastmod: now },
]
chunks.forEach((chunk, i) => {
const filename = `sitemap-games-${i}.xml`
const xml = renderSitemap(chunk)
fs.writeFileSync(path.join(publicDir, filename), xml)
sitemaps.push({ loc: `${BASE_URL}/${filename}`, lastmod: now })
})
// Write sitemap index
const indexXml = renderSitemapIndex(sitemaps)
fs.writeFileSync(path.join(publicDir, "sitemap.xml"), indexXml)
} else {
// Single sitemap
const allEntries = [...staticEntries, ...gameEntries, ...deviceEntries]
const xml = renderSitemap(allEntries)
fs.writeFileSync(path.join(publicDir, "sitemap.xml"), xml)
}
console.log(`Sitemaps generated successfully! Total URLs: ${staticEntries.length + allDynamicEntries.length}`)
}
// Run if called directly
generateSitemaps().catch((err) => {
console.error("Failed to generate sitemaps:", err)
process.exit(1)
})
export { generateSitemaps }