This repository contains an Astro-based multilingual CV generation system that creates both web pages and professional PDFs from TOML configuration files.
Complete development workflow (like GitHub Actions pipeline):
# 1. Install dependencies
npm ci
# 2. Generate PDFs (the site links to them under public/pdfs/)
./scripts/generate_cv.sh config.cv.toml public/pdfs/Michael_Boiman_CV_DE.pdf de
./scripts/generate_cv.sh config.cv.toml public/pdfs/Michael_Boiman_CV_EN.pdf en
# or, equivalently:
npm run pdf:de && npm run pdf:en
# 3. Build the website
npm run build
# 4. Start development server
npm run devThen open:
- German CV: http://localhost:4321/de/
- English CV: http://localhost:4321/en/
public/pdfs/.
- CV Content:
config.cv.toml— all CV data (profile, experience, projects, skills) with multilingual support, parsed bysrc/lib/toml-loader.ts - UI Strings:
src/lib/i18n.ts— localized interface strings (DE/EN) - Custom Configurations: Can be created for specific CV variants
- Languages: German (
de) and English (en), routed under/de/and/en/(Astro i18n,prefixDefaultLocale: true)
src/
├── components/ # CVPage, CVMinimal, CVFuture, ViewSwitch, AgentWidget, StoryPage, DarkModeToggle
├── layouts/ # Page layouts
├── lib/ # toml-loader, i18n, markdown, cv-links, station-details, experience
└── pages/
├── de/ # German routes (index, minimal, future, story, datenschutz, impressum)
└── en/ # English routes
public/ # Static assets (fonts, images, generated PDFs)
- PDF Generation:
scripts/html_to_pdf.jsuses Puppeteer to generate PDFs directly from the TOML config (own print template, independent of the web view) - Website Build:
astro buildrenders the static multilingual site - Static Assets: PDFs live in
public/pdfs/and are served as downloadable assets
The same CV renders in three views. All three read the same config.cv.toml,
the same src/lib/i18n.ts and the same sort rule, so a correction reaches every
view at once. A switch at the top of each view (src/components/ViewSwitch.astro)
moves between them. Its fourth entry, the project match, is a tool rather than a
view (see below).
| View | Route | Component | Look |
|---|---|---|---|
| Classic | /de/, /en/ |
CVPage.astro |
The original page: cards, sidebar, agent card in the hero |
| Minimal | /de/minimal/, /en/minimal/ |
CVMinimal.astro |
Typographic, two columns, entries fold open with a plus (after the Jev product page) |
| Future | /de/future/, /en/future/ |
CVFuture.astro |
Not a document: the career as one interactive graph (see below) |
| Match | /de/match/, /en/match/ |
CVMatch.astro |
A posting against the CV, judged by Jev line by line (see below) |
Minimal, Future and Match are on trial: they carry noindex and are left out of
the sitemap (astro.config.mjs).
One screen, no scrolling on a desktop. Stations are bars on a time axis, talks are marks on the axis, competencies and projects are rows below, and a line joins a competency to every station and project whose text names it. Hover or focus lights a node and its lines; a click opens it in the side panel, which shows the profile until something is picked. The agent sits in a command line at the bottom; when it points at an entry, that node lights and the panel opens it. Below 900 px the panel stands above the graph and the graph scrolls sideways.
- Layout at build time:
src/lib/career-graph.tsturns the TOML into coordinates and SVG paths; the browser only highlights and switches panels. - Competency nodes are a curated list (
SKILLSin that file), each with the patterns it matches. A line exists only where a pattern occurs in that entry's text, and a competency joining fewer than two entries is left out. - Tests:
npm run test:graph(bars never overlap, rows stay in the frame, every line joins two existing nodes). Both deploys run it. - Entry links use the same ids as the other views (
#exp-dvag,#project-…), plus#skill-<key>. Selecting a node writes its id into the address bar, so the URL is always a link to what is on screen. - A tailored link marks its entries in yellow instead of reordering.
- Two scales on one axis: the last eleven years at full width, the years
before at 40 percent, marked with
//on the axis (and explained in its tooltip), so the dense recent years get the room. - Each competency shows how many entries name it; its panel adds stations, projects and the year of its earliest station.
- The command line filters while you type (every word must occur in an entry's text; a competency matches by name), Enter sends the question to the agent.
- "Zeitreise" / "Time lapse" is a signal flow. The picture never moves: time runs evenly along the axis and the year labels light up as it passes. Each station bar grows from its start to its end; when it reaches its middle, a point of light runs down each of its lines and draws the line behind it. Every arriving signal counts the competency up by one and makes it brighter, so each ends at the count the resting graph shows (stations plus projects). When time reaches a project's year, signals run from its competencies to it. About 14 seconds. Hidden for visitors who ask for reduced motion.
- It plays once by itself on a visitor's first visit (
localStoragekeycv-future-lapse-seen), except when the visitor arrives through a tailored link or an entry link; any click, key or wheel stops it. - Projects appear in their own year where
config.cv.tomlgives one (year = 2024, with the source as a comment beside it). A project without a year appears once the last of its competencies has. Years are never estimated into the TOML: no source, no year.
https://mboiman.github.io/de/minimal/?for=DB%20InfraGO&focus=db-vertrieb,tuev-sued,e-invoicing-platform
focus:anchorvalues fromconfig.cv.toml, strongest first. The named stations and projects move to the top of their list in that order, open, and carry a "Passend"/"Matching" tag. Unknown anchors are ignored.for: the recipient's name, shown in one line above the sections (in Future: the matching nodes are marked in the graph instead of moved).- Evaluated in the browser only (
src/lib/cv-links.ts). Nothing about an application is written into this public repository.
#exp-<anchor> (stations, talks) and #project-<anchor> open that entry and
scroll to it, in all three views. In Minimal and Future every open entry carries
a "Link zu diesem Eintrag"/"Link to this entry" link.
Paste a posting, upload a file (txt, md, pdf, docx; read in the browser, the text lands in the text field first) or pick one of three example projects. Jev, the typed-judgement model of TypeSafe, answers small questions; the score is code.
- Per line of the posting, one request with the whole CV in the state: is it
a requirement (
is_req), is it mandatory (must), which of eight areas (axis), which CV entry shows it (evidence, a choice that points atcv.<id>by path) and how well (level, 0 to 3). - Per posting, one request: how central each area is (
demand:*) and what kind of role it is. - The score (
scoreMatchinsrc/lib/match.ts): coverage over the lines, mandatory lines weigh double; topic fit per area from that area's lines; 70/30. The page lets a visitor move both weights without a new request.
The run plays calmly: rows light up as their answer arrives, a line draws to the evidence, the net diagram and the ring fill, the counters show the measured response time, tokens and cost. The status line says one thing while Jev works and one when it is done.
| File | What it holds |
|---|---|
src/lib/match.ts |
Areas, CV entries, line splitting, the requests, reading answers, the score |
src/lib/match-demos.ts |
The three invented example postings (DE and EN) |
src/data/match/*.json |
Stored Jev measurements of the examples and of the CV profile |
src/lib/match-file.ts |
Reading an upload in the browser (pdf.js on demand, docx without a zip library) |
src/lib/match-config.ts |
MATCH_ENDPOINT of the live worker; empty means examples only |
workers/jev-match/ |
Cloudflare Worker for own text: holds the key, stores nothing; allowed origin only, 10 runs a minute per address (IPv6 per /64), 30 for everyone together, fixed error codes |
scripts/jev-measure.mjs |
Measures the examples and the profile again (npm run measure:match) |
src/lib/match-pdf.ts |
The PDF export: evaluation pages plus the CV PDF |
src/lib/match-report.ts |
What goes into that PDF from one run; shared by the page and the command line |
scripts/jev-match.mjs |
The match from the command line for an application bundle (npm run match) |
scripts/match-og.mjs |
The link preview picture with the net diagram |
test/match.test.mjs |
npm run test:match: splitting, requests, score, the PDF, and that stored runs still fit the texts and the CV |
Export. "Evaluation as PDF" builds the report in the browser (src/lib/match-pdf.ts,
pdf-lib loaded on click, Helvetica because pdf-lib embeds no woff2): score, net
diagram, open points, every line with its evidence, the method, then the CV PDF
of the page language appended.
The agent can start it. The widget declares match in ui_capabilities; the
agent may end an answer with ⟦ui:match⟧ (the visitor's last message is the
posting) or ⟦ui:match <demo-id>⟧. The widget shows a button, and only the
visitor's click starts the run: on /match/ through the bridge-agent:match
event, from any other view through sessionStorage (cv-match-pending). The
agent's rules are in the bridge repo, agents/mboiman/system-prompt.md.
Link preview. /match/ has its own og:image with the net diagram of the
first example (public/images/og-match-{de,en}.png), rendered with the site's
font by npm run og:match and committed. Run it again after measuring.
Look. Blue on paper, blue on navy in dark mode, with the theme button of
the other views; the agent panel takes the same colours under html.cv-match.
The key. The TypeSafe key is never in the repository. The measure script
reads it from TYPESAFE_API_KEY or the macOS keychain entry typesafe-api
(account mboiman); the worker has it as a secret
(cd workers/jev-match && wrangler secret put TYPESAFE_API_KEY).
Measuring again after changing a demo text or the CV:
npm run measure:match
# the home connection got 403 from TypeSafe on 2026-09-26; then go through the worker:
cd workers/jev-match && wrangler dev --remote --env dev --port 8799 \
--var TYPESAFE_API_KEY:$(security find-generic-password -s typesafe-api -a mboiman -w) \
--var MEASURE_TOKEN:messlauf
JEV_VIA=http://localhost:8799 JEV_TOKEN=messlauf npm run measure:matchFrom the command line, for an application (the job-application skill in the bridge repo calls this):
npm run match -- --file posting.txt --title "AI Quality Engineer" \
--json applications/match_firma.json --out applications/match_firma.pdf
MATCH_PDF=applications/match_firma.pdf ./scripts/generate_application.sh \
config.cv.toml applications/bewerbung_firma.pdf de cover_letter_firma.jsonWith the evaluation in the bundle the letter JSON usually asks for the short
CV: "cv": { "short": true, "focus": [anchors], "fullUrl": "<tailored link>" }
prints the focused stations in full without tool lists, every other station as
one row, only the focused projects, and a last line linking the full CV (about
2 pages instead of 5). "enclosure" replaces the letter's enclosure line.
--cv-follows makes the evaluation's last line announce the CV behind it.
The first call asks Jev through the live worker and writes the same evaluation
the page exports, without the CV (--with-cv appends it); stdout is a JSON
summary per line. --from <json> rebuilds the PDF from a stored run without
asking again, --credit "<line>" adds small lines after the method. The worker
lets the script past its origin check with the measure token, a Worker secret
MEASURE_TOKEN and the keychain entry jev-match-token (account mboiman),
set on 2026-09-28. The bundle is then cover letter, evaluation, CV. Give the
script only the task and requirement lists of a posting: a title or an intro
sentence counts as a line too.
test:match fails when a demo text no longer matches its stored run or an
evidence entry left the CV, and warns when the profile was measured on an older CV.
Live since 2026-09-26 at https://jev-match.mboiman.workers.dev (Michael's
private Cloudflare account, account_id in wrangler.toml). The worker carries
the CV entries it was built with, so gh-pages.yml redeploys it on every push
to main (a pull request only bundles it with --dry-run). That needs the
repository secret CLOUDFLARE_API_TOKEN, a Cloudflare token with the single
permission Account > Workers Scripts > Edit on the private account; without it
the step warns and skips. By hand: cd workers/jev-match && wrangler deploy --env="". The key is a Worker secret,
set from the keychain without touching a file:
security find-generic-password -s typesafe-api -a mboiman -w | wrangler secret put TYPESAFE_API_KEY --env="". The privacy pages already describe this path
(section 4).
A click on the view switch stores the choice (localStorage key cv-view). The
classic page and the root page then send the visitor to that view before
anything paints (src/layouts/BaseLayout.astro, src/pages/index.astro). A
plain visit never changes the stored view, so a shared link cannot switch it.
Printing Minimal or Future opens every entry and drops the controls, in one column. The formal document stays the PDF.
The chat panel (AgentWidget.astro) takes the look of the view it is on: grey,
square and flat in Minimal, dark with the mint accent in Future. The styling
lives in the view's own stylesheet under html.cv-minimal or html.cv-future,
so the classic widget is untouched. The Future hero reads the agent's live state
and facts from its agent card through the same [data-agent-live] and
[data-agent-facts] hooks as the classic hero.
| File | What it holds |
|---|---|
src/components/ViewSwitch.astro |
The switch between the views and the match; colours come from the page through --vs-* variables |
src/lib/cv-links.ts |
Tailored link, entry links, print, agent prompt buttons; pages opt in through data-focus-* attributes |
src/lib/station-details.ts |
How a station's details split into open text, "more" and the tool list |
src/lib/career-graph.ts |
Layout of the career graph: axis, lanes, competency and project rows, lines |
# Generate PDFs with language specification
./scripts/generate_cv.sh <config.toml> <output.pdf> <language>
# Examples:
./scripts/generate_cv.sh config.cv.toml cv_german.pdf de
./scripts/generate_cv.sh config.cv.toml cv_english.pdf en
# Minimal print style (grey ink, large name, plain headings, greyscale portrait)
./scripts/generate_cv.sh config.cv.toml cv_german_minimal.pdf de --style=minimal
# Standalone mode (uses only the specified config)
./scripts/generate_cv.sh config.custom.toml output.pdf de --standalone- Professional A4 layout optimized for printing
- Compressed images for smaller file sizes
- Multilingual support (German/English)
- Direct generation from TOML configuration (no dependency on the built site)
- Two print styles from
scripts/lib/pdf-theme.js:classic(default) andminimal. Same pages and page breaks in both. Both deploys publishMichael_Boiman_CV_{DE,EN}.pdf(classic, linked from Classic and Future) andMichael_Boiman_CV_{DE,EN}_Minimal.pdf(linked from Minimal). An unknown style aborts the render.
npm ci # install dependencies
npm run dev # Astro dev server (http://localhost:4321)
npm run build # production build to dist/
npm run preview # preview the production build
# Quick PDF generation for testing
./scripts/generate_cv.sh config.cv.toml test_output.pdf deThe CV page embeds a chat widget (src/components/AgentWidget.astro) that talks
to a self-hosted A2A agent (JSON-RPC + SSE). The agent answers questions about
experience and stack, reads availability as free/busy, and takes meeting
requests that are only answered after personal approval.
The GitHub Actions workflow (.github/workflows/gh-pages.yml) automatically:
- Installs dependencies (
npm ci) - Generates PDFs in correct order
- Builds the Astro site
- Deploys to GitHub Pages
A manual preview workflow exists for feature branches (preview-deploy.yml).
Generate professional applications with tailored cover letters that map CV qualifications directly to job requirements.
# Interactive workflow via Claude Command
/bewerbung
# Direct script usage
./scripts/generate_application.sh config.cv.toml bewerbung_firma.pdf de cover_letter_data.json- Requirement Mapping: Cover letter shows exactly where in the CV each qualification is found
- Professional Layout: Consistent design between cover letter and CV
- Print style per application:
"style": "minimal"in the cover letter JSON renders letter and CV in the minimal style; default is"classic" - Tailored online CV: the letter links to the Minimal view with
?for=<company>&focus=<anchors>(see "Three Views of the CV") - AI-Assisted: Claude Command guides through the entire process
- Use
/bewerbungcommand → Interactive process with Claude - Analyze job posting → Extract requirements and company info
- Generate cover letter data → Create requirement mapping
- Generate application PDF → Cover letter + CV combined
The generated cover letter includes:
- Header: Same design as CV for consistency
- Company-specific introduction
- Requirement Mapping Section:
- "You need: [Requirement]"
- "I offer: [Your qualification]"
- "See CV: [Page reference]"
- Professional closing
The result is a professional PDF with the cover letter as page 1 and the complete CV following.
- Node.js and npm (Astro build + PDF generation via Puppeteer)
- Dependencies are auto-installed via
npm ci