update
This commit is contained in:
@ -1,6 +1,6 @@
|
||||
---
|
||||
// 语言由 Astro.currentLocale 运行时决定,实现见 components/views/AccountPage.astro(en/cn 共用)。
|
||||
import AccountPage from "@/components/views/AccountPage.astro"
|
||||
// 语言由 Astro.currentLocale 运行时决定,实现见 components/views/Account.astro(en/cn 共用)。
|
||||
import Account from "@/components/views/Account.astro"
|
||||
---
|
||||
|
||||
<AccountPage />
|
||||
<Account />
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
---
|
||||
export const prerender = false
|
||||
|
||||
// 旧 /item/[id] 保留为 301 重定向,按 id 类型前缀回退到对应类型化路由,避免外链 / SEO 权重失效。
|
||||
// r=runway → /runway-looks,s=street → /street-snaps,其余(含图像 i/j 经 parentUid 已回退)回落 runway。
|
||||
import { ROUTES, clientHref } from "@/lib/routes"
|
||||
|
||||
const id = Astro.params.id || ""
|
||||
const isSnap = id[0] === "s"
|
||||
const target = isSnap ? ROUTES.streetSnap(id) : ROUTES.runwayLook(id)
|
||||
return Astro.redirect(clientHref(Astro.currentLocale || "cn", target), 301)
|
||||
---
|
||||
@ -1,6 +1,6 @@
|
||||
---
|
||||
// 语言由 Astro.currentLocale 运行时决定,实现见 components/views/LoginPage.astro(en/cn 共用)。
|
||||
import LoginPage from "@/components/views/LoginPage.astro"
|
||||
// 语言由 Astro.currentLocale 运行时决定,实现见 components/views/Login.astro(en/cn 共用)。
|
||||
import Login from "@/components/views/Login.astro"
|
||||
---
|
||||
|
||||
<LoginPage />
|
||||
<Login />
|
||||
|
||||
@ -2,9 +2,9 @@
|
||||
export const prerender = false
|
||||
|
||||
// 走秀详情:类型化路由 /runway-looks/[id]。
|
||||
// 语言由 Astro.currentLocale 运行时决定,渲染实现见 components/views/ItemPage.astro(en/cn 共用)。
|
||||
// 语言由 Astro.currentLocale 运行时决定,渲染实现见 components/views/RunwayLooksDetail.astro(en/cn 共用)。
|
||||
// id 必须在此取出后透传:prerender 只认页面模块的导出,Astro.params 留在页面更直观。
|
||||
import ItemPage from "@/components/views/ItemPage.astro"
|
||||
import RunwayLooksDetail from "@/components/views/RunwayLooksDetail.astro"
|
||||
---
|
||||
|
||||
<ItemPage id={Astro.params.id} type="runway" />
|
||||
<RunwayLooksDetail id={Astro.params.id} />
|
||||
|
||||
@ -2,9 +2,9 @@
|
||||
export const prerender = false
|
||||
|
||||
// 街拍详情:类型化路由 /street-snaps/[id]。
|
||||
// 语言由 Astro.currentLocale 运行时决定,渲染实现见 components/views/ItemPage.astro(en/cn 共用)。
|
||||
// 语言由 Astro.currentLocale 运行时决定,渲染实现见 components/views/StreetSnapsDetail.astro(en/cn 共用)。
|
||||
// id 必须在此取出后透传:prerender 只认页面模块的导出,Astro.params 留在页面更直观。
|
||||
import ItemPage from "@/components/views/ItemPage.astro"
|
||||
import StreetSnapsDetail from "@/components/views/StreetSnapsDetail.astro"
|
||||
---
|
||||
|
||||
<ItemPage id={Astro.params.id} type="street" />
|
||||
<StreetSnapsDetail id={Astro.params.id} />
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
---
|
||||
// 语言由 Astro.currentLocale 运行时决定,实现见 components/views/AccountPage.astro(en/cn 共用)。
|
||||
import AccountPage from "@/components/views/AccountPage.astro"
|
||||
// 语言由 Astro.currentLocale 运行时决定,实现见 components/views/Account.astro(en/cn 共用)。
|
||||
import Account from "@/components/views/Account.astro"
|
||||
---
|
||||
|
||||
<AccountPage />
|
||||
<Account />
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
---
|
||||
export const prerender = false
|
||||
|
||||
// 旧 /item/[id] 保留为 301 重定向,按 id 类型前缀回退到对应类型化路由,避免外链 / SEO 权重失效。
|
||||
// r=runway → /runway-looks,s=street → /street-snaps,其余(含图像 i/j 经 parentUid 已回退)回落 runway。
|
||||
import { ROUTES, clientHref } from "@/lib/routes"
|
||||
|
||||
const id = Astro.params.id || ""
|
||||
const isSnap = id[0] === "s"
|
||||
const target = isSnap ? ROUTES.streetSnap(id) : ROUTES.runwayLook(id)
|
||||
return Astro.redirect(clientHref(Astro.currentLocale || "en", target), 301)
|
||||
---
|
||||
@ -1,6 +1,6 @@
|
||||
---
|
||||
// 语言由 Astro.currentLocale 运行时决定,实现见 components/views/LoginPage.astro(en/cn 共用)。
|
||||
import LoginPage from "@/components/views/LoginPage.astro"
|
||||
// 语言由 Astro.currentLocale 运行时决定,实现见 components/views/Login.astro(en/cn 共用)。
|
||||
import Login from "@/components/views/Login.astro"
|
||||
---
|
||||
|
||||
<LoginPage />
|
||||
<Login />
|
||||
|
||||
@ -2,9 +2,9 @@
|
||||
export const prerender = false
|
||||
|
||||
// 走秀详情:类型化路由 /runway-looks/[id]。
|
||||
// 语言由 Astro.currentLocale 运行时决定,渲染实现见 components/views/ItemPage.astro(en/cn 共用)。
|
||||
// 语言由 Astro.currentLocale 运行时决定,渲染实现见 components/views/RunwayLooksDetail.astro(en/cn 共用)。
|
||||
// id 必须在此取出后透传:prerender 只认页面模块的导出,Astro.params 留在页面更直观。
|
||||
import ItemPage from "@/components/views/ItemPage.astro"
|
||||
import RunwayLooksDetail from "@/components/views/RunwayLooksDetail.astro"
|
||||
---
|
||||
|
||||
<ItemPage id={Astro.params.id} type="runway" />
|
||||
<RunwayLooksDetail id={Astro.params.id} />
|
||||
|
||||
@ -2,9 +2,9 @@
|
||||
export const prerender = false
|
||||
|
||||
// 街拍详情:类型化路由 /street-snaps/[id]。
|
||||
// 语言由 Astro.currentLocale 运行时决定,渲染实现见 components/views/ItemPage.astro(en/cn 共用)。
|
||||
// 语言由 Astro.currentLocale 运行时决定,渲染实现见 components/views/StreetSnapsDetail.astro(en/cn 共用)。
|
||||
// id 必须在此取出后透传:prerender 只认页面模块的导出,Astro.params 留在页面更直观。
|
||||
import ItemPage from "@/components/views/ItemPage.astro"
|
||||
import StreetSnapsDetail from "@/components/views/StreetSnapsDetail.astro"
|
||||
---
|
||||
|
||||
<ItemPage id={Astro.params.id} type="street" />
|
||||
<StreetSnapsDetail id={Astro.params.id} />
|
||||
|
||||
12
src/pages/robots.txt.ts
Normal file
12
src/pages/robots.txt.ts
Normal file
@ -0,0 +1,12 @@
|
||||
// src/pages/robots.txt.ts — 动态 robots,自动指向当前域名的 sitemap。
|
||||
import type { APIRoute } from 'astro'
|
||||
|
||||
export const prerender = false
|
||||
|
||||
export const GET: APIRoute = () => {
|
||||
const site = ((import.meta.env as any).PUBLIC_SITE_URL ?? 'https://xisoa.com').replace(/\/$/, '')
|
||||
const body = `User-agent: *\nAllow: /\n\nSitemap: ${site}/sitemap.xml\n`
|
||||
return new Response(body, {
|
||||
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
|
||||
})
|
||||
}
|
||||
141
src/pages/sitemap.xml.ts
Normal file
141
src/pages/sitemap.xml.ts
Normal file
@ -0,0 +1,141 @@
|
||||
// src/pages/sitemap.xml.ts — 动态 sitemap 端点(SSR 运行期由 Node 生成)
|
||||
//
|
||||
// 为什么用「端点」而不是构建期静态文件:本站详情页是按需 SSR、文章 ID 在数据库里动态增长,
|
||||
// 构建期拿不到全量,故在每次 Google 抓取时由服务端实时拉列表生成,永远最新。
|
||||
//
|
||||
// 注意:
|
||||
// 1. 在 .env 里配置 PUBLIC_SITE_URL=https://你的域名(含协议、不含结尾斜杠),否则回退占位符。
|
||||
// 2. 列表接口的分页字段名若与下方假设不同(data.list / page+limit),按后端实际返回微调 fetchIds。
|
||||
// 3. 每条详情页同时输出 /en 与 /cn 两个 <loc> 并互标 hreflang,顺带补齐双语站点的 hreflang 信号。
|
||||
|
||||
import type { APIRoute } from 'astro'
|
||||
import { config } from '@/lib/config'
|
||||
|
||||
// SSR 模式下端点按需执行;显式声明避免被当作静态预渲染。
|
||||
export const prerender = false
|
||||
|
||||
// 站点绝对域名(公开环境变量,server/client 均可用;as any 规避未声明的 PUBLIC_* 类型报错)
|
||||
const SITE_URL = ((import.meta.env as any).PUBLIC_SITE_URL ?? 'https://xisoa.com').replace(/\/$/, '')
|
||||
const LOCALES = ['en', 'cn'] as const
|
||||
const hreflangOf = (l: (typeof LOCALES)[number]) => (l === 'cn' ? 'zh-CN' : 'en')
|
||||
|
||||
// 简单内存缓存:Google 不会高频抓,但避免每次请求都打一遍后端列表接口。
|
||||
let cache: { xml: string; ts: number } | null = null
|
||||
const TTL = 10 * 60 * 1000
|
||||
|
||||
interface SitemapEntry {
|
||||
loc: string
|
||||
alts: { lang: string; href: string }[]
|
||||
}
|
||||
|
||||
function abs(path: string): string {
|
||||
return SITE_URL + path
|
||||
}
|
||||
|
||||
/** 翻页拉取某列表接口下全部文章 ID(容错多种返回结构)。 */
|
||||
async function fetchIds(path: string): Promise<string[]> {
|
||||
const base = (config.baseApiSsr ?? '').replace(/\/$/, '')
|
||||
if (!base) return []
|
||||
const ids: string[] = []
|
||||
const limit = 200
|
||||
for (let page = 1; page <= 1000; page++) {
|
||||
let res: Response
|
||||
try {
|
||||
res = await fetch(`${base}${path}?page=${page}&limit=${limit}`)
|
||||
} catch {
|
||||
break
|
||||
}
|
||||
if (!res.ok) break
|
||||
let json: any
|
||||
try {
|
||||
json = await res.json()
|
||||
} catch {
|
||||
break
|
||||
}
|
||||
const items: any[] =
|
||||
json?.data?.list ??
|
||||
json?.data?.items ??
|
||||
json?.data ??
|
||||
json?.list ??
|
||||
json?.items ??
|
||||
[]
|
||||
if (!Array.isArray(items) || items.length === 0) break
|
||||
for (const it of items) if (it?.id) ids.push(String(it.id))
|
||||
if (items.length < limit) break
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
function esc(s: string): string {
|
||||
return s
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
}
|
||||
|
||||
function render(entries: SitemapEntry[]): string {
|
||||
const urlEls = entries
|
||||
.map((e) => {
|
||||
const alts = e.alts
|
||||
.map((a) => ` <xhtml:link rel="alternate" hreflang="${a.lang}" href="${esc(a.href)}"/>`)
|
||||
.join('\n')
|
||||
return ` <url>\n <loc>${esc(e.loc)}</loc>\n${alts}\n </url>`
|
||||
})
|
||||
.join('\n')
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">
|
||||
${urlEls}
|
||||
</urlset>`
|
||||
}
|
||||
|
||||
export const GET: APIRoute = async () => {
|
||||
if (cache && Date.now() - cache.ts < TTL) {
|
||||
return new Response(cache.xml, {
|
||||
headers: { 'Content-Type': 'application/xml; charset=utf-8' },
|
||||
})
|
||||
}
|
||||
|
||||
const runwayIds = await fetchIds('/api/v1/public/runway-looks')
|
||||
const snapIds = await fetchIds('/api/v1/public/street-snaps')
|
||||
|
||||
const entries: SitemapEntry[] = []
|
||||
|
||||
// —— 静态页(首页 + 两个列表页),双语各一份 + 互相 hreflang ——
|
||||
for (const l of LOCALES) {
|
||||
const home = abs(`/${l}`)
|
||||
entries.push({
|
||||
loc: home,
|
||||
alts: LOCALES.map((l2) => ({ lang: hreflangOf(l2), href: abs(`/${l2}`) })),
|
||||
})
|
||||
for (const sec of ['runway-looks', 'street-snaps'] as const) {
|
||||
const p = `/${l}/${sec}`
|
||||
entries.push({
|
||||
loc: abs(p),
|
||||
alts: LOCALES.map((l2) => ({ lang: hreflangOf(l2), href: abs(`/${l2}/${sec}`) })),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// —— 详情页:双语 + hreflang(x-default 指向 en)——
|
||||
const pushDetail = (sec: 'runway-looks' | 'street-snaps', id: string) => {
|
||||
const en = `/en/${sec}/${id}`
|
||||
const cn = `/cn/${sec}/${id}`
|
||||
entries.push({
|
||||
loc: abs(en),
|
||||
alts: [
|
||||
{ lang: 'en', href: abs(en) },
|
||||
{ lang: 'zh-CN', href: abs(cn) },
|
||||
{ lang: 'x-default', href: abs(en) },
|
||||
],
|
||||
})
|
||||
}
|
||||
for (const id of runwayIds) pushDetail('runway-looks', id)
|
||||
for (const id of snapIds) pushDetail('street-snaps', id)
|
||||
|
||||
const xml = render(entries)
|
||||
cache = { xml, ts: Date.now() }
|
||||
return new Response(xml, {
|
||||
headers: { 'Content-Type': 'application/xml; charset=utf-8' },
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user