Let me paint you a picture.
It's 2024. I've been in IT for 14 years — Flash games at 13, mobile GameDev, frontend, fullstack, and for the last few years, DevOps. I've managed 750+ servers with Ansible, cut deploy times from 8 minutes to 2, saved ~$250/month on AWS costs through Terraform optimizations. I've shipped games with 100M+ downloads. I have an Upwork Top Rated Plus badge.
And HR keeps telling me I don't have enough experience.
Not because I'm unqualified. Because my resume didn't tell the story right. Because I was sending a Google Doc that tried to be everything to everyone — and ended up being nothing to anyone.
So I did what any reasonable DevOps engineer would do when faced with a tooling problem.
I over-engineered a solution.
The Actual Problem
Here's what my resume situation looked like before:
- One Russian
.docxfor local job applications - One English
.docxfor international / remote - A "DevOps focused" copy somewhere
- A "GameDev focused" copy somewhere else
- A file literally named
CV_Gusarov_FINAL_v3_actually_final.docx
Every time I updated one, I forgot to update the others. Every time I applied to a GameDev role, I had to manually shuffle sections around. Every time I wanted to send a PDF, I opened LibreOffice, crossed my fingers, and watched the formatting break.
This is a data consistency problem. And data consistency problems have a solution: a single source of truth.
The Architecture
I called the project CV Hub. Here's what I wanted it to do:
- Store all resume data in one place
- Generate a website automatically
- Export to PDF, DOCX, and TXT on every push
- Support multiple languages without duplicating content
- Support multiple role-specific versions from the same data
The stack I landed on: Astro for the site, YAML as the data layer, GitHub Actions for CI/CD, GitHub Pages for hosting. Total infrastructure cost: $0.
Let me walk through each piece.
The YAML Structure
Everything lives in YAML. Not a database, not a CMS, not Notion — plain YAML files that you can read, diff, and version control like any other config.
There's no bilingual key soup (title.en / title.ru inside the same file). Instead, each language is a separate file: en.yaml and ru.yaml. Same structure, different content. Clean diffs, no mental overhead when editing.
A base CV file looks like this:
name: "Alexander Gusarov"
title: "DevOps / Platform Engineer | Kubernetes · Terraform · AWS"
summary: >
DevOps engineer with 14+ years in IT. Managed production Kubernetes
clusters, built IaC from scratch with Terraform + Ansible on AWS,
shipped monitoring microservices in Go.
contacts:
- label: GitHub
url: https://github.com/KeeGooRoomiE
- label: Telegram
url: https://t.me/spartan121
achievements:
- "Managed 750+ Linux servers with Ansible, SLA 1.5–2 min"
- "Reduced AWS costs from $550 to $300/month (−45%)"
- "Cut deploy time from 8 to 2 minutes (−75%)"
skills:
- group: Orchestration
items: [Kubernetes, Helm, Docker]
- group: IaC & Automation
items: [Terraform, Ansible]
- group: Cloud
items: [AWS — EC2, S3, RDS, VPC, IAM, ALB]
- group: Languages
items: [Go, Python, Bash]
experience:
- company: "InfoScale"
role: "DevOps Engineer"
period: "Dec 2024 — Jan 2026"
description:
- "Administered Kubernetes production clusters in Yandex Cloud"
- "Built full IaC solution with Terraform + Ansible on AWS (~20 services)"
- "Reduced MTTR by 60% with custom Grafana/VictoriaMetrics dashboards"
stack: [Kubernetes, Helm, Terraform, Ansible, AWS, Go, Grafana]
The key insight: your resume data and your resume presentation are two different concerns. YAML handles the data. Astro handles the presentation. They never mix.
Multi-Role Overlay System
This was the part I was most excited about architecting.
I have genuinely different professional identities: DevOps engineer, GameDev (shipped titles with 100M+ downloads), Frontend/Fullstack developer. Each of these roles needs a different emphasis — different projects front and center, different skills highlighted, different summary framing.
The naive solution: maintain separate full YAML files per role. The problem: now you have four sources of truth, and the sync nightmare is back.
My solution: per-language base files + minimal delta files per profile.
src/content/cv/
en.yaml ← base CV in English
ru.yaml ← base CV in Russian
en_devops.yaml ← DevOps delta (only what changes)
ru_devops.yaml ← DevOps delta in Russian
en_gamedev.yaml ← GameDev delta
ru_gamedev.yaml
The delta file contains only what changes. Everything else is inherited from the base:
# en_devops.yaml — only overrides
title: "DevOps / Platform Engineer | Kubernetes · Terraform · AWS"
summary: >
DevOps-focused summary with emphasis on infrastructure...
skills:
- group: Orchestration
items: [Kubernetes, Helm, Docker]
- group: IaC & Automation
items: [Terraform, Ansible]
experience:
- company: InfoScale # no fields = take everything from base
- company: AZNResearch
role: "Backend Engineer" # override role, rest from base
description:
- "Developed backend microservices with .NET Core"
- "Introduced Git workflow and CI pipelines"
Before the build, a merge.mjs script runs and produces merged artifacts in public/cv/:
src/content/cv/en.yaml + en_devops.yaml
↓ merge.mjs
public/cv/en_devops.yaml ← merged, ready to render
Astro pages read from public/cv/ — not from source directly. This keeps the merge logic in one place and out of the component layer.
The profiles registry ties everything together:
# src/content/profiles/profiles.yml
profiles:
- id: default
label: "Generalist"
slug: "" # URL root /
spec: null # no delta, use base as-is
- id: devops
label: "DevOps"
slug: "devops" # URL: /devops, /devops/ru
spec: devops # reads en_devops.yaml, ru_devops.yaml
- id: gamedev
label: "Game Developer"
slug: "gamedev"
spec: gamedev
One update to an experience entry in en.yaml propagates to all profiles automatically. The delta only needs to exist where something actually differs.
i18n Without a Framework
I deliberately avoided i18n libraries. They add complexity, and for a resume site the translation problem is actually simple.
The content layer uses the "one file per language" approach — en.yaml and ru.yaml are independent, same structure, different text. No nested keys, no bilingual soup, clean diffs.
For UI strings (labels, navigation, button text), there's a separate translations.yaml:
# src/content/i18n/translations.yaml
nav:
showcase:
en: "Showcase"
ru: "Проекты"
cv:
skills:
en: "Skills"
ru: "Навыки"
experience:
en: "Experience"
ru: "Опыт"
download:
en: "Download"
ru: "Скачать"
A small makeT helper resolves keys with a three-level fallback:
const t = makeT(translations.data, lang);
t('nav.showcase') // → "Showcase" or "Проекты"
t('cv.skills') // → "Skills" or "Навыки"
Fallback chain: requested lang → en → the key path itself as a string. So missing translations never crash the build — they just show the key, which is immediately obvious in review.
Language state lives in the URL: /devops is English, /devops/ru is Russian. Every page is independently shareable and indexable. The language switcher preserves the current profile, the profile dropdown preserves the current language. No cookies, no localStorage, no hydration shenanigans.
The Export Pipeline
This was honestly the trickiest part to get right.
I wanted three export formats: PDF (for HR), DOCX (for ATS systems that parse Word files better), and TXT (plain text for copy-paste into application forms).
The build runs in four steps, in order:
npm run cv:build # 1. merge YAMLs → public/cv/
npm run resume:generate # 2. DOCX + TXT for all merged files
npm run resume:pdf # 3. PDF via Playwright headless render
astro build # 4. static site
Step 1 (merge.mjs) produces all the merged artifacts in public/cv/. Steps 2 and 3 read those artifacts and output files into public/downloads/. Step 4 builds the Astro site, which already has the downloads sitting in the right place.
File naming follows a consistent pattern:
resume_en.pdf ← default profile, English
resume_ru.pdf ← default profile, Russian
resume_en_devops.pdf ← DevOps profile, English
resume_ru_devops.pdf ← DevOps profile, Russian
resume_en_gamedev.pdf ← GameDev profile, English
The download URLs are built at page generation time:
const specSuffix = profileData.spec ? `_${profileData.spec}` : '';
const pdfUrl = `${base}/downloads/resume_${langId}${specSuffix}.pdf`;
So /devops serves resume_en_devops.pdf, /gamedev/ru serves resume_ru_gamedev.pdf. Each page offers the right file for its profile × language combination.
PDF generation uses Playwright — headless browser renders the built HTML with a proper two-column A4 layout. This took the most time to get right. Print CSS is a dark art: @page rules, page break behavior, orphan/widow control, font embedding — all of it needs careful attention. Budget more time for this than you think you'll need.
Astro as the Rendering Layer
Why Astro specifically?
I evaluated a few options: plain HTML templates (too manual), Next.js (overkill for a static site), Jekyll (Ruby, no thanks), Hugo (fine, but Go templating is awkward for this use case).
Astro won because:
Zero JS by default. The resume site doesn't need interactivity beyond language switching and role dropdown. Astro ships zero client-side JavaScript unless you explicitly opt in. The result is a site that loads instantly.
Component model. I could build
<ResumeSection>,<ProjectCard>,<ExperienceEntry>as proper components with typed props, rather than copypasting HTML fragments.Static generation with dynamic routing. The
[...slug].astropattern lets me define one template and generate pages for every profile at build time. No runtime server needed.Content collections. Astro has a built-in concept of typed content collections — perfect for YAML-driven data. I get TypeScript types for my resume data for free.
The project structure ended up clean:
src/
components/
ResumeHeader.astro
ExperienceSection.astro
ProjectCard.astro
LanguageToggle.astro
RoleDropdown.astro
pages/
index.astro ← redirects to default profile
[...slug].astro ← dynamic profile pages
404.astro
data/
profiles/
base.yaml
devops.yaml
gamedev.yaml
frontend.yaml
utils/
i18n.ts
profileLoader.ts
exportHelpers.ts
What I Learned
A few things surprised me during this build.
YAML is surprisingly good for structured content. I went in skeptical — it's finicky about indentation, and deep nesting gets messy. But for resume data specifically, the structure maps cleanly to YAML's strengths. Anchors and aliases let me deduplicate repeated values. The files are readable without tooling. Git diffs on YAML are meaningful.
Static generation is underrated for personal sites. The whole site is a folder of HTML files. No server, no database, no runtime. It deploys to GitHub Pages in under 2 minutes and serves from CDN globally. For something that updates maybe once a week, this is exactly the right architecture.
Print CSS is a dark art. If you're generating PDFs from HTML, budget more time than you think for @media print styles. Page break behavior, orphan/widow control, margin boxes — all of it requires careful attention and lots of test prints.
Building tools for yourself is the best way to understand what you actually need. I shipped features I thought I needed (fancy animations, a complex filtering system for projects) and ended up removing them. The things that stayed were the things I actually used every day: clean export, fast language switching, role overlays.
The Result
The live site is at keegooroomie.github.io/cv_hub.
Four profiles, two languages, three export formats. Fully automated. Hosted for free. Updated with a git push.
Does it help with the job search? Honestly, yes. Having a URL to send instead of an attachment changes the conversation a little. Having role-specific versions means I'm not asking a GameDev hiring manager to mentally filter through Kubernetes metrics to find the relevant parts.
More importantly — I never have to manually sync resume versions again. That alone was worth the weekend.
Use It Yourself
The whole thing is open source and built to be forked:
👉 github.com/KeeGooRoomiE/cv_hub
Fork it, replace my YAML with yours, push to GitHub, enable Pages — you have a working versioned resume site with automated exports. The README has a setup guide.
If you find it useful, a ⭐ on the repo goes a long way.
And if you build something on top of it or run into issues — I'm genuinely curious to hear about it. Drop a comment or open an issue.
Alexander Gusarov — DevOps / Platform Engineer. 14+ years in IT, currently open to new opportunities.
GitHub: KeeGooRoomiE · Telegram: @spartan121
Top comments (0)