fix(cms): 页面文案可编辑性修复(结构压平)+ 样式旋钮退役 + favicon 直传

用户实测「瞬间/书架 说明文字」在后台无法编辑。根因:Sveltia 对「可选对象
嵌套可选对象」的子编辑器渲染缺陷线(sveltia-cms #446/#654,双层嵌套正踩
雷区)——压平为单层可选对象 + markdown 直挂,与已验证可编辑的主页/关于
页同构。顺应用户第二问:样式旋钮(字号/颜色/字体三档)整体退役,排版
并入内联类(Insert▸排版,预览直接可见),视觉基线由 .page-note scoped
样式承载(13px/ink-4,与原默认一致零回归);favicon 从贴路径 string
改为 image 直传(落 public/icons/,yaml 存 /icons/…)。

验证: pnpm astro build exit 0;config-check 226 字段全过;content-lint 53 页 0/0/0;E2E 61/61

Spec: none (用户实测报告的后台编辑 bug 紧急修复)

Co-authored-by: GLM-5.3
This commit is contained in:
wait4xandGLM-5.3 committed 2026-11-05 11:42:40 +08:00
1 parent cf331ccf3d
commit eb5e3f851e
6 files changed
+37 -132

No files matched your search

+7 -17
View File
@@ -1,21 +1,11 @@
# 页面标题下的说明文案(瞬间 / 书架):文字、字号、颜色、字体都在这里改。 # 页面标题下的说明文案(瞬间 / 书架):纯 markdown,后台「页面文案」直接改;
# 缺省即回落内置默认,删字段不会破版。 # 排版(字体/颜色)用内联类,与文章正文同一套(编辑器 Insert ▸ 排版 可视化选)。
# 说明:color 取 tokens 里的灰阶层级(ink-1..ink-4);font 取排版类白名单里的字体类。 # 删字段即回落内置默认,不破版。
moments: moments:
note: note_zh: 零碎的记录:随手拍、一句话感想、开源动态。
zh: 零碎的记录:随手拍、一句话感想、开源动态。 note_en: 'Fragmented notes: casual shots, one-liners, and open-source updates.'
en: 'Fragmented notes: casual shots, one-liners, and open-source updates.'
style:
size: 13
color: ink-4
font: font-sans
books: books:
note: note_zh: 点一行展开封面与简介 · 置顶的书默认展开
zh: 点一行展开封面与简介 · 置顶的书默认展开 note_en: 'Tap a row for the cover and blurb · pinned books start open'
en: 'Tap a row for the cover and blurb · pinned books start open'
style:
size: 13
color: ink-4
font: font-sans # 与改造前一致;想用楷体改成 font-kai
+12 -56
View File
@@ -154,69 +154,22 @@ collections:
label: 瞬间 / 书架 标题下的说明 label: 瞬间 / 书架 标题下的说明
file: content/site/pages.yaml file: content/site/pages.yaml
format: yaml format: yaml
hint: 这两页大标题下面那一行字,文字与样式都在这里改 hint: 这两页大标题下面那一行字的文字;字体/颜色用编辑器 Insert ▸ 排版 内联标注(预览直接可见)
fields: fields:
- label: 瞬间页 - label: 瞬间页说明
name: moments name: moments
widget: object widget: object
required: false required: false
fields: fields:
- label: 说明文字 - { label: 中文, name: note_zh, widget: markdown, required: false, hint: '一行小字;排版(字体/颜色)选中文字 → Insert ▸ 排版' }
name: note - { label: English, name: note_en, widget: markdown, required: false }
widget: object - label: 书架页说明
required: false
hint: 支持 markdown 与排版类(字体/颜色)
fields:
- { label: 中文, name: zh, widget: markdown, required: false }
- { label: English, name: en, widget: markdown, required: false }
- label: 样式
name: style
widget: object
required: false
hint: 留空用全站默认;越界值会被自动夹取(不会破版)
fields:
- { label: 字号(px,11–16), name: size, widget: number, required: false, value_type: int, min: 11, max: 16 }
- label: 颜色
name: color
widget: select
required: false
hint: 只开放灰阶层级,避免正文被染色
options: [ink-1, ink-2, ink-3, ink-4]
- label: 字体
name: font
widget: select
required: false
options: [font-sans, font-serif, font-kai, font-huiwen, font-kuaile, font-xiaowei, font-huangyou, font-mashanzheng, font-liujianmaocao, font-smiley, font-neoxihei]
- label: 书架页
name: books name: books
widget: object widget: object
required: false required: false
fields: fields:
- label: 说明文字 - { label: 中文, name: note_zh, widget: markdown, required: false, hint: '一行小字;排版(字体/颜色)选中文字 → Insert ▸ 排版' }
name: note - { label: English, name: note_en, widget: markdown, required: false }
widget: object
required: false
hint: 支持 markdown 与排版类(字体/颜色)
fields:
- { label: 中文, name: zh, widget: markdown, required: false }
- { label: English, name: en, widget: markdown, required: false }
- label: 样式
name: style
widget: object
required: false
hint: 留空用全站默认;越界值会被自动夹取(不会破版)
fields:
- { label: 字号(px,11–16), name: size, widget: number, required: false, value_type: int, min: 11, max: 16 }
- label: 颜色
name: color
widget: select
required: false
options: [ink-1, ink-2, ink-3, ink-4]
- label: 字体
name: font
widget: select
required: false
options: [font-sans, font-serif, font-kai, font-huiwen, font-kuaile, font-xiaowei, font-huangyou, font-mashanzheng, font-liujianmaocao, font-smiley, font-neoxihei]
- name: home - name: home
label: 主页设置 label: 主页设置
file: content/site/home.yaml file: content/site/home.yaml
@@ -309,9 +262,12 @@ collections:
hint: '留空 = 内置渐变占位;上传后自动优化为 60px WebP' hint: '留空 = 内置渐变占位;上传后自动优化为 60px WebP'
- label: 站点图标 favicon - label: 站点图标 favicon
name: favicon name: favicon
widget: string widget: image
required: false required: false
hint: 'public 下的路径,如 /icons/my.ico 或 /icons/my.svg;留空回退 /favicon.svg。经「图标」媒体库上传' # media_folder 按 file 集合语义相对 content/site/ 解析(写 content/site/assets 会嵌套翻倍——5l 实测教训)
media_folder: ../../public/icons
public_folder: /icons
hint: '直接上传 .svg/.ico/.png(建议 64–512px 方图);留空回退 /favicon.svg'
- label: 社交链接(顺序即展示顺序) - label: 社交链接(顺序即展示顺序)
name: socials name: socials
widget: list widget: list
+1 -1
View File
@@ -78,7 +78,7 @@ const L = {
{pinnedCount > 0 && <span class="pinned-count mono">{pinnedCount} {L.pinned}</span>} {pinnedCount > 0 && <span class="pinned-count mono">{pinnedCount} {L.pinned}</span>}
</h1> </h1>
<!-- 标题下说明:由 content/site/pages.yaml 的 books.note/style 驱动 --> <!-- 标题下说明:由 content/site/pages.yaml 的 books.note/style 驱动 -->
<p class:list={['page-note', note.className]} style={note.style} set:html={note.html} /> <p class="page-note" set:html={note.html} />
<ul class="books"> <ul class="books">
{items.map((b) => ( {items.map((b) => (
+1 -1
View File
@@ -41,7 +41,7 @@ const iso = (d: Date) => d.toISOString();
<BaseLayout title={`${t('moments_title', locale)} · wait4x`}> <BaseLayout title={`${t('moments_title', locale)} · wait4x`}>
<h1 class="page-title">{t('moments_title', locale)}</h1> <h1 class="page-title">{t('moments_title', locale)}</h1>
<!-- 标题下说明:文案/字号/颜色/字体均由后台「页面文案 → 页面说明」控制(site-content.pageNote) --> <!-- 标题下说明:文案/字号/颜色/字体均由后台「页面文案 → 页面说明」控制(site-content.pageNote) -->
<p class:list={['page-note', note.className]} style={note.style} set:html={note.html} /> <p class="page-note" set:html={note.html} />
<!-- 整页计数(Phase 5h T4):这是本页唯一的计数点(page 模式); <!-- 整页计数(Phase 5h T4):这是本页唯一的计数点(page 模式);
每条瞬间卡片底部的徽标是 readonly,读者开评论不会虚增浏览数 --> 每条瞬间卡片底部的徽标是 readonly,读者开评论不会虚增浏览数 -->
<ViewMeta class="mom-page-meta" pageKey={locale === 'en' ? '/en/moments/' : '/moments/'} {locale} variant="chip" /> <ViewMeta class="mom-page-meta" pageKey={locale === 'en' ? '/en/moments/' : '/moments/'} {locale} variant="chip" />
+7 -9
View File
@@ -163,16 +163,14 @@ const _exifShapeOk: _PhotoExifOut extends PhotoExif
: never : never
: never = true; : never = true;
/** 页面说明文案的字段形状(瞬间 / 书架共用):文本双语 + 三档样式 */ /**
* 页面说明文案的字段形状(瞬间 / 书架共用):扁平双语。
* 样式旋钮(size/color/font)已退役——排版统一走内联类(Insert▸排版),
* 也避开了 Sveltia「可选对象嵌可选对象不渲染子编辑器」的缺陷结构(#446/#654)。
*/
const pageNoteSchema = z.object({ const pageNoteSchema = z.object({
note: bilingual.optional(), note_zh: z.string().optional(),
style: z note_en: z.string().optional(),
.object({
size: z.number().optional(),
color: z.string().optional(),
font: z.string().optional(),
})
.optional(),
}); });
export const collections = { export const collections = {
+9 -48
View File
@@ -164,23 +164,11 @@ export async function renderRich(md: string, opts: { inline?: boolean } = {}): P
return html.replace(/^\s*<p>/i, '').replace(/<\/p>\s*$/i, '').trim(); return html.replace(/^\s*<p>/i, '').replace(/<\/p>\s*$/i, '').trim();
} }
export interface PageNoteStyle {
/** 像素字号(已夹取到 11–16) */
size: number;
/** 已过白名单的 ink 档位(ink-1..ink-4) */
color: string;
/** 已过白名单的排版字体类(font-sans 等) */
font: string;
}
export interface PageNote { export interface PageNote {
/** 已渲染的行内 HTML(note 走 renderRich inline,避免 <p> 套 <p>) */ /** 已渲染的行内 HTML(note 走 renderRich inline,避免 <p> 套 <p>) */
html: string; html: string;
/** 直接可挂的行内样式串(字号 + 颜色) */ /** 挂到元素上的类名(基础 .page-note;字体/颜色由文案内联排版类表达) */
style: string;
/** 字体类名(白名单内),与 style 一起挂到元素上 */
className: string; className: string;
resolved: PageNoteStyle;
} }
/** 标题下说明文案的默认值(= 改造前硬编码的两句;删 yaml 字段即回到这里) */ /** 标题下说明文案的默认值(= 改造前硬编码的两句;删 yaml 字段即回到这里) */
@@ -195,20 +183,12 @@ const PAGE_NOTE_DEFAULTS = {
}, },
} as const; } as const;
/** 颜色白名单:只认灰阶 ink 档位(后台可填任意值,但绝不能把正文染成任意颜色/破对比度) */
const NOTE_COLORS = ['ink-1', 'ink-2', 'ink-3', 'ink-4'] as const;
/** 字号夹取:<11px 读不清、>16px 会盖过正文层级 */
const NOTE_SIZE_MIN = 11;
const NOTE_SIZE_MAX = 16;
const NOTE_SIZE_DEFAULT = 13;
/** /**
* 页面标题下说明文案(瞬间 / 书架)+ 可选样式。 * 页面标题下说明文案(瞬间 / 书架):纯 markdown,排版用内联类(与正文同一套白名单)。
* 后台可改文字与字号/颜色/字体;样式全部经白名单校验后回落,非法值不会破版: * 样式旋钮(size/color/font)已退役——视觉基线(13px / ink-4 / sans)由两页
* · size 夹取 11–16(缺省 13) * .page-note 的 scoped 样式承载;要变字体/颜色就在文案里写内联排版类(Insert▸排版),
* · color 只认 ink-1..ink-4(其它回落 ink-4) * 后台预览直接可见。结构同时压平为 note_zh/note_en,避开 Sveltia
* · font 只认排版类白名单(其它回落 font-sans) * 「可选对象嵌可选对象不渲染子编辑器」的缺陷结构(#446/#654)。
* 无 yaml 字段时文字回落内置默认,与改造前逐字一致(零回归)。
*/ */
export async function pageNote( export async function pageNote(
page: 'moments' | 'books', page: 'moments' | 'books',
@@ -216,29 +196,10 @@ export async function pageNote(
): Promise<PageNote> { ): Promise<PageNote> {
const entry = await getEntry('sitePages', 'pages'); const entry = await getEntry('sitePages', 'pages');
const data = entry?.data?.[page]; const data = entry?.data?.[page];
const text = pickBi(data?.note, PAGE_NOTE_DEFAULTS[page], locale); const fallback = PAGE_NOTE_DEFAULTS[page];
const text = (locale === 'en' ? data?.note_en : data?.note_zh) || fallback[locale];
const html = await renderRich(text, { inline: true }); const html = await renderRich(text, { inline: true });
return { html, className: 'page-note' };
const rawSize = data?.style?.size;
const size =
typeof rawSize === 'number' && Number.isFinite(rawSize)
? Math.min(NOTE_SIZE_MAX, Math.max(NOTE_SIZE_MIN, Math.trunc(rawSize)))
: NOTE_SIZE_DEFAULT;
const rawColor = data?.style?.color?.trim() ?? '';
const color = (NOTE_COLORS as readonly string[]).includes(rawColor) ? rawColor : 'ink-4';
const rawFont = data?.style?.font?.trim() ?? '';
const font = ALLOWED_PAGE_FONTS.has(rawFont) ? rawFont : 'font-sans';
// 字体必须写成 inline font-family:global.css 的 .font-* 类都挂在富文本容器作用域内
// (.prose-*/.rich/.mom-text/.prose-body),裸 .page-note 拿不到,加类无效(实测)
return {
html,
style: `font-size:${size}px; color: var(--${color}); font-family: ${fontVarFor(font)};`,
className: font,
resolved: { size, color, font },
};
} }
/** /**