feat: add SharePanel component for social sharing with QR code support
- 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.
This commit is contained in:
134
frontend/scripts/submit-indexnow.mjs
Normal file
134
frontend/scripts/submit-indexnow.mjs
Normal file
@@ -0,0 +1,134 @@
|
||||
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
|
||||
})
|
||||
Reference in New Issue
Block a user