PPysolate SpineREAD THE CODE · UNDERSTAND THE SYSTEM
课程与章节
CODE READING / 39

代码优先的页面外壳

Docs.astro / 首页 / 阅读样式

本章目录

本章要回答的问题: 一个章节页面只提供自己的标题和正文时,谁负责把它变成完整 HTML、侧栏、页内目录和移动导航?

调用者是内容页、源码页和首页,它们都把数据和正文交给 Docs.astro。布局输入是标题、描述、headings、可选的 wide 和生成的 navigation JSON;输出是浏览器可显示的文档 HTML。首页还输入覆盖摘要,源码页输入 symbol headings。布局不创建 Guest、不执行示例,也不决定源码清单。

1. 布局前置脚本确定当前导航上下文

site/src/layouts/Docs.astro · L1–L9 · 打开完整源码

---
import nav from '../../.generated/navigation.json';
import '../styles/global.css';
import '../styles/code-reading.css';
const { title = 'Pysolate Spine', description = '从真实代码读懂 PLM', headings = [], wide = false } = Astro.props;
const current = Astro.url.pathname;
const activeSection = nav.find(p=>current===`/${p.slug}/`)?.section || (current==='/'||current.startsWith('/code/')?'reading':'implementation');
const groups = [{id:'reading', title:'代码带读 · 主线'}, {id:'deep', title:'机制深度解析'}, {id:'patterns', title:'Go 组合速成'}, {id:'basics', title:'前置课程 · 从零开始'}, {id:'research', title:'研究探索'}, {id:'implementation', title:'实现与面试练习'}];
---

布局导入 navigation JSON 和两份 CSS。它从 Astro.props 取得 title、description、headings、wide,并给它们默认值;这些是父页面参数,不是 Go 函数参数。Astro.url.pathname 给出构建当前页面时的站内路径。

activeSection 先在导航记录中找精确 slug,找不到时把首页和 /code/ 归入 reading,其余未登记路径归入 implementation。groups 是侧栏的五个分组,数组顺序直接决定导航显示顺序。源码页面虽然不一定有普通讲义 entry,但它仍能通过路径规则获得代码带读分组。

2. <head> 交付页面级发布提示

site/src/layouts/Docs.astro · L10–L21 · 打开完整源码

<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <meta name="robots" content="noindex, nofollow, noarchive, nosnippet" />
  <meta name="googlebot" content="noindex, nofollow, noarchive, nosnippet" />
  <meta name="description" content={description} />
  <meta name="color-scheme" content="light dark" />
  <link rel="icon" href="/favicon.svg" type="image/svg+xml" />
  <title>{title} · Pysolate Spine</title>
</head>

布局输出标准文档骨架、中文语言标记、UTF-8 和响应式 viewport。两条 robots meta 把 noindex、nofollow、noarchive、nosnippet 写进每个页面,同时单独给 googlebot 同样的指令。它们是搜索索引提示,不是登录或文件访问控制。

description 来自父页面 props,color-scheme 告诉浏览器支持明暗配色,favicon 由 public 静态资源提供,title 把当前标题和站点名拼成标签页标题。花括号是 Astro 构建模板插值,不会重新运行页面引用的 Go/Python 源码。

3. 顶栏和移动导航复用同一份导航数据

site/src/layouts/Docs.astro · L22–L28 · 打开完整源码

<body>
<a class="skip" href="#main">跳到正文</a>
<header>
  <a class="brand" href="/"><span class="mark">P</span><span><strong>Pysolate Spine</strong><small>READ THE CODE · UNDERSTAND THE SYSTEM</small></span></a>
  <nav class="top"><a href="/read/">代码带读</a><a href="/deep/">深度解析</a><a href="/go-patterns/">Go 组合速成</a><a href="/foundations/">基础查阅</a><a href="/research-positioning/">研究探索</a><a href="/guide/">实现</a><a href="/code/">源码</a><span class="quiet">NO INDEX</span></nav>
</header>
<details class="mobile-nav"><summary>课程与章节</summary><nav>{groups.map(g=><details class="nav-section" open={activeSection===g.id}><summary>{g.title}</summary>{nav.filter(p=>p.section===g.id).map(p=><a href={`/${p.slug}/`} aria-current={current===`/${p.slug}/`?'page':undefined}>{p.title}</a>)}</details>)}<a href="/code/">源码索引</a></nav></details>

body 先放跳到 #main 的 skip link,再输出品牌链接和顶栏入口。顶栏链接进入代码带读、深度解析、Go 组合、基础课程、实现和源码索引;这些 URL 是站点页面路由,不是 runtime 配置。

移动导航使用原生 details/summary,无需客户端状态管理。外层 groups.map 为五组生成折叠区,open={activeSection===g.id} 让当前组默认展开;每组再用 filter 取得 section 相同的 nav 项,map 生成章节链接。当前链接才设置 aria-current="page",辅助技术和 CSS 因此都能识别当前位置。最后单独保留源码索引入口。

同一段模板包含“分组 → 筛选 → 生成链接”三层数据流。数组中新增一项会影响页面导航,但不会让浏览器扫描仓库。

4. 桌面侧栏、正文插槽和页内目录

site/src/layouts/Docs.astro · L29–L36 · 打开完整源码

<div class:list={['shell', { wide }]}>
  <aside class="sidebar"><div class="rail-label">沿执行路径理解每块代码</div>{groups.map(g=><details class="nav-section" open={activeSection===g.id}><summary>{g.title}</summary><nav>{nav.filter(p=>p.section===g.id).map((p,i)=><a href={`/${p.slug}/`} aria-current={current===`/${p.slug}/`?'page':undefined}><span>{String(g.id==='implementation'?i+1:i).padStart(2,'0')}</span>{p.title}</a>)}</nav></details>)}<a class="source-link" href="/code/">浏览实际源码 ↗</a><p class="rail-note">先问这一块完成什么。<br/>再跟状态和调用,语法随用随查。</p></aside>
  <main id="main"><slot /></main>
  <aside class="toc">{headings.length>0 && <><div class="rail-label">本页内容</div><nav>{headings.filter(h=>h.depth===2).map(h=><a href={`#${h.slug}`}>{h.text}</a>)}</nav></>}</aside>
</div>
<footer>面向理解与协作的精简实现 <span>公开链接 · 禁止搜索索引 · 无访问鉴权</span></footer>
</body>
</html>

shell 总包含基础类,wide 为真时再加宽布局类。桌面侧栏在导航上方显示“沿执行路径理解每块代码”,再按 groups 渲染章节,并为每个章节显示组内序号:implementation 从 1 开始,其他组从 0 开始;padStart 只改变显示格式。

<slot /> 是父页面正文的交接点。内容页把章节标题、目录、饭点课 notice 和 <article> 放进这里;源码页把源码索引提示、函数目录和 Code 组件放进这里;首页把路线卡片和命令卡放进这里。布局不需要知道这些内容的具体来源。侧栏底部的提示“先问这一块完成什么。再跟状态和调用,语法随用随查。”把同一阅读策略落在导航外壳中。

右侧 toc 只有存在 headings 时才输出,并只保留 depth 为 2 的标题。链接的 hash 来自构建期 heading slug,所以点击目录只是浏览器片段跳转。页脚再次说明公开链接、禁止搜索索引和无访问鉴权,避免把视觉导航误读为权限机制。

5. 首页把覆盖结果转成阅读入口

site/src/pages/index.astro · L1–L10 · 打开完整源码

---
import Layout from '../layouts/Docs.astro';
import coverage from '../../.generated/reading-coverage.json';
const paths = [
  {href:'/read-entry/', title:'先跟完一次普通执行', description:'从 main 的参数出发,走过 Go、Wasm、C、Python 和 Host 工具。'},
  {href:'/read-futures/', title:'再读 PLM 与 Future', description:'跟随一次提前读取,看任务怎样登记、完成并在原位置交付。'},
  {href:'/read-prefix-host/', title:'接入流式源码', description:'看接收、解析、提前准备与最终领取怎样协作。'},
  {href:'/read-prepared/', title:'最后读内存复用', description:'先 full-copy,再跟进 Linux COW 的系统调用与所有权。'},
];
---

首页导入布局和 .generated/reading-coverage.json。paths 是四个阅读路线对象,每项包含 href、title 和 description:普通执行、PLM/Future、流式 prefix、内存复用。第二条路线现在明确写成“跟随一次提前读取,看任务怎样登记、完成并在原位置交付”,把 Future 的登记、完成和原位置交付作为普通读者可观察的交接。四个对象是教学顺序和链接数据,不是运行时的四种模式。

页面在构建时读取 coverage JSON,因此首页显示的是准备阶段已经计算出的覆盖文件数量。它没有在浏览器中扫描源码,也没有因为显示一个命令就执行它。

site/src/pages/index.astro · L11–L18 · 打开完整源码

<Layout title="沿真实代码学习">
  <section class="hero"><div class="eyebrow">FOLLOW THE EXECUTION</div><h1>跟完一次执行,<br/>读懂<em>整块代码。</em></h1><p class="lede">先看这一步为什么存在、接到什么状态。<br/>再沿函数调用理解源码,跟清结果与资源交给谁。</p><div class="actions"><a class="button" href="/read/">先看执行地图 →</a><a class="button secondary" href="/read-entry/">从 main 开始带读</a></div></section>
  <section><div class="section-heading"><span class="eyebrow">先走主线,再展开旁支</span><h2>一次调用,逐层读懂</h2></div><div class="cards">{paths.map((p,i)=><a class="card" href={p.href}><span class="card-number">{String(i+1).padStart(2,'0')}</span><h3>{p.title}</h3><p>{p.description}</p><span class="arrow">↗</span></a>)}</div></section>
  <section class="command-card"><div><span class="eyebrow">先预测,再运行</span><h2>每一层都接回实际结果</h2></div><pre><code>make demo     <span># 普通执行</span>
make explain  <span># 实际 PLM 转换</span>
make prepared-cow <span># Linux 写时复制</span></code></pre></section>
  <div class="notice"><strong>按职责分块,保留完整覆盖。</strong>带读片段从仓库提取,覆盖 {coverage.length} 个教学源文件的全部非空行。每块都接回目的、状态变化与调用去向;覆盖检查负责防漏,讲解仍需结合源码审读。<a href="/foundations/">背景课程</a>和 Go 语法可以随用随查。</div>
</Layout>

首页将标题交给布局;hero 使用 FOLLOW THE EXECUTION,主标题直接说“跟完一次执行,读懂整块代码”,导语要求先看步骤为何存在、接到什么状态,再沿调用理解结果与资源去向。两个按钮分别连接执行地图 /read/ 和从 main 开始的 /read-entry/。paths.map 按数组位置生成四张卡片,避免复制四份 HTML;命令卡用 pre/code 显示需要读者在项目终端执行的 make 命令。

notice 把“按职责分块,保留完整覆盖”写进首页,并插入 {coverage.length}。文案同时说明带读片段覆盖教学源文件的全部非空行,每块应接回目的、状态变化与调用去向;覆盖检查负责防漏,讲解仍需结合源码审读。背景课程和 Go 语法随用随查,不是阅读主线的前置门槛。

6. 片段行号的视觉交接

site/src/styles/code-reading.css · L1–L19 · 打开完整源码

/* Excerpt numbers refer to the original source, not to this Markdown page. */
.code-excerpt { --line-start: 0; margin: 1rem 0 1.5rem; }
.code-excerpt pre code { counter-reset: source-line var(--line-start); }
.code-excerpt pre code .line { counter-increment: source-line; }
.code-excerpt pre code .line::before {
  content: counter(source-line);
  display: inline-block;
  width: 3.5em;
  padding-right: 1.2em;
  color: #87909b;
  text-align: right;
  user-select: none;
}
.code-excerpt pre { padding-left: .6rem; }
.reading-start { border-left: 3px solid var(--accent, #c57343); padding-left: 1rem; }
@media (max-width: 720px) {
  .code-excerpt pre { font-size: .78rem; }
  .code-excerpt pre code .line::before { width: 2.8em; padding-right: .8em; }
}

这份补充样式专门接收 excerpts.mjs 写入的 .code-excerpt 和 --line-start。代码块重置 CSS counter,每个高亮器 .line 递增一次,伪元素显示原始源码行号;因此片段从 L21 开始时,第一行不会重新显示为 1。

行号的固定宽度、右对齐和 user-select:none 让它既可读又不会被复制进代码;.reading-start 给提示起点加左边框。720px 以下缩小字号和行号区域,但不改源码或偏移规则。

完整源码页的行号由 site-source-view.md 所述 transformer 写入 HTML id,片段行号由这里的 CSS counter 呈现;它们共享同一套原始行号输入,却服务于两种不同的 HTML 结构。

7. 从父页面到浏览器的完整交接

一次章节请求对应的静态构建路径可以写成:

生成 Markdown / JSON
  → 页面 frontmatter 与 headings
  → Docs.astro 的 head、nav、slot
  → global.css 与 code-reading.css
  → HTML、hash 定位和 details 折叠

布局消费的是页面数据,不负责生成这些数据;CSS 消费的是 class 和属性,不负责验证内容;浏览器消费的是最终 HTML,不会回到仓库读取源文件。发布前的实际链路和 QA 分别见 站点构建管线 与 构建后验证。

读完本章,应能回答:slot 为什么让同一个布局承载首页、讲义页和源码页?侧栏的当前组如何由 URL 得出?覆盖摘要从哪里来,为什么它不能替代正文审读?