🌱 Stage 1 metaarchitecture

Reveriel:一个交互式知识系统的设计

问题:为什么书本不够好

书本三大罪状

  • 死的,没有更新,或者更新极慢,以年为单位
  • 死的,没有交互,只是单方面的向你传递信息,不需要读者的反馈。
  • 单媒体的,只有文字和静态图片,无法充分刺激人的大脑,难以被记忆、理解。

另一方面,一个好的游戏——比如《传送门》或《Baba Is You》——根本不会给你一本规则手册。它给你一个安全的、可以试错的环境,让你通过做来学习。失败没有惩罚,成功有即时的反馈。难度逐步增加,每一个新关卡都建立在上一个关卡学到的技能之上。

🎯 知识挑战 (1 题) 0 / 1 源码 ↗

加载挑战中...

这个博客的信仰是:游戏是比死书本更先进的知识载体。 它不是一个”技术博客”——它是一个交互式知识系统,伪装成博客的样子。

总有一天,世界上几乎所有的知识都来自 AI, 而对于人类来说,理解知识会成为最重要的事情。


整体架构:一个目录 + 一个构建脚本

这个框架没有数据库,没有管理后台,没有黑盒。它的全部输入是文件系统上的一个目录,输出是一堆可以用浏览器打开的 HTML 文件。

作者写的(输入)              框架处理的              读者看到的(输出)
─────────────────      ────────────────      ─────────────────

src/content/posts/                             https://reveriel.com/
  shader-intro/                               /posts/shader-intro/
    index.mdx         →  MDX → HTML       →   带有交互组件的页面
    demos/                                    ShaderPlayground 可编辑
      circle.wgsl     →  资源打包         →   WGSL 代码嵌入页面
      sim.py          →  Pyodide 引用     →   读者浏览器里跑 Python

核心原则只有四个:

  1. 文件即文章。 一个目录 = 一篇文 = 一个自包含的知识单元。目录名就是 URL。
  2. 组件即交互。 需要互动的地方,插入一个组件标签(如 <ShaderPlayground />),而不是写一堆 <script> 标签。
  3. 按需加载。 WASM、shader、Pyodide 只在使用时加载。首页的速度和纯文本博客一样快。
  4. 对搜索引擎友好。 静态生成 + 语义 HTML + Open Graph 标签 + sitemap,让知识能被发现。

目录结构:每篇文章是一个项目

src/content/posts/
  shader-intro/                  ← 目录名 = 文章 slug = URL
    index.mdx                    ← 正文(唯一必需的文件)
    demos/                       ← 属于这篇文章的交互式 demo 代码
      circle.wgsl                ← ShaderPlayground 引用的 shader
      raymarch.wgsl
      attention-viz.ts           ← Canvas 2D 可视化脚本
      ml-sim.py                  ← Pyodide 跑的 Python
    assets/                      ← 静态资源
      diagram.png
      model.glb                  ← 3D 模型

每篇文章的代码资源和文章放在一起,不是散落在全局的 /static/ 或 /public/ 里。这样你写完一篇文章后,它就是一个完整的、可以独立理解的知识包。

为什么这样设计

传统的 CMS(比如 WordPress)把内容和资源分开:文章在数据库里,图片在一个媒体库,代码片段嵌在 HTML 里。时间长了,它们之间的关系就丢了——你没法把一篇文章的完整上下文(文字 + 代码 + 资源)打包成一个目录复制给别人。

把文章目录当作项目的根意味着:

  • Git 可以精确追踪一篇文章的所有修改(包括代码)
  • 你可以把一篇文章的目录整个复制给别人,对方就能构建和运行
  • 不会有”这张图到底在哪个文章用到了”的问题

内容管线:从 MDX 到交互式页面

数据流是这样的:

index.mdx (你写的)
    │
    ├── YAML frontmatter    →  元数据(title, stage, tags...)
    │
    ├── Markdown 正文       →  Astro MDX 解析器
    │                         →  Shiki 语法高亮
    │                         →  HTML
    │
    └── 组件标签             →  Astro 组件渲染
        <ShaderPlayground />  →  WebGPU 初始化脚本
        <CodeCell />          →  Pyodide / JS 运行时
        <QuizChain />         →  交互式测验逻辑
        <ProofStep />         →  可折叠的 <details>
        <GiscusComments />    →  GitHub Discussions 评论区

构建时(npm run build),Astro 遍历所有 .mdx 文件,提取 frontmatter,渲染 Markdown 为 HTML,把组件标签替换为真实的交互式组件代码,然后输出纯静态文件到 dist/ 目录。

关键点:输出的是静态 HTML + CSS + JS,不需要服务器。 部署到 GitHub Pages、Cloudflare Pages 或任何一个能托管静态文件的地方就行。


组件系统:交互是怎么工作的

这个框架提供了一套交互式组件库。每个组件解决一类特定的教学需求:

下面是一个 Shader Playground。试着改里面 WGSL 代码的最后一行的 0.5,改成 0.0,然后按 Ctrl+Enter。

✏️ Shader Playground
源码 ↗
 

你刚才改的是一个 fragment shader 的代码。ShaderPlayground 组件在读者的浏览器里做三件事:

  1. 请求 WebGPU 设备
  2. 把你写的 WGSL 编译为 GPU shader module
  3. 渲染一个全屏四边形,把 shader 的结果画到 canvas 上

如果读者的浏览器不支持 WebGPU,组件会显示降级信息。如果读者根本没有滚动到这个组件的位置,它不会初始化——IntersectionObserver 懒加载。

组件一览

组件解决的问题读者体验
<ShaderPlayground>如何直观地理解 shader改代码 → 实时看效果
<CodeCell>代码不是用来”看”的在浏览器里运行 JS / Python
<QuizChain>如何确认读者真的懂了递进式问答,答对解锁下一题
<ProofStep>数学证明容易让人迷失读者自己控制展开节奏
<Simulation>参数变化的意义无法用文字描述拖滑块看实时变化
<WasmDemo>如何跑高性能计算Rust → WASM,按需加载
<GiscusComments>读者想讨论、提问、质疑GitHub Discussions 评论,懒加载

一个 CodeCell 的实例——不只是展示代码,读者可以编辑和运行:

🟨 JavaScript 源码 ↗
点击"运行"以执行代码

什么是 Astro 组件?

你可能会问:ShaderPlayground、CodeCell 这些标签是从哪来的?

它们是 Astro 组件——后缀为 .astro 的文件,存放在 src/components/ 目录下。一个 .astro 文件长这样:

---
// 前半部分(--- 之间):JavaScript / TypeScript,在构建时运行
interface Props {
  width?: number
  height?: number
}
const { width = 640, height = 360 } = Astro.props
---

<!-- 后半部分:HTML 模板,构建时输出为静态 HTML -->
<div class="interactive-block">
  <canvas id="my-canvas" width={width} height={height}></canvas>
</div>

<script>
  // <script> 标签里的 JS 在读者浏览器里运行
  const canvas = document.getElementById('my-canvas')
  // ... WebGPU 初始化 ...
</script>

关键理解: Astro 组件有两个执行环境——

在哪里运行什么时候做什么
--- 之间的代码构建时(服务器/你的电脑上)处理 props、读取文件、生成 HTML
<script> 标签里的代码运行时(读者浏览器里)操作 DOM、初始化 WebGPU、响应用户输入

构建完成后,<script> 之外的模板部分已经变成了纯 HTML 字符串。这也是为什么这个博客不需要服务器——所有的逻辑要么在构建时执行完了,要么在读者浏览器里按需加载。

WASM:在浏览器里跑 Rust

当交互需要的计算量超过 JavaScript 的能力范围时(比如物理模拟、图像处理、数值计算),可以把 Rust 代码编译为 WebAssembly,直接在浏览器里运行。

下面是一个 Mandelbrot 集的 WASM 渲染器。核心计算用 Rust 写,编译为 .wasm 文件,通过 <WasmDemo> 组件加载:

🦀 WASM Demo mandelbrot_bg.wasm 源码 ↗

加载 WASM 模块中...

💡 点击画布放大,右键缩小

WASM 的构建流程:

# 1. 在文章目录下创建 Rust 项目
src/content/posts/hello-interactive-world/demos/mandelbrot/
  ├── Cargo.toml
  └── src/lib.rs

# 2. 编译
cd demos/mandelbrot
wasm-pack build --target web --out-dir ../pkg

# 3. 在 MDX 中引用产物
<WasmDemo wasm="./demos/pkg/xxx_bg.wasm" js="./demos/pkg/xxx.js" />

WASM 模块也是懒加载的——只有读者滚动到它的位置时才开始下载和初始化。

SEO:让知识被找到

一个博客如果没有人读到,交互做得再好也没有意义。Reveriel 从第一天起就内置了 SEO 基础设施:

每篇文章自动生成:

  • <title> 和 <meta description>(从 frontmatter summary 读取)
  • Open Graph 标签(og:title, og:description, og:image, og:type: article)—— 决定分享到社交媒体的卡片
  • Twitter Card(summary_large_image)—— Twitter / Slack / Telegram 的链接预览
  • article:published_time 和 article:tag —— 搜索引擎的结构化时间 + 标签
  • 语义 HTML(<article>, <header>, <time>, <nav>)—— 搜索引擎理解页面结构

待规划:

  • sitemap.xml — @astrojs/sitemap 自动生成
  • RSS Feed — @astrojs/rss 让文章被 RSS 阅读器抓取
  • JSON-LD 结构化数据 — Google Rich Results(Article, BreadcrumbList)

评论区:知识需要讨论

每篇文章底部集成了 Giscus——一个基于 GitHub Discussions 的评论系统:

  • 读者用 GitHub 账号登录 → 在文章下留言
  • 评论数据存在你的 GitHub repo 的 Discussions 里(不依赖第三方数据库)
  • 通过 IntersectionObserver 懒加载——不滚动到评论区不加载脚本
  • 暗色主题,中文界面

如果没有配置 Giscus,评论区显示提示文字,不影响页面。


递进式难度:Stage 系统

不是所有知识都应该用同样的交互密度呈现。框架定义了 5 个 Stage:

Stage 1 🌱:  90% 阅读 + 10% 演示          (读者刚入门,先看)
Stage 2 🌿:  70% 阅读 + 20% 交互 + 10% 挑战  (开始动手)
Stage 3 🌳:  40% 阅读 + 40% 交互 + 20% 挑战  (动手为主)
Stage 4 ⚡:  20% 阅读 + 50% 交互 + 30% 挑战  (综合运用)
Stage 5 🔥:  10% 方向 + 70% 自由探索 + 20% 创造 (读者自己探索前沿)

脚手架的逐渐撤走是游戏设计的核心原则:

  • Stage 1 的文章几乎没有背景要求,交互以”看”为主
  • Stage 3 的文章开始组合多个知识点,交互以”做”为主
  • Stage 5 的文章给出方向而不是答案——读者应该能自己写一个 path tracer,而不是跟着教程敲

这篇文章本身是 Stage 1。它不需要任何背景知识,交互密度很低——你只需要读。


一个完整的文章是怎么写出来的

假设你要写一篇”Shader 入门:从像素理解 GPU”。你做的事情:

第一步:创建目录

mkdir -p src/content/posts/shader-from-pixels/demos
touch src/content/posts/shader-from-pixels/index.mdx

第二步:写 frontmatter

---
title: "Shader 入门:从像素理解 GPU"
date: 2026-07-30
stage: 1
tags: [graphics, shader]
summary: "每一个像素都是一个独立的小程序。理解这一点是图形学的第一道门。"
---

第三步:写正文 + 嵌入组件

import ShaderPlayground from '@components/ShaderPlayground.astro'

## 像素是并行的

当你在屏幕上看到 1920×1080 个像素时,GPU 并没有一个一个地计算它们的颜色。
每个像素有自己的"微型程序"在同时运行。

<ShaderPlayground shader="./demos/first-pixel.wgsl" editable />

第四步:把 shader 代码放在 demos/ 里

// demos/first-pixel.wgsl
@fragment
fn main(@builtin(position) coord: vec4f) -> @location(0) vec4f {
  let uv = coord.xy / vec2f(1920.0, 1080.0);
  return vec4f(uv.x, uv.y, 0.5, 1.0);
}

第五步:npm run build → 部署


证明:这个设计是正确的

📐 证明:文件驱动的交互式博客比数据库驱动的 CMS 更适合知识传播

前提 1:知识应该可复现。 一篇文章 + 它的交互式代码应该构成一个封闭的、可执行的系统。读文章的人可以在自己浏览器里重新运行文章中的所有计算。

前提 2:知识应该被验证。 读完不等于学会。交互式挑战(QuizChain、CodeCell 的修改任务)让读者运用刚学到的概念,从而确认自己真的理解了。

文件驱动(而不是数据库驱动)满足前提 1:文章目录是自包含的、可版本控制的、可复制的。交互式组件满足前提 2。

因此,这个设计比传统的 Markdown → HTML 静态博客更适合知识传播。∎


接下来

以下是网站的路线图:

状态内容说明
✅ 完成框架骨架Astro 5 + MDX + Shiki 高亮
✅ 完成交互式组件库ShaderPlayground, CodeCell, QuizChain, ProofStep, WasmDemo, Simulation
✅ 完成品牌 & SEOOG / Twitter Card 标签、语义 HTML、Giscus 评论系统
✅ 完成旧文章迁移10 篇非数学类文章已迁入,数学类文章留在 archive/ 待 math 渲染方案就绪
🔜 现在sitemap + RSS@astrojs/sitemap + @astrojs/rss
🔜 现在第一篇真正的交互式文章Shader 入门或线性代数可视化
📋 近期数学渲染解决 MDX + LaTeX 的构建时解析问题,迁入 archive/ 里的数学文章
📋 近期WebGPU compute shader不只是渲染,还能在 GPU 上做并行计算
📋 远期读者进度追踪localStorage 记录已解锁的概念
📋 远期多语言支持概念 ID 跨语言共享,zh.mdx / en.mdx

本文的源码

在 GitHub 查看目录 ↗

页面上每个交互组件的真实实现,构建时直接从 demos/ 读取。

mandelbrot/src/lib.rsrust · 88 行 GitHub ↗
use wasm_bindgen::prelude::*;

/// Compute Mandelbrot iterations for a single point.
/// Returns the iteration count (capped at max_iter).
fn mandelbrot(cx: f64, cy: f64, max_iter: u32) -> u32 {
    let mut x = 0.0;
    let mut y = 0.0;
    let mut iter = 0;
    while x * x + y * y <= 4.0 && iter < max_iter {
        let xt = x * x - y * y + cx;
        y = 2.0 * x * y + cy;
        x = xt;
        iter += 1;
    }
    iter
}

/// HSV → RGB helper for coloring the fractal.
fn hsv_to_rgb(h: f64, s: f64, v: f64) -> (u8, u8, u8) {
    let c = v * s;
    let hp = h / 60.0;
    let x = c * (1.0 - ((hp % 2.0) - 1.0).abs());
    let (r1, g1, b1) = match hp as u32 {
        0 => (c, x, 0.0),
        1 => (x, c, 0.0),
        2 => (0.0, c, x),
        3 => (0.0, x, c),
        4 => (x, 0.0, c),
        _ => (c, 0.0, x),
    };
    let m = v - c;
    (
        ((r1 + m) * 255.0) as u8,
        ((g1 + m) * 255.0) as u8,
        ((b1 + m) * 255.0) as u8,
    )
}

/// Render the Mandelbrot set into a byte buffer (RGBA).
/// Returns a Vec<u8> of length width * height * 4.
#[wasm_bindgen]
pub fn render(
    width: u32,
    height: u32,
    center_x: f64,
    center_y: f64,
    zoom: f64,
    max_iter: u32,
) -> Vec<u8> {
    let scale = 4.0 / zoom; // zoom level → viewport size in complex plane
    let aspect = width as f64 / height as f64;
    let x_min = center_x - scale * aspect / 2.0;
    let y_min = center_y - scale / 2.0;
    let dx = scale * aspect / width as f64;
    let dy = scale / height as f64;

    let size = (width * height * 4) as usize;
    let mut pixels = vec![0u8; size];

    for py in 0..height {
        let cy = y_min + py as f64 * dy;
        for px in 0..width {
            let cx = x_min + px as f64 * dx;
            let iter = mandelbrot(cx, cy, max_iter);
            let idx = ((py * width + px) * 4) as usize;

            if iter == max_iter {
                // In the set — black
                pixels[idx] = 0;
                pixels[idx + 1] = 0;
                pixels[idx + 2] = 0;
                pixels[idx + 3] = 255;
            } else {
                // Outside — color by iteration count
                let t = iter as f64 / max_iter as f64;
                let hue = (t * 360.0 + 200.0) % 360.0;
                let (r, g, b) = hsv_to_rgb(hue, 0.8, 0.9);
                pixels[idx] = r;
                pixels[idx + 1] = g;
                pixels[idx + 2] = b;
                pixels[idx + 3] = 255;
            }
        }
    }

    pixels
}
mandelbrot/Cargo.tomltoml · 11 行 GitHub ↗
[package]
name = "mandelbrot"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
wasm-bindgen = "0.2"

💬 评论加载中...