A template engine that generates HTML and PDF resumes from a single JSON data source.
# Install dependencies
bun install
# Start development server with live reload
bun dev
# Build HTML and PDF for production
bun run build
# Build HTML only (faster)
bun run build:htmlOutputs are generated in the docs/ folder.
Start the dev server for real-time preview:
bun devThis starts a local server at http://localhost:3000 with:
- Live reload - Browser auto-refreshes when files change
- File watching - Monitors
resume.json,src/html/, andpublic/ - Hot reloading - Uses Bun's
--hotmode for instant server updates without restart - Fast rebuilds - Only rebuilds HTML (skips PDF for speed)
The dev server is ideal for iterating on your resume content and styling. When you're ready to generate the final PDF, run bun run build.
For sensitive information (phone number, address, etc.) that shouldn't be in the public resume:
- Create
.hidden/secret.jsonwith private data:
{
"contact": {
"phone": "+1 (555) 123-4567"
}
}- Run
bun run build- this generates:docs/<name>_cv.pdf- Public version (no phone).hidden/<name>_cv.pdf- Private version (with phone)
PDF filenames are automatically generated from the name field (e.g., "Juan Almanza" → juan_almanza_cv.pdf).
The .hidden/ folder is gitignored. The secret data is deep-merged with resume.json, so you can override any field.
# macOS
brew install texlive
# Ubuntu/Debian
sudo apt-get install texlive-full
# Windows
# Install MiKTeX or TeX Livecv/
├── resume.json # Your resume data (edit this)
├── .hidden/ # Private data (gitignored)
│ ├── secret.json # Sensitive info (phone, etc.)
│ └── <name>_cv.pdf # Private PDF with all data
├── src/
│ ├── engine.ts # Template engine core
│ ├── index.ts # Build script
│ ├── dev.ts # Development server
│ ├── html/
│ │ └── index.html # HTML template
│ └── latex/
│ └── resume.tex # LaTeX template
├── public/
│ └── theme.css # HTML styling
└── docs/ # Build output (public)
├── index.html
└── <name>_cv.pdf
Edit resume.json with your information:
{
"name": "Your Name",
"location": "City, State",
"contact": {
"email": "you@example.com",
"website": { "label": "yoursite.com", "url": "https://yoursite.com" },
"github": { "label": "username", "url": "https://github.com/username" }
},
"education": [
{
"institution": "University",
"degree": "BS in Field",
"duration": "Aug 2020 – May 2024",
"highlights": ["**Coursework:** Subject 1, Subject 2"]
}
],
"experience": [
{
"company": "Company",
"position": "Title",
"location": "City, State",
"duration": "Jan 2023 – Present",
"highlights": [
"Achievement with **bold** emphasis and metrics."
]
}
],
"projects": [
{
"name": "Project Name",
"url": "https://project.com",
"url_label": "project.com",
"highlights": ["Description of what you built."]
}
],
"volunteering": [
{ "org": "Organization", "description": "What you did." }
],
"honors": [
{ "title": "Award", "description": "Details", "year": "2024" }
],
"skills": [
{ "category": "Languages", "items": "Python, JavaScript, C++" }
]
}Access data using dot notation for nested values.
HTML templates:
<h1>{{name}}</h1>
<p>{{contact.email.personal}}</p>LaTeX templates (use @{...}@ to avoid conflicts with LaTeX syntax):
\textbf{@{name}@}
\href{mailto:@{contact.email.personal}@}{Email}Include other template files using <<path>>. Paths are relative to the current template's directory.
<!-- In src/html/index.html -->
<<header.html>>
<<components/email.html>>% In src/latex/resume.tex
<<sections/education.tex>>Iterate over arrays with [[for item in array]]:
[[for job in experience]]
<div class="job">
<h3>{{job.company}}</h3>
<p>{{job.position}}</p>
[[for task in job.responsibilities]]
<li>{{task}}</li>
[[endfor]]
</div>
[[endfor]]Loops can be nested. The inner loop accesses the parent's iterator variable.
Show content only if a field exists:
[[if contact.phone]]
<p>Phone: {{contact.phone}}</p>
[[endif]][[if contact.phone]] $\cdot$ \phone{@{contact.phone}@}[[endif]]Conditionals can be nested and work with any field path.
LaTeX alternative syntax with {{#each}}:
{{#each skills}}
\item {{this}}
{{/each}}Use markdown-style **bold** in your JSON data:
{
"summary": "Expert in **machine learning** and **data analysis**."
}This automatically converts to:
- HTML:
<strong>machine learning</strong> - LaTeX:
\textbf{machine learning}
Email addresses in HTML are automatically formatted as user [at] domain.com for spam protection. This does not apply to mailto: links, which preserve the original format.
These characters are automatically escaped in LaTeX output:
| Character | Escaped As |
|---|---|
\ |
\textbackslash{} |
& |
\& |
% |
\% |
$ |
\$ |
# |
\# |
_ |
\_ |
{ |
\{ |
} |
\} |
^ |
\textasciicircum{} |
~ |
\textasciitilde{} |
The build process runs pdflatex twice to ensure proper resolution of references and links. Temporary files (.aux, .log, .out) are automatically cleaned up.
- HTML: Edit
public/theme.cssfor web styling - LaTeX: Modify packages and formatting in
src/latex/resume.tex
- Create a new template file (e.g.,
src/html/publications.html) - Add the include directive to the main template:
<<publications.html>> - Add corresponding data to
resume.json
Reusable components go in src/html/components/. Include them with:
<<components/social-link.html>>- Verify
pdflatexis installed:which pdflatex - Check for LaTeX syntax errors in the console output
- Ensure all required LaTeX packages are installed
- Look for unescaped special characters (the engine handles most, but custom LaTeX code needs manual escaping)
- Check for typos in variable names
- Verify the path matches your JSON structure (use dot notation:
contact.email.personal) - Ensure arrays use the correct
[[for]]syntax
- Run
bun installto ensure dependencies are installed - Check that
resume.jsonis valid JSON (no trailing commas, proper quotes)
The template engine exports these functions for programmatic use:
import { compile, read, write, minifyHTML } from "./src/engine";
// Compile a template with data
const html = compile("html/index.html", resumeData);
// Read a file relative to src/
const content = read("../resume.json");
// Write a file relative to src/
write("../docs/output.html", content);
// Minify HTML output
const minified = minifyHTML(html);MIT