Back to articles
CategoryDemos

How I built a tool to tailor resumes using Harvard's official method

August 25, 2026
Tags
Artificial IntelligenceProductivityFrontendCareerOpen Source
How I built a tool to tailor resumes using Harvard's official method

Hero

Estimated reading time: 5 min

Look at ResumeTailor's home screen: right away you see two straightforward cards at the top, one to drop your resume (or click to export your LinkedIn profile as a PDF and upload the file) and another to paste the job description. Right below, the results panel builds the final document in real time, displaying badges with the model used and the exact duration of each step.

Pay close attention here: this is not a tool built to generate generic resumes for you to blast everywhere. Definitely not. ResumeTailor was designed around the official Harvard career guide to rewrite your background with total focus on one specific job opening. That's the whole point.

The problem

Sending the exact same PDF to fifty different job openings is the quickest way to get ignored in ten seconds flat. ATS filters and recruiters scan documents searching for the exact wording of the posting, and if you applied for a data analyst position with backend-heavy copy, your profile gets tossed straight into the noise pile.

And there's another thing: lots of people trick themselves into believing a good resume has to be five pages long. It doesn't. Companies want to see career stability and clear focus on what actually matters for the role, not an endless laundry list of every single tool you touched in the last ten years. But sitting at your desk and manually rewriting every single line of your work history for every application eats up hours of mind-numbing work.

So people cut corners: they dump everything into a generic ChatGPT prompt. The result is awful. The AI starts making up metrics, inventing tools you never used, and spitting out hollow marketing fluff that any recruiter can smell a mile away.

The solution

To solve this, I built ResumeTailor. It is an app focused on taking your background and aligning every single line with the guidelines from the official Harvard career guide.

The workflow is dead simple. You grab your resume or export your LinkedIn profile as a PDF, drop it into the tool, paste the job description with company details, and only then generate your tailored resume plus the cover letter.

Pay close attention here: it does not invent facts. There is a strict grounding contract in the prompt that forbids the model from fabricating companies, metrics, or job titles you never had. It stays true to your real career, translated into the exact vocabulary of the job opening.

You can run your own tests right on the tools section of my website. That is the whole goal.

Features

To make this work in practice, I built the app with straightforward, no-nonsense features:

  • Flexible data input: you can paste text manually or just drag and drop a PDF. If you go to your LinkedIn profile and click export to PDF, the tool parses everything right in your browser via PDF.js without sending your file to any remote server.
  • Parallel multi-model pipeline: when you click "Generate all", the app fires three concurrent calls through OpenRouter for Summary, Experience, and Skills. If one section fails, it won't break the rest, and you can even pick a different AI model for each block.
  • Single-call mode: if you want a consistent voice across the whole document, this button builds the resume from top to bottom and pulls in extra sections like Education, Certifications, and Languages whenever your original background backs it up.
  • Harvard-style cover letter: a dedicated screen with five optional fields. Pay close attention here: if you leave the company or contact field blank, the prompt degrades cleanly instead of hallucinating fake details.
  • Editable contact info and clean PDF: the dashboard pulls your links and phone number cleanly, letting you tweak them before generating an A4 PDF server-side without ugly browser print headers. That's the whole point.

Tech stack

To get this thing off the ground without a bloated backend, I kept the architecture dead simple.

On the frontend, the core runs on React 19, TypeScript 6.0, and Vite 8. The UI uses Tailwind CSS 4 with shadcn/ui components, paired with Motion for slick animations. When you upload your LinkedIn PDF, text extraction runs straight on your machine using PDF.js. Zero wasted bandwidth.

The AI call streams directly from the browser to the OpenRouter API. The default model is openai/gpt-5.6-sol-pro on max reasoning effort (max), completely skipping any middleman server.

The only piece running on Node is the POST /api/pdf endpoint. I built a custom Vite plugin that parses the markdown using markdown-it and fires up Puppeteer with headless Chromium to stamp out the A4 PDF matching the exact required dimensions. To lock everything down, I wrote over 840 tests in Vitest checking everything from XML prompt contracts down to header invariants.

The most interesting challenge

The biggest nightmare when putting AI to work on resumes is outright hallucination. If you just ask the model to "adapt my background to the official Harvard guide", it gets carried away: swaps your words for flashy action verbs and invents out of thin air that you led fifty engineers or doubled revenue. Pure fiction.

To stop that behavior cold, I put together a goal-first XML prompt template. It defines a strict grounding contract and splits the reasoning into mandatory internal phases before spitting out the final answer:

xml
<!-- src/assets/prompt.xml -->
<grounding_contract>
  <rule id="resume" apply="true" />
</grounding_contract>

Exporting clean PDFs on the server

Trying to print a resume straight from the browser with window.print() is asking for trouble. Chrome insists on stamping the date at the top, slaps an about:blank URL in the footer, and still manages to sneak a blank page at the start because of the app CSS wrappers. You can't send a document like that to your dream job.

The way out was ditching the browser print dialog completely and building a dedicated route. The client sends markdown via POST /api/pdf, a Vite plugin compiles it into clean, isolated HTML, and Puppeteer runs headless Chromium in the background to spit out a clean A4.

typescript
// server/vite-plugin-resume-pdf.ts
import { createPdfMiddleware } from './pdfMiddleware';
import puppeteer from 'puppeteer';

export async function renderA4Pdf(htmlContent: string): Promise<Buffer> {
  const browser = await puppeteer.launch({ headless: 'shell' });
  const page = await browser.newPage();
  await page.setContent(htmlContent, { waitUntil: 'networkidle0' });
  const pdfBuffer = await page.pdf({
    format: 'A4',
    printBackground: true,
    displayHeaderFooter: false,
    margin: { top: '0px', bottom: '0px', left: '0px', right: '0px' }
  });
  await browser.close();
  return Buffer.from(pdfBuffer);
}

The displayHeaderFooter: false flag kills any header junk, while headless: 'shell' cuts memory usage without needing a bloated full-Chrome install. The downside is obvious: hosting static files on a serverless bucket breaks PDF generation. To run this in production, you'll need a Node runtime running this middleware alongside the app. That's the trade-off.

What I learned

Building this tool taught me four very practical lessons about developing with reasoning models.

First: Harvard's action verb list makes a massive difference. When you force the model to pick punchy verbs instead of hiding behind corporate buzzwords, the resume immediately stands taller.

Second: locking your app into a single provider is shooting yourself in the foot. Letting the user pick any model from OpenRouter's catalog gives them real freedom over cost and speed without boxing in the product.

Third: asking nicely in the prompt doesn't fix hallucinations. You need explicit validation contracts and check gates. Without decidable rules, the model makes stuff up.

And finally, max-effort reasoning means respecting machine time. I learned the hard way that normal requests die along the way if you don't hold the connection open over SSE streaming with keep-alive comments. Programmed patience.

Try it out!

The tool is live and ready for you to run. Head over to https://frederico-kluser.com/tools and look for ResumeTailor.

The quickest test you can do right now is simple: go to your LinkedIn profile, click export as PDF, and upload the file straight into the app. If you'd rather not mess with your own data right away, no problem, you can load the dummy sample profiles right in the UI. Grab the job description for that specific role you want, paste it in, and generate the tailored resume.

Give it a test drive, let me know what you think, and open an issue on the repo with feedback. If you know anyone hunting for a job right now, pass this post their way.

Generic applications go straight into the trash; laser-focused ones change everything. That's the whole point.