# CVX content schema — complete reference This document is self-contained: with only this file and a person's CV facts, you can generate a complete, valid `cv-content/` folder for [CVX](https://github.com/hrtips/cvx). It is written for both humans and AI assistants. ## How CVX works - `npx @hrtips/cvx init` scaffolds `cv-content/` with a complete example CV (Bruce Wayne's). - `npx @hrtips/cvx validate` checks every file at once and reports exact errors with suggested fixes (`--strict` also fails on unknown keys, `--json` for machine-readable output; exit `0` ok / `2` problems found). Run it after every edit. - `npx @hrtips/cvx build` renders `cv-content/` to a PDF in the current directory. - `npx @hrtips/cvx build --ats` renders an ATS-safe single-column variant instead. - Every content file is validated against the [canonical JSON Schema](../schema/v1/cvx.schema.json); the scaffolded files carry `$schema` headers for editor autocomplete. - The output PDF is named from `personal.yaml`'s `name`: lowercased, spaces → hyphens (`Bruce Wayne` → `bruce-wayne.pdf`; the ATS variant appends `-ats`). - Every `.yaml` file in `cv-content/` is auto-discovered by filename. A missing file, an empty file, or `[]` simply drops that section from the CV — no error. (Required in practice: `personal.yaml`, `summary.yaml`, `experience.yaml` — `validate` checks this.) - `init` is a convenience, not a prerequisite — `build` renders any `cv-content/` folder with valid YAML; built-in themes and layouts need no extra files. A missing photo renders fine (the sidebar simply omits it). - All rendering is local; no network calls, no accounts. ## YAML rules that matter here - Quote any string containing a colon (`"Director: Operations"`) or starting with a special character. - Periods/date ranges are free text — the convention in the example content is an en-dash: `2005 – Present`, `Jan 2022 – Dec 2023`. - Do not invent facts. Every entry must be truthful to the source CV; this especially matters for `keywords.yaml` (ATS parsers cross-check keywords against body text). --- ## File: `personal.yaml` (object) Identity header + contact block. Only the keys listed here render — arbitrary extra keys are ignored. | Key | Type | Required | Renders | |---|---|---|---| | `name` | string | **yes** | Header name; also derives the output filename | | `title` | string | recommended | Job title line under the name | | `company` | string | optional | Company line under the title | | `phone` | string | optional | Contact row (phone icon) | | `phoneHref` | string | optional | Makes the phone row clickable, e.g. `"tel:+12015552283"` | | `email` | string | optional | Contact row (envelope icon); clickable automatically via `mailto:` | | `linkedin` | string | optional | Contact row (LinkedIn icon), display text e.g. `linkedin.com/in/brucewayne` | | `linkedinHref` | string | optional | Makes the LinkedIn row clickable (full URL) | | `facebook` | string | optional | Contact row (Facebook icon) | | `facebookHref` | string | optional | Makes the Facebook row clickable | | `location` | string | optional | Contact row (pin icon), e.g. `"Gotham City, USA"` | | `links` | list of `{label, href}` | optional | Extra contact rows — blog, portfolio, GitHub, personal site. `label` is optional and falls back to the URL. Keeps working alongside the built-in `linkedin`/`facebook` rows | Contact rows appear only for keys that are present, with any `links` last. The ATS layout's header uses `phone`, `email`, `linkedin`, `location` (not `facebook`) and appends any `links`. ```yaml name: Bruce Wayne title: Founder & Field Commander – Gotham Operations company: Wayne Enterprises phone: "+1 (201) 555-2283" phoneHref: "tel:+12015552283" email: bruce.wayne@wayne-enterprises.com linkedin: linkedin.com/in/brucewayne linkedinHref: "https://www.linkedin.com/in/brucewayne" links: - label: Wayne Foundation href: "https://www.wayne-foundation.org" ``` ## File: `summary.yaml` (list of bullets) The professional-summary bullets at the top of page 1. Aim for 3–6 bullets, each a single sentence focused on scope, specialisation, or headline achievements. ```yaml - "Strategic operations leader with 20+ years' experience, progressing from solo field operative to Field Commander of a citywide security network." - "Specialised in recruiting and developing elite field talent." ``` A bullet is usually a plain string, but any bullet (here or in `experience[].bullets`) may instead be an object embedding a clickable link: `text` (before the link), `link` — an object of `href` + `label` (the clickable part) — and optional `suffix` (after it): ```yaml - text: "Published the " link: { href: "https://example.com/report", label: "annual security report" } suffix: " read by 40k+ practitioners." ``` ## File: `experience.yaml` (list of entries) Work history, most recent first. Keep each role its own entry — ATS keywords are auto-derived from `role` and `progression` titles, so roles merged into one "earlier roles" entry (or mentioned only in bullet text) drop out of the keyword metadata. Older roles can simply carry fewer bullets. Per entry: | Key | Type | Required | Renders | |---|---|---|---| | `role` | string | **yes** | Bold role headline | | `company` | string | recommended | Left side of the meta line | | `period` | string | recommended | Right side of the meta line, free text | | `location` | string | optional | Muted line under the meta line | | `description` | string | optional | One-line italic company/role description | | `progression` | list of `{title, period}` | optional | Indented title-history block (promotions within the role) | | `bullets` | list of bullets | optional | Impact bullets — start with a verb, quantify where truthful; 3–6 per recent role, fewer for older ones. Plain strings, or the `{text, link, suffix}` object form (see `summary.yaml`) | ```yaml - role: Founder & Field Commander – Gotham Operations company: The Batman period: 2005 – Present description: Self-directed vigilante operation safeguarding Gotham City. progression: - title: Commander, Batman Incorporated period: 2011 – Present - title: Solo Operative, The Dark Knight period: 2005 – 2008 bullets: - Established and scaled a citywide security operation from a solo initiative to a franchised network. - Reduced organised-crime activity in Gotham by an estimated 60% through data-driven surveillance. ``` ## File: `education.yaml` (list of entries) Most recent first. Per entry: `degree` (string), `institution` (string), `period` (string). ```yaml - degree: BSc, Criminology & Chemistry institution: Gotham University period: 1994 – 1998 ``` ## File: `certifications.yaml` (list of entries) Professional certifications, most recent first — kept separate from `achievements.yaml` (awards). Per entry: `name` (**required**), `issuer` (string), `year` (string). Omit the file if there are none. ```yaml - name: "LFD259: Kubernetes for Developers" issuer: The Linux Foundation year: "2023" ``` ## File: `publications.yaml` (list of entries) Publications and written work, most recent first. Per entry: `title` (**required**), `venue` (string), `year` (string). Omit the file if there are none. ```yaml - title: "Applied Deterrence: Non-Lethal Intervention at Scale" venue: Gotham Journal of Criminology year: "2021" ``` ## File: `languages.yaml` (list of entries) Languages spoken. Per entry: `language` (**required**), `proficiency` (string, free text — e.g. Native, Professional, Conversational). Omit the file if there are none. ```yaml - language: English proficiency: Native - language: Japanese proficiency: Professional ``` ## File: `competencies.yaml` (list of strings) Skill tags rendered as pills in the sidebar. Keep each 1–3 words; 6–12 items reads best. ```yaml - Strategic Planning - Criminal Investigation - Crisis Response ``` ## File: `achievements.yaml` (list of entries) Awards and recognitions. Field names are historical: `year` is the **bold headline** (usually the award name, not a year), `text` is the muted attribution line, conventionally `"— Year, Awarding Organisation"`. ```yaml - year: Key to the City text: "— Office of the Mayor, Gotham City" ``` ## File: `referees.yaml` (list of entries, or `[]`) Per entry: `name`, `title`, `company`, `email`, `phone` (all strings). An explicit empty list `[]` prints "available upon request". ```yaml - name: Diana Prince title: Founding Member, Justice League company: Themysciran Embassy email: d.prince@justiceleague.org phone: "+1 (202) 555-0177" ``` ## File: `keywords.yaml` (optional; list of strings, or map of group → list) Extra ATS/AI-parser keywords embedded in the PDF's `Keywords` metadata field (never printed on the page). Competencies and job titles are auto-derived already, so list only what those don't cover. Groups are flattened; headings are not printed. **Keep every keyword truthful to the CV body.** ```yaml - Operations Management - Risk Management # or grouped: Leadership: [Executive Leadership, Team Building] ``` ## File: `config.yaml` (object) | Key | Type | Default | Meaning | |---|---|---|---| | `schemaVersion` | integer | `1` | Content schema major version — new keys may appear within a major; inert keys can be removed (the CHANGELOG names them) | | `theme` | `teal` \| `coral` \| `mono` | `teal` | Colour scheme (`mono` is black-and-white, ATS-optimised) | | `layout` | `two-column` \| `single-column` \| custom layout filename | `two-column` | Page structure | | `atsKeywords.enabled` | boolean | `true` | Master switch for keyword metadata | | `atsKeywords.autoDerive` | boolean | `true` | Also derive keywords from competencies + job titles | | `atsKeywords.max` | integer | all | Cap on embedded keywords (body-derived terms kept first) | ## File: `images/profile.` Square photo, 400×400px or larger. Extensions auto-detected in precedence order: `jpg`, `jpeg`, `png`, `webp` (case-insensitive). Used by the two-column layout; the ATS variant has no photo. ## Directory: `layouts/` (optional custom layouts) A `.yaml` file here becomes selectable as `layout: `. Structure: ```yaml template: two-column # or single-column — the page shell to use pages: first: # main: page 1. sidebar: start of the flow. sidebar: [identity-photo, contact, achievements] main: [summary, spacer: 27, experience] continuation: # main: middle pages. sidebar: flow continues. sidebar: [identity-compact, education, certifications, competencies, languages, publications] main: [experience:continued] last: # main: the closing page. sidebar: end of the flow. sidebar: [identity-compact, referees] main: [experience:continued] ``` > **How the three page buckets are read (changed in the layout-engine work).** > The **sidebar** lists across `first` / `continuation` / `last` are concatenated > into **one ordered flow**, and the engine measures that flow and decides which > page each section lands on. So `last.sidebar: [referees]` means *"referees > comes last in the sidebar"*, **not** *"referees renders on the final page"* — on > a CV whose sidebar fits in two pages, a section declared under `continuation` > may well render on page 3, and one declared under `last` on page 2. The buckets > are how you express **order**; pagination is measured, not declared. (Identity > slots are the exception: `identity-photo`/`identity-compact` are injected at the > top of every page's sidebar, `first`'s on page 1 and `continuation`'s > thereafter, and are never packed.) > > The **main** lists are still per-page-kind: `first.main` renders on page 1, > `last.main` on the final page, `continuation.main` in between. Valid section keys: `identity-photo`, `identity-compact`, `contact`, `achievements`, `education`, `certifications`, `publications`, `languages`, `competencies`, `referees` (sidebar); `summary`, `experience`, `experience:continued`, `header-ats` (main); `spacer: N` (N points of vertical space, either slot). The lists above say where each section is *normally* placed, not where it is *allowed*: any section key is legal in either slot. One caveat if you move one into `main` — the planner measures only `summary` and `experience` there, so a section you place in a main slot renders correctly but is not counted in the page count or the overflow figures. The build says so with the `main-slot-unmeasured` fact, and compares the finished PDF's sheet count against the plan (`physical-pages-exceed-plan`) so an unmeasured spill cannot pass silently. --- ## Checklist for generating a complete cv-content/ 1. `personal.yaml` — name (required), title, contact details with `*Href` links where known. 2. `summary.yaml` — 3–6 single-sentence bullets. 3. `experience.yaml` — every role, most recent first, with quantified truthful bullets. 4. `education.yaml`, `certifications.yaml`, `publications.yaml`, `languages.yaml`, `competencies.yaml`, `achievements.yaml`, `referees.yaml` — or `[]` / omit to drop. 5. `keywords.yaml` — only truthful terms not already covered by competencies/titles. 6. `config.yaml` — usually just `theme` + `layout`. Pagination is automatic (the old page-1 keys were removed). 7. Ask for the photo in your **first** message to the user (it can't be generated) — but never block on it; the CV renders cleanly without one. If you ran `init`, replace or delete the scaffolded example photo at `images/profile.jpg` (it's Bruce Wayne's) before building. Then run `npx @hrtips/cvx build`.