【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
124 lines
6.3 KiB
Plaintext
124 lines
6.3 KiB
Plaintext
---
|
||
// 基础布局:字体按轨瘦身 + 非阻塞加载(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>
|