Build a site like this with AI, part 3 of 3
One file for the resume page and the PDF
A JSON resume renders the web page at build time, and Playwright prints the same data to a PDF during the deploy.
I wanted a resume page and a downloadable PDF that never disagree. The fix is to keep the resume in one file, web/src/data/resume.json, and generate both from it.
The data
The file is plain JSON: a summary, skill groups, each role with its bullets, education and certifications. A job with several roles or kinds of engagement nests them under a sections list instead of bullets.
{
"name": "Richard Clegg",
"title": "Lead Data Engineer",
"experience": [
{
"role": "Data Engineer",
"company": "Grainbridge",
"period": "2021",
"bullets": [
"Designed the data lake and warehouse on AWS S3 serving QuickSight and SageMaker.",
"Built PySpark ETL on AWS Glue and ingestion from RDS and DynamoDB."
]
}
]
}
To build it, I gave Claude Code my old Word resumes and asked it to merge them into this shape. Then I asked it to check my certifications against my Credly profile, which showed which ones had lapsed so the site lists only active ones.
The page
Astro imports JSON directly, so the resume page (web/src/pages/resume.astro) is a template over the data. Simplified:
---
import resume from "@/data/resume.json"
---
{resume.experience.map((job) => (
<article>
<h3>{job.role}, {job.company}</h3>
<ul>{job.bullets.map((b) => <li>{b}</li>)}</ul>
</article>
))}
The PDF
scripts/build-pdf.mjs reads the same file, builds a plain print layout as an HTML string, and asks headless Chromium to print it:
const browser = await chromium.launch()
const page = await browser.newPage()
await page.setContent(html)
const pdf = await page.pdf({ format: "Letter" })
writeFileSync(join(root, "public/Richard-Clegg-Resume.pdf"), pdf)
The npm build script runs it before Astro, so the PDF lands in public/ and ships with the site:
"build": "node scripts/build-pdf.mjs && astro build"
In CI, the deploy’s build command installs Chromium first with npx playwright install --with-deps chromium. The PDF is in .gitignore, so the repo never holds a stale copy.
Updating it
I edit resume.json and push. The page and the PDF update together in about two minutes.