feat(cms): 后台扩展五件——关于页 markdown 化/社交/头像/favicon/分轨首页

- T1 关于页=markdown 文档:about.yaml intro 扩为多段 markdown(zh/en),
  AboutPage 用 @astrojs/markdown-remark 的 createMarkdownProcessor(进程
  内单例复用)+ set:html 渲染,正文 p/ul/strong/a 对齐站点排版;名片与
  技能卡结构不动
- T2 社交链接单一数据源:home.yaml socials[](label/url/icon)+
  SocialLinks.astro(图标注册表白名单:github/rss/email/bilibili/x,
  未登记 icon 回退纯文字),首页 zh/en 内联硬编码社交块删除
- T3 头像与 favicon 可上传:schema avatar: image()(Astro Image 出 60px
  WebP,未设回退渐变 SVG);home.yaml favicon → BaseLayout link[rel=icon]
  按扩展名给 type,留空回退 /favicon.svg
- T4 首页分轨:PostList perTrack → 阅读轨/技术轨两段各限 6,各自出口
  (阅读→/books/ 技术→/archives/,en 同构);E2E 断言改为两段各≤6+各自
  出口+阅读在前
- T5 后台列表日期:posts summary「{{pubDate}} · {{title}}」、moments
  「{{fields.pubDate}} · {{fields.text}}」+sortable;photos 无 frontmatter
  日期(JSON sidecar)保持分组 · 标题并在文档写明原因
- 顺手修 content-lint 漏报:中文文件名在 HTML 里为 %XX 编码,脚本直接
  拼盘上路径导致 6 条"缺图"误报 → 加 decodeURI
- 清理:删除书封误传遗留的双重嵌套目录文件

车道:op-gitea(5/5,build+E2E 37/37+lint 47 页 0/0/0);收编复验:
build 46 页 + E2E 37/37 连跑(首跑两红为车道并发构建擦 dist 的瞬时态,
复跑绿)

验证: node v26.13.0 + astro 7.4.4 全量构建 46 页 exit 0;E2E 37/37;
      content-lint 0/0/0;books details/命令面板 playwright 实测

Spec: none (后台能力扩展,车道级交付)

Co-authored-by: GLM-5.3
This commit is contained in:
wait4xandGLM-5.3 committed 2026-11-02 15:46:41 +08:00
1 parent 227bc24797
commit 328ab68f18
16 files changed
+409 -146

No files matched your search

+68 -2
View File
@@ -18,10 +18,12 @@ interface Props {
limit?: number;
/** 截断后的「全部文章 →」出口链接;缺省不渲染 */
moreHref?: string;
/** 分轨模式:阅读轨与技朮轨各取 N 条(首页两段式,各段自带「更多」出口) */
perTrack?: number;
/** true = 归档折叠形态:按年 details(最新年开)+年内按月分组 */
fold?: boolean;
}
const { posts, locale = 'zh', titleOverrides, limit, moreHref, fold = false } = Astro.props;
const { posts, locale = 'zh', titleOverrides, limit, moreHref, perTrack, fold = false } = Astro.props;
const visible = typeof limit === 'number' ? posts.slice(0, limit) : posts;
// 轨道标签走字典(en 首页复用本组件时的双语位);zh 保持原词表语义
const TRACK_LABEL: Record<PostCategory, string> = {
@@ -58,7 +60,49 @@ const byMonth = (list: CollectionEntry<'posts'>[]) => {
};
---
{fold ? (
{perTrack ? (
<div class="post-list per-track">
{(
[
{ key: 'reading' as PostCategory, more: locale === 'en' ? '/en/books/' : '/books/' },
{ key: 'tech' as PostCategory, more: locale === 'en' ? '/en/archives/' : '/archives/' },
] as const
).map(({ key, more }) => {
const rows = posts.filter((post) => post.data.category === key).slice(0, perTrack);
if (rows.length === 0) return null;
return (
<section class="track-sec" aria-label={TRACK_LABEL[key]}>
<h2 class="track-sec-head">
<span>{TRACK_LABEL[key]}</span>
<a class="more-link tap" href={more}>{t('home_more_posts', locale)}</a>
</h2>
<ul>
{rows.map((post) => (
<li>
<a class="row tap" href={hrefOf(post.id)}>
{key === 'reading' && (
<i
class="emo"
style={`background:${post.data.mood ?? 'var(--mood)'}`}
title="情绪色"
aria-hidden="true"
/>
)}
<span class="row-title">{titleOverrides?.[post.id] ?? post.data.title}</span>
{key !== 'reading' &&
post.data.tags.map((tag) => <code class="tag">{`#${tag}`}</code>)}
<time class="row-date" datetime={post.data.pubDate.toISOString()}>
{mmdd(post.data.pubDate)}
</time>
</a>
</li>
))}
</ul>
</section>
);
})}
</div>
) : fold ? (
<div class="post-list post-list-fold">
{[...byYear.entries()].map(([year, list], i) => (
<details class="year-fold" open={i === 0}>
@@ -244,6 +288,28 @@ const byMonth = (list: CollectionEntry<'posts'>[]) => {
}
.more-link:hover { border-bottom-color: var(--accent); }
/* ---- 首页分轨形态(perTrack):阅读/技朮两段,各段自带更多出口 ---- */
.track-sec + .track-sec { margin-top: var(--space-5); }
.track-sec-head {
display: flex;
align-items: baseline;
gap: 0.35rem;
justify-content: space-between;
margin: 0 0 var(--space-1);
font-size: 13px;
font-weight: 600;
color: var(--ink-3);
letter-spacing: 0.05em;
}
.track-sec-head::before {
content: '';
width: 7px;
height: 7px;
border-radius: 2px;
background: var(--ink-4);
margin-right: 0.35rem;
}
/* ---- 归档折叠形态(fold)---- */
.year-fold { border-top: 1px solid var(--line); }
.year-fold:last-child { border-bottom: 1px solid var(--line); }