diff --git a/content/site/about.yaml b/content/site/about.yaml index c613cf8..1f317e6 100644 --- a/content/site/about.yaml +++ b/content/site/about.yaml @@ -1,28 +1,87 @@ -# 关于页正文(后台「页面文案 → 关于页」可改,markdown 编辑) -# intro = 多段 markdown 正文,渲染在「关于」标题下方、名片之前;留空回落默认文案 -# 支持的语法:段落、列表、粗斜体、行内代码、链接(渲染走 Astro 的 markdown 管线) +# 关于页(后台「页面文案 → 关于页」可改) +# ── 一级:blocks 块式正文(按顺序渲染,可拖拽排序;某块 enabled: false 即隐藏) +# ── 二级:intro 多段 markdown(仅在 blocks 为空时使用) +# ── 三级:内置默认文案(两者都空时兜底,构建永不炸) +layout: + # 名片区(头像/名字/身份语/社交)开关:头像等数据事实源在「主页设置」 + showIdCard: true + +blocks: + - type: prose + width: narrow + body: + zh: | + 写作者 · 读者 · 快门按压者,拥抱开源。 + + 这里是我三条轨道的公开工作台:**读过的书**留下评注,**写过的代码**进项目墙,**按下的快门**收入图库。 + + 所有内容都有一个明确去处: + + - 长文沉淀在文章与书架 + - 短句与随手拍落在瞬间 + - 影像收进图库,配一点 EXIF 与一句策展 + + 本站本身即开源,构建与发布全链都在仓里。 + 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** I pressed 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 a bit of EXIF and one line of curation + + This site is itself open source — the full build and deploy pipeline lives in the repo. + + - type: tracks + title: + zh: 兴趣版图 + en: Interests + items: + - icon: book + name: + zh: 阅读 + en: Reading + desc: + zh: 文学与戏剧为主,读到哪写到哪。 + en: Mostly literature and drama; I write as I read. + - icon: terminal + name: + zh: 技术 + en: Tech + desc: + zh: Linux、容器与自托管,把家里的小机器折腾成顺手的工具。 + en: Linux, containers, and self-hosting — turning the little machines at home into handy tools. + - icon: camera + name: + zh: 摄影 + en: Photography + desc: + zh: 街拍与山野,随身一台无反,在等光的间隙按快门。 + en: Street and landscapes with a mirrorless camera, pressing the shutter while waiting for the light. + + - type: prose + title: + zh: 本站如何构建 + en: How this site is built + body: + zh: 页面由 Astro 静态生成,不含运行时框架 JS;内容住在自托管 gitea——本地编辑器与 gitea 网页写的是同一个 git 仓库,push 之后由 homelab 自动构建发布;评论走自托管 Artalk。页脚 built from 一行链到本次构建对应的 commit,构建过程透明可追溯。 + en: Pages are statically generated by Astro with no runtime framework JS. Content lives on a self-hosted gitea — my local editor and the gitea web UI write to the same git repository, and each push is built and published automatically by the homelab. Comments run on self-hosted Artalk. The built-from line in the footer links to the commit behind this build, keeping the process transparent and traceable. + +# ---- 二级回退(blocks 为空时使用)---- intro: zh: | 写作者 · 读者 · 快门按压者,拥抱开源。 这里是我三条轨道的公开工作台:**读过的书**留下评注,**写过的代码**进项目墙,**按下的快门**收入图库。 - - 所有内容都有一个明确去处: - - - 长文沉淀在文章与书架 - - 短句与随手拍落在瞬间 - - 影像收进图库,配一点 EXIF 与一句策展 - - 本站本身即开源,构建与发布全链都在仓里。 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** I pressed 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 a bit of EXIF and one line of curation - - This site is itself open source — the full build and deploy pipeline lives in the repo. +# 页面级排版覆盖(层 2) +typography: + font: font-huiwen + accent: c-red diff --git a/public/admin/config.yml b/public/admin/config.yml index 36e1ec3..b49b7dd 100644 --- a/public/admin/config.yml +++ b/public/admin/config.yml @@ -119,6 +119,23 @@ collections: fields: - label: 身份语 name: tagline + - label: 页面排版覆盖(可选) + name: typography + widget: object + required: false + hint: 本页正文默认字体 / 强调色 / 字号缩放(0.8–1.25);不填用全站默认,只影响本页 + fields: + - label: 正文字体类 + name: font + widget: select + required: false + options: [font-serif, font-kai, font-sans, font-mono, font-song, font-kai-sys, font-hei-sys, font-fangsong, font-huiwen, font-kuaile] + - label: 强调色类 + name: accent + widget: select + required: false + options: [ink-accent, ink-accent-soft, c-blue, c-blue-soft, c-teal, c-teal-soft, c-green, c-green-soft, c-yellow, c-yellow-soft, c-orange, c-orange-soft, c-red, c-red-soft, c-pink, c-pink-soft, c-purple, c-purple-soft] + - { label: 字号缩放, name: scale, widget: number, required: false, value_type: float, min: 0.8, max: 1.25, step: 0.05 } widget: object fields: - { label: 中文, name: zh, widget: string, required: false } @@ -183,20 +200,294 @@ collections: - name: about label: 关于页 file: content/site/about.yaml + # 块内图片上传落 content/site/assets/,yaml 写 ./assets/xxx(Astro 图片管线) + media_folder: content/site/assets + public_folder: ./assets fields: - - label: 正文(markdown,多段) + - label: 页面块(拖拽排序;每块可折叠) + name: blocks + widget: list + typeKey: type + required: false + summary: '{{fields.type}}' + hint: '点「+ 添加」选块型。关掉某块用该块内的「显示」开关' + types: + - label: 正文(markdown) + name: prose + widget: object + fields: + - { label: 显示, name: enabled, widget: boolean, required: false, default: true } + - label: 小标题(可选) + name: title + widget: object + required: false + fields: + - { label: 中文, name: zh, widget: string, required: false } + - { label: 英文, name: en, widget: string, required: false } + - label: 正文(markdown) + name: body + widget: object + fields: + - { label: 中文, name: zh, widget: markdown, required: false } + - { label: 英文, name: en, widget: markdown, required: false } + - label: 宽度 + name: width + widget: select + required: false + default: narrow + options: [{ label: 版心宽度, value: narrow }, { label: 通栏, value: full }] + - label: 版图(图标+名称+描述) + name: tracks + widget: object + fields: + - { label: 显示, name: enabled, widget: boolean, required: false, default: true } + - label: 标题 + name: title + widget: object + fields: + - { label: 中文, name: zh, widget: string, required: false } + - { label: 英文, name: en, widget: string, required: false } + - label: 条目 + name: items + widget: list + summary: '{{fields.name.zh}}' + fields: + - label: 图标 + name: icon + widget: select + required: false + options: [book, terminal, camera, code, git, rss, email, github, bilibili, x, star, link] + hint: 白名单外或留空 → 显示首字占位 + - label: 名称 + name: name + widget: object + fields: + - { label: 中文, name: zh, widget: string, required: false } + - { label: 英文, name: en, widget: string, required: false } + - label: 描述 + name: desc + widget: object + required: false + fields: + - { label: 中文, name: zh, widget: text, required: false } + - { label: 英文, name: en, widget: text, required: false } + - label: 卡片(2/3 列) + name: cards + widget: object + fields: + - { label: 显示, name: enabled, widget: boolean, required: false, default: true } + - label: 标题(可选) + name: title + widget: object + required: false + fields: + - { label: 中文, name: zh, widget: string, required: false } + - { label: 英文, name: en, widget: string, required: false } + - { label: 列数, name: columns, widget: number, required: false, default: 3, min: 2, max: 3, value_type: int } + - label: 卡片条目 + name: items + widget: list + summary: '{{fields.title.zh}}' + fields: + - label: 标题 + name: title + widget: object + fields: + - { label: 中文, name: zh, widget: string, required: false } + - { label: 英文, name: en, widget: string, required: false } + - label: 描述 + name: desc + widget: object + required: false + fields: + - { label: 中文, name: zh, widget: text, required: false } + - { label: 英文, name: en, widget: text, required: false } + - { label: 链接(可选), name: href, widget: string, required: false } + - label: 角标(可选) + name: tag + widget: object + required: false + fields: + - { label: 中文, name: zh, widget: string, required: false } + - { label: 英文, name: en, widget: string, required: false } + - label: 时间线 + name: timeline + widget: object + fields: + - { label: 显示, name: enabled, widget: boolean, required: false, default: true } + - label: 标题(可选) + name: title + widget: object + required: false + fields: + - { label: 中文, name: zh, widget: string, required: false } + - { label: 英文, name: en, widget: string, required: false } + - label: 条目 + name: items + widget: list + summary: '{{fields.time}} · {{fields.title.zh}}' + fields: + - { label: 时间, name: time, widget: string, hint: '自由文本,如 2024 — 2026' } + - label: 标题 + name: title + widget: object + fields: + - { label: 中文, name: zh, widget: string, required: false } + - { label: 英文, name: en, widget: string, required: false } + - label: 描述(可选) + name: desc + widget: object + required: false + fields: + - { label: 中文, name: zh, widget: text, required: false } + - { label: 英文, name: en, widget: text, required: false } + - label: 引文 + name: quote + widget: object + fields: + - { label: 显示, name: enabled, widget: boolean, required: false, default: true } + - label: 引文 + name: text + widget: object + fields: + - { label: 中文, name: zh, widget: text, required: false } + - { label: 英文, name: en, widget: text, required: false } + - label: 出处(可选) + name: cite + widget: object + required: false + fields: + - { label: 中文, name: zh, widget: string, required: false } + - { label: 英文, name: en, widget: string, required: false } + - label: 图片 + name: figure + widget: object + fields: + - { label: 显示, name: enabled, widget: boolean, required: false, default: true } + - { label: 图片(必填,请用后台上传), name: image, widget: image, hint: '禁止填外链:只能上传(落 content/site/assets/),走 Astro 图片管线' } + - { label: 替代文本(无障碍), name: alt, widget: string, required: false } + - label: 图注(可选) + name: caption + widget: object + required: false + fields: + - { label: 中文, name: zh, widget: string, required: false } + - { label: 英文, name: en, widget: string, required: false } + - label: 链接清单 + name: linklist + widget: object + fields: + - { label: 显示, name: enabled, widget: boolean, required: false, default: true } + - label: 标题(可选) + name: title + widget: object + required: false + fields: + - { label: 中文, name: zh, widget: string, required: false } + - { label: 英文, name: en, widget: string, required: false } + - label: 链接条目 + name: items + widget: list + summary: '{{fields.label.zh}} → {{fields.href}}' + fields: + - label: 名称 + name: label + widget: object + fields: + - { label: 中文, name: zh, widget: string, required: false } + - { label: 英文, name: en, widget: string, required: false } + - { label: 链接, name: href, widget: string } + - label: 备注(可选) + name: note + widget: object + required: false + fields: + - { label: 中文, name: zh, widget: string, required: false } + - { label: 英文, name: en, widget: string, required: false } + - label: 行动按钮 + name: cta + widget: object + fields: + - { label: 显示, name: enabled, widget: boolean, required: false, default: true } + - label: 说明文字 + name: text + widget: object + fields: + - { label: 中文, name: zh, widget: string, required: false } + - { label: 英文, name: en, widget: string, required: false } + - { label: 按钮链接, name: href, widget: string } + - label: 按钮文字 + name: label + widget: object + fields: + - { label: 中文, name: zh, widget: string, required: false } + - { label: 英文, name: en, widget: string, required: false } + - label: 分隔 + name: divider + widget: object + fields: + - { label: 显示, name: enabled, widget: boolean, required: false, default: true } + - label: 样式 + name: style + widget: select + required: false + default: line + options: [{ label: 细分隔线, value: line }, { label: 纯留白, value: space }] + - label: 名片区开关 + name: layout + widget: object + required: false + hint: 关掉则不显示头像/名字/社交(它们的数据在「主页设置」) + fields: + - { label: 显示名片区, name: showIdCard, widget: boolean, required: false, default: true } + - label: 正文回退(blocks 为空时用,markdown,多段) name: intro widget: object - hint: 渲染在「关于」标题下方、名片之上;支持段落/列表/粗斜体/链接;留空用默认文案 + required: false + hint: 仅在「页面块」为空时渲染;与块同时存在时以块为准 fields: - { label: 中文, name: zh, widget: markdown, required: false } - { label: 英文, name: en, widget: markdown, required: false } + - label: 页面排版覆盖(可选) + name: typography + widget: object + required: false + hint: 本页正文默认字体 / 强调色 / 字号缩放(0.8–1.25);不填用全站默认,只影响本页 + fields: + - label: 正文字体类 + name: font + widget: select + required: false + options: [font-serif, font-kai, font-sans, font-mono, font-song, font-kai-sys, font-hei-sys, font-fangsong, font-huiwen, font-kuaile] + - label: 强调色类 + name: accent + widget: select + required: false + options: [ink-accent, ink-accent-soft, c-blue, c-blue-soft, c-teal, c-teal-soft, c-green, c-green-soft, c-yellow, c-yellow-soft, c-orange, c-orange-soft, c-red, c-red-soft, c-pink, c-pink-soft, c-purple, c-purple-soft] + - { label: 字号缩放, name: scale, widget: number, required: false, value_type: float, min: 0.8, max: 1.25, step: 0.05 } - name: opensource label: 开源页 file: content/site/opensource.yaml fields: - label: 叙述(链接前) name: blurb + - label: 页面排版覆盖(可选) + name: typography + widget: object + required: false + hint: 本页正文默认字体 / 强调色 / 字号缩放(0.8–1.25);不填用全站默认,只影响本页 + fields: + - label: 正文字体类 + name: font + widget: select + required: false + options: [font-serif, font-kai, font-sans, font-mono, font-song, font-kai-sys, font-hei-sys, font-fangsong, font-huiwen, font-kuaile] + - label: 强调色类 + name: accent + widget: select + required: false + options: [ink-accent, ink-accent-soft, c-blue, c-blue-soft, c-teal, c-teal-soft, c-green, c-green-soft, c-yellow, c-yellow-soft, c-orange, c-orange-soft, c-red, c-red-soft, c-pink, c-pink-soft, c-purple, c-purple-soft] + - { label: 字号缩放, name: scale, widget: number, required: false, value_type: float, min: 0.8, max: 1.25, step: 0.05 } widget: object fields: - { label: 中文, name: zh, widget: string, required: false } diff --git a/src/components/blocks/BlockRenderer.astro b/src/components/blocks/BlockRenderer.astro new file mode 100644 index 0000000..cc345f0 --- /dev/null +++ b/src/components/blocks/BlockRenderer.astro @@ -0,0 +1,34 @@ +--- +// 块统一渲染器:switch(type) → 子组件;外壳带 data-block 属性(E2E 断言抓手) +import type { ResolvedBlock } from '../../lib/page-blocks'; +import Prose from './Prose.astro'; +import Tracks from './Tracks.astro'; +import Cards from './Cards.astro'; +import Timeline from './Timeline.astro'; +import Quote from './Quote.astro'; +import Figure from './Figure.astro'; +import LinkList from './LinkList.astro'; +import Cta from './Cta.astro'; +import Divider from './Divider.astro'; + +interface Props { + block: ResolvedBlock; +} +const { block } = Astro.props; +--- + +
+ {block.type === 'prose' && } + {block.type === 'tracks' && } + {block.type === 'cards' && } + {block.type === 'timeline' && } + {block.type === 'quote' && } + {block.type === 'figure' &&
} + {block.type === 'linklist' && } + {block.type === 'cta' && } + {block.type === 'divider' && } +
+ + diff --git a/src/components/blocks/Cards.astro b/src/components/blocks/Cards.astro new file mode 100644 index 0000000..42917ec --- /dev/null +++ b/src/components/blocks/Cards.astro @@ -0,0 +1,75 @@ +--- +// cards 块:技能/资源卡(2 或 3 列;复用 .track-row 的边框与内距语言) +import type { ResolvedBlock } from '../../lib/page-blocks'; + +interface Props { + block: Extract; +} +const { block } = Astro.props; +--- + +{block.title &&

{block.title}

} + + + diff --git a/src/components/blocks/Cta.astro b/src/components/blocks/Cta.astro new file mode 100644 index 0000000..76825ed --- /dev/null +++ b/src/components/blocks/Cta.astro @@ -0,0 +1,48 @@ +--- +// cta 块:单按钮行动点(克制:同屏最多 1 个,由编辑纪律保证) +import type { ResolvedBlock } from '../../lib/page-blocks'; + +interface Props { + block: Extract; +} +const { block } = Astro.props; +const isExternal = block.href.startsWith('http'); +--- + +
+ {block.text &&

{block.text}

} + + {block.label} + +
+ + diff --git a/src/components/blocks/Divider.astro b/src/components/blocks/Divider.astro new file mode 100644 index 0000000..4e1bce0 --- /dev/null +++ b/src/components/blocks/Divider.astro @@ -0,0 +1,20 @@ +--- +// divider 块:节奏控制(line = hairline 分隔;space = 纯留白) +import type { ResolvedBlock } from '../../lib/page-blocks'; + +interface Props { + block: Extract; +} +const { block } = Astro.props; +--- + +{block.style === 'space' ? :
} + + diff --git a/src/components/blocks/Figure.astro b/src/components/blocks/Figure.astro new file mode 100644 index 0000000..c5df9ab --- /dev/null +++ b/src/components/blocks/Figure.astro @@ -0,0 +1,38 @@ +--- +// figure 块:图片(Astro 图片管线,宽度自适应;caption 图注) +import { Image } from 'astro:assets'; +import type { ResolvedBlock } from '../../lib/page-blocks'; + +interface Props { + block: Extract; +} +const { block } = Astro.props; +--- + +
+ {block.alt} + {block.caption &&
{block.caption}
} +
+ + diff --git a/src/components/blocks/LinkList.astro b/src/components/blocks/LinkList.astro new file mode 100644 index 0000000..8729a18 --- /dev/null +++ b/src/components/blocks/LinkList.astro @@ -0,0 +1,58 @@ +--- +// linklist 块:外链清单(比裸 markdown 链接可控:带 note 与统一行距) +import type { ResolvedBlock } from '../../lib/page-blocks'; + +interface Props { + block: Extract; +} +const { block } = Astro.props; + +const isExternal = (href: string) => href.startsWith('http'); +--- + +{block.title &&

{block.title}

} + + + diff --git a/src/components/blocks/Prose.astro b/src/components/blocks/Prose.astro new file mode 100644 index 0000000..a3eb237 --- /dev/null +++ b/src/components/blocks/Prose.astro @@ -0,0 +1,46 @@ +--- +// prose 块:自由正文(markdown 已由读取层渲染为 HTML,净化在 site-content 的插件链) +import type { ResolvedBlock } from '../../lib/page-blocks'; + +interface Props { + block: Extract; +} +const { block } = Astro.props; +--- + +{block.title &&

{block.title}

} +
+ + diff --git a/src/components/blocks/Quote.astro b/src/components/blocks/Quote.astro new file mode 100644 index 0000000..3d13e0e --- /dev/null +++ b/src/components/blocks/Quote.astro @@ -0,0 +1,36 @@ +--- +// quote 块:引文(复用 .quote-line 语言 + 楷体) +import type { ResolvedBlock } from '../../lib/page-blocks'; + +interface Props { + block: Extract; +} +const { block } = Astro.props; +--- + +
+

{block.text}

+ {block.cite && — {block.cite}} +
+ + diff --git a/src/components/blocks/Timeline.astro b/src/components/blocks/Timeline.astro new file mode 100644 index 0000000..17ed981 --- /dev/null +++ b/src/components/blocks/Timeline.astro @@ -0,0 +1,66 @@ +--- +// timeline 块:经历/项目时间线(竖线语言,仅用既有 token) +import type { ResolvedBlock } from '../../lib/page-blocks'; + +interface Props { + block: Extract; +} +const { block } = Astro.props; +--- + +{block.title &&

{block.title}

} +
    + {block.items.map((item) => ( +
  1. + {item.time} +
    + {item.title} + {item.desc &&

    {item.desc}

    } +
    +
  2. + ))} +
+ + diff --git a/src/components/blocks/Tracks.astro b/src/components/blocks/Tracks.astro new file mode 100644 index 0000000..3a900ea --- /dev/null +++ b/src/components/blocks/Tracks.astro @@ -0,0 +1,70 @@ +--- +// tracks 块:兴趣/能力版图(复用原 AboutPage 的 .track-row 视觉语言) +import type { ResolvedBlock } from '../../lib/page-blocks'; +import { iconPath } from '../../lib/icons'; + +interface Props { + block: Extract; +} +const { block } = Astro.props; +--- + +

{block.title}

+
    + {block.items.map((item) => ( +
  • + +
    + {item.name} + {item.desc &&

    {item.desc}

    } +
    +
  • + ))} +
+ + diff --git a/src/components/pages/AboutPage.astro b/src/components/pages/AboutPage.astro index 00835f0..dbc1af6 100644 --- a/src/components/pages/AboutPage.astro +++ b/src/components/pages/AboutPage.astro @@ -3,70 +3,86 @@ import BaseLayout from '../../layouts/BaseLayout.astro'; import IdCard from '../../components/IdCard.astro'; import { t } from '../../i18n/ui'; import type { Locale } from '../../lib/locale'; -import { aboutIntroMarkdown, renderMarkdown } from '../../lib/site-content'; +import { aboutIntroMarkdown, pageTypography, renderMarkdown } from '../../lib/site-content'; +import { getAboutPage } from '../../lib/page-blocks'; +import BlockRenderer from '../blocks/BlockRenderer.astro'; // /en/ 侧由 T4 薄页传 locale='en';zh 路由无人传参,默认 zh 零回归 interface Props { locale?: Locale; } const { locale = 'zh' } = Astro.props; -// 身份陈述段改由 content/site/about.yaml 供给(多段 markdown,后台可改),缺值回落默认 -const introHtml = await renderMarkdown(await aboutIntroMarkdown(locale)); +// 三级回落(零回归):blocks 驱动 → intro markdown → 内置默认文案 +const { blocks, showIdCard } = await getAboutPage(locale); +const introHtml = + blocks.length === 0 ? await renderMarkdown(await aboutIntroMarkdown(locale)) : ''; +const typo = await pageTypography('siteAbout', 'about'); ---

{t('about_title', locale)}

-
+
+ {blocks.length > 0 ? ( + <> + {blocks.map((block) => )} + {showIdCard && } + + ) : ( + <> +
- + -
-

{t('about_tracks_title', locale)}

-
    -
  • - -
    - {t('about_track_reading_name', locale)} -

    {t('about_track_reading_desc', locale)}

    -
    -
  • -
  • - -
    - {t('about_track_tech_name', locale)} -

    {t('about_track_tech_desc', locale)}

    -
    -
  • -
  • - -
    - {t('about_track_photo_name', locale)} -

    {t('about_track_photo_desc', locale)}

    -
    -
  • -
-
+
+

{t('about_tracks_title', locale)}

+
    +
  • + +
    + {t('about_track_reading_name', locale)} +

    {t('about_track_reading_desc', locale)}

    +
    +
  • +
  • + +
    + {t('about_track_tech_name', locale)} +

    {t('about_track_tech_desc', locale)}

    +
    +
  • +
  • + +
    + {t('about_track_photo_name', locale)} +

    {t('about_track_photo_desc', locale)}

    +
    +
  • +
+
-
-

{t('about_build_title', locale)}

-

{t('about_build_desc', locale)}

-
+
+

{t('about_build_title', locale)}

+

{t('about_build_desc', locale)}

+
+ + )} +