Install
openclaw skills install @samonysh/latex-to-nextra-site将 project-cookbook-latex 生成的 LaTeX 工程转换为基于 Nextra 的中文电子文档网站,保留 PlantUML/drawio/Mermaid 图表、代码块、LaTeX 公式与表格,输出可上传 GitHub 并一键部署到 Vercel 或 Docker 的项目文件夹。
openclaw skills install @samonysh/latex-to-nextra-siteBilingual note: This English edition mirrors
SKILL.zh-CN.md. For the authoritative Chinese version, seeSKILL.zh-CN.md.
本 Skill 用于把 project-cookbook-latex 等流程产出的章节化中文 LaTeX 工程自动转换为基于 Nextra v3(Next.js 14 文档主题) 的现代电子文档网站,交付一个可:
git init && git push 上传 GitHubdocker compose up -d --build 本地/服务器自托管的完整项目目录。
本文档基于
18-UML-SysML-CookBook项目(52 主体章 + 4 附录 + 42 张 PlantUML,约 18 万字)的实战经验编写。所有"陷阱"小节都来自实际踩过的坑。
| Behaviour | Default | Rationale |
|---|---|---|
| Network access | OFF | Only fetch remote resources when user explicitly approves. |
| Host toolchain install | OFF / fail-closed | Never auto-run pip install, npm install -g, apt install, etc. Report missing deps and wait for manual confirmation. |
LaTeX -shell-escape | OFF | Disabled by default; only enabled when user consents AND environment is sandboxed AND content is trusted. |
Core principles:
-shell-escape on untrusted input.| 行为 | 默认值 | 说明 |
|---|---|---|
| 网络访问 | 关闭 | 仅在用户明确同意后访问网络资源。 |
| 主机工具链安装 | 关闭 / 失败即止 | 不会自动运行 pip install、npm install -g、apt install 等。缺失依赖时报告给用户并等待手动确认。 |
LaTeX -shell-escape | 关闭 | 默认不启用 -shell-escape;仅在用户明确同意、沙箱/容器化环境、受信内容三者同时满足时才可开启。 |
核心原则:
-shell-escape。本技能接受多种输入格式:
| 输入类型 | 说明 |
|---|---|
| Markdown / TXT 文件 | 最常见输入,直接处理。 |
| JSON 结构化数据 | 包含 title、chapters[]、content 字段时作为结构化大纲处理。 |
| 直接 LLM 上下文 | 当用户以对话方式提供内容时,自动从上下文提取大纲与正文。 |
| URL / 仓库地址 | 仅在用户明确同意后抓取远程内容。 |
当用户提出下列任一需求时,立即调用本 Skill:
project-cookbook-latex 生成的工程(含 latex/main.tex、chapters/*.tex、figures/、diagrams/、metadata/)转换为可在网页上阅读的电子书。本 Skill 是编排型 Skill。执行时按需联动:
project-cookbook-latex —— 当用户尚未生成 LaTeX 工程时,先调用其产出标准化工程。plantuml —— 把 .puml 渲染为 SVG + PNG。优先使用 Docker(plantuml/plantuml:latest)而非 plantuml.jar,因为:
docker run。drawio —— 把 .drawio 导出为 .drawio.svg。md-image-to-tos —— 仅当用户要求将站点图片上传到对象存储(而非走仓库内 public/)时调用。chapters/*.tex 对应一个 .mdx 页面。zh)为主语言;同步生成 en/ 占位骨架。<div className="overflow-x-auto"> 包裹的 HTML 表格。lstlisting / tcblisting → 带语言标识的代码围栏。$...$ 与 $$...$$ 保留,KaTeX 渲染。\input 顺序时,必须执行重编号 + 跨章引用链接化(详见第 6 节)。public/downloads/。生成完成前必须通过以下检查;不通过则继续修复:
npm run build 成功,不得有 MDX 解析 error(warning 允许)。/ 自动重定向到 /zh(middleware.js 必须存在)。en 内容为占位),/zh/... 与 /en/... 均可解析。\input 顺序)。\hline、\toprule、裸 & 列分隔符)。[第 N 章](/zh/chXX-...)。docker compose up -d --build 后 Up healthy,http://localhost:3000/zh 返回 200。package.json、next.config.mjs、theme.config.tsx、middleware.js,Node 声明 >=18.17.0。<ProjectName>-site/
├── README.md # 项目说明、本地启动
├── DEPLOY.md # GitHub + Vercel + Docker 部署步骤
├── Dockerfile # 多阶段构建(deps → builder → runner)
├── docker-compose.yml # 单服务 + healthcheck
├── .dockerignore
├── package.json
├── package-lock.json
├── .npmrc
├── next.config.mjs # Nextra + i18n + KaTeX + standalone
├── theme.config.tsx # Logo、暗黑模式、搜索、页脚
├── middleware.js # / → /zh 重定向(必需)
├── tsconfig.json
├── .gitignore
├── vercel.json # 可选
├── public/
│ ├── figures/ # LaTeX 原始 figures + PlantUML/Drawio 渲染产物
│ ├── diagrams-src/ # 原始 .puml / .drawio
│ └── downloads/
│ ├── <ProjectName>.pdf
│ └── <ProjectName>-LaTeX.zip
├── pages/
│ ├── _meta.ts # 顶层(zh / en 暴露为 page)
│ ├── _app.tsx
│ ├── zh/
│ │ ├── _meta.ts # 中文侧边栏(含 separator 分组)
│ │ ├── index.mdx
│ │ ├── preface.mdx
│ │ ├── ch00-overview.mdx … ch48-arch-oo-ddd-bridge.mdx
│ │ ├── appendix-a-plantuml.mdx … appendix-d-references.mdx
│ │ └── downloads.mdx
│ └── en/
│ ├── _meta.ts
│ ├── index.mdx
│ └── ...(占位骨架)
├── components/
│ ├── PdfDownload.tsx
│ └── DiagramSource.tsx
├── styles/
│ └── globals.css # callout / figure / 表格滚动 / KaTeX 微调
└── scripts/
├── tex2mdx.mjs # LaTeX → MDX 转换器
├── render-plantuml.mjs # Docker 批量渲染 .puml → .svg + .png
├── export-drawio.mjs # 批量导出 .drawio → .drawio.svg
├── build-meta.mjs # 由章节顺序生成 _meta.ts
├── renumber-content.mjs # 跨章引用重编号 + 链接化
├── check-mdx.mjs # MDX 语法预检(可选)
└── check-yaml.mjs # frontmatter YAML 预检(可选)
至少识别:
main.tex 与 chapters/)。main.tex 中 \input{chapters/...} 顺序为准)。\part{} 不与文件名字典序对齐时,阅读顺序 ≠ \input 顺序。例如 UML CookBook 项目中阅读顺序为 ch00 → ch38 → ch39 → ch01 → ... → ch48 → ch40 → ch41 → ...,侧边栏标号必须按这个顺序而不是 ch## 数字。务必询问用户章节是否需要重编号。figures/、diagrams/、可能的 image/。dist/*.pdf、*.zip,可作为下载资源。如缺少关键输入,简洁询问:
使用 Nextra v3 + Next.js 14(pages router)。
package.json 关键依赖:
{
"name": "<project-name>-site",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"prepare:content": "node scripts/tex2mdx.mjs && node scripts/build-meta.mjs && node scripts/renumber-content.mjs",
"prepare:diagrams": "node scripts/render-plantuml.mjs && node scripts/export-drawio.mjs",
"convert": "npm run prepare:diagrams && npm run prepare:content"
},
"dependencies": {
"next": "^14.2.15",
"nextra": "^3.2.4",
"nextra-theme-docs": "^3.2.4",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@types/node": "^20.12.12",
"@types/react": "^18.3.3",
"typescript": "^5.4.5"
},
"engines": { "node": ">=18.17.0" }
}
next.config.mjs(与实战工程完全一致):
import nextra from 'nextra'
const withNextra = nextra({
theme: 'nextra-theme-docs',
themeConfig: './theme.config.tsx',
defaultShowCopyCode: true,
latex: {
renderer: 'katex',
options: { strict: 'ignore', trust: true }
},
search: { codeblocks: false },
mdxOptions: {
rehypePrettyCodeOptions: { theme: 'github-dark-dimmed' }
}
})
export default withNextra({
output: 'standalone', // 关键:Docker 镜像需要 standalone
i18n: {
locales: ['zh', 'en'],
defaultLocale: 'zh'
},
reactStrictMode: true,
images: { unoptimized: true } // 关键:让 Next.js 不强制走 Image Optimization
})
关键警告:构建期会出现
[nextra] Next.js doesn't support i18n by locale folder names.这是已知告警而非错误。Nextra v3 的pages/zh/+pages/en/文件夹布局与 Next.js 内置 i18n 同时存在,Nextra 自己处理路由;告警可忽略。
theme.config.tsx 关键项(中文优先):
import type { DocsThemeConfig } from 'nextra-theme-docs'
const config: DocsThemeConfig = {
logo: <span style={{ fontWeight: 700 }}>项目 CookBook</span>,
project: { link: 'https://github.com/<user>/<repo>' },
docsRepositoryBase: 'https://github.com/<user>/<repo>/tree/main',
darkMode: true,
i18n: [
{ locale: 'zh', name: '简体中文' },
{ locale: 'en', name: 'English' }
],
search: { placeholder: '搜索文档…' },
editLink: { content: '在 GitHub 上编辑此页 →' },
feedback: { content: '问题或建议?' },
footer: { content: '© 2026 项目 CookBook · 基于 Nextra 构建' },
toc: { backToTop: true }
}
export default config
middleware.js(强制)——让根路径自动重定向到默认语言:
import { NextResponse } from 'next/server'
export const config = {
matcher: ['/((?!api|_next|.*\\..*).*)']
}
export function middleware(req) {
const { pathname } = req.nextUrl
if (pathname === '/') {
return NextResponse.redirect(new URL('/zh', req.url))
}
return NextResponse.next()
}
.npmrc(屏蔽 pnpm 配置告警;如果用户用 npm 可忽略此文件):
# pnpm 兼容;npm 会忽略未知项但会打印 warn,可以删除
auto-install-peers=true
shamefully-hoist=true
npm warn Unknown project config "auto-install-peers"是非阻塞告警。要么保留(用 pnpm)要么删除.npmrc(纯 npm)。
先做图,后写正文——否则 mdx 中的图片引用会全是 404。
关键经验:
使用 Docker,不要用 plantuml.jar。plantuml/plantuml:latest 已包含 Java + plantuml.jar + 字体;零安装、跨平台一致。
PlantUML 输出文件名由 @startuml NAME 决定,而不是输入文件名。例如 fig35-book-overview.puml 含 @startuml book-overview → 输出是 book-overview.svg。MDX 中既可能引用 fig35-book-overview.svg 也可能引用 book-overview.svg,所以渲染脚本必须同时产出两个文件名(重命名或复制)。
中文必须显式声明 CJK 字体。每个 .puml 必须含:
skinparam defaultFontName "WenQuanYi Micro Hei"
否则中文渲染为豆腐方块 □□□(不会报错,但肉眼可见)。plantuml/plantuml:latest 镜像内自带这套字体。
必须 UTF-8 charset:docker run ... -charset UTF-8,否则非 ASCII 字符可能丢失。
bundled plantuml skill 的 PS1 脚本不要用:其在 GBK locale 的中文 PowerShell 下因带中文/特殊字符的字符串解析失败。始终直接调用 docker run。
scripts/render-plantuml.mjs 关键骨架:
import { promises as fs } from "node:fs";
import path from "node:path";
import { execSync } from "node:child_process";
const srcDir = path.resolve("public/diagrams-src");
const outDir = path.resolve("public/figures");
await fs.mkdir(outDir, { recursive: true });
const files = (await fs.readdir(srcDir)).filter(f => f.endsWith(".puml")).sort();
let ok = 0, fail = 0;
for (const file of files) {
const figBase = file.replace(/\.puml$/, "");
const firstLine = (await fs.readFile(path.join(srcDir, file), "utf8")).split(/\r?\n/, 1)[0];
const m = firstLine.match(/@startuml\s+(\S+)/);
const alias = m ? m[1] : null;
try {
for (const fmt of ["svg", "png"]) {
const cmd =
`docker run --rm -v "${srcDir}:/data" -v "${outDir}:/out" ` +
`plantuml/plantuml:latest -charset UTF-8 -output /out -t${fmt} /data/${file}`;
execSync(cmd, { stdio: ["ignore", "ignore", "pipe"], windowsHide: true });
}
const baseName = alias || figBase;
if (baseName !== figBase) {
// Produce BOTH names so any reference in mdx resolves.
try { await fs.copyFile(path.join(outDir, baseName + ".svg"), path.join(outDir, figBase + ".svg")); } catch {}
try { await fs.copyFile(path.join(outDir, baseName + ".png"), path.join(outDir, figBase + ".png")); } catch {}
}
ok++;
} catch (e) { fail++; }
}
console.log(`Done. ${ok} ok, ${fail} fail.`);
process.exit(fail === 0 ? 0 : 1);
<latex-root>/diagrams/*.drawiopublic/figures/*.drawio.svgdrawio-desktop CLI 或 Docker rlespinasse/drawio-desktop-headless。LaTeX 工程通常不直接使用 Mermaid。若用户在源 Markdown 笔记中用过,在 MDX 中保留 ```mermaid 代码围栏即可——Nextra v3 客户端会自动渲染。
直接拷贝 figures/*.{png,svg,jpg} 到 public/figures/。.pdf 矢量图用 pdf2svg 或 inkscape --export-type=svg 转换。
scripts/tex2mdx.mjs)本 Skill 的核心。正确性高于速度。可基于 unified + mdast 自写,或用 pandoc --from=latex --to=gfm 打底 + 后处理。
推荐策略:自写解析器(更可控)。原因:
tcolorbox、xltabular、ElegantBook 自定义环境(definition / theorem / keypoints 块)支持极差。<div className="callout xxx"> 这种带样式的 JSX。关键转换映射表:
| LaTeX 结构 | 转换目标 | 处理规则 |
|---|---|---|
\chapter{...} | frontmatter title: + 不输出顶层 H1 | Nextra 用 frontmatter title 渲染页头 |
\section{...} | ## ... | 同时是右侧 TOC 节点 |
\subsection{...} | ### ... | |
\textbf{x} / \emph{x} | **x** / *x* | |
\href{url}{text} | [text](url) | |
\includegraphics[...]{figures/x} | <img src="/figures/x.svg" /> | 路径补 /figures/;扩展名优先 svg 回退 png |
\begin{figure} ... \caption{} \label{fig:x} | <figure id="fig-x"> 包裹 <img/> + <figcaption/> | \label{fig:x} 必须转为 id="fig-x" 否则跨页锚点 404 |
\begin{tabular} ... \caption{} \label{tab:y} | <figure id="tab-y"> 包裹 <table/> | 表号同样需要 id |
\ref{fig:x} / \autoref{fig:x} | [图 X](#fig-x) | mdx 中显示为可点击锚点 |
\toprule / \midrule / \bottomrule / \hline | 表格分隔符 --- | 直接移除 LaTeX 命令 |
| 含合并单元格 / 多行表头 / 超 5 列 | <div className="overflow-x-auto"><table>...</table></div> | 必须包 overflow 容器 |
\begin{lstlisting}[language=Python] | 三个反引号 + python 围栏 | 语言名映射;文件名保留为代码注释 |
\begin{tcblisting}{...} | 代码围栏 | title 保留为第一行注释 |
$...$ / $$...$$ | 原样保留 | Nextra latex 接管 |
\cite{key} | [[key]] 或链接到 references 页 | |
\input{chapters/xxx} | 拆分文件 | 由 main.tex input 顺序生成侧边栏 |
自定义 \begin{tcolorbox}[colback=...] | <div className="callout xxx">...</div> | 见下方 callout 约定 |
Callout 约定(与 globals.css 配合):
<div className="callout chapteroutline">
#### 📘 本章大纲
本章建立...
</div>
<div className="callout keypoints">
#### ✅ 核心要点
</div>
<div className="callout recipe">
#### 🍳 Recipe: ...
</div>
<div className="callout pitfall">
#### ⚠️ 常见陷阱
</div>
关键陷阱(实战踩坑):
<、>、{、} 极敏感:正文中的裸 < / > 必须转义为 < / >,或包裹在反引号代码中。% 与数字相邻:mdx 中 5% 在某些组合下被识别异常;保险做法是用 5%。% ... 必须丢弃,不能进入 MDX(会被识别为 JSX props 或文本噪声)。~ 不间断空格 → 半角空格或 。title::Nextra 用它做面包屑与 <title>。< 紧跟字母或数字会被 MDX 当成 JSX 标签开始:例如 <5 分钟 这种文本必须写成 <5 分钟。_meta.ts 生成关键:Nextra v3 用 _meta.ts(TypeScript 默认导出对象)而不是 _meta.json。后者虽兼容但不支持 type: 'separator' 分组,强烈不推荐。
pages/zh/_meta.ts 示例(含 separator 分组、reading-order 编号):
// 自动生成 - 请勿手工编辑(修改 scripts/build-meta.mjs)
export default {
index: "🏠 首页",
'---sep-0': { type: 'separator', title: "📖 前言" },
"preface": "前言",
'---sep-1': { type: 'separator', title: "I · 总论" },
"ch00-overview": "0. 总论与本书地图",
"ch38-sw-design-principles": "1. 软件设计基本原则", // reading-order 编号
"ch39-sdlc-methodologies": "2. SDLC 与方法论演进",
'---sep-2': { type: 'separator', title: "II · UML 2.5" },
"ch01-modeling": "3. 建模认识论",
// ...
"appendix-a-plantuml": "附录 A:PlantUML 速查",
"downloads": "📥 下载",
}
注意点:
key 必须与 .mdx 文件名(不含扩展名)一致。
separator 用前缀 ---sep-N 命名以保持稳定排序(短横线确保 JS 对象内键有序遍历)。
value 中的编号是 reading order(阅读顺序)序号,不一定是文件名里的 chXX。如示例中 ch38-sw-design-principles 的展示名是 "1. 软件设计基本原则"。
顶层 pages/_meta.ts 用于把 zh 与 en 暴露为 page:
export default {
zh: { title: '简体中文', type: 'page' },
en: { title: 'English', type: 'page' }
}
英文目录 pages/en/_meta.ts 至少建立同结构骨架。
问题场景:当 pages/zh/_meta.ts 的阅读顺序与文件名字典序不一致(这在重排后的 CookBook 中极常见),就出现两层不匹配:
ch38-sw-design-principles 在侧边栏显示为 "1. ..."。\ref 输出的 "第 38 章" 还是老编号,与侧边栏 "1. ..." 不一致。解决方案:在 scripts/tex2mdx.mjs 跑完后,再跑一遍 scripts/renumber-content.mjs,对 MDX 正文里的所有 "第 N 章" 引用做智能改写:
// scripts/renumber-content.mjs 核心逻辑(精简版)
//
// 1. READING_ORDER 数组按阅读顺序声明所有章节 slug。
// 2. 建 oldNum → newNum 与 oldNum → slug 两张映射表。
// 3. 用正则匹配 "第 <expr> 章" 整体,<expr> 是数字 / 范围 / 列表 / 混合。
// 4. 解析 expr,把每个 OLD 编号映射为 NEW 编号。
// 5. NEW 编号排序、合并连续区间,渲染为 "[第 a–b、c 章](/zh/<slug-of-min-new>)"。
// 6. 链接目标 = NEW 编号最小者对应的 slug(保证点击后第一眼就到本范围)。
// 7. 跳过:frontmatter、代码块、行内代码、已有 markdown 链接、HTML 属性值。
const READING_ORDER = [
"ch00-overview",
"ch38-sw-design-principles",
"ch39-sdlc-methodologies",
"ch01-modeling",
// ... 完整顺序,按 _meta.ts 顺序
];
const oldToNew = new Map();
const oldToSlug = new Map();
for (let i = 0; i < READING_ORDER.length; i++) {
const m = READING_ORDER[i].match(/^ch(\d+)/);
if (!m) continue;
oldToNew.set(parseInt(m[1], 10), i);
oldToSlug.set(parseInt(m[1], 10), READING_ORDER[i]);
}
// 正则要覆盖:第 N 章 / 第 N-M 章 / 第 N、M 章 / 第 N 与 M 章 / 第 N-M、K 章
const re = new RegExp(
"第\\s*((?:\\d+)(?:\\s*(?:[-\u2013\u2014\u3001,\uFF0C]|\u4E0E|\u548C)\\s*\\d+)*)\\s*章",
"g",
);
规则要点:
---...---)、fenced code(``` 之间)、inline code(单反引号之间)、existing markdown link([...](...))、HTML 属性值(href=、src=、id=、alt=、title=、className=)。[第 9–11、17 章] 点击会跳到 reading-order 第 9 章(实际文件 ch45-practical-oo.mdx)。第 9、10、11、17 章 → 第 9–11、17 章。tex2mdx.mjs(保留 OLD 编号)再 renumber-content.mjs。直接对手工编辑后的文件二次运行会跳过已链接化部分(按 idempotency 是 OK 的)。参考实战完整脚本:18-UML-SysML-CookBook/UML-SysML-CookBook-site/scripts/renumber-content.mjs。
next.config.mjs 中 latex: { renderer: 'katex', options: { strict: 'ignore', trust: true } }。$E=mc^2$、块级 $$\int_0^1 f(x) dx$$ 原样保留。\newcommand)必须在 tex2mdx.mjs 阶段就展开成 KaTeX 支持的语法;维护一份"宏展开映射表"。strict: 'ignore' 让 KaTeX 不抛出对未识别命令的错误(避免单个公式打挂整页)。中文优先:pages/zh/ 是事实标准内容源;middleware.js 让根路径自动重定向到 /zh。
英文骨架:pages/en/ 至少建立与 zh 同结构的占位文件,每个加:
---
title: <对应英文标题>
---
> 🚧 English translation is in progress. Contributions welcome.
theme.config.tsx i18n 中 zh 排在前。
dist/<Project>.pdf → public/downloads/<Project>.pdf
dist/<Project>-LaTeX.zip → public/downloads/
components/PdfDownload.tsx:
export default function PdfDownload({ href, label }: { href: string; label: string }) {
return (
<a className="download-btn" href={href} download>⬇️ {label}</a>
)
}
pages/zh/downloads.mdx 提供下载页(侧边栏底部 separator 之后)。
/* Callout 卡片 —— 章节大纲 / 核心要点 / Recipe / Pitfall */
.callout {
border-left: 4px solid;
padding: 0.75em 1em;
margin: 1em 0;
border-radius: 4px;
}
.callout h4 { margin-top: 0; }
.callout.chapteroutline { background: rgba(59,130,246,0.08); border-color: #3b82f6; }
.callout.keypoints { background: rgba(34,197,94,0.08); border-color: #22c55e; }
.callout.recipe { background: rgba(245,158,11,0.08); border-color: #f59e0b; }
.callout.pitfall { background: rgba(239,68,68,0.08); border-color: #ef4444; }
/* 暗色模式微调 */
.dark .callout.chapteroutline { background: rgba(59,130,246,0.16); }
.dark .callout.keypoints { background: rgba(34,197,94,0.16); }
.dark .callout.recipe { background: rgba(245,158,11,0.16); }
.dark .callout.pitfall { background: rgba(239,68,68,0.16); }
/* figure + caption */
figure { margin: 1.25em 0; text-align: center; }
figure img { max-width: 100%; height: auto; }
figcaption {
font-size: 0.875em;
color: var(--nx-color-fg-default, #6b7280);
margin-top: 0.5em;
}
/* 表格横向滚动(防止超宽表撑破移动端布局) */
.overflow-x-auto { overflow-x: auto; }
.overflow-x-auto > table { min-width: 540px; }
/* 下载按钮 */
.download-btn {
display: inline-block;
padding: 0.5em 1em;
margin-right: 0.5em;
border-radius: 6px;
background: #2563eb;
color: #fff;
font-weight: 600;
text-decoration: none;
}
.download-btn:hover { background: #1d4ed8; }
.download-btn.secondary { background: #6b7280; }
.download-btn.secondary:hover { background: #4b5563; }
/* 封面网格 */
.cover-grid {
display: grid;
grid-template-columns: auto 1fr;
gap: 1.5em;
align-items: start;
margin: 1em 0 2em;
}
@media (max-width: 640px) {
.cover-grid { grid-template-columns: 1fr; }
}
pages/_app.tsx:
import type { AppProps } from 'next/app'
import '../styles/globals.css'
export default function App({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
}
提供本地/服务器自托管能力,与 Vercel 并列为推荐部署方式。
Dockerfile(三阶段构建):
# ===== 阶段 1: 依赖安装 =====
FROM node:18-alpine AS deps
WORKDIR /app
RUN apk add --no-cache libc6-compat
COPY package.json package-lock.json* .npmrc* ./
RUN if [ -f package-lock.json ]; then \
npm ci --no-audit --no-fund; \
else \
npm install --no-audit --no-fund; \
fi
# ===== 阶段 2: 构建 =====
FROM node:18-alpine AS builder
WORKDIR /app
ENV NEXT_TELEMETRY_DISABLED=1
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
# ===== 阶段 3: 运行时 =====
FROM node:18-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
ENV PORT=3000
ENV HOSTNAME=0.0.0.0
RUN addgroup -g 1001 -S nodejs && adduser -S nextjs -u 1001
# 关键:Next.js standalone 输出
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]
关键点:必须在
next.config.mjs中开启output: 'standalone',否则.next/standalone/server.js不会生成。
docker-compose.yml:
services:
cookbook:
build:
context: .
dockerfile: Dockerfile
image: <project-name>-site:latest
container_name: <project-name>
restart: unless-stopped
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- PORT=3000
- HOSTNAME=0.0.0.0
- NEXT_TELEMETRY_DISABLED=1
healthcheck:
test: ["CMD-SHELL", "wget -q -O - http://127.0.0.1:3000/zh > /dev/null 2>&1 || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
.dockerignore(关键:避免拷贝 node_modules / .next / git 等大文件):
node_modules
.next
.vercel
out
dist
.git
.gitignore
.dockerignore
Dockerfile
docker-compose.yml
README.md
DEPLOY.md
*.log
.DS_Store
.env*.local
coverage
.idea
.vscode
*.tsbuildinfo
一键启动:
docker compose up -d --build
# 容器会自动 healthcheck,约 30 秒后 Up healthy
# 浏览器打开 http://localhost:3000 → 自动跳转 /zh
镜像体积参考:Alpine + Node 18 + Next standalone ≈ 250–270 MB。
Docker 部署验证清单:
docker ps --filter "name=<project-name>"
# 期望:STATUS = Up X seconds (healthy)
docker logs -f <project-name>
# 期望:Ready in NNNms
curl -sI http://127.0.0.1:3000/ # 期望 308 → /zh
curl -sI http://127.0.0.1:3000/zh # 期望 200
curl -sI http://127.0.0.1:3000/zh/ch00-overview # 期望 200
curl -sI http://127.0.0.1:3000/figures/<some-fig>.svg # 期望 200
DEPLOY.md 指引:
## 上传到 GitHub
cd <ProjectName>-site
git init && git add . && git commit -m "feat: init nextra docs from latex"
gh repo create <user>/<repo> --public --source=. --remote=origin --push
## 部署到 Vercel
1. 打开 https://vercel.com/new
2. Import Git Repository → 选择刚才的仓库
3. Framework Preset:Next.js(自动识别)
4. Build Command:next build(默认)
5. Output Directory:.next(默认)
6. Node.js Version:18.x 或 20.x
7. 点击 Deploy
可选生成 vercel.json(一般无需):
{ "framework": "nextjs", "buildCommand": "next build", "installCommand": "npm install" }
.gitignorenode_modules
.next
.vercel
dist
*.log
.DS_Store
.env*.local
注意 public/figures/ 不能被 ignore——这是站点资源。
0. 询问关键参数:LaTeX 路径、主语言、Logo、GitHub URL、是否需要 Docker、是否需要按阅读顺序重编号
1. 定位 LaTeX 工程,识别 main.tex 的 \input 顺序,列出全部 chapters/*.tex
2. 与用户确认 reading order(侧边栏阅读顺序),保存为 READING_ORDER 数组
3. 扫描 diagrams/ 与 figures/,建立"图表清单 + 来源 + 目标路径"
4. 渲染 PlantUML:docker run plantuml/plantuml:latest 批量产 .svg + .png + 双别名
5. 渲染 Draw.io:drawio-desktop 或 Docker drawio-headless 批量产 .drawio.svg
6. 拷贝 LaTeX figures/* 到 public/figures/;.pdf 矢量图先转 svg
7. 初始化 Nextra 工程:package.json + next.config.mjs (output: standalone) + theme.config.tsx + middleware.js
8. 写 styles/globals.css(callout / figure / overflow-x-auto / 下载按钮)
9. 写 scripts/tex2mdx.mjs:自写解析器或 pandoc + 后处理
10. 批量转换 chapters/*.tex → pages/zh/*.mdx(含 frontmatter title、figure id 锚点、callout 包装)
11. 生成英文骨架 pages/en/*.mdx(仅 frontmatter + TODO 提示)
12. 写 scripts/build-meta.mjs,生成 pages/zh/_meta.ts 和 pages/en/_meta.ts(含 separator)
13. (reading order ≠ \input order 时)写 scripts/renumber-content.mjs,把跨章引用链接化
14. 拷贝 PDF / LaTeX zip 到 public/downloads/,写 PdfDownload 组件 + downloads.mdx
15. npm install && npm run build —— 验证生产构建(warning 允许,error 必须为零)
16. 写 Dockerfile + docker-compose.yml + .dockerignore;docker compose up -d --build 验证 healthy
17. 写 README.md(介绍 + 本地启动)与 DEPLOY.md(GitHub + Vercel + Docker 三路)
18. 输出最终目录链接 + 验证报告
原因:.puml 未指定 CJK 字体。
优化:每个 .puml 加 skinparam defaultFontName "WenQuanYi Micro Hei";render-plantuml.mjs 启动时校验"含 CJK 字符但无 CJK 字体声明"。
fig35-x.svg 不存在)原因:PlantUML 输出名跟随 @startuml NAME,与文件名 fig35-x.puml 不一致。
优化:渲染脚本必须同时产出两个文件名(一份按文件名 figXX,一份按 alias)。
原因:脚本中 $($var:foo)x$($var:bar) 在中文 GBK PowerShell 下解析失败。
优化:绕开 PS1,直接 docker run plantuml/plantuml:latest。
<"原因:LaTeX 正文里的裸 <、> 被 MDX 当作 JSX 标签开始。
优化:转换阶段全局转义裸 </> 为 </>,或包裹在反引号代码中。
原因:pandoc 默认按词换行,中文遇空格被切。 优化:转换后用正则把 CJK 字符之间的换行/空格收紧(保留段落空行)。
原因:reading order ≠ 文件名字典序,只改了 _meta.ts 没跑 renumber-content.mjs。
优化:每次改阅读顺序后必须重跑 node scripts/renumber-content.mjs。
原因:脚本不幂等,二次运行把已是 NEW 编号的链接又按 OLD 编号映射一次。
优化:skip mask 必须包含 existing markdown links;先在干净的 MDX(刚 tex2mdx 完)上跑。如果必须二次运行,唯一安全做法是先 node scripts/tex2mdx.mjs 把 MDX 回滚到 OLD 编号状态再重跑。
原因:next.config.mjs 未设 output: 'standalone',.next/standalone/server.js 不存在;或 public/ 没拷贝。
优化:检查 next.config.mjs 配置;Dockerfile 中 COPY --from=builder /app/public ./public 必须存在。
原因:英文目录缺少同名文件。
优化:构建 pages/en/ 时严格镜像 pages/zh/ 的文件名结构;缺失页面生成占位 mdx。
原因:Nextra v3 的 pages/zh/ + pages/en/ 文件夹布局与 Next.js 内置 i18n 同时存在。
优化:这是非阻塞告警,Nextra 自己接管路由,可忽略。
原因:.npmrc 含 pnpm 专属配置项。
优化:要么删除 .npmrc(纯 npm),要么忽略(用 pnpm)。非阻塞。
原因:默认 Mermaid 主题与暗背景对比度低。
优化:在 theme.config.tsx 中通过 Nextra 的 mermaid 主题配置切换 default / dark。
原因:宽表未加滚动容器。
优化:所有 HTML 表格统一包裹 <div className="overflow-x-auto">;styles/globals.css 给 table 设 min-width: 540px。
原因:未启用 latex: { renderer: 'katex' },或自定义宏 KaTeX 不支持。
优化:检查 next.config.mjs;维护宏展开映射表;strict: 'ignore' 容错。
原因:依赖未声明 / next.config.mjs 引入路径不对。
优化:确认 dependencies 里有 nextra 与 nextra-theme-docs,且版本兼容(v3.x + Next.js 14.x)。
原因:把 node_modules 或大量 PDF 一并提交。
优化:完善 .gitignore;若 PDF > 50MB,移出仓库改放对象存储。
原因:脚本顶部 const SRC_LATEX = 'D:/old-path/latex' 没跟着项目迁移更新。
优化:脚本第一行的 SRC_LATEX 必须随项目搬迁同步更新;推荐改为读取环境变量 process.env.SRC_LATEX || default。
原因:-v 挂载路径未加引号。
优化:始终用 -v "${path}:/data" 包双引号;用 Node 的 path.resolve() 确保绝对路径。
LaTeX 源 / 章节顺序 / 图表更新后的标准刷新流程:
# 1. 重渲所有 PlantUML(需 Docker)
node scripts/render-plantuml.mjs
# 2. LaTeX → MDX(覆盖式重生,保留 OLD 编号)
node scripts/tex2mdx.mjs
# 3. 重写侧边栏标签(reading-order 编号)
node scripts/build-meta.mjs
# 4. 把正文中的 "第 N 章" 跨章引用链接化为 NEW 编号
node scripts/renumber-content.mjs
# 5. 验证
npm run build
# 6. 部署
docker compose up -d --build # 自托管
# 或 git push && Vercel 自动 redeploy
请把 d:\TARE-WORK\11-信息论 工程转换成基于 Nextra 的中文电子文档网站,
保留 PlantUML 图表和封面图,支持明暗模式切换,并打包成可上传 GitHub、
可一键部署到 Vercel 与 Docker 的项目文件夹。
侧边栏需要按阅读顺序重编号。
执行步骤:
d:\TARE-WORK\11-信息论\main.tex 与 chapters/、diagrams/*.puml、figures/*。READING_ORDER 数组。docker run plantuml/plantuml:latest 渲染 .puml 为 SVG + PNG(不走 plantuml skill 的 PS1 脚本)。d:\TARE-WORK\信息论-site/ 下生成 Nextra 工程。main.tex 顺序 tex2mdx.mjs 生成 pages/zh/*.mdx(保留 OLD 编号)。_meta.ts(含 separator 分组)。renumber-content.mjs,把所有 "第 N 章" 跨章引用按 reading order 重编号并链接化。public/downloads/。Dockerfile、docker-compose.yml、.dockerignore、middleware.js、styles/globals.css。npm run build 验证;docker compose up -d --build 验证 healthy。README.md 与 DEPLOY.md。最终交付时应包含:
computer:// 链接。npm install && npm run dev)。docker compose up -d --build)+ 容器健康状态截图/输出。/zh、典型章节、PlantUML SVG 均返回 200。