update
This commit is contained in:
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
22
src/pages/404.astro
Normal 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>
|
||||
Reference in New Issue
Block a user