$my-yaml-resume

v2.0.0 React 19 · TypeScript · Vite GPL-3.0-or-later YAML-driven

A single-file YAML resume that renders into a clean, filterable web page. Recruiters can click a skill and instantly see what you know, how long you've used it, where, and when.

What you know
Skills attached to every job, project, and degree
How you know it
Each skill traces back to the work that produced it
What you used it on
Filter to surface only matching responsibilities
When you used it
Years used + last touched, computed live
i

The standout feature is the skill filter: click any tag and the resume dims non-matching entries while surfacing per-skill stats (years used · projects · year range).

01 Features

Filterable skill tags: click any tag to scope the resume
YAML data source: one file, no DB, no CMS
Dark mode: follows system or toggle in UI
Responsive: readable on phone, tablet, desktop
Customizable styles: single CSS file
Print-friendly: exports clean to PDF
Setup in minutes: edit two files and publish
Git Pages publish: one-command deploy

02 Project structure

The codebase is small and deliberately flat. Most edits happen in public/ and src/.

skill-filtering-resume/ ├── public/ │ ├── resume.yaml// your resume: edit this │ ├── resume.example.yaml// reference data │ └── fonts/ ├── src/ │ ├── components/// Experience, Projects, Filters, … │ ├── contexts/// SkillFilter, SkillKey, Theme providers │ ├── hooks/ │ │ └── useSkillStats.ts// computes years · uses · range │ ├── App.tsx │ ├── Resume.tsx │ ├── types.ts// authoritative YAML shape │ ├── style.css// edit colors & layout here │ └── theme.ts ├── scripts/ │ └── publish.mjs// build & push to a pages branch ├── .env.example// copy to .env and edit ├── index.html ├── package.json └── vite.config.ts

03 Requirements

  • Node.js ≥ 20.x (anything that runs Vite 8)
  • npm, pnpm, or yarn (examples use npm)
  • git: required for the bundled publishing script
  • A text editor that doesn't fight YAML's whitespace

04 Install & run

From a fresh clone of the repository:

# 1. install deps
$ npm install

# 2. create your env file
$ cp .env.example .env

# 3. point it at your resume (or use the example)
$ cp public/resume.example.yaml public/resume.yaml

# 4. start the dev server
$ npm run dev
  ➜  Local:   http://localhost:5173/

Then open http://localhost:5173/; edits to public/resume.yaml hot-reload.

05 The YAML file

Everything you see on the page comes from one file. The full schema lives in src/types.ts; the short version is below.

description: "Software engineer focused on…"
name:        "Kostya Arminleg"
title:       "Software Engineer"
location:    "Example City, USA"

links:
  github:   "https://github.com/…"
  linkedin: "https://linkedin.com/in/…"

# Pretty-name your skill slugs.  Slugs are what you tag with;
# titles are what's rendered.
skillkey:
  react:      "React"
  typescript: "TypeScript"
  postgresql: "PostgreSQL"

experience:
  - name:        "Example Co."
    title:       "Senior Engineer"
    start:       "2021/07/12"
    end:         "2025/02/28"
    skills:      [agile, linear]
    responsibilities:
      - description: "Led the dashboard rewrite."
        skills:      [react, typescript, vite]

Same shape for projects and education. Any field marked hidden: true is skipped at render time, so you can keep older roles in the file without showing them.

FieldTypeNotes
namestringJob / school / project title
start / endYYYY/MM/DDDrives the "years used" math
skillsstring[]References slugs from skillkey
responsibilitiesobject[]Each one carries its own skills
hiddenbooleanHide without deleting

06 Environment vars

Copy .env.example to .env and adjust. The Vite-prefixed vars get baked into the build; the others are read by the publish script.

VITE_RESUME_FILE=/resume.yaml             # the YAML to load at runtime
VITE_DEFAULT_TITLE="My Resume"
VITE_DATE_FORMAT_MONTH_YEAR="MMM YYYY"
VITE_DATE_FORMAT_YEAR="YYYY"
VITE_ICON_SIZE=32

# publish.mjs reads these
RESUME_BUILD_DIR="dist"
RESUME_GIT_PAGES_BRANCH="pages"
RESUME_CUSTOM_DOMAINS="resume.example.com"

07 Customize

  1. Colors & typography: open src/style.css. Tokens are at the top.
  2. Layout: sections live under src/components/ (one file per section).
  3. Skill-stat math: src/hooks/useSkillStats.ts derives years and project counts from your YAML; tweak the heuristic there.
  4. Theme provider: src/contexts/ThemeContext.tsx handles dark/light + the boxed window chrome.
i

Tip: the only file you must edit is public/resume.yaml. Everything else has sensible defaults.

08 Publishing

The repo ships a one-command publish script that builds the site and pushes it to a separate pages branch. It works out of the box with Codeberg Pages or GitHub Pages.

$ npm run publish

The script (scripts/publish.mjs) does, in order:

  1. Clean any prior worktree and the build branch
  2. Create a fresh worktree on an orphan pages branch
  3. Run vite build into a temp dir
  4. Empty the worktree (preserving .git) and copy the build in
  5. Drop a .domains file if you've set RESUME_CUSTOM_DOMAINS
  6. Commit and force-push to origin/pages
  7. Tear down the worktree and temp dir
!

Heads up: the publish script force-pushes the pages branch and clears $RESUME_BUILD_DIR before building. Don't keep anything you care about in there.

09 npm scripts

CommandWhat it does
npm run devVite dev server with hot reload
npm run buildType-check (tsc -b) then build for production
npm run previewServe the production build locally
npm run lintESLint over the whole tree
npm run publishBuild & deploy to the pages branch

10 The skill filter

Three React contexts cooperate to make filtering feel instantaneous:

  • SkillKeyContext: owns the slug → pretty-name map and a getSkillTitle() helper.
  • SkillFilterContext: owns the active filter set + toggleSkillFilter() and isActive(skills) predicates.
  • useSkillStats: given a slug, returns { years, projects, firstUsed, lastUsed } by walking the parsed YAML.

Every renderable block calls isActive(theseSkills) and dims itself if no filter matches. The <Filterable> wrapper is the standard primitive; see src/components/Filterable.tsx.

// inside a section/responsibility
const { isActive } = useSkillFilter();
return (
  <div className={isActive(skills) ? '' : 'dimmed'}>
    …
  </div>
);

11 License

Released under the GNU General Public License v3.0 or later. You can use, modify, and redistribute it; derivative works must remain under GPL-3.0+. Full text is in LICENSE.

© 2026 Sara McCutcheon <code@saram.cc>. This program is distributed without warranty; see gnu.org/licenses for details.