Files
blog/src/layouts/BaseLayout.astro
T
wait4xandGLM-5.3 8debd99e61 feat(theme-home): 样式即数据 + 首页抽组件去重(5g T3/T4)
【T3 样式即数据】
- content/site/theme.yaml 九组白名单旋钮(accent 明暗两值 / radius_img /
  text_h1-h3 / prose_size_reading|tech / leading_reading|tech /
  space_3-6 / flow_p 与 flow_h2_post / dur);--measure、断点、纸墨、
  code 配色按方案 §3.6 红线不开放
- src/lib/theme.ts:getTheme() 钳制 + 单调性校验(h1>h2>h3、
  space3<4<5<6 任一不满足整组回落默认);themeVarsCss() 产出
  `:root:root{…}` 与 `[data-theme="dark"]:root:root{…}`,靠特异性压过
  tokens 的 :root,不依赖注入顺序
- BaseLayout head 内联 `<style is:inline set:html={themeStyle}>`:构建期
  静态、运行时零 JS 零请求
- scripts/build-perf/contrast-check.mjs 读产物断言正文/强调 × 亮暗四项
  ≥4.5:1,不达标 exit 1;build.sh 守卫挂载([ -f ] 守卫先行)
  实测:默认 12.40/10.53/6.30/7.06 全过;负例 accent #cccccc → 1.61:1 exit 1
  两坑入档:tokens 的 --bg 是 3 位简写需归一化;选 tokens CSS 要用
  `--bg:#` 定义断言(includes('--bg') 会误命中 var(--bg-soft) 引用)
- 后台 theme 单例 17 字段全中文 hint + 取值范围(分组约束写进 hint)

【T4 首页去重与细化】
- 新增 src/components/pages/HomePage.astro 承接全部区块(名片区富文本
  tagline/subtitle/社交、公告、文章列表、最新影像、热力图、项目墙、
  心跳),locale 一统;en 轨吃 postsEn 译题映射、图库链走 /en/gallery/,
  心跳仍仅 zh(零回归)
- 两 index.astro 缩成 4 行薄壳,双份漂移隐患消除
- home.yaml 三字段:projectsLimit(1-99 夹取,缺省 6)、announcement
  (双语 markdown 走 renderRich,空则不渲染)、postsTracks(三轨开关,
  在 HomePage 内过滤而非改 PostList,守边界)

验证: 构建 48 页 exit 0;contrast-check 四项 PASS;E2E 37/37(快照口径);
      实测 theme 变量内联、公告 markdown 粗体渲染、projectsLimit 限流、
      photo 轨关闭生效;验收数据已还原

Spec: 2026-11-02_blog-5g-pages (T3/T4)

Co-authored-by: GLM-5.3
2026-11-02 18:58:11 +08:00

124 lines
6.3 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
// 基础布局:字体按轨瘦身 + 非阻塞加载(Lighthouse P0:614 个 @font-face
// 全量入关曾致 Perf 62);变字体二轮——静态三档合并 wght 轴单文件
// (CSS 345→115KB,主线程解析减半,FCP 5.7s 主因见 lh-local 审计)
import sansVar from '@fontsource-variable/noto-sans-sc/index.css?url';
import mono400 from '@fontsource/jetbrains-mono/400.css?url';
import mono700 from '@fontsource/jetbrains-mono/700.css?url';
import kuaile from '@fontsource/zcool-kuaile/index.css?url';
import serifVar from '@fontsource-variable/noto-serif-sc/index.css?url';
import kai from 'lxgw-wenkai-screen-webfont/lxgwwenkaiscreen.css?url';
import '../styles/global.css';
import Header from '../components/Header.astro';
import Footer from '../components/Footer.astro';
import ThemeScript from '../components/ThemeScript.astro';
import Keyboard from '../components/geek/Keyboard.astro';
import { localeOf, localePath } from '../lib/locale';
import { siteFavicon } from '../lib/site-content';
import SearchDialog from '../components/SearchDialog.astro';
import BuildInfo from '../components/geek/BuildInfo.astro';
import { ClientRouter } from 'astro:transitions';
import { themeCss } from '../lib/theme';
interface Props {
title: string;
description?: string;
/** 正文轨:reading 书评纸墨 / tech 技术(body 级字体氛围) */
track?: 'reading' | 'tech';
}
const { title, description = 'wait4x 的博客:阅读 · 技术 · 摄影', track = 'tech' } = Astro.props;
// hreflang 双向:zh 无前缀 ↔ /en/;x-default 指向 zh(中文是源语言)
const rawPath = Astro.url.pathname.replace(/\/index\.html$/, '');
const zhPath = localeOf(rawPath) === 'en' ? localePath(rawPath, 'zh') : rawPath;
const enPath = localePath(rawPath, 'en');
const withSlash = (p: string) => (p.endsWith('/') ? p : `${p}/`);
// 站点图标可配置(后台「主页设置 → 站点图标」):缺省回退 /favicon.svg
const favicon = await siteFavicon();
// 自指 canonical(T2):SEO 权重收敛到当前页自身,与 hreflang 组合覆盖双轨
const canonicalUrl = new URL(withSlash(rawPath), Astro.site).href;
const themeStyle = await themeCss();
---
<!doctype html>
<html lang="zh" data-track={track}>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<link rel="icon" type={favicon.type || undefined} href={favicon.href} />
<meta name="generator" content={Astro.generator} />
<meta name="description" content={description} />
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)" />
<meta name="theme-color" content="#191d23" media="(prefers-color-scheme: dark)" />
<title>{title}</title>
<ThemeScript />
<script is:inline>
// 背景辉光跟随(一次性注册:document 级监听 × raf 节流,VT 换页存活);
// pointer:fine 且非 reduced-motion 才启用,其余设备辉光层保持隐藏
if (matchMedia('(pointer: fine)').matches && !matchMedia('(prefers-reduced-motion: reduce)').matches) {
let raf = 0, mx = 50, my = 30;
document.addEventListener('mousemove', (e) => {
mx = (e.clientX / innerWidth) * 100;
my = (e.clientY / innerHeight) * 100;
if (!raf) raf = requestAnimationFrame(() => {
raf = 0;
document.documentElement.style.setProperty('--mx', mx + '%');
document.documentElement.style.setProperty('--my', my + '%');
});
}, { passive: true });
}
</script>
<ClientRouter />
<!-- 非阻塞字体 CSS:preload+onload 回切(Lighthouse P0-1:渲染阻塞 708KB 元凶) -->
<link rel="preload" as="style" href={sansVar} onload="this.rel='stylesheet'" />
<link rel="preload" as="style" href={mono400} onload="this.rel='stylesheet'" />
<link rel="preload" as="style" href={mono700} onload="this.rel='stylesheet'" />
{track === 'reading' && (
<>
<link rel="preload" as="style" href={serifVar} onload="this.rel='stylesheet'" />
<link rel="preload" as="style" href={kai} onload="this.rel='stylesheet'" />
<link rel="preload" as="style" href={kuaile} onload="this.rel='stylesheet'" />
<!-- 汇文明朝(OFL-1.1,文风开源切片包 339 片,按 unicode-range 按需拉取) -->
<link rel="preload" as="style" href="/fonts/subset/hwmc/fonts.css" onload="this.rel='stylesheet'" />
</>
)}
<noscript>
<link rel="stylesheet" href={sansVar} />
<link rel="stylesheet" href={mono400} />
{track === 'reading' && <link rel="stylesheet" href={serifVar} />}
</noscript>
<!-- PWA:manifest + 双方案 theme-color(明暗随系统)+ iOS 图标 -->
<link rel="manifest" href="/manifest.webmanifest" />
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#fafafa" />
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#14161a" />
<link rel="apple-touch-icon" href="/icons/icon-192.png" />
<script is:inline>
// SW 注册一次性守卫:View Transitions 下内联脚本不重跑,双保险避免重复注册
if ('serviceWorker' in navigator && !navigator.serviceWorker.controller) {
addEventListener('load', () => navigator.serviceWorker.register('/sw.js'));
}
</script>
<link rel="canonical" href={canonicalUrl} />
<link rel="alternate" hreflang="zh" href={new URL(withSlash(zhPath), Astro.site)} />
<link rel="alternate" hreflang="en" href={new URL(withSlash(enPath), Astro.site)} />
<link rel="alternate" hreflang="x-default" href={new URL(withSlash(zhPath), Astro.site)} />
<!-- 样式即数据:theme.yaml 白名单旋钮(构建期内联,零运行时;:root:root 靠特异性压 tokens.css) -->
<style is:inline set:html={themeStyle}></style>
</head>
<body>
<!-- 背景辉光:跟随鼠标的 ink 色径向渐变(丙灰阶语言,3-4% 透明度;
pointer:fine + 非 reduced-motion 才有 JS 驱动,其余静态隐藏) -->
<div class="bg-glow" aria-hidden="true"></div>
<a class="skip-link" href="#main">跳到正文</a>
<Header />
<main id="main" class="container">
<slot />
</main>
<Footer>
<BuildInfo slot="build-info" />
</Footer>
<Keyboard />
<SearchDialog />
</body>
</html>