Files
blog/src/pages/en/index.astro
T
wait4xandGLM-5.3 264148f0a6 feat(cms): 内容管理闭环——站点单例三件套+文章视频字段
- A 站点文案单例:content/site/{home,about,opensource}.yaml +
  src/lib/site-content.ts 访问层(字段级回落默认,默认值=原文案逐字
  一致,零回归);接线主页 tagline(en 首页新增该块)、关于页 intro、
  开源页叙述三段 + PINNED 参数化(projects.ts: loadProjects(pinned)
  /orderPinned,PINNED_DEFAULT 仅缺省兜底)
- loader 语义修正(返工如实入档):Astro 7 的 file() 把 YAML 顶层对象
  按 Object.entries 拆成多条目(id=key、data=value),单例字段必炸
  schema;改 glob 精确到单文件 = 真单例语义,data 即文件根对象,
  Sveltia 可编辑根字段零嵌套(比包 main: 层的方案后台体验更好)
- B 文章媒体:posts +video/+poster;Sveltia file 字段
  (media_folder=public/media、public_folder=/media、hint ≤50MB);
  正文后渲染 video controls preload=metadata playsinline,无则不渲染;
  图片链路核对本已一致(./assets 走 Astro 管线);链接 markdown 原生
- docs/写作指南.md 补「后台上传」节(三类媒体 + 三单例操作 + 体积纪律)

Sveltia 集合:posts / moments / photos / site_pages(三单例)

车道:op-gitea(实测 tagline 改 yaml 产物联动、置空回落默认、pinned
改名排序变、视频临时件渲染全对、构建绿 + lint 45 页 0/0/0);收编
复验:构建 44 页 + E2E 37/37 + 三 yaml 扁平键就位

验证: pnpm astro build 44 页 exit 0;E2E 37/37 PASS;content-lint
      0/0/0;播放器/单例/置顶 playwright 实测全过

Spec: none (内容管理闭环,车道级交付)

Co-authored-by: GLM-5.3
2026-11-02 12:42:10 +08:00

170 lines
5.5 KiB
Plaintext

---
// /en/ 首页:与 zh 版结构逐区对齐(名片区/文章列表/最新影像/热力图/项目墙)
import { Image } from 'astro:assets';
import { getCollection } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.astro';
import IdCard from '../../components/IdCard.astro';
import PostList from '../../components/PostList.astro';
import ProjectWall from '../../components/geek/ProjectWall.astro';
import Heatmap from '../../components/geek/Heatmap.astro';
import { t } from '../../i18n/ui';
import { homeTagline } from '../../lib/site-content';
const posts = (await getCollection('posts', ({ data }) => !data.draft && !data.tags.includes('hidden'))).sort(
(a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime(),
);
// 列表标题吃 postsEn 译题(缺译回退 zh,与 /en/archives 同策略)
const enTitle = Object.fromEntries(
(await getCollection('postsEn')).map((e) => [e.id, e.data.title]),
);
const photos = (await getCollection('photos')).slice(0, 4);
// 身份语与 zh 版同源(content/site/home.yaml 的 en 字段),缺值回落默认
const tagline = await homeTagline('en');
---
<BaseLayout title="wait4x" description="wait4x's blog: reading · code · photography">
<IdCard>
<p class="tagline">{tagline}</p>
<div class="socials">
<a
class="tap social"
href="https://github.com/wait4xx"
rel="noopener noreferrer"
aria-label="GitHub"
title="GitHub"
>
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor" aria-hidden="true">
<path
d="M12 2C6.48 2 2 6.58 2 12.25c0 4.53 2.87 8.37 6.84 9.73.5.1.68-.22.68-.49 0-.24-.01-.88-.01-1.73-2.78.62-3.37-1.37-3.37-1.37-.45-1.18-1.11-1.5-1.11-1.5-.91-.63.07-.62.07-.62 1 .07 1.53 1.06 1.53 1.06.89 1.57 2.34 1.12 2.91.85.09-.66.35-1.11.63-1.37-2.22-.26-4.56-1.14-4.56-5.07 0-1.12.39-2.03 1.03-2.75-.1-.26-.45-1.3.1-2.7 0 0 .84-.28 2.75 1.05a9.3 9.3 0 0 1 5 0c1.91-1.33 2.75-1.05 2.75-1.05.55 1.4.2 2.44.1 2.7.64.72 1.03 1.63 1.03 2.75 0 3.94-2.34 4.8-4.57 5.06.36.32.68.94.68 1.9 0 1.37-.01 2.47-.01 2.81 0 .27.18.6.69.49A10.26 10.26 0 0 0 22 12.25C22 6.58 17.52 2 12 2Z"
></path>
</svg>
</a>
<a class="tap social" href="/en/rss.xml" aria-label="RSS feed" title="RSS feed">
<svg
viewBox="0 0 24 24"
width="16"
height="16"
fill="none"
stroke="currentColor"
stroke-width="1.8"
stroke-linecap="round"
aria-hidden="true"
>
<path d="M4 11.5a8.5 8.5 0 0 1 8.5 8.5M4 4.5a15.5 15.5 0 0 1 15.5 15.5"></path>
<circle cx="5.4" cy="18.6" r="1.5" fill="currentColor" stroke="none"></circle>
</svg>
</a>
<a class="tap social" href="mailto:admin@wait4x.online" aria-label="Email" title="Email">
<svg
viewBox="0 0 24 24"
width="16"
height="16"
fill="none"
stroke="currentColor"
stroke-width="1.8"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<rect x="3" y="5.5" width="18" height="13" rx="2.5"></rect>
<path d="m4 7.5 8 5.5 8-5.5"></path>
</svg>
</a>
</div>
</IdCard>
<PostList {posts} locale="en" titleOverrides={enTitle} limit={6} moreHref="/en/archives/" />
<section class="latest" aria-label={t('latest_photos', 'en')}>
<div class="latest-head">
<span class="lab">{t('latest_photos', 'en')}</span>
<a class="tap more" href="/en/gallery/">{t('enter_gallery', 'en')}</a>
</div>
<div class="thumbs">
{photos.map((photo) => (
<a class="thumb" href="/en/gallery/" aria-label={`Gallery: ${photo.data.title}`}>
<Image
src={photo.data.image}
alt={photo.data.alt ?? photo.data.title}
widths={[180, 360]}
sizes="(min-width: 820px) 20vw, 42vw"
/>
</a>
))}
</div>
</section>
<Heatmap
labels={{
title: t('heatmap_title', 'en'),
summary: t('heatmap_summary', 'en'),
less: t('heatmap_less', 'en'),
more: t('heatmap_more', 'en'),
note: t('heatmap_note', 'en'),
}}
/>
<ProjectWall locale="en" />
</BaseLayout>
<style>
.tagline {
font-size: 14px;
color: var(--ink-3);
margin-top: 4px;
letter-spacing: 0.01em;
}
.socials {
display: flex;
align-items: center;
gap: var(--space-2);
margin-top: var(--space-2);
}
.social {
color: var(--ink-3);
border-radius: 8px;
transition: color var(--dur) var(--ease);
}
.social:hover { color: var(--accent); }
.latest { margin-top: var(--space-6); }
.latest-head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: var(--space-3);
}
.lab {
font-size: 12.5px;
color: var(--ink-4);
letter-spacing: 0.08em;
}
.more {
font-size: 13px;
color: var(--accent);
padding-inline: var(--space-2);
transition: color var(--dur) var(--ease);
}
.more:hover { color: var(--ink-1); }
.thumbs {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--space-2);
margin-top: var(--space-3);
}
.thumb {
aspect-ratio: 1;
border-radius: 8px;
overflow: hidden;
}
.thumb :global(img) {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform var(--dur) var(--ease);
}
.thumb:hover :global(img) { transform: scale(1.03); }
@media (min-width: 820px) {
.thumbs { grid-template-columns: repeat(4, 1fr); }
}
</style>