How I Built a Live Web Editor for RenderCV Using Gemini

Resumes as Code: The Elegance of RenderCV

If you are an engineer or an academic, writing resumes in MS Word is a formatting nightmare, and custom LaTeX templates are often too verbose to maintain.

Recently, I stumbled upon a brilliant Python project called RenderCV. It allows you to write your resume content in a clean, structured YAML file and generates a beautifully formatted PDF with perfect typography using Typst under the hood. It treats your resume like code, which means you can version-control it, focus strictly on the content, and let the tool handle page margins, alignment, and styling.

However, RenderCV was designed primarily as a command-line interface (CLI) tool. Every time you make a change, you have to run rendercv render John_Doe_CV.yaml, check the output, open the PDF viewer, and check the alignment. If you make a validation or schema error, you have to read CLI compiler tracebacks.

I wanted to bridge this gap by adding a Live Web Editor—a local single-page web app with a side-by-side Monaco editor, live PDF preview, instant schema validation, and interactive theme/section toggles.

With the help of Gemini, I successfully built and integrated this web interface directly into RenderCV! Here is how we did it.


The Tech Stack and Architecture

To keep the web editor lightweight, fast, and dependency-minimal, we chose:

  1. Backend: FastAPI + Uvicorn — perfect for writing high-performance APIs and serving static assets.
  2. Frontend: Vanilla HTML5 + Modern CSS + Javascript — avoiding bloated build steps (like Webpack or Vite) for a local CLI helper. We used the Monaco Editor (via CDN) for a VS Code-grade YAML writing experience and Lucide Icons for smooth visual elements.
  3. Rendering: RenderCV Python API — calling the library’s internal models directly to parse, validate, and build Typst and PDF documents in-memory or inside temp directories.

1. Designing the FastAPI Backend (web_app.py)

The core of the server is the /api/render endpoint. When the user types YAML in the web UI, the frontend sends it to this endpoint. The backend handles three major tasks:

  1. Rendering the PDF: We compile the YAML string using RenderCV’s internal compilation functions and return the raw PDF bytes.
  2. Section Visibility Toggling: RenderCV allows you to hide or show sections of your CV. We parse the incoming YAML, identify all available CV sections, strip any sections the user chose to hide in the UI dropdown, and compile the final document.
  3. Structured Error Mapping: When validation fails, RenderCV throws a RenderCVUserValidationError. We intercept this error, extract the exact line and column numbers of the invalid YAML field, and return a clean JSON error response so Monaco can display red squiggly lines at the exact position of the error!

Here is how the /api/render endpoint looks in 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):
"""Compile the provided YAML to a PDF and return the PDF file bytes."""
with tempfile.TemporaryDirectory() as tmpdir:
tmp_path = pathlib.Path(tmpdir)
try:
# Parse YAML into RenderCV's internal dictionary
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,
)

# Extract all sections to return to the 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())

# Programmatically remove hidden sections
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)

# Validate and build model
model = build_rendercv_model_from_commented_map(input_dict, tmp_path, overlay_sources)

# Generate the Typst source code and compile to PDF
typst_path = generate_typst(model)
pdf_path = generate_pdf(model, typst_path)
pdf_bytes = pdf_path.read_bytes()

# Send sections list in headers (URL-encoded 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:
# Map Pydantic validation errors back to YAML editor coordinates
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 validation failed", "errors": errors}
)
# ... other error handling

2. A Premium Web Interface (index.html)

For the user interface, we wanted something that felt premium, responsive, and fully immersive. Gemini helped me code a beautiful modern layout featuring:

  • HSL Dark Theme: Deep slate backgrounds, semi-transparent overlays (backdrop-filter: blur), and vibrant purple linear-gradient accents.
  • Side-by-Side Splitscreen: Monaco Editor on the left, live PDF viewer (using an <iframe> loaded with a local Blob URL) on the right.
  • Dynamic Controls:
    • A Theme Switcher dropdown to load templates (classic, engineering, moderncv, etc.) and merge your current CV data into them automatically.
    • A Sections Dropdown which queries the backend headers and generates checklists to dynamically hide/show resume sections (e.g. hiding “Projects” or “References” when exporting target-specific versions of your CV).
    • An inline Validation Log console at the bottom of the editor that expands to show error details. Clicking on any validation error automatically highlights and focuses the line in the Monaco editor.

To compile, the user can either click the “Render PDF” button or use the keyboard shortcut Cmd + Enter. The PDF compiles in less than a second!


3. Wrapping it into the CLI (web_command.py)

To make it incredibly easy for users to start, we added a new command to the RenderCV Typer-based CLI:

1
rendercv web

This launches the local Uvicorn FastAPI server and opens your web browser automatically:

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="Start the RenderCV web editor for live editing and PDF compilation."
)
@handle_user_errors
def cli_command_web(
host: str = "127.0.0.1",
port: int = 8000,
no_browser: bool = False,
):
typer.echo(f"Starting RenderCV web editor on 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)

Reflections on Coding with Gemini

Building this feature with Gemini was exceptionally fast. What would have normally taken days of reading internal source code and configuring Monaco API configurations was solved in hours:

  1. Understanding the AST/YAML library: Gemini correctly identified that we should use ruamel.yaml to parse the YAML file to preserve user quotes, comments, and structure when switching themes.
  2. Error mapping: Gemini figured out exactly how to traverse RenderCVUserValidationError coordinates and construct the Monaco validation marker payloads (monaco.editor.setModelMarkers) so the editor is fully synchronized with Pydantic validation.
  3. Polishing UX: It crafted CSS variables and layout offsets that look gorgeous, scale nicely across multiple screen resolutions, and are super responsive.

If you write resumes, go check out RenderCV—and try running rendercv web to experience the live editor yourself!

Happy coding!