我是如何利用 Gemini 为 RenderCV 构建实时网页编辑器的

简历即代码:RenderCV 的优雅之处

如果你是一名工程师或学术界人士,用 MS Word 撰写简历简直是一场排版噩梦,而自定义的 LaTeX 模板又往往过于冗长,难以维护。

最近,我偶然发现了一个非常棒的 Python 项目——RenderCV。它允许你在一个干净、结构化的 YAML 文件中编写简历内容,并在后台使用 Typst 生成排版完美、排版精美的 PDF。它把你的简历当成代码来对待,这意味着你可以对其进行版本控制,只专注于内容,让工具来处理页边距、对齐和样式。

然而,RenderCV 最初主要被设计为一个命令行界面 (CLI) 工具。每次你做出修改,都必须运行 rendercv render John_Doe_CV.yaml,查看输出,打开 PDF 查看器并检查对齐。如果遇到验证或 schema 错误,你还得阅读 CLI 编译器的回溯信息。

我希望通过添加一个实时网页编辑器 (Live Web Editor) 来弥补这一缺憾——这是一个本地单页 Web 应用程序,包含左右分栏的 Monaco 编辑器、实时 PDF 预览、即时模式验证以及交互式主题/模块开关。

Gemini 的帮助下,我成功构建了这个 Web 界面并将其直接集成到了 RenderCV 中!以下是我们的实现过程。


技术栈与架构

为了保持网页编辑器的轻量、快速和最小化依赖,我们选择了:

  1. 后端: FastAPI + Uvicorn —— 非常适合编写高性能 API 和托管静态资源。
  2. 前端: 原生 HTML5 + 现代 CSS + Javascript —— 避免了本地 CLI 辅助工具臃肿的构建步骤(如 Webpack 或 Vite)。我们使用了 Monaco Editor(通过 CDN)以获得 VS Code 级别的 YAML 编写体验,并使用 Lucide Icons 来呈现精美的视觉元素。
  3. 渲染: RenderCV Python API —— 直接调用库的内部模型,在内存或临时目录中解析、验证并构建 Typst 和 PDF 文档。

1. 设计 FastAPI 后端 (web_app.py)

服务器的核心是 /api/render 接口。当用户在网页 UI 中输入 YAML 时,前端会将数据发送到该接口。后端主要处理三个任务:

  1. 渲染 PDF: 我们使用 RenderCV 内部的编译函数编译 YAML 字符串,并返回原始 PDF 字节流。
  2. 模块显示隐藏控制: RenderCV 允许你显示或隐藏简历中的某些板块。我们解析传入的 YAML,识别所有可用的简历板块,过滤掉用户在 UI 下拉菜单中选择隐藏的板块,然后编译生成最终文档。
  3. 结构化错误映射: 当验证失败时,RenderCV 会抛出 RenderCVUserValidationError。我们拦截此错误,提取出无效 YAML 字段的确切行号和列号,并返回干净的 JSON 错误响应,以便 Monaco 能在错误发生的精确位置显示红色波浪线!

以下是 /api/render 接口在 Python 中的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
@app.post("/api/render")
def render_pdf(request: RenderRequest):
"""将提供的 YAML 编译为 PDF 并返回 PDF 文件字节流。"""
with tempfile.TemporaryDirectory() as tmpdir:
tmp_path = pathlib.Path(tmpdir)
try:
# 将 YAML 解析为 RenderCV 的内部字典
input_dict, overlay_sources = build_rendercv_dictionary(
request.yaml,
output_folder=tmp_path,
dont_generate_png=True,
dont_generate_markdown=True,
dont_generate_html=True,
)

# 提取所有板块以便返回给 UI
all_sections = []
if "cv" in input_dict and "sections" in input_dict["cv"] and input_dict["cv"]["sections"]:
all_sections = list(input_dict["cv"]["sections"].keys())

# 程序化移除被隐藏的板块
if request.hide_sections:
for sec in request.hide_sections:
if "cv" in input_dict and "sections" in input_dict["cv"] and input_dict["cv"]["sections"]:
input_dict["cv"]["sections"].pop(sec, None)

# 验证并构建模型
model = build_rendercv_model_from_commented_map(input_dict, tmp_path, overlay_sources)

# 生成 Typst 源代码并编译为 PDF
typst_path = generate_typst(model)
pdf_path = generate_pdf(model, typst_path)
pdf_bytes = pdf_path.read_bytes()

# 在响应头中发送板块列表 (URL 编码的 JSON)
headers = {
"X-RenderCV-Sections": urllib.parse.quote(json.dumps(all_sections)),
"Access-Control-Expose-Headers": "X-RenderCV-Sections"
}
return Response(content=pdf_bytes, media_type="application/pdf", headers=headers)

except RenderCVUserValidationError as e:
# 将 Pydantic 验证错误映射回 YAML 编辑器坐标
errors = []
for err in e.validation_errors:
start_line = err.yaml_location[0][0] if err.yaml_location else None
start_col = err.yaml_location[0][1] if err.yaml_location else None
errors.append({
"message": err.message,
"line": start_line,
"col": start_col,
"schema_location": err.schema_location
})
return JSONResponse(
status_code=422,
content={"type": "validation_error", "detail": "YAML 验证失败", "errors": errors}
)
# ... 其他错误处理

2. 精美的网页界面 (index.html)

对于用户界面,我们希望它具有高级感、响应迅速且完全沉浸。Gemini 帮我编写了一个漂亮的现代布局,包含以下特色:

  • HSL 暗黑主题: 深色石板路背景、半透明覆盖层(backdrop-filter: blur)以及充满活力的紫色线性渐变点缀。
  • 左右分栏: 左侧是 Monaco 编辑器,右侧是实时 PDF 预览器(使用加载了本地 Blob URL 的 <iframe>)。
  • 动态控制面板:
    • 主题切换器下拉菜单,用于加载模板(classic、engineering、moderncv 等)并自动将你当前的简历数据合并其中。
    • 板块控制下拉菜单,通过向后端查询响应头数据生成复选框,从而动态显示/隐藏简历板块(例如在导出针对性简历时,隐藏“项目”或“证明人”板块)。
    • 编辑器底部的内联验证日志控制台,展开可显示错误详情。点击任何验证错误,Monaco 编辑器会自动跳转并定位到该行。

要进行编译,用户可以点击“Render PDF”按钮,或使用快捷键 Cmd + Enter。编译 PDF 只需不到一秒钟!


3. 将其封装进 CLI (web_command.py)

为了让用户极易上手,我们为基于 Typer 的 RenderCV CLI 添加了一个新命令:

1
rendercv web

这会自动启动本地的 Uvicorn FastAPI 服务器并打开浏览器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@app.command(
name="web",
help="启动 RenderCV 网页编辑器以进行实时编辑和 PDF 编译。"
)
@handle_user_errors
def cli_command_web(
host: str = "127.0.0.1",
port: int = 8000,
no_browser: bool = False,
):
typer.echo(f"正在启动 RenderCV 网页编辑器,地址:http://{host}:{port}")

if not no_browser:
def open_browser():
time.sleep(1.2)
webbrowser.open(f"http://{host}:{port}")

threading.Thread(target=open_browser, daemon=True).start()

uvicorn.run(web_app, host=host, port=port)

与 Gemini 共同编程的反思

使用 Gemini 开发这个功能的速度出奇地快。原本需要花几天时间阅读内部源码并配置 Monaco API 选项的工作,在几个小时内就完成了:

  1. 理解 AST/YAML 库: Gemini 正确指出我们应该使用 ruamel.yaml 来解析 YAML 文件,以便在切换主题时保留用户的引号、注释和原有结构。
  2. 错误位置映射: Gemini 精确地算出了如何遍历 RenderCVUserValidationError 坐标,并构造出 Monaco 验证标记负载(monaco.editor.setModelMarkers),使得编辑器与 Pydantic 的校验完全同步。
  3. 打磨 UX: 它精心设计了 CSS 变量和布局间距,看起来非常精美,在不同的屏幕分辨率下都能够完美适配并保持流畅响应。

如果你经常需要写简历,去了解一下 RenderCV 吧——并试着运行 rendercv web 亲身体验一下实时网页编辑器!

祝 coding 愉快!