Files
ClassHelper/website/serve.mjs
T
LoveChengke ded10118f5 feat(website,docs,build): 首屏改成大标题 + 整行渐变填色,官网与文档站合并发布到 GitHub Pages
官网(`website/`)

- 首屏标题放大一档(`--fs-display` 上限 3.6rem → 4rem),第二行改成**整行渐变填色**,
  照 ClassIsland 官网那种「大标题 + 中间一段渐变 + 一行副标 + 一排徽章 + 两个按钮」的写法;
- 标题下补一排 mono 芯片(版本 / Windows / Linux·Docker / 触摸屏),主按钮换成
  「立即下载」+「GitHub 仓库」;
- 顶部导航与页脚补「文档站」入口;页脚原本指向 GitHub blob 的那三个链接改成指向文档站
  —— 那几个文件已经移到 `docs/management/` 下,原链接是 404。

> ⚠ 渐变那一行**不参与逐字揭示**:`main.js` 的 `setupHeadlineReveal` 会跳过 `.reveal-grad`。
> 拆成 per-char 的 `inline-block` 之后每个字各自建一层合成,祖先的 `background-clip: text`
> 就穿不过去,渐变会整行失效 —— 所以它按整行入场(透明度 + 轻微上浮)。

站点发布(`.github/workflows/pages.yml`,取代 `docs.yml`)

- **一个仓库在 GitHub Pages 上只有一个站点**,所以官网与文档站拼成一份产物:
  官网在根、文档站在 `/docs/` 下,组装逻辑在 `scripts/build-pages.mjs`;
- 站点根 404 由 `website/404.html` 提供(含 `{{BASE}}` 占位符,由脚本与 workflow 按仓库名注入);
- 官网目录里的 `serve.mjs` 与 `tools/` 是开发用的,不发布;
- `website/serve.mjs` 把 `../docs/_site` 挂在 `/docs/` 下,让本地预览与线上同形 ——
  否则页脚那几个「文档站」链接在本地全是 404;
- 文档站模板顶栏补一个「官网」入口(`{{_rel}}../`)。

版本迭代

- `scripts/version.mjs` 补上**官网上手写的三处版本号**(顶栏徽标 / 首屏芯片 / 页脚)。
  官网是纯静态站、没有构建步骤,没有哪段代码会去读 `package.json`,漏改的后果是
  首页一直挂着旧版本号 —— 而那是别人看到的第一眼。落点从 10 处变成 13 处,
  `AGENTS.md` §4.4 与 `docs/dev/release.md` 同步。
2026-10-06 16:44:23 +08:00

78 lines
3.0 KiB
JavaScript
Raw Permalink 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.
/**
* 官网本地预览用的最小静态服务器 —— 只是为了 `node serve.mjs` 能直接看页面。
* 正式发布时把 website/ 与 docs/_site/ 一起交给静态托管即可,不需要这个文件。
*
* node website/serve.mjs → http://127.0.0.1:5180
* PORT=8080 node website/serve.mjs
*
* 线上两者是同一个 GitHub Pages 站点:官网在根,文档站在 /docs/ 下(见 .github/workflows/pages.yml)。
* 这里把 docs/_site 挂到 /docs/ 就是为了让本地与线上同形 —— 否则页脚那几个「文档站」链接
* 在本地全是 404。文档站没构建过时 /docs/ 会 404,先跑一次 `pnpm docs:build`。
*/
import { createServer } from 'node:http';
import { readFile, stat } from 'node:fs/promises';
import { extname, join, normalize, resolve, sep } from 'node:path';
import { fileURLToPath } from 'node:url';
const ROOT = resolve(fileURLToPath(new URL('.', import.meta.url)));
const DOCS_ROOT = resolve(ROOT, '..', 'docs', '_site');
const PORT = Number(process.env.PORT ?? 5180);
const MIME = {
'.html': 'text/html; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.js': 'text/javascript; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.webmanifest': 'application/manifest+json',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
'.map': 'application/json',
};
const server = createServer(async (request, response) => {
const url = new URL(request.url ?? '/', 'http://localhost');
const requested = decodeURIComponent(url.pathname);
// 文档站挂在 /docs/ 下(线上也是这样)
const underDocs = requested === '/docs' || requested.startsWith('/docs/');
const base = underDocs ? DOCS_ROOT : ROOT;
const relative = underDocs ? requested.replace(/^\/docs\/?/, '/') : requested;
const target = normalize(join(base, relative === '/' ? 'index.html' : relative));
// 目录穿越防护:解析结果必须仍在它所属的根里
if (target !== base && !target.startsWith(base + sep)) {
response.writeHead(403).end('Forbidden');
return;
}
try {
const info = await stat(target);
const file = info.isDirectory() ? join(target, 'index.html') : target;
const body = await readFile(file);
response.writeHead(200, {
'content-type': MIME[extname(file)] ?? 'application/octet-stream',
'cache-control': 'no-store',
});
response.end(body);
} catch {
if (underDocs) {
response
.writeHead(404, { 'content-type': 'text/html; charset=utf-8' })
.end(
'<p style="font:16px/1.7 system-ui;padding:40px">文档站还没构建。先跑一次 <code>pnpm docs:build</code>。</p>',
);
return;
}
response.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' }).end('404');
}
});
server.listen(PORT, '127.0.0.1', () => {
console.log(`官网预览:http://127.0.0.1:${PORT}(文档站在 /docs/)`);
});