This commit is contained in:
toom1996
2026-09-29 19:59:22 +08:00
parent b4fa41338b
commit 335e264e45
12 changed files with 143 additions and 103 deletions

View File

@ -19,7 +19,7 @@
<div class="showscard-left">
<div class="showscard-img" :class="{ 'is-loaded': it._imgLoaded }">
<span class="showsspin" aria-hidden="true" x-html="SPIN_SVG"></span>
<img :src="toAbsUrl(it.cover)" :alt="cardTitle(it)" loading="lazy" @load="it._imgLoaded = true" @error="it._imgLoaded = true" :class="{ 'is-loaded': it._imgLoaded }" />
<img :src="toAbsUrl(it.cover)" :srcset="toAbsSrcset(it.cover)" sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw" :alt="cardTitle(it)" loading="lazy" decoding="async" @load="it._imgLoaded = true" @error="it._imgLoaded = true" :class="{ 'is-loaded': it._imgLoaded }" />
</div>
</div>
<div class="showscard-right">
@ -31,7 +31,7 @@
<template x-for="(slot, idx) in getCardSlots(it)" :key="idx">
<div class="showscard-thumb" :class="{ 'placeholder': !slot.src, 'is-loaded': slot.loaded }">
<span class="showsspin" aria-hidden="true" x-html="SPIN_SVG" x-show="slot.src"></span>
<img x-show="slot.src" :src="toAbsUrl(slot.src)" alt="" loading="lazy" @load="slot.loaded = true" @error="slot.loaded = true" :class="{ 'is-loaded': slot.loaded }" />
<img x-show="slot.src" :src="toAbsUrl(slot.src)" :srcset="toAbsSrcset(slot.src)" sizes="80px" alt="" loading="lazy" decoding="async" @load="slot.loaded = true" @error="slot.loaded = true" :class="{ 'is-loaded': slot.loaded }" />
<span class="showscard-thumb-more" x-show="slot.isLast && slot.moreLabel" x-text="slot.moreLabel"></span>
</div>
</template>

View File

@ -36,6 +36,18 @@ const defaultKeywords =
const metaDescription = description ?? defaultDescription
const metaKeywords = keywords ?? defaultKeywords
// 社交分享默认图(站方应提供一张 1200x630 的公开图放 public/og-default.jpg)。
const OG_IMAGE = "/og-default.jpg"
// 站点级结构化数据(WebSite),帮助搜索引擎理解站点身份与语言。
const ldJson = {
"@context": "https://schema.org",
"@type": "WebSite",
name: SITE_NAME,
url: Astro.url.href,
inLanguage: locale === "cn" ? "zh-CN" : "en",
}
// 把当前语言注入 API 请求层:所有服务端(SSG/SSR)取数统一走 X-Locale 头,
// 后端据此选 *_en / *_cn 列。客户端 fetch 则自行按 URL path 推断,无需此处。
setApiLocale(locale)
@ -99,6 +111,25 @@ const headers = {
<title>{siteTitle}</title>
<meta name="description" content={metaDescription} />
<meta name="keywords" content={metaKeywords} />
<link rel="canonical" href={Astro.url.href} />
<!-- Open Graph -->
<meta property="og:site_name" content={SITE_NAME} />
<meta property="og:type" content="website" />
<meta property="og:title" content={siteTitle} />
<meta property="og:description" content={metaDescription} />
<meta property="og:url" content={Astro.url.href} />
<meta property="og:locale" content={locale === "cn" ? "zh_CN" : "en_US"} />
<meta property="og:image" content={OG_IMAGE} />
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={siteTitle} />
<meta name="twitter:description" content={metaDescription} />
<meta name="twitter:image" content={OG_IMAGE} />
<!-- 结构化数据 -->
<script type="application/ld+json" is:inline set:html={JSON.stringify(ldJson)}></script>
</head>
<body class="bg-white text-black min-h-screen flex flex-col px-5 py-5 md:px-[50px] md:py-[30px] font-sans">

View File

@ -16,7 +16,7 @@
* 所有方法内的 `this` 在运行时都指向 Alpine 组件实例。
*/
import { toAbs } from "@/lib/request"
import { toAbs, toSrcset } from "@/lib/request"
import { getUser } from "@/lib/auth"
import { clientHref, ROUTES } from "@/lib/routes"
import { buildPageList, SPIN_SVG } from "@/lib/ui"
@ -99,6 +99,7 @@ export function createCollectionPage(opts: CollectionPageOptions) {
nextPage() { this.goPage(this.page + 1) },
toAbsUrl(url: string) { return toAbs(url) },
toAbsSrcset(url: string) { return toSrcset(url) },
itemHref(it: { id: string | number }) {
// 按当前列表模块的收藏类型选对应类型化路由(与列表页 slug 一致)。
const route = opts.favType === "street" ? ROUTES.streetSnap(it.id) : ROUTES.runwayLook(it.id)

View File

@ -37,6 +37,32 @@ export function toAbs(url: string): string {
return /^https?:\/\//i.test(url) ? url : baseUrl() + url
}
// 响应式 srcset:基于同一图片 URL 生成多档宽度的候选集,交给浏览器按设备 DPR/视口挑选,
// 避免「一张大图在所有屏幕都下载」造成的带宽浪费(图片站性能关键项)。
//
// 适配本站两类远程图 URL:
// - 缤纷云 S4 CoreIX 样式串:key!style:high → key!w:480 / !w:768 ...(替换 style 段为宽度段)
// - 带查询串:?w=1080&q=80 → 改写 w= 参数
// 本地 /uploads 没有缩放服务,返回空(不生成 srcset,退回单图)。
const SRCSET_WIDTHS = [480, 768, 1080, 1600]
export function toSrcset(url: string): string {
if (!url) return ""
const abs = toAbs(url)
if (!/^https?:\/\//i.test(abs)) return ""
const at = (u: string, w: number): string => {
if (/\!style:/.test(u)) return u.replace(/\!style:[^!]+/, `!w:${w}`)
if (/\!w:\d+/.test(u)) return u.replace(/\!w:\d+/, `!w:${w}`)
try {
const u2 = new URL(u)
u2.searchParams.set("w", String(w))
return u2.toString()
} catch {
return u + (u.includes("?") ? "&" : "?") + "w=" + w
}
}
return SRCSET_WIDTHS.map((w) => `${at(abs, w)} ${w}w`).join(", ")
}
// 按需渲染(on-demand SSR)详情 base:必须是绝对 URL(Node fetch 不吃相对路径)。
export function ssrBase(): string {
return config.baseApiSsr.replace(/\/$/, "")

22
src/pages/404.astro Normal file
View File

@ -0,0 +1,22 @@
---
import Layout from '@/layouts/Layout.astro'
import { getI18n } from '@/lib/i18n'
import { href, ROUTES } from '@/lib/routes'
const { t, locale } = getI18n(Astro)
// 显式返回 404 状态码,避免被当作 200 软 404 损害 SEO。
Astro.response.status = 404
---
<Layout title={t('Page not found')}>
<div class="flex flex-1 flex-col items-center justify-center min-h-[50vh] text-center gap-6">
<h1 class="text-6xl font-black tracking-tight">404</h1>
<p class="text-sm text-[var(--c-gray-700)] uppercase tracking-[0.2em]">{t('Page not found')}</p>
<a
href={href(locale, ROUTES.home)}
class="px-5 py-2 border border-black text-[12px] uppercase tracking-[0.5px] hover:bg-black hover:text-white transition-colors"
>
{t('Home')}
</a>
</div>
</Layout>