- Implemented SharePanel component in `SharePanel.astro` for sharing content on social media platforms. - Integrated QR code generation for WeChat sharing using the `qrcode` library. - Added localization support for English and Chinese languages. - Created utility functions in `seo.ts` for building article summaries and FAQs. - Introduced API routes for serving IndexNow key and generating full LLM catalog and summaries. - Enhanced SEO capabilities with structured data for articles and pages.
135 lines
3.8 KiB
JavaScript
135 lines
3.8 KiB
JavaScript
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
|
|
})
|