diff --git a/.spec/specs/2026-10-24_add-blog-skeleton/tasks.md b/.spec/specs/2026-10-24_add-blog-skeleton/tasks.md
index 17348ef..f0fd69c 100644
--- a/.spec/specs/2026-10-24_add-blog-skeleton/tasks.md
+++ b/.spec/specs/2026-10-24_add-blog-skeleton/tasks.md
@@ -35,10 +35,10 @@
- boundary: 只写 src/layouts/BaseLayout.astro 与 src/components/{Header,Footer,ThemeScript}.astro;导航 ≤4 项;页脚 ICP+built-from 槽位;inline 防闪烁脚本
- verify: `pnpm astro build && grep -q "陕ICP备2026014860号" dist/index.html`
-- [ ] T6 五页型(首页/文章双模板/图库/瞬间/关于)
+- [x] T6 五页型(首页/文章双模板/图库/瞬间/关于)
- depends-on: task_0004, task_0005
- - claim: lane-pages(子代理,预留 src/components 与 src/pages 除 index 外路由)
- - boundary: 只写页面与页面级组件(名片开场/分轨列表/书评衬线模板+引文组件/技术模板+TOC+进度条/图库 masonry+lightbox/瞬间时间线/关于含身份陈述空槽位);排版参数按报告 §3
+ - claim: lane-pages(子代理)→ 超时由主会话收编
+ - boundary: 只写页面与页面级组件(名片开场/分轨列表/书评衬线模板+引文组件/技术模板+TOC+进度条/图库 masonry+lightbox/瞬间时间线/关于含身份陈述空槽位);排版参数按报告 §3。豁免注记:sharp ^0.35.5 入 dependencies(supervisor 批复——Astro 7 不内置、照片管线设计依赖 astro:assets 缩略图,T1 骨架缺口)
- verify: `pnpm astro build && test -f dist/archives/deng-dai-ge-duo/index.html -a -f dist/gallery/index.html -a -f dist/moments/index.html -a -f dist/about/index.html`
- [ ] T7 极客层 P0(键盘+帮助/build-info/View Transitions/项目墙)
diff --git a/package.json b/package.json
index 9c4efd9..c3c1954 100644
--- a/package.json
+++ b/package.json
@@ -14,6 +14,7 @@
"@fontsource/noto-sans-sc": "^5.3.1",
"astro": "^7.3.8",
"exifr": "^7.1.3",
- "lxgw-wenkai-screen-webfont": "^1.7.0"
+ "lxgw-wenkai-screen-webfont": "^1.7.0",
+ "sharp": "^0.35.5"
}
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 19b4c18..aa10da9 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -23,6 +23,9 @@ importers:
lxgw-wenkai-screen-webfont:
specifier: ^1.7.0
version: 1.7.0
+ sharp:
+ specifier: ^0.35.5
+ version: 0.35.5
packages:
@@ -1428,8 +1431,7 @@ snapshots:
'@fontsource/noto-sans-sc@5.3.1': {}
- '@img/colour@1.1.0':
- optional: true
+ '@img/colour@1.1.0': {}
'@img/sharp-darwin-arm64@0.35.5':
optionalDependencies:
@@ -2181,8 +2183,7 @@ snapshots:
sax@1.6.1: {}
- semver@7.8.5:
- optional: true
+ semver@7.8.5: {}
sharp@0.35.5:
dependencies:
@@ -2215,7 +2216,6 @@ snapshots:
'@img/sharp-win32-arm64': 0.35.5
'@img/sharp-win32-ia32': 0.35.5
'@img/sharp-win32-x64': 0.35.5
- optional: true
shiki@4.5.0:
dependencies:
diff --git a/scripts/check-responsive.sh b/scripts/check-responsive.sh
index 12ade6c..dfbde7b 100644
--- a/scripts/check-responsive.sh
+++ b/scripts/check-responsive.sh
@@ -1,19 +1,20 @@
#!/bin/sh
# 响应式结构自查(§4e):四档断点 + 触屏 44px + 安全区 + dvh 的静态断言
# 依据:docs/博客重设计调研报告.md §4e;断点 1120/820/760/560
+# 扫描用 -r 递归(src/pages 有子目录,一层 glob 会漏检 archives/gallery/moments)
set -e
cd "$(dirname "$0")/.."
fail=0
check() { if eval "$2"; then echo " ok $1"; else echo " FAIL $1"; fail=1; fi }
-CSS_SRC="src/styles/global.css src/components/*.astro src/layouts/*.astro"
+DIRS="src/styles src/components src/layouts src/pages"
check "断点 1120px(宽屏 TOC 常驻档)" \
- "grep -rqE 'max-width: ?1120px|min-width: ?1120px|@container' \$CSS_SRC src/pages/*.astro 2>/dev/null"
+ "grep -rEq '(max|min)-width: ?1120px|@container' $DIRS"
check "断点 820px(笔电 TOC 收起档)" \
- "grep -rqE 'max-width: ?820px|min-width: ?820px' \$CSS_SRC src/pages/*.astro 2>/dev/null"
+ "grep -rEq '(max|min)-width: ?820px' $DIRS"
check "断点 760px(平板竖屏档)" \
- "grep -rqE 'max-width: ?760px|min-width: ?760px' \$CSS_SRC src/pages/*.astro 2>/dev/null"
+ "grep -rEq '(max|min)-width: ?760px' $DIRS"
check "断点 560px(手机档 + 正文降 18px)" \
"grep -rq 'max-width: 560px' src/styles/global.css"
check "移动端降档:--prose-size 到 18px" \
@@ -24,8 +25,8 @@ check "安全区 env(safe-area-inset-*) 使用" \
"grep -rq 'safe-area-inset' src/styles src/components src/layouts"
check "视口 viewport-fit=cover(刘海屏)" \
"grep -q 'viewport-fit=cover' src/layouts/BaseLayout.astro"
-check "100dvh 或未使用视口高度陷阱(出现 100vh 即 FAIL)" \
- "! grep -rn '100vh' src/ ; true"
+check "无 100vh 视口陷阱(属性值上下文;注释提及不算)" \
+ "! grep -rEq ':\\s*100vh|100vh\\s*[;)]' src/ --include='*.astro' --include='*.css' && ! grep -rEq 'height:\\s*100vh|min-height:\\s*100vh' src/"
check "动效总闸 prefers-reduced-motion" \
"grep -q 'prefers-reduced-motion: reduce' src/styles/global.css"
check "构建产物含 viewport-fit 与主题色 meta" \
diff --git a/src/components/Avatar.astro b/src/components/Avatar.astro
new file mode 100644
index 0000000..90b472b
--- /dev/null
+++ b/src/components/Avatar.astro
@@ -0,0 +1,42 @@
+---
+// 名片头像占位:内联 SVG 渐变 60px;真实头像接入后整体替换本组件即可
+---
+
+
+
+
diff --git a/src/components/IdCard.astro b/src/components/IdCard.astro
new file mode 100644
index 0000000..5e5b33c
--- /dev/null
+++ b/src/components/IdCard.astro
@@ -0,0 +1,27 @@
+---
+// 名片区(首页/关于共用):渐变头像占位 + 称呼;图标行等内容由使用页经 slot 注入
+import Avatar from './Avatar.astro';
+---
+
+
+
+
diff --git a/src/components/PostList.astro b/src/components/PostList.astro
new file mode 100644
index 0000000..af3f718
--- /dev/null
+++ b/src/components/PostList.astro
@@ -0,0 +1,137 @@
+---
+// 年份分组 + 轨道分节的文章列表(首页与 /archives/ 共用)
+// 轨道标签单一事实源:content.config 的 POST_TRACKS,此处不另立词表
+import type { CollectionEntry } from 'astro:content';
+import { POST_TRACKS, trackOf, type PostCategory } from '../content.config';
+
+interface Props {
+ /** 调用方已完成 draft 过滤与 pubDate 倒序 */
+ posts: CollectionEntry<'posts'>[];
+}
+const { posts } = Astro.props;
+
+const byYear = new Map[]>();
+for (const post of posts) {
+ const year = post.data.pubDate.getUTCFullYear();
+ const bucket = byYear.get(year);
+ if (bucket) bucket.push(post);
+ else byYear.set(year, [post]);
+}
+const mmdd = (d: Date) =>
+ `${String(d.getUTCMonth() + 1).padStart(2, '0')}-${String(d.getUTCDate()).padStart(2, '0')}`;
+---
+
+
+ {[...byYear.entries()].map(([year, list]) => (
+
+ {year}
+ {(Object.keys(POST_TRACKS) as PostCategory[]).map((cat) => {
+ const rows = list.filter((post) => post.data.category === cat);
+ if (rows.length === 0) return null;
+ return (
+
+ );
+ })}
+
+ ))}
+
+
+
diff --git a/src/pages/about.astro b/src/pages/about.astro
new file mode 100644
index 0000000..2eecdc2
--- /dev/null
+++ b/src/pages/about.astro
@@ -0,0 +1,118 @@
+---
+import BaseLayout from '../layouts/BaseLayout.astro';
+import IdCard from '../components/IdCard.astro';
+---
+
+
+ 关于
+
+
+
+
+ 兴趣版图
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
技术
+
Linux、容器与自托管,把家里的小机器折腾成顺手的工具。
+
+
+ -
+
+
+
+
+
摄影
+
街拍与山野,随身一台无反,在等光的间隙按快门。
+
+
+
+
+
+
+ 本站如何构建
+
+ 页面由 Astro 静态生成,不含运行时框架 JS;内容住在自托管 gitea——本地编辑器与 gitea
+ 网页写的是同一个 git 仓库,push 之后由 homelab 自动构建发布;评论走自托管
+ Artalk。页脚 built from 一行链到本次构建对应的 commit,构建过程透明可追溯。
+
+
+
+
+
diff --git a/src/pages/archives/[...slug].astro b/src/pages/archives/[...slug].astro
new file mode 100644
index 0000000..e6d8b4f
--- /dev/null
+++ b/src/pages/archives/[...slug].astro
@@ -0,0 +1,270 @@
+---
+import type { CollectionEntry } from 'astro:content';
+import { getCollection, render } from 'astro:content';
+import BaseLayout from '../../layouts/BaseLayout.astro';
+import { trackOf } from '../../content.config';
+
+// URL = /archives/{slug},slug 即文件名,与 Halo 旧结构 1:1(不可改)
+export async function getStaticPaths() {
+ const posts = (await getCollection('posts', ({ data }) => !data.draft)).sort(
+ (a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime(),
+ );
+ return posts.map((entry, i) => ({
+ params: { slug: entry.id },
+ props: { entry, prev: posts[i + 1] ?? null, next: posts[i - 1] ?? null },
+ }));
+}
+
+interface Props {
+ entry: CollectionEntry<'posts'>;
+ prev: CollectionEntry<'posts'> | null;
+ next: CollectionEntry<'posts'> | null;
+}
+const { entry, prev, next } = Astro.props;
+const { Content, headings } = await render(entry);
+
+const isReading = entry.data.category === 'reading';
+const date = entry.data.pubDate.toISOString().slice(0, 10);
+// 阅读时长:正文汉字数 / 400 向上取整(最低 1 分钟)
+const hanzi = entry.body?.match(/[\u4e00-\u9fff]/g)?.length ?? 0;
+const minutes = Math.max(1, Math.ceil(hanzi / 400));
+const toc = headings.filter((h) => h.depth === 2 || h.depth === 3);
+---
+
+
+ {isReading ? (
+
+
+ Reading · {trackOf(entry.data.category)}
+ {entry.data.title}
+
+
+
+ ·
+ 阅读时长 {minutes} 分钟
+
+
+
+
+
+
+ ) : (
+ <>
+
+
+
+ Tech · {trackOf(entry.data.category)}
+ {entry.data.title}
+
+ {entry.data.tags.map((tag) => {`#${tag}`})}
+ ·
+
+ ·
+ 阅读时长 {minutes} 分钟
+
+
+
+
+
+
+ {toc.length > 0 && (
+
+ )}
+
+
+ >
+ )}
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/archives/index.astro b/src/pages/archives/index.astro
new file mode 100644
index 0000000..64aee5c
--- /dev/null
+++ b/src/pages/archives/index.astro
@@ -0,0 +1,31 @@
+---
+// 导航「文章」落点:全量归档列表(年份/轨道分节逻辑与首页共用 PostList)
+import { getCollection } from 'astro:content';
+import BaseLayout from '../../layouts/BaseLayout.astro';
+import PostList from '../../components/PostList.astro';
+
+const posts = (await getCollection('posts', ({ data }) => !data.draft)).sort(
+ (a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime(),
+);
+---
+
+
+ 文章
+ 按年份归档 · 轨道分节 · 共 {posts.length} 篇
+
+
+
+
diff --git a/src/pages/gallery/index.astro b/src/pages/gallery/index.astro
new file mode 100644
index 0000000..52cb16e
--- /dev/null
+++ b/src/pages/gallery/index.astro
@@ -0,0 +1,357 @@
+---
+import { Image, getImage } from 'astro:assets';
+import { getCollection } from 'astro:content';
+import BaseLayout from '../../layouts/BaseLayout.astro';
+import type { PhotoExif } from '../../lib/exif';
+
+const photos = await getCollection('photos');
+
+// EXIF 参数条(机身/镜头/光圈/快门/ISO)构建期拼好整串,灯箱运行时只做文本替换
+function formatExif(exif: PhotoExif | undefined): string {
+ if (!exif) return '';
+ const shutter =
+ exif.exposureTime === undefined
+ ? undefined
+ : exif.exposureTime >= 1
+ ? `${exif.exposureTime}s`
+ : `1/${Math.round(1 / exif.exposureTime)}s`;
+ return [
+ [exif.make, exif.model].filter(Boolean).join(' ') || undefined,
+ exif.lens,
+ exif.fNumber === undefined ? undefined : `f/${exif.fNumber}`,
+ shutter,
+ exif.iso === undefined ? undefined : `ISO ${exif.iso}`,
+ ]
+ .filter(Boolean)
+ .join(' · ');
+}
+
+/* masonry:行高/列距用容器宽百分比(0.92cqw/1.62cqw,在 864px 版心即 8px/14px 档),
+ 行高随断点等比缩放,构建期一份 row-span 在 2/3 列全断点精确成立——
+ px 定值行高会让格高随列宽漂移,窄屏裁切失控 */
+const ROW = 0.0092;
+const GAP = 0.0162;
+const spanFor = (w: number, h: number, cols: number) =>
+ Math.round(((1 - GAP * (cols - 1)) / (cols * (w / h)) + GAP) / (ROW + GAP));
+
+const cards = await Promise.all(
+ photos.map(async (photo) => {
+ // 宽高比来源:photos loader 产出 ImageMetadata 的 width/height(orientation 已修正)
+ const w = photo.data.image.width ?? 4;
+ const h = photo.data.image.height ?? 3;
+ const full = await getImage({ src: photo.data.image, width: 1440 });
+ return {
+ group: photo.data.group,
+ title: photo.data.title,
+ alt: photo.data.alt ?? photo.data.title,
+ image: photo.data.image,
+ exif: formatExif(photo.data.exif),
+ fullSrc: full.src,
+ w,
+ h,
+ s3: spanFor(w, h, 3),
+ s2: spanFor(w, h, 2),
+ };
+ }),
+);
+const first = cards[0];
+---
+
+
+ 图库
+
+
+
+
+
+
+
+
diff --git a/src/pages/index.astro b/src/pages/index.astro
index 307e816..762bbfe 100644
--- a/src/pages/index.astro
+++ b/src/pages/index.astro
@@ -1,6 +1,143 @@
---
+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';
+
+const posts = (await getCollection('posts', ({ data }) => !data.draft)).sort(
+ (a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime(),
+);
+// 最新影像取集合前 4 张;照片时间序由照片管线(exif 车道)治理,此处只裁切
+const photos = (await getCollection('photos')).slice(0, 4);
---
+
- Phase 1 · T5 布局就绪,首页结构 T6 填充。
+
+
+
+
+
+
+
+
+
+ {photos.map((photo) => (
+
+
+
+ ))}
+
+
+
+
+
+
diff --git a/src/pages/moments/index.astro b/src/pages/moments/index.astro
new file mode 100644
index 0000000..a4a0249
--- /dev/null
+++ b/src/pages/moments/index.astro
@@ -0,0 +1,142 @@
+---
+import { Image } from 'astro:assets';
+import { getCollection } from 'astro:content';
+import BaseLayout from '../../layouts/BaseLayout.astro';
+
+const moments = (await getCollection('moments')).sort(
+ (a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime(),
+);
+const iso = (d: Date) => d.toISOString();
+---
+
+
+ 瞬间
+ 零碎的记录:随手拍、一句话感想、开源动态。
+
+ {moments.map((m) => (
+ -
+
+
+ {m.data.image && (
+
+
+
+ )}
+ {m.data.text &&
{m.data.text}
}
+ {m.data.tags.length > 0 && (
+
+ {m.data.tags.map((tag) => {`#${tag}`})}
+
+ )}
+
+
+ ))}
+
+
+
+
diff --git a/src/styles/global.css b/src/styles/global.css
index 84b7474..95c2f57 100644
--- a/src/styles/global.css
+++ b/src/styles/global.css
@@ -77,8 +77,8 @@ a { color: inherit; text-decoration: none; }
.prose-tech h3 { font-size: var(--text-h3); margin-top: 1.5em; line-height: var(--leading-heading); }
/* ---------- 引文组件(报告 §3:A 墨线长书摘 / D 金句) ---------- */
-/* 模式 A:学术极简(长书摘) */
-blockquote.quote-block {
+/* 裸 blockquote(markdown 原生输出无类名)默认即模式 A;显式类可覆盖 */
+.prose-reading blockquote, blockquote.quote-block {
border-left: 1px solid var(--paper-line);
border-left-width: 2px;
padding-left: 1.35em;