# resume-cv

A free, MIT-licensed **single-page resume / CV** template for individual job-seekers — engineers, designers, knowledge workers. Demonstrated by a fictional **Nadia Costa**, a senior software engineer with 8 years of experience. Print-friendly to a clean A4 or Letter page; one click downloads PDF via the browser.

**Light + dark · Source Serif 4 + Inter Tight · Editorial monochrome with slate-700 accent.**

![Preview](./preview.png)

**Live demo:** [templates.allanninal.dev/resume-cv/](https://templates.allanninal.dev/resume-cv/)

---

## Features

- **Locked print stylesheet for A4 + Letter.** Hit Cmd/Ctrl-P (or click "Download PDF") and a clean one-page document renders. Sticky nav, theme toggle, donate link, paper-size toggle — all hidden in `@media print`. Forced light-on-print regardless of dark mode. Tested in Chrome, Firefox, and Safari.
- **`window.print()` for PDF — zero build dependency.** No Puppeteer, no Playwright PDF runner, no server. The "Download PDF" button calls `window.print()` and the locked `@page` stylesheet does the rest. Works on GitHub Pages.
- **Source Serif 4 + Inter Tight typography pairing** — both variable, both free, neither used in any prior day's template (Days 1-8). Reads as "editorial print document," not tech-startup interface. Source Serif 4 carries the display name + section labels; Inter Tight handles body, bullets, metadata.
- **Editorial monochrome palette** — `#FFFFFF` base, `#111111` text, `#334155` (slate-700) accent for section labels and links. No vivid color. Signals "professional document." Dark mode lifts to slate-900 surfaces with slate-100 text.
- **Specific fictional persona (Nadia Costa)** — 4 jobs with outcome-metric bullets, 2 schools, 4 selected projects with tech stacks, 3 skill categories, 3 spoken languages. A visitor can evaluate the template immediately without imagination. Every "John Doe" template fails this bar.
- **No skill bars, no progress meters, no sidebar photo, no decoration.** The three most-criticised CV-template anti-patterns. Skills render as comma-separated tag lists; education renders as plain typeset entries.
- **Schema.org Person** with `jobTitle`, `email`, `sameAs[]` (LinkedIn / GitHub / website), `alumniOf[]` (two `EducationalOrganization` entries), `worksFor` (most recent employer as `Organization`), `knowsAbout[]` (skill keywords). Build-time JSON-LD.
- **Light/dark with system-pref default** — `prefers-color-scheme` + `localStorage` override + a sun/moon toggle in the sticky header. Print always renders light regardless of mode.
- **Paper-size toggle (A4 / Letter)** — visible in the header on screen, hidden on print. Persists choice to `localStorage`.
- **GH Pages deploy pre-wired** — `pages.yml` workflow, `base` path resolved from repo name, MIT license.
- **Full test suite — 44 Playwright tests** including: page renders, JSON-LD shape, OG image absolute + 1200×630, OG type `profile`, Twitter card, sitemap, robots.txt; all 4 main sections present (Summary / Experience / Selected Projects / Education) plus aside (Skills / Languages); experience has 4 jobs, projects has 4 entries, education has 2 entries; theme toggle persists; paper toggle defaults to A4 and switches to Letter; **Download PDF button calls `window.print()`** (verified by spying on `window.print`); **print emulation hides nav/toggles/footer**; **print forces light colors regardless of dark mode**; **no skill bars / progress meters in markup**; **no body images** (no subject photo); axe clean in both themes; Lighthouse all four ≥ 95.

## Stack

- **Framework:** Astro 4 (static output)
- **Styles:** Tailwind CSS, CSS custom properties for theme tokens, `@page` + `@media print` rules in custom CSS
- **Fonts:** Source Serif 4 Variable + Inter Tight Variable (self-hosted via @fontsource-variable)
- **Testing:** Playwright, @axe-core/playwright, Lighthouse CI, linkinator

## Quick start

```bash
unzip resume-cv-source.zip -d my-cv
cd my-cv
npm install
cp .env.example .env
npm run dev
```

## Customize

### 1. Personal data

Edit `src/data/resume.ts`. Six exports — `person`, `links`, `experience[]`, `projects[]`, `education[]`, `skills[]`, `languages[]` — are the source of truth for the entire page.

```ts
export const person = {
  fullName: 'Your Name',
  title: 'Your Job Title',
  location: 'City, Country',
  openToRemote: true,
  email: 'you@example.com',
  emailDisplay: 'you@yourdomain.com',  // optional display label
  phoneDisplay: '+1 555 0100',          // display-only
  pronouns: 'they/them',
  shortBio: '...',
  summary: '...',
};
```

Each `experience[]` entry has `company`, `role`, `period`, `location`, and `bullets[]` (2-3 outcome-metric bullets). Each `projects[]` entry has `name`, `description`, `stack[]`, `href`. Each `education[]` entry has `institution`, `degree`, `period`, optional `detail`. `skills[]` is grouped by category. `languages[]` is `name` + `level`.

### 2. Print to PDF

Click the "Download PDF" button in the sticky header. Your browser's print dialog opens with a one-page render of the resume; choose "Save as PDF" as the destination.

The print stylesheet (`@media print` in `src/styles/global.css`) hides the chrome, scales the h1 down, strips tag pill backgrounds, and forces white background + black text regardless of theme. The default `@page` size is A4 with 12mm × 14mm margins; switch to Letter via the toggle (or change `@page { size: Letter; }` in CSS).

If the resume runs over one page with your content, common fixes:
- Trim bullets to the 2–3 highest-impact items per role.
- Cut older roles (over 10 years out) to a single line.
- Reduce the `@page` margin to 10mm × 12mm in `src/styles/global.css`.
- Reduce section gaps in the `@media print` block (`main > * + * { margin-top: 1rem }`).

### 3. Theme tokens

Edit `:root` and `html.dark` in `src/styles/global.css`. The accent comes in two shades:

- `--color-accent: #334155` (slate-700) — section labels, eyebrows, link hover, focus rings
- `--color-accent-deep: #1E293B` (slate-800) — emphasis text, primary button bg

If you swap the accent, verify both shades pass WCAG AA at 14px+ on `--color-bg`. Editorial monochrome is the recommended register; vivid accents undercut the "professional document" signal.

### 4. SEO / schema.org

The Astro layout (`src/layouts/Base.astro`) emits a Person JSON-LD block at build time. Edit `src/data/resume.ts` and the schema updates automatically — `name`, `jobTitle`, `email`, `sameAs`, `alumniOf`, `worksFor`, `knowsAbout` are all derived from the data.

### 5. What this template doesn't have

- **No photo of the subject.** Bias risk in US/UK/DACH tech hiring contexts; layout fragility on print. If you genuinely want a photo, add an `<img>` to the header with explicit dimensions and the `data-print-hide` attribute (or include it in print at your discretion).
- **No "References available on request" section.** Hiring managers contact references directly.
- **No skill progress bars or percentages.** "JavaScript 90%" is meaningless; categorical lists are ATS-readable and design-clean.
- **No cover letter.** Separate document; out of scope.
- **No JSON Resume schema export.** Worth porting if you want — see [jsonresume.org](https://jsonresume.org).

---

## Deploy to GitHub Pages

> The source repo is private — there is no Fork button. Download the source ZIP from [templates.allanninal.dev/resume-cv/](https://templates.allanninal.dev/resume-cv/) and follow these steps.

1. **Unzip + init**:

   ```bash
   unzip resume-cv-source.zip -d my-cv
   cd my-cv
   git init && git add . && git commit -m "init from resume-cv template"
   ```

2. **Push** to a new GitHub repository.

3. **Settings → Pages → Source: GitHub Actions.** The included `pages.yml` builds and deploys.

### Custom domain

Add `CNAME` in `public/`, set `PUBLIC_SITE_URL` and `PUBLIC_BASE_PATH=/` in repo Variables → Actions.

---

## Testing

```bash
npm run test:all
# = npm run build && npm run test && npm run test:links && npm run test:lighthouse
```

| Suite | What it covers |
|---|---|
| `tests/pages.spec.ts` | Renders with h1, no console errors; meta description; **Person JSON-LD** with `name`, `jobTitle`, `email`, `sameAs[]`, `alumniOf[]` (2 entries), `knowsAbout`, `worksFor`; OG image absolute + 1200×630; OG type `profile`; Twitter `summary_large_image`; canonical absolute; sitemap; robots.txt |
| `tests/interactions.spec.ts` | All 4 main sections (Summary / Experience / Selected Projects / Education) and aside sections (Skills / Languages); experience has 4 jobs; projects has 4 entries; education has 2 entries; skills has 3 categories; languages has 3 entries; LinkedIn uses accent-deep color in header; theme toggle flips dark class + persists; paper toggle (A4 default → Letter, persists); **Download PDF calls `window.print()`** (spy verified); **print emulation hides chrome AND forces light colors regardless of dark mode**; **no progress bars / skill meters in markup**; **no body images** (no subject photo) |
| `tests/responsive.spec.ts` | No horizontal scroll at 4 viewports; mobile h1 above the fold; Download PDF ≥ 36px tap target; desktop two-column layout (main + aside) |
| `tests/theme.spec.ts` | Both modes render; system pref default; Source Serif 4 is the h1 font; Inter Tight is the body font |
| `tests/a11y.spec.ts` | Zero axe violations in light AND dark mode |
| `tests/links.spec.ts` | All `target="_blank"` links use `rel="noopener noreferrer"`; mailto well-formed |

---

## Support and donations

This template is **free and MIT-licensed**.

If it saved you time: [**Ko-fi — ko-fi.com/allanninal**](https://ko-fi.com/allanninal).

---

## License

MIT — see [LICENSE](./LICENSE).

Part of the [365-template series](https://templates.allanninal.dev/) — Day 9 of 365 free, MIT-licensed static website templates.
