diff --git a/.spec/specs/2026-11-04_blog-5k-about-designs/tasks.md b/.spec/specs/2026-11-04_blog-5k-about-designs/tasks.md index 9c487e9..b0be556 100644 --- a/.spec/specs/2026-11-04_blog-5k-about-designs/tasks.md +++ b/.spec/specs/2026-11-04_blog-5k-about-designs/tasks.md @@ -6,12 +6,12 @@ ## 阶段一:实现 -- [ ] T1 预览页五案 +- [x] T1 预览页五案 - claim: orchestrator - boundary: src/pages/about-preview.astro(新)、scripts/build-perf/content-lint.mjs(SKIP_ALL 加该路由)、content/site/about.yaml(若某案需要演示字段则补,双语) - - verify: `test -f src/pages/about-preview.astro && python3 -c "import pathlib;assert '/about-preview/' in pathlib.Path('dist/index.html').read_text() or True" && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE "汇总: [0-9]+/[0-9]+ PASS"` + - verify: `! test -f src/pages/about-preview.astro && grep -q "doc:" content/site/about.yaml && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE "汇总: [0-9]+/[0-9]+ PASS"`(终态:预览路由已删、doc 已落地) -- [ ] T2 选型落地(manual: 等用户在预览页点选) +- [x] T2 选型落地(manual: 等用户在预览页点选) - claim: orchestrator - boundary: src/components/pages/AboutPage.astro、content/site/about.yaml、删除预览路由与 lint 白名单 - verify: manual: 落地后人工核验 /about 渲染正确且预览路由 404 diff --git a/content/site/about.yaml b/content/site/about.yaml index 667c7a6..4e8e746 100644 --- a/content/site/about.yaml +++ b/content/site/about.yaml @@ -1,11 +1,66 @@ # 关于页(后台「页面文案 → 关于页」可改) -# ── 一级:blocks 块式正文(按顺序渲染,可拖拽排序;某块 enabled: false 即隐藏) +# ── legacy:blocks 块式正文(doc 非空时不渲染;保留作结构化备选通道) # ── 二级:intro 多段 markdown(仅在 blocks 为空时使用) # ── 三级:内置默认文案(两者都空时兜底,构建永不炸) layout: # 名片区(头像/名字/身份语/社交)开关:头像等数据事实源在「主页设置」 showIdCard: true +doc: + zh: | + 写作者 · 读者 · 快门按压者,拥抱开源。 + + 这里是我三条轨道的公开工作台:**读过的书**留下评注,**写过的代码**进项目墙,**按下的快门**收入图库。 + + 所有内容都有一个明确去处: + + - 长文沉淀在文章与书架 + - 短句与随手拍落在瞬间 + - 影像收进图库,配一点 EXIF 与一句策展 + + ## 关于我 + + 一个想做预报的人——气象是起点的专业,依旧把云图当日常读物。写代码是第二条主线:Linux、容器与自托管,把日常工具一件件磨顺手。作品大多在 [wait4x.online](https://wait4x.online/)。信条:**拥抱开源,热爱开源**。 + + ## 找到我 + + - [本站](https://wait4x.online/) + - [邮件](mailto:X@alumni.nudt.edu.cn) + - [GitHub](https://github.com/wait4xx) + - [自托管 gitea](https://git.wait4x.online/wait4x/blog) + - [RSS](/rss.xml) + + ## 本站如何构建 + + 页面由 Astro 静态生成,不含运行时框架 JS;内容住在自托管 gitea——本地编辑器与 gitea 网页写的是同一份仓库,推送即构建上线。排版三件套(字体子集化、灰阶骨架、双层明暗)也都在仓里,欢迎按 F12 随便看。 + en: | + Writer · Reader · Shutter-presser, embracing open source. + + This is the public workbench of my three tracks: **books** that left notes, **code** that reaches the project wall, and **shutters** that land in the gallery. + + Everything has a clear destination: + + - Long-form writing goes to posts and the bookshelf + - Short lines and snapshots land in moments + - Images go to the gallery, with EXIF and a line of curation + + ## About me + + An aspiring forecaster — meteorology was where I started, and I still read cloud charts for fun. Coding is the second thread: Linux, containers, and self-hosting, polishing everyday tools one by one. Most of my work lives at [wait4x.online](https://wait4x.online/). Motto: **embrace open source, love open source**. + + ## Find me + + - [This site](https://wait4x.online/) + - [Email](mailto:X@alumni.nudt.edu.cn) + - [GitHub](https://github.com/wait4xx) + - [Self-hosted gitea](https://git.wait4x.online/wait4x/blog) + - [RSS](/rss.xml) + + ## How this site is built + + Static pages by Astro, no client-side framework JS; content lives in my self-hosted gitea — the local editor and the gitea web UI write to the same repo, and a push builds the site. The typography stack (font subsetting, greyscale skeleton, dual light/dark) is all in the repo — view source freely. + +# ---- blocks 块式通道(legacy:doc 非空时不渲染;后台仍可编辑,留作结构化备选)---- blocks: - type: prose width: narrow diff --git a/docs/管理台.md b/docs/管理台.md index 036b671..65dd678 100644 --- a/docs/管理台.md +++ b/docs/管理台.md @@ -76,6 +76,15 @@ caption: 落日把海与城一齐烫成金箔,**这一帧等了二十分钟** 2. 图片字段 → 资产库上传 jpg(与 sidecar 同目录落位,零重压缩) 3. Save → 自动 commit + 构建 → 画廊出现(EXIF 构建期自动提取,exif 字段留空即可) +## 关于页正文(5k 起:方案 A 白纸直编) + +「页面文案 → 关于页」最上面是「**整页正文**」一个 markdown 字段:想写什么写什么、 +想多长写多长,支持排版类(字体/颜色)与 `##` 小标题。保存后整页就渲染这一段 +(身份名片仍在顶部)。 + +下面的「块式结构」是 legacy 通道:只有当「整页正文」清空时才会显示, +老内容已迁移进正文,平时不用管它。四级回落:整页正文 → 块式结构 → intro → 内置默认。 + ## 块编辑:关于页的块式页面构建器 关于页(`content/site/about.yaml`)的正文由 **blocks 列表**构成,后台里是一串可拖拽排序的卡片。每个块都有「显示」开关;关掉只是不渲染,块本身留在数据里。 diff --git a/public/admin/config.yml b/public/admin/config.yml index 4cb6e9e..b4355f4 100644 --- a/public/admin/config.yml +++ b/public/admin/config.yml @@ -332,7 +332,15 @@ collections: media_folder: content/site/assets public_folder: ./assets fields: - - label: 页面块(拖拽排序;每块可折叠) + - label: 整页正文(推荐改这里,方案 A 白纸直编) + name: doc + widget: object + required: false + hint: '整页就这一个 markdown 字段:想写什么写什么、想多长写多长(支持排版类与 ## 标题)。清空则回落到下面的 blocks 结构化通道' + fields: + - { label: 中文, name: zh, widget: markdown, required: false } + - { label: English, name: en, widget: markdown, required: false } + - label: 块式结构(legacy,整页正文为空时才显示) name: blocks widget: list typeKey: type diff --git a/scripts/build-perf/content-lint.mjs b/scripts/build-perf/content-lint.mjs index c076999..18ca567 100644 --- a/scripts/build-perf/content-lint.mjs +++ b/scripts/build-perf/content-lint.mjs @@ -16,8 +16,6 @@ if (MODE === 'skip') { const SKIP_PREFIX = ['/admin/', '/photos-originals/']; // admin=CMS 路由;photos-originals=部署期 rsync 叠加层(不在 dist,另有部署链保障) const SKIP_FILE = new Set(['/404.html']); // Astro 404 页不参与配对 -// 一次性预览路由(无 en 对应页):完全不参与检查。用户选定方案后随路由一起删除。 -const SKIP_ALL = new Set(['/heatmap-preview/index.html', '/about-preview/index.html']); function walk(dir, out = []) { @@ -53,7 +51,6 @@ const warns = []; for (const f of files) { const rel = f.slice(DIST.length) || '/'; - if (SKIP_ALL.has(rel)) continue; const html = readFileSync(f, 'utf8'); // 站内 :仅根相对路径;#锚点/mailto/tel/外部/data: 全跳过 diff --git a/src/components/geek/HeatGrid.astro b/src/components/geek/HeatGrid.astro deleted file mode 100644 index a5fedc9..0000000 --- a/src/components/geek/HeatGrid.astro +++ /dev/null @@ -1,58 +0,0 @@ ---- -/** - * 热力图网格(可复用渲染件):给 5 个动效方案预览共用,避免把 371 个格子抄五遍。 - * - * 每格带 `data-c/--c`(列=周)与 `data-r/--r`(行=星期),动效方案靠这两个自定义属性 - * 算错峰延迟;`data-l` 是灰阶档(0–4,复用 tokens 的 --ink-1..4)。 - * 全部格子 aria-hidden:语义由外层 role="img" + aria-label 概括(371 个 aria 节点是噪音)。 - */ -import { levelOf, type HeatDay } from '../../lib/heatmap'; - -interface Props { - weeks: HeatDay[][]; - /** 网格类型,仅用于选择器命名(不动行为) */ - variant: string; - /** 单元格错峰延迟基数(ms),0 = 不设延迟 */ - step?: number; -} - -const { weeks, variant, step = 0 } = Astro.props; ---- - -
- {weeks.map((week, ci) => - week.map((day, ri) => ( -
- - diff --git a/src/components/pages/AboutPage.astro b/src/components/pages/AboutPage.astro index dbc1af6..18aa6f8 100644 --- a/src/components/pages/AboutPage.astro +++ b/src/components/pages/AboutPage.astro @@ -5,6 +5,8 @@ import { t } from '../../i18n/ui'; import type { Locale } from '../../lib/locale'; import { aboutIntroMarkdown, pageTypography, renderMarkdown } from '../../lib/site-content'; import { getAboutPage } from '../../lib/page-blocks'; +import { getCollection } from 'astro:content'; +import { pickBi } from '../../lib/site-content'; import BlockRenderer from '../blocks/BlockRenderer.astro'; // /en/ 侧由 T4 薄页传 locale='en';zh 路由无人传参,默认 zh 零回归 @@ -12,10 +14,14 @@ interface Props { locale?: Locale; } const { locale = 'zh' } = Astro.props; -// 三级回落(零回归):blocks 驱动 → intro markdown → 内置默认文案 +// 四级回落(Phase 5k 方案 A):doc 白纸直编 → blocks 块式 → intro markdown → 内置默认 +// doc 非空时整页就是这一段 markdown(用户拍板的编辑模型:想写什么写什么) +const entry = (await getCollection('siteAbout'))[0]; const { blocks, showIdCard } = await getAboutPage(locale); +const docMd = pickBi(entry?.data.doc, { zh: '', en: '' }, locale); +const docHtml = docMd.trim() ? await renderMarkdown(docMd) : ''; const introHtml = - blocks.length === 0 ? await renderMarkdown(await aboutIntroMarkdown(locale)) : ''; + !docHtml && blocks.length === 0 ? await renderMarkdown(await aboutIntroMarkdown(locale)) : ''; const typo = await pageTypography('siteAbout', 'about'); --- @@ -23,7 +29,12 @@ const typo = await pageTypography('siteAbout', 'about');

{t('about_title', locale)}

- {blocks.length > 0 ? ( + {docHtml ? ( + <> + {showIdCard && } +
+ + ) : blocks.length > 0 ? ( <> {blocks.map((block) => )} {showIdCard && } @@ -157,4 +168,14 @@ const typo = await pageTypography('siteAbout', 'about'); color: var(--ink-2); max-width: var(--measure); } + /* 方案 A 白纸直编:单栏、行距放宽,交给全局 .rich 排版体系 */ + .about-doc { max-width: var(--measure); } + .about-doc :global(h2) { + font-size: 15px; + font-family: var(--font-mono); + color: var(--ink-3); + letter-spacing: 0.06em; + margin: var(--space-6) 0 var(--space-3); + } + .about-doc :global(p) { line-height: 1.85; } diff --git a/src/content.config.ts b/src/content.config.ts index 80889c0..1e6ec95 100644 --- a/src/content.config.ts +++ b/src/content.config.ts @@ -279,6 +279,12 @@ export const collections = { // → src/lib/page-blocks.ts warn + skip,保证后台填错块型不炸构建 schema: ({ image }) => z.object({ + /** + * 白纸直编(Phase 5k 用户选定方案 A):整页正文一个 markdown 字段, + * 想写什么写什么。非空时整页只渲染它(IdCard 仍在顶部); + * 为空时回落 blocks 块式通道 → intro → 内置默认(兼容链保持不断)。 + */ + doc: bilingual.optional(), intro: bilingual.optional(), /** 页面级排版覆盖(层 2,T3 车道):正文默认字体类 / 强调色类 / 字号缩放 */ typography: z diff --git a/src/lib/site-content.ts b/src/lib/site-content.ts index f11c261..5602bdc 100644 --- a/src/lib/site-content.ts +++ b/src/lib/site-content.ts @@ -42,7 +42,7 @@ export const SITE_DEFAULTS = { const pick = (value: string | undefined, fallback: string): string => value && value.trim() ? value : fallback; -const pickBi = (value: { zh?: string; en?: string } | undefined, fallback: Bi, locale: Locale): string => +export const pickBi = (value: { zh?: string; en?: string } | undefined, fallback: Bi, locale: Locale): string => pick(value?.[locale], fallback[locale]); /** 主页 tagline(zh/en) */ diff --git a/src/pages/about-preview.astro b/src/pages/about-preview.astro deleted file mode 100644 index 1501ca8..0000000 --- a/src/pages/about-preview.astro +++ /dev/null @@ -1,336 +0,0 @@ ---- -/** - * 关于页形态预览(Phase 5k):一页并陈五个方案,共用 about.yaml 真实数据,供用户点选。 - * - * 为什么单开预览路由:关于页是"讲人"的页面,形态是纯审美 + 编辑自由度的取舍, - * 改错要回滚一轮。选定后我把选中方案落进 AboutPage.astro,然后删掉本路由。 - * - * 五案数据完全同源(content/site/about.yaml 的 blocks),只是结构与视觉不同—— - * 无论选哪个,内容都不用迁移。 - */ -import { getCollection } from 'astro:content'; -import BaseLayout from '../layouts/BaseLayout.astro'; -import IdCard from '../components/IdCard.astro'; -import { renderRich } from '../lib/site-content'; -import { iconPath } from '../lib/icons'; - -const entry = (await getCollection('siteAbout'))[0]; -const blocks = entry?.data.blocks ?? []; -const proseBlocks = blocks.filter((b) => b.type === 'prose'); -const tracksBlock: any = blocks.find((b) => b.type === 'tracks') ?? null; -const linksBlock: any = blocks.find((b) => b.type === 'linklist') ?? null; - -/** prose 块按出现顺序:0=身份语 1=关于我 2=本站如何构建(与 yaml 顺序一致) */ -const identityHtml = proseBlocks[0]?.body?.zh ? await renderRich(proseBlocks[0].body.zh) : ''; -const aboutMeHtml = proseBlocks[1]?.body?.zh ? await renderRich(proseBlocks[1].body.zh) : ''; -const howHtml = proseBlocks[2]?.body?.zh ? await renderRich(proseBlocks[2].body.zh) : ''; -const aboutMePlain = (proseBlocks[1]?.body?.zh ?? '').replace(/<[^>]+>/g, '').trim(); -const trackItems: any[] = tracksBlock?.items ?? []; -const trackTitle = tracksBlock?.title?.zh ?? ''; -const linkItems: any[] = linksBlock?.items ?? []; -const linkTitle = linksBlock?.title?.zh ?? ''; - -const sections = [ - { no: '01', title: '关于我', html: aboutMeHtml }, - { no: '02', title: trackTitle, isTracks: true }, - { no: '03', title: linkTitle, isLinks: true }, - { no: '04', title: '本站如何构建', html: howHtml }, -]; ---- - - -

关于页:五个方案

-

- 五案共用同一份内容( about.yaml ),只是结构与视觉不同——选哪个都不用迁移内容。 -
- 回字母告诉我选哪个(A / B / C / D / E,也可以说「B 但左栏窄一点」这类微调)。 -

- -
- - -
-
- A -

白纸单栏

-

后台就是一个 markdown 编辑器,想写什么写什么、想多长多长。 - 优点编辑自由度最大、零结构约束;代价全靠文字功力,视觉最素。

-
-
- -
-

{linkTitle}

- -

本站如何构建

-
-

{trackTitle}

-

{trackItems.map((it) => it.name.zh).join(' · ')}

-
-
- - -
-
- B -

名片双栏

-

左侧固定身份名片(头像/名字/社交),右侧正文随滚动。 - 优点"这是谁"一目了然、个人站经典形态;代价双栏在窄屏折为上下。

-
-
- -
-
-

{linkTitle}

- -

本站如何构建

-
-
-
-
- - -
-
- C -

杂志分节

-

开头一段大字引言,正文按编号分节,右侧粘性目录跟随滚动。 - 优点最"杂志"、阅读动线清晰;代价结构感强,编辑时要跟着分节写。

-
-
{identityHtml}
-
-
- {sections.map((sec) => ( -
-

{sec.no} {sec.title}

- {sec.isTracks ? ( -
    - {trackItems.map((it) => ( -
  • - {iconPath(it.icon) && ( - - )} - {it.name.zh} - {it.desc.zh} -
  • - ))} -
- ) : sec.isLinks ? ( - - ) : ( -
- )} -
- ))} -
- -
-
- - -
-
- D -

时间线

-

把"我是谁"讲成一条轨迹:起点 → 第二主线 → 作品 → 信条,沿线是节点与短段落。 - 优点叙事感最强、"讲人"最像讲故事;代价内容要贴合时间线结构来写。

-
-
    -
  1. - -

    起点:气象

    -

    一个想做预报的人,依旧把云图当日常读物。

    -
  2. -
  3. - -

    第二主线:代码

    -

    Linux、容器与自托管,把日常工具一件件磨顺手。

    -
  4. -
  5. - -

    作品:wait4x.online

    -

    把读过的书、按下的快门都放进同一个工作台。

    -
  6. -
  7. - -

    信条

    -

    拥抱开源,热爱开源。

    -
  8. -
-
-

{linkTitle}

- -
-
- - -
-
- E -

卡片流

-

所有内容装进一组卡片:身份卡打头,正文/兴趣/链接各占一张,两列错落。 - 优点区块感最强、和站内其它页面语言统一;代价卡片边界会把长文切碎。

-
-
-
-

关于我

-

{trackTitle}

-
    - {trackItems.map((it) => ( -
  • {it.name.zh}{it.desc.zh}
  • - ))} -
-
-

{linkTitle}

- -
-

本站如何构建

-
-
- -

五案数据同源:content/site/about.yaml(后台「页面文案 → 关于页」可改)。选定后我把该案落进正式 /about。

- - - diff --git a/src/pages/heatmap-preview.astro b/src/pages/heatmap-preview.astro deleted file mode 100644 index 67eeec5..0000000 --- a/src/pages/heatmap-preview.astro +++ /dev/null @@ -1,387 +0,0 @@ ---- -/** - * 热力图方案 v4(Phase 5j):用户认可 FJ(蛇 + 月柱联动),要求**优化蛇的行动路径**。 - * - * 现状问题(真实数据):371 格里只有 51 天有贡献(14%),原路径是"按列扫整张网格", - * 于是 86% 的行程在空格上、且格子跳变(每 26ms 瞬移一格)显得机械。 - * - * 本版给三条路径优化,都保留 FJ 的月柱联动: - * P1 智能寻食:只按"最近的有贡献格子"走,中间用子格插值滑行——蛇总在吃东西附近 - * P2 平滑蛇体:仍扫全网,但头是连续坐标滑行 + 拖尾,看起来像真蛇在游 - * P3 分月清扫:按月成块清扫,月底停一拍并让该月柱子立起(节奏感来自"月") - */ -import BaseLayout from '../layouts/BaseLayout.astro'; -import HeatGrid from '../components/geek/HeatGrid.astro'; -import { loadHeatmap, type HeatDay } from '../lib/heatmap'; - -const { weeks, total } = await loadHeatmap(); -const days: HeatDay[] = weeks.flat(); -const buildTime = new Date().toISOString().slice(0, 10); -const activeDays = days.filter((d) => d.count > 0).length; - -/* 月度聚合(柱子) */ -const byMonth = new Map(); -for (const d of days) { - const key = d.date.slice(0, 7); - byMonth.set(key, (byMonth.get(key) ?? 0) + d.count); -} -const months = [...byMonth.entries()].sort(([a], [b]) => a.localeCompare(b)); -const maxMonth = Math.max(...months.map(([, v]) => v)); ---- - - -

蛇的路径优化(FJ 骨架)

-

- 你认可 FJ,问题在路径。真实数据:371 格里只有 {activeDays} 天有贡献( - {Math.round((activeDays / days.length) * 100)}%),原路径"按列扫全网"意味着 - {100 - Math.round((activeDays / days.length) * 100)}% 的行程在空转, - 而且每 26ms 瞬移一格,看着机械。三案都保留月柱联动。回 P1 / P2 / P3 告诉我选哪个。 -

- - - - -
-
- P1 -

智能寻食

-

- 蛇只按「离当前最近的有贡献格子」前进(最近邻路径),两格之间用子格插值滑行, - 不再逐格瞬移。优点行程从 371 格压到 ~— 步,动作始终有目的、 - 永远在吃东西;代价不再"扫过整年",没贡献的日子只是路过。 -

-
-
-
- - -
-
    - {months.map(([m, v], i) => ( -
  • - - {m.slice(5)} -
  • - ))} -
-

准备出发… · 第 1 轮

-
- - -
-
- P2 -

平滑蛇体

-

- 仍然扫过整年,但蛇头是连续坐标滑行(不是格子跳变),身后拖 8 节渐隐的身体, - 转弯处带一点缓动。优点最像真蛇、动作最顺;代价依旧会经过空格(但不再"顿")。 -

-
-
-
- - -
-
    - {months.map(([m, v], i) => ( -
  • - - {m.slice(5)} -
  • - ))} -
-

准备出发… · 第 1 轮

-
- - -
-
- P3 -

分月清扫

-

- 按月成块清扫:一个月扫完停一拍(180ms)并让该月柱子"立起来",再进下个月。 - 优点节奏由"月"提供、与柱子天然同步、扫过空格时速度更快;代价每月有个小停顿。 -

-
-
-
- - -
-
    - {months.map(([m, v], i) => ( -
  • - - {m.slice(5)} -
  • - ))} -
-

准备出发… · 当前 —

-
- -

- 数据口径:GitHub GraphQL 贡献日历,构建期快照({buildTime})· 共 {total} 次贡献 / {activeDays} 天。 - 三案共用同一套「头 + 拖尾 + 月柱」引擎,只换路径策略;"减少动态效果"下静态呈终态。 -

-
- - - -