open-prompt / src /app /sitemap.ts
anky2002's picture
perf: sitemap uses revalidate=3600 instead of force-dynamic (reduces DB load)
ba2e060 verified
import { MetadataRoute } from 'next'
import type { PrismaClient } from '@prisma/client'
import { getAllToolsComplete } from '@/lib/tools/index'
/**
* Resolve the canonical site URL at runtime.
*/
function getBaseUrl(): string {
return (
process.env.SITE_URL ||
process.env.NEXT_PUBLIC_APP_URL ||
'https://open-prompt.netlify.app'
)
}
// Revalidate sitemap every hour instead of regenerating on every request
export const revalidate = 3600
/**
* Safely obtain a Prisma client via dynamic import.
*/
async function getPrisma(): Promise<PrismaClient | null> {
try {
const { prisma } = await import('@/lib/prisma')
return prisma
} catch (error) {
console.error('Sitemap: failed to initialise Prisma client:', error)
return null
}
}
// Tool categories (static list)
const toolCategories = [
'prompting', 'marketing', 'branding', 'copywriting', 'business',
'email', 'product', 'hr', 'personal-brand', 'operations',
'social-media', 'education', 'development', 'creative', 'research',
]
// Main sitemap function
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const BASE_URL = getBaseUrl()
// 1. Static pages
const staticPages: MetadataRoute.Sitemap = [
{ url: BASE_URL, lastModified: new Date(), changeFrequency: 'daily', priority: 1 },
{ url: `${BASE_URL}/explore`, lastModified: new Date(), changeFrequency: 'daily', priority: 0.9 },
{ url: `${BASE_URL}/tools`, lastModified: new Date(), changeFrequency: 'weekly', priority: 0.9 },
{ url: `${BASE_URL}/thunderdome`, lastModified: new Date(), changeFrequency: 'daily', priority: 0.8 },
{ url: `${BASE_URL}/workflows`, lastModified: new Date(), changeFrequency: 'weekly', priority: 0.8 },
{ url: `${BASE_URL}/frameworks`, lastModified: new Date(), changeFrequency: 'monthly', priority: 0.7 },
{ url: `${BASE_URL}/leaderboard`, lastModified: new Date(), changeFrequency: 'daily', priority: 0.7 },
{ url: `${BASE_URL}/image-prompts`, lastModified: new Date(), changeFrequency: 'daily', priority: 0.8 },
{ url: `${BASE_URL}/characters`, lastModified: new Date(), changeFrequency: 'weekly', priority: 0.7 },
{ url: `${BASE_URL}/categories`, lastModified: new Date(), changeFrequency: 'weekly', priority: 0.7 },
{ url: `${BASE_URL}/creators`, lastModified: new Date(), changeFrequency: 'daily', priority: 0.7 },
{ url: `${BASE_URL}/collections`, lastModified: new Date(), changeFrequency: 'daily', priority: 0.6 },
{ url: `${BASE_URL}/extension`, lastModified: new Date(), changeFrequency: 'monthly', priority: 0.6 },
{ url: `${BASE_URL}/about`, lastModified: new Date(), changeFrequency: 'monthly', priority: 0.5 },
{ url: `${BASE_URL}/pricing`, lastModified: new Date(), changeFrequency: 'monthly', priority: 0.7 },
{ url: `${BASE_URL}/blog`, lastModified: new Date(), changeFrequency: 'weekly', priority: 0.6 },
{ url: `${BASE_URL}/docs`, lastModified: new Date(), changeFrequency: 'weekly', priority: 0.6 },
{ url: `${BASE_URL}/guides`, lastModified: new Date(), changeFrequency: 'weekly', priority: 0.6 },
{ url: `${BASE_URL}/forum`, lastModified: new Date(), changeFrequency: 'daily', priority: 0.6 },
{ url: `${BASE_URL}/workflow-library`, lastModified: new Date(), changeFrequency: 'weekly', priority: 0.6 },
{ url: `${BASE_URL}/privacy`, lastModified: new Date(), changeFrequency: 'yearly', priority: 0.3 },
{ url: `${BASE_URL}/terms`, lastModified: new Date(), changeFrequency: 'yearly', priority: 0.3 },
]
// 2. Tool category pages
const toolCategoryPages: MetadataRoute.Sitemap = toolCategories.map((category) => ({
url: `${BASE_URL}/tools/${category}`,
lastModified: new Date(),
changeFrequency: 'weekly' as const,
priority: 0.7,
}))
// 3. Individual tool pages (177+ tools)
let toolPages: MetadataRoute.Sitemap = []
try {
const allTools = getAllToolsComplete()
toolPages = allTools.map((tool) => ({
url: `${BASE_URL}/tools/${tool.slug}`,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: 0.65,
}))
} catch {
console.warn('Sitemap: could not load tools list')
}
const pages: MetadataRoute.Sitemap = [...staticPages, ...toolCategoryPages, ...toolPages]
const prisma = await getPrisma()
if (!prisma) {
console.warn('Sitemap: returning static-only sitemap (no DB)')
return pages
}
// 4. Dynamic DB pages
const [promptPages, creatorPages, collectionPages, imagePromptPages, characterPages, workflowPages] =
await Promise.all([
fetchPromptPages(prisma, BASE_URL),
fetchCreatorPages(prisma, BASE_URL),
fetchCollectionPages(prisma, BASE_URL),
fetchImagePromptPages(prisma, BASE_URL),
fetchCharacterPages(prisma, BASE_URL),
fetchWorkflowPages(prisma, BASE_URL),
])
return [
...pages,
...promptPages,
...creatorPages,
...collectionPages,
...imagePromptPages,
...characterPages,
...workflowPages,
]
}
// Helpers
async function fetchPromptPages(prisma: PrismaClient, baseUrl: string): Promise<MetadataRoute.Sitemap> {
try {
const prompts = await prisma.prompt.findMany({
where: { visibility: 'public' },
select: { slug: true, updatedAt: true },
orderBy: { totalRuns: 'desc' },
take: 5000,
})
return prompts.map((prompt) => ({
url: `${baseUrl}/p/${prompt.slug}`,
lastModified: prompt.updatedAt,
changeFrequency: 'weekly' as const,
priority: 0.8,
}))
} catch (error) {
console.error('Sitemap: error fetching prompts:', error)
return []
}
}
async function fetchCreatorPages(prisma: PrismaClient, baseUrl: string): Promise<MetadataRoute.Sitemap> {
try {
const creators = await prisma.user.findMany({
where: {
username: { not: null },
prompts: { some: { visibility: 'public' } },
},
select: { username: true, updatedAt: true },
take: 1000,
})
return creators
.filter((c) => c.username)
.map((creator) => ({
url: `${baseUrl}/creator/${creator.username}`,
lastModified: creator.updatedAt,
changeFrequency: 'weekly' as const,
priority: 0.65,
}))
} catch (error) {
console.error('Sitemap: error fetching creators:', error)
return []
}
}
async function fetchCollectionPages(prisma: PrismaClient, baseUrl: string): Promise<MetadataRoute.Sitemap> {
try {
const collections = await prisma.collection.findMany({
where: { isPublic: true },
select: { id: true, updatedAt: true },
take: 1000,
})
return collections.map((collection) => ({
url: `${baseUrl}/collections/${collection.id}`,
lastModified: collection.updatedAt,
changeFrequency: 'weekly' as const,
priority: 0.5,
}))
} catch (error) {
console.error('Sitemap: error fetching collections:', error)
return []
}
}
async function fetchImagePromptPages(prisma: PrismaClient, baseUrl: string): Promise<MetadataRoute.Sitemap> {
try {
const imagePrompts = await prisma.imagePrompt.findMany({
where: { visibility: 'public' },
select: { slug: true, updatedAt: true },
orderBy: { totalUses: 'desc' },
take: 2000,
})
return imagePrompts.map((p) => ({
url: `${baseUrl}/image-prompts/${p.slug}`,
lastModified: p.updatedAt,
changeFrequency: 'weekly' as const,
priority: 0.7,
}))
} catch (error) {
console.error('Sitemap: error fetching image prompts:', error)
return []
}
}
async function fetchCharacterPages(prisma: PrismaClient, baseUrl: string): Promise<MetadataRoute.Sitemap> {
try {
const characters = await prisma.character.findMany({
where: { visibility: 'public' },
select: { slug: true, updatedAt: true },
orderBy: { totalChats: 'desc' },
take: 1000,
})
return characters.map((c) => ({
url: `${baseUrl}/characters/${c.slug}`,
lastModified: c.updatedAt,
changeFrequency: 'weekly' as const,
priority: 0.65,
}))
} catch (error) {
console.error('Sitemap: error fetching characters:', error)
return []
}
}
async function fetchWorkflowPages(prisma: PrismaClient, baseUrl: string): Promise<MetadataRoute.Sitemap> {
try {
const workflows = await prisma.workflow.findMany({
where: { visibility: 'public' },
select: { slug: true, updatedAt: true },
orderBy: { totalRuns: 'desc' },
take: 500,
})
return workflows.map((w) => ({
url: `${baseUrl}/workflows/${w.slug}`,
lastModified: w.updatedAt,
changeFrequency: 'weekly' as const,
priority: 0.6,
}))
} catch (error) {
console.error('Sitemap: error fetching workflows:', error)
return []
}
}