const DEFAULT_API_PATH = '/api' const INDEXNOW_ENDPOINT = 'https://api.indexnow.org/indexnow' function normalizeBase(value) { return String(value || '').trim().replace(/\/$/, '') } function ensureAbsolute(base, path) { return `${normalizeBase(base)}${String(path).startsWith('/') ? path : `/${path}`}` } async function fetchJson(url) { const response = await fetch(url, { headers: { Accept: 'application/json', }, }) if (!response.ok) { throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`) } return response.json() } function collectStaticRoutes(siteSettings) { const routes = [ '/', '/about', '/articles', '/categories', '/tags', '/timeline', '/reviews', '/friends', '/rss.xml', '/sitemap.xml', '/llms.txt', '/llms-full.txt', '/indexnow-key.txt', ] if (siteSettings?.ai_enabled || siteSettings?.ai?.enabled) { routes.push('/ask') } return routes } async function main() { const indexNowKey = String(process.env.INDEXNOW_KEY || '').trim() if (!indexNowKey) { throw new Error('Missing INDEXNOW_KEY environment variable.') } const configuredSiteUrl = normalizeBase(process.env.SITE_URL || process.env.PUBLIC_SITE_URL || '') const configuredApiBaseUrl = normalizeBase( process.env.INTERNAL_API_BASE_URL || process.env.PUBLIC_API_BASE_URL || '', ) const bootstrapApiBaseUrl = configuredApiBaseUrl || `${configuredSiteUrl}${DEFAULT_API_PATH}` if (!bootstrapApiBaseUrl) { throw new Error('Missing SITE_URL/PUBLIC_SITE_URL or API base URL for IndexNow submission.') } const siteSettings = await fetchJson(`${bootstrapApiBaseUrl}/site_settings`).catch(() => null) const siteUrl = normalizeBase(configuredSiteUrl || siteSettings?.site_url || '') if (!siteUrl) { throw new Error('Unable to determine canonical SITE_URL for IndexNow submission.') } const apiBaseUrl = configuredApiBaseUrl || `${siteUrl}${DEFAULT_API_PATH}` const [posts, categories, tags, reviews] = await Promise.all([ fetchJson(`${apiBaseUrl}/posts`).catch(() => []), fetchJson(`${apiBaseUrl}/categories`).catch(() => []), fetchJson(`${apiBaseUrl}/tags`).catch(() => []), fetchJson(`${apiBaseUrl}/reviews`).catch(() => []), ]) const urls = new Set(collectStaticRoutes(siteSettings).map((path) => ensureAbsolute(siteUrl, path))) for (const post of posts) { if (!post?.slug || post?.noindex === true) continue urls.add(ensureAbsolute(siteUrl, `/articles/${encodeURIComponent(post.slug)}`)) } for (const category of categories) { const token = category?.slug || category?.name if (!token) continue urls.add(ensureAbsolute(siteUrl, `/categories/${encodeURIComponent(token)}`)) } for (const tag of tags) { const token = tag?.slug || tag?.name if (!token) continue urls.add(ensureAbsolute(siteUrl, `/tags/${encodeURIComponent(token)}`)) } for (const review of reviews) { if (!review?.id) continue urls.add(ensureAbsolute(siteUrl, `/reviews/${review.id}`)) } const payload = { host: new URL(siteUrl).host, key: indexNowKey, keyLocation: ensureAbsolute(siteUrl, '/indexnow-key.txt'), urlList: [...urls], } const response = await fetch(INDEXNOW_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json; charset=utf-8', }, body: JSON.stringify(payload), }) const responseText = await response.text().catch(() => '') if (!response.ok) { throw new Error(`IndexNow submission failed: ${response.status} ${response.statusText}\n${responseText}`) } console.log(`IndexNow submitted ${payload.urlList.length} URLs for ${siteUrl}`) if (responseText) { console.log(responseText) } } main().catch((error) => { console.error(error instanceof Error ? error.message : error) process.exitCode = 1 })