A data-driven CV that feels like a product, not a static document.
This project renders a multilingual CV from JSON, then compiles everything into a single deploy-ready HTML file for GitHub Pages.
Dark mode and light mode side by side:
| Dark | Light |
|---|---|
![]() |
![]() |
Add your public URL here:
https://<username>.github.io/<repository>/
For this repository, that is typically:
https://txitxo0.github.io/folio-cv/
Note on auto-filling in README:
The workflow variable ${{ steps.deployment.outputs.page_url }} only exists at runtime in GitHub Actions and cannot dynamically rewrite README.md on GitHub UI. For a stable README, keep this URL manually maintained.
Instead of maintaining CV text in multiple documents, the source of truth is one JSON contract:
- Easy to edit and version.
- Easy to localize (any locale key such as
en,es,fr,pt-BR,de,it, etc). - Strictly validated through JSON Schema.
- Consistent output across local and CI builds.
Every build automatically embeds invisible metadata watermarks in the compiled HTML to prove origin and track distribution:
What is watermarked:
- Proof of origin (project name:
folio-cv) - Original author attribution (GitHub user:
txitxo0) - Build timestamp (UTC ISO 8601)
Where it's stored:
- Hidden HTML
<meta>tags in the document<head> - Invisible to the rendered page, visible in HTML source code
- Preserved in PDF exports (as document properties/metadata)
Why hardcoded:
The watermark is intentionally hardcoded to the original project (folio-cv by txitxo0). This serves two purposes:
- Proof of origin – Even if someone clones or forks this project, the watermark points back to the original creator.
- Distribution tracking – Each build is timestamped, making it possible to trace when a copy was created and distributed.
For users cloning this project:
When you clone and build folio-cv, the watermark will still reference the original project. This is intentional and transparent. You are free to modify the JSON data, styles, and functionality, but the watermark remains as attribution to the original project source.
Transparency:
- Watermark logic is visible in
build.py→generate_watermark_metadata()function - No obfuscation or hidden code
- Design choice is documented here in README
index.html
build.py
requirements.txt
README.md
assets/
css/
cv.css
js/
cv.js
data/
cv-data.json
cv-schema.json
images/
photo.jpg
dist/
index.html
Install dependencies:
python -m pip install -r requirements.txtRun in development mode:
python -m http.server 8080Open:
http://localhost:8080
Build standalone output:
python build.pyBuild artifact:
dist/index.html
Preview compiled artifact:
cd dist
python -m http.server 8081Open:
http://localhost:8081
index.htmlprovides structure.assets/js/cv.jsreadsassets/data/cv-data.json.- JSON drives header, summary, timeline, skills, formal page, and footer.
build.pyvalidates JSON againstassets/data/cv-schema.json.build.pyinlines CSS, JS, JSON, favicon, and local photo into one HTML file.
Primary data source:
assets/data/cv-data.json
Validation schema:
assets/data/cv-schema.json
The root is multilingual:
{
"$schema": "./cv-schema.json",
"cv": {
"en": { "...": "locale data" },
"es": { "...": "locale data" },
"pt-BR": { "...": "locale data" }
}
}Locale keys support BCP47-like tags (for example en, es, pt-BR, zh-Hant, en-US).
This project is locale-agnostic by design. It does not require specific locale names such as en or es.
What this means in practice:
- Any valid locale key can be used under
cv(es,pt,pt-BR,de,it,ja, etc.). build.pyvalidates every locale entry against the same schema contract.build.pyinlines local photos for all locales, not only the first one.- Runtime locale selection in
assets/js/cv.jsis generic and works with any locale key found in the JSON. <html lang>is set at runtime from the active locale.
Locale resolution order at runtime:
?lang=<locale>query param (if present and valid).- Saved locale from localStorage (
cv-locale) if valid. - First locale key in
cvas fallback.
To add a new locale safely:
- Add a new node under
cv, for exampleesorpt-BR. - Include all required sections (
meta,ui,personal,experience,skills,formal). - Run
python build.pyto validate schema and generatedist/index.html. - Optional: run
python check_build.pyto confirm inlined output integrity.
Each locale requires these sections:
metauipersonalexperienceskillsformal
Used by footer and versioning.
| Property | Type | Required | Description |
|---|---|---|---|
lastUpdated |
string | yes | Last update label (year or date). |
version |
string | yes | Human-readable data version. |
branding |
string | yes | Footer brand text. |
All locale-specific labels used by the UI.
| Property | Type | Required |
|---|---|---|
langLabel |
string | yes |
tagProfile |
string | yes |
tagCareer |
string | yes |
tagStack |
string | yes |
timelineNow |
string | yes |
timelinePast |
string | yes |
available |
string | yes |
stackNote |
string | yes |
updatedPrefix |
string | yes |
btnTheme |
string | yes |
btnPrint |
string | yes |
fWorkExp |
string | yes |
fEducation |
string | yes |
fLanguages |
string | yes |
fCertifications |
string | yes |
fTechSkills |
string | yes |
Header and summary content.
| Property | Type | Required | Description |
|---|---|---|---|
name |
string | yes | Full name. |
title |
string | yes | Professional headline. |
photo |
string | yes | Relative path or URL. |
location |
string | yes | Location label. |
contact |
object | yes | Contact info block. |
summary |
string | yes | Main summary paragraph. |
summaryImpact |
string | yes | Secondary impact paragraph. |
summaryImpactEnabled |
boolean | no | Toggle for summaryImpact. |
contact object:
| Property | Type | Required |
|---|---|---|
phone |
string | yes |
email |
string | yes |
linkedin |
string | yes |
linkedinUrl |
string (URI) | yes |
Visual timeline cards on page 1.
| Property | Type | Required | Notes |
|---|---|---|---|
id |
string | yes | Stable id. |
period |
string | yes | Date range. |
company |
string | yes | Company or institution. |
role |
string | yes | Role title. |
description |
string | yes | Supporting text. |
type |
string | yes | work, education, other. |
highlight |
boolean | yes | Featured styling. |
enabled |
boolean | no | Visibility toggle. |
Data for bars and radar.
skills.levels:
- object map with numeric string keys (
"1","2","3") and label values.
skills.categories[]:
| Property | Type | Required | Notes |
|---|---|---|---|
name |
string | yes | Category title. |
color |
string | yes | Hex color #RRGGBB. |
pastelColor |
string | no | Optional visual override. |
skills |
array | yes | Child skills. |
enabled |
boolean | no | Visibility toggle. |
skills.categories[].skills[]:
| Property | Type | Required | Notes |
|---|---|---|---|
name |
string | yes | Skill label. |
level |
integer | yes | Minimum value is 1. |
enabled |
boolean | no | Visibility toggle. |
Second-page formal CV content.
| Property | Type | Required |
|---|---|---|
address |
string | yes |
workExperience |
array | yes |
education |
array | yes |
languages |
array | yes |
certifications |
array | yes |
skills |
array | yes |
formal.workExperience[]:
| Property | Type | Required |
|---|---|---|
id |
string | yes |
period |
string | yes |
company |
string | yes |
location |
string | yes |
role |
string | yes |
description |
string | yes |
stack |
string | yes |
enabled |
boolean | no |
formal.education[]:
| Property | Type | Required |
|---|---|---|
id |
string | yes |
period |
string | yes |
institution |
string | yes |
location |
string | yes |
degree |
string | yes |
detail |
string | yes |
enabled |
boolean | no |
formal.languages[]:
| Property | Type | Required |
|---|---|---|
language |
string | yes |
level |
string | yes |
detail |
string | no |
formal.certifications[]:
| Property | Type | Required |
|---|---|---|
name |
string | yes |
issuer |
string | yes |
year |
string | yes |
detail |
string | yes |
formal.skills[]:
| Property | Type | Required |
|---|---|---|
category |
string | yes |
items |
string | yes |
Supported toggles:
experience[].enabledformal.workExperience[].enabledformal.education[].enabledskills.categories[].enabledskills.categories[].skills[].enabledpersonal.summaryImpactEnabled
Behavior:
enabled: falsehides the node.enabled: trueshows the node.- If omitted, node is visible (backward compatible).
Example:
{
"id": "visma",
"role": "SRE | DevOps Engineer",
"enabled": false
}Validation is enforced in two layers:
- Editor-time through
"$schema": "./cv-schema.json"incv-data.json. - Build-time in
build.pyusingjsonschema.
Build fails when:
- JSON syntax is invalid.
- Data does not comply with schema.
GitHub Pages deployment is managed by workflow:
.github/workflows/pages.yml
Flow:
- Trigger on push to
mainor manual dispatch. - Run
python build.py. - Publish
dist/as Pages artifact.
- If data does not load in development, do not use
file://; run a local server. - If output looks stale, rerun
python build.pyand hard refresh. - If build reports missing
jsonschema, runpython -m pip install -r requirements.txt. - If VS Code keeps old schema warnings, save both JSON files and reload window.
This project is licensed under the PolyForm Noncommercial License 1.0.0.
- Forking is allowed.
- Non-commercial use, modification, and redistribution are allowed.
- Commercial use is not allowed without prior written permission from the copyright holder.
See LICENSE for details.

