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:
1 parent
227bc24797
commit
328ab68f18
16 files changed
+409
-146
No files matched your search
+48
-30
@@ -1,42 +1,60 @@
|
||||
---
|
||||
// 名片头像占位:内联 SVG 渐变 60px;真实头像接入后整体替换本组件即可
|
||||
// 名片头像:后台「主页设置 → 头像」上传后在 content/site/assets/ 落图,
|
||||
// 经 Astro 图片管线出 60px WebP;未配置则回退内置渐变占位(历史视觉不变)
|
||||
import { Image } from 'astro:assets';
|
||||
import { homeAvatar } from '../lib/site-content';
|
||||
|
||||
const src = await homeAvatar();
|
||||
---
|
||||
|
||||
<svg
|
||||
class="avatar"
|
||||
viewBox="0 0 60 60"
|
||||
width="60"
|
||||
height="60"
|
||||
role="img"
|
||||
aria-label="头像占位"
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id="avatar-grad" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0" stop-color="var(--mood-3)"></stop>
|
||||
<stop offset="1" stop-color="var(--accent)"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="60" height="60" rx="14" fill="url(#avatar-grad)"></rect>
|
||||
<circle cx="44" cy="15" r="10" fill="#fff" opacity=".25"></circle>
|
||||
<path d="M0 46 20 28 34 40 46 32 60 46 60 60 0 60Z" fill="#000" opacity=".14"></path>
|
||||
<text
|
||||
x="30"
|
||||
y="38"
|
||||
text-anchor="middle"
|
||||
font-size="16"
|
||||
font-weight="700"
|
||||
fill="#fff"
|
||||
opacity=".92"
|
||||
font-family="var(--font-mono)"
|
||||
{src ? (
|
||||
<Image
|
||||
class="avatar"
|
||||
src={src}
|
||||
alt="wait4x 头像"
|
||||
width={60}
|
||||
height={60}
|
||||
format="webp"
|
||||
quality={82}
|
||||
/>
|
||||
) : (
|
||||
<svg
|
||||
class="avatar"
|
||||
viewBox="0 0 60 60"
|
||||
width="60"
|
||||
height="60"
|
||||
role="img"
|
||||
aria-label="头像占位"
|
||||
>
|
||||
w4
|
||||
</text>
|
||||
</svg>
|
||||
<defs>
|
||||
<linearGradient id="avatar-grad" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0" stop-color="var(--mood-3)"></stop>
|
||||
<stop offset="1" stop-color="var(--accent)"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="60" height="60" rx="14" fill="url(#avatar-grad)"></rect>
|
||||
<circle cx="44" cy="15" r="10" fill="#fff" opacity=".25"></circle>
|
||||
<path d="M0 46 20 28 34 40 46 32 60 46 60 60 0 60Z" fill="#000" opacity=".14"></path>
|
||||
<text
|
||||
x="30"
|
||||
y="38"
|
||||
text-anchor="middle"
|
||||
font-size="16"
|
||||
font-weight="700"
|
||||
fill="#fff"
|
||||
opacity=".92"
|
||||
font-family="var(--font-mono)"
|
||||
>
|
||||
w4
|
||||
</text>
|
||||
</svg>
|
||||
)}
|
||||
|
||||
<style>
|
||||
.avatar {
|
||||
flex: none;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
object-fit: cover;
|
||||
}
|
||||
</style>
|
||||
@@ -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); }
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
// 社交链接行(首页名片区):数据唯一事实源 = content/site/home.yaml 的 socials
|
||||
// icon 走内置路径注册表;未登记 icon → 纯文字按钮(label 即文字),保证后台随便加
|
||||
import { homeSocials } from '../lib/site-content';
|
||||
|
||||
// 只收 24×24 viewBox 的 path/d 片段:后台可加新条目但图标集需代码侧扩展,避免 XSS 面
|
||||
const ICON_PATHS: Record<string, string> = {
|
||||
github:
|
||||
'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',
|
||||
rss: 'M4 11a9 9 0 0 1 9 9M4 4a16 16 0 0 1 16 16M5 19a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z',
|
||||
email: 'M3 7.5A2.5 2.5 0 0 1 5.5 5h13A2.5 2.5 0 0 1 21 7.5v9A2.5 2.5 0 0 1 18.5 19h-13A2.5 2.5 0 0 1 3 16.5zM4 7.5l8 5.5 8-5.5',
|
||||
bilibili:
|
||||
'M6 4l2 2h8l2-2M4 8h16a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2Zm3 4v4m10-4v4',
|
||||
x: 'M4 4l7.5 9.5L4.5 20h2.2l6-6.7 5 6.7H22l-7.7-9.9L21 4h-2.2l-5.4 6L9 4z',
|
||||
};
|
||||
|
||||
const socials = await homeSocials();
|
||||
const iconOf = (icon?: string) => (icon && ICON_PATHS[icon] ? ICON_PATHS[icon] : null);
|
||||
---
|
||||
|
||||
<div class="socials">
|
||||
{socials.map((s) => {
|
||||
const isExternal = s.url.startsWith('http');
|
||||
return (
|
||||
<a
|
||||
class="tap social"
|
||||
href={s.url}
|
||||
rel={isExternal ? 'noopener noreferrer' : undefined}
|
||||
aria-label={s.label}
|
||||
title={s.label}
|
||||
>
|
||||
{iconOf(s.icon) ? (
|
||||
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor" aria-hidden="true">
|
||||
<path d={iconOf(s.icon)}></path>
|
||||
</svg>
|
||||
) : (
|
||||
<span class="social-text">{s.label}</span>
|
||||
)}
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.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);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
.social:hover { color: var(--accent); }
|
||||
.social-text {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12.5px;
|
||||
padding-inline: 2px;
|
||||
}
|
||||
</style>
|
||||
@@ -3,21 +3,21 @@ import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import IdCard from '../../components/IdCard.astro';
|
||||
import { t } from '../../i18n/ui';
|
||||
import type { Locale } from '../../lib/locale';
|
||||
import { aboutIntro } from '../../lib/site-content';
|
||||
import { aboutIntroMarkdown, renderMarkdown } from '../../lib/site-content';
|
||||
|
||||
// /en/ 侧由 T4 薄页传 locale='en';zh 路由无人传参,默认 zh 零回归
|
||||
interface Props {
|
||||
locale?: Locale;
|
||||
}
|
||||
const { locale = 'zh' } = Astro.props;
|
||||
// 身份陈述段改由 content/site/about.yaml 供给(后台可改),缺值回落默认
|
||||
const intro = await aboutIntro(locale);
|
||||
// 身份陈述段改由 content/site/about.yaml 供给(多段 markdown,后台可改),缺值回落默认
|
||||
const introHtml = await renderMarkdown(await aboutIntroMarkdown(locale));
|
||||
---
|
||||
|
||||
<BaseLayout title={`${t('about_title', locale)} · wait4x`}>
|
||||
<h1 class="page-title">{t('about_title', locale)}</h1>
|
||||
|
||||
<p class="intro">{intro}</p>
|
||||
<div class="intro" set:html={introHtml} />
|
||||
|
||||
<IdCard />
|
||||
|
||||
@@ -84,6 +84,16 @@ const intro = await aboutIntro(locale);
|
||||
max-width: var(--measure);
|
||||
margin-top: var(--space-3);
|
||||
}
|
||||
/* markdown 正文内部元素(多段/列表/链接)与站点排版对齐 */
|
||||
.intro :global(p) { margin-block: 0 var(--space-2); }
|
||||
.intro :global(ul) {
|
||||
margin: var(--space-1) 0 var(--space-2);
|
||||
padding-left: 1.2em;
|
||||
list-style: disc;
|
||||
}
|
||||
.intro :global(li) { margin-block: 0.2em; }
|
||||
.intro :global(a) { color: var(--accent); text-decoration: underline; text-underline-offset: 3px; }
|
||||
.intro :global(strong) { color: var(--ink-1); font-weight: 600; }
|
||||
.sec-title {
|
||||
font-size: 14px;
|
||||
font-family: var(--font-mono);
|
||||
|
||||
Reference in new issue
Block a user