Erro
That cool thing to look interesting in the world of work
About
Yup. I host my LaTeX CV on a dedicated repository on GitHub and release every version of it using dedicated Releases. I really enjoyed the idea to review it as a coding project, making it buildable as a common project using build automation tools and release its binary (PDF) version.
Build it yourself
If you want to fork the project and produce your own resume, the local toolchain is:
- Python
>=3.13anduvfor the templating script (uv syncpulls Jinja2 and PyYAML). maketo drive the build (make, then artifacts land underbuild/).pdflatexto compile the LaTeX targets — on Debian/Ubuntu installtexlive-latex-recommendedandtexlive-fonts-extra; HTML and plain-text targets need no LaTeX.
Source data lives under src/langs/*.yml and templates under src/templates/*.{tex,html,txt}.j2.
Download
To download latest version, please, reach latest GitHub repository release. Otherwise, if you want to grab it from the shell (or in any other way you prefer), you can use (or adapt) the following snippet:
lang="en" # supported: en, it
variant="europass" # supported: europass, personal
release="https://github.com/streambinder/erro/releases/latest"
curl "${release}/download/${variant}_${lang}.pdf" -o resume.pdf
The same release also publishes non-PDF variants generated from the very same data source:
web_{en,it}.html— single-page HTML rendition, designed to be iframed on a personal page.plain_{en,it}.txt— plain-text rendition, handy for diffing or piping into other tools.
New releases are tagged YYYY-MM-DD (UTC). A same-day re-push rolls the tag forward in place rather than minting a new one, so latest always points at the freshest build of the day.
Design
The first issue I faced was about few companies being very strict on the allowed CV format, hence asking for the EuroPass one, for example. On the other hand, I really enjoyed my custom format and didn't want to just drop it. And what if another company would have come asking for a CV formatted following another format?
All of this led me considering templating my documents: many tex files as many formats I wanted to support and a single database file keeping the information used to fill the templates.
On the other hand, I wanted my CV to be internationalized, maintaining a different version of each different format for each language supported.
After a first round in which I did implement all the features on my own (look at erro@be0c83e if interested), I moved to a more solid structure.
Templating
Different formats, or templates, are to be made and accessing same data based on a sort of identifiers. Despite how simple this issue could look, it's definitely not, at least if you want to keep something to be somehow proud.
Looking for a nice way to replace my old and misfiring templating engine, I discovered jinja.
This library allows you to define block_start_string, variable_start_string and many other nice things to make it able to detect where exactly in your template you want placeholders and identifiers to be replaced with actual content.
This scales really good for LaTeX documents, actually:
- Python renderer:
latex = jinja2.Environment(
block_start_string='\\jblock{',
block_end_string='}',
variable_start_string='\\jvar{',
variable_end_string='}'
)
latex.get_template('template.tex') \
.stream(author='streambinder') \
.dump('resume.tex')
- LaTeX template
template.tex:
\begin{document}
\jvar{author}'s CV.
\jblock{for i in range(5)}
\jblock{if i % 2 == 0}
\textit{ \jvar{i} }
\jblock{endif}
\jblock{endfor}
\end{document}
- LaTeX rendered
resume.tex:
\begin{document}
streambinder's CV.
\textit{ 0 }
\textit{ 2 }
\textit{ 4 }
\end{document}
Making the engine take the rendering parameters from a database file was pretty easy, too. The same result above can be achieved the following way:
with open('database.yaml', 'r') as database_fd:
latex.get_template('template.tex')
.stream(yaml.safe_load(database_fd))
.dump('resume.tex')
With database.yaml like below:
author: streambinder
Internationalization
Once you have a perfectly working template engine with support for external data source, supporting internationalization is straightforward: introduce a different database file for each language.
- Python renderer:
for lang in ['en', 'it']:
with open('database_{}.yaml'.format(lang), 'r') as database_fd:
latex.get_template('template.tex')
.stream(yaml.safe_load(database_fd))
- YAML english database
database_en.yaml:
author: streambinder in english
- YAML italian database
database_it.yaml:
author: streambinder in italiano
Inline formatting in the database
The YAML data source supports a tiny markdown-inspired subset that gets translated on the fly to whatever the target template renders:
*word*— emphasis. Becomes\emph{word}for LaTeX targets,<em>word</em>for HTML, plainwordfor text.[label](https://example.com)— link. Becomes\href{https://example.com}{label}for LaTeX,<a href="https://example.com">label</a>for HTML,label (https://example.com)for text.
LaTeX special characters in surrounding prose (& % $ # _ { } ~ ^ \) are escaped automatically before rendering, so the YAML stays portable across the LaTeX, HTML and plain-text outputs without per-target authoring.
Output formats
Each language config is fanned out across three Jinja environments (tex, html, txt) with different delimiter sets, so the same data drives three distinct artifact families:
- LaTeX →
personal_{en,it}.pdf,europass_{en,it}.pdf(compiled viapdflatex). - HTML →
web_{en,it}.html(used as the iframe target ondavidepucci.it/resume). - Plain text →
plain_{en,it}.txt(greppable, diffable, machine-friendly variant).
All variants ship together as assets of a single GitHub Release; see download for the URL pattern.