← Projects

Project write-up · steven.wilssens.com

Building steven.wilssens.com

A résumé run like a product: one source of truth for the pages, the PDF and the AI; a scored gate on the résumé; readers counted, machines included; and a loop that turns a recruiter’s unanswered question into a pull request.

Steven Wilssens · October 2026 · 12 min read

10
countries visitors came from in the first three weeks
100
percent of loading, response and stability measurements rated good
18
résumé changes approved through the interview loop
5
pull requests opened by Claude Code from a conversation, all merged
Share on LinkedIn ↗

This site went live on 26 September 2026, two weeks after my last day at Microsoft. It is a React site prerendered to static HTML on Cloudflare, with its own analytics, a generated résumé, a private MCP server and an AI assistant, and the whole thing is run from one data file and one Claude conversation.

It is for recruiters and hiring managers, who arrive from a LinkedIn message or a search and decide in seconds. What they look for first is not a story: level, team size, location, work authorization, availability, and a résumé they can forward. Those are in the hero on every device, and the résumé is one click away on every page. The sceptic who asks “15% of what, measured how?” is the second reader; search engines and AI assistants are the third, and both get their own sections below.

01One file, five outputs

The rule that shaped the codebase is the one I would set for any data product: a fact lives in exactly one place. Here that place is a single TypeScript file, src/resume.ts: my profile, every role with a stable key, the impact numbers, skills, selected work, honors, education, recommendations and press. No component hard-codes a fact about me.

One file about me, five things built from itsrc/resume.tsprofile · roles with stable keys · impact numbers, each with its bulletskills · selected work · honors · education · recommendations · pressNever a fact about me anywhere else.The pagesprerendered HTMLThe résumé PDFrendered and scoredMarkdown + llms.txtone copy per pageThe assistantits system promptThe MCP serverreview and proposalsThe tests fail when the PDF no longer matches the file, so a content change always comes with a new résumé.
The pages, the résumé PDF, the Markdown copies, the assistant’s knowledge and the MCP server are all built from the same file.
  • The résumé PDF is generated, never edited. A script renders it from the file in a headless browser, and a test extracts the PDF’s text and checks that every role, date and recent bullet is in it. The résumé a recruiter forwards cannot say something different from the site.
  • The assistant and the MCP server import the same file. When a bullet changes, the assistant’s answers change in the next deploy, without anyone rewriting a prompt. The LinkedIn share image is generated from the headline and the photo for the same reason.

This is unremarkable engineering, and that is the point. A résumé in Word, a site in a page builder and a LinkedIn profile edited by hand drift apart within a month, and recruiters notice drift before they notice design.

02A gate on the résumé

A résumé is judged by three readers with different eyes: a recruiter skimming for level and scope, an applicant tracking system extracting text and keywords, and an editor who notices a present-tense verb in a role that ended. The build has a checker that reads the PDF as all three.

A new résumé has to score at least as well as the one it replacesEdit the datasrc/resume.tsRender the PDFHTML to PDF, headlessScore both files100 − 25 per error − 5 per warningSame or better: replaceLower: keep the currentThe findings name the data to fix: a role, a date, a number without a bullet. Never the checker.What the score readsPages and file size · extractable text and glyphs · contact details · headline and summary · every role, company and datethe bullets of the two latest roles · tense · team size · keywords recruiters search for · numbers with context
The résumé build scores the new PDF and the current one, and only replaces the file when the score does not drop.

It checks that the text is extractable without icon-font glyphs, that the file is two pages or fewer, that the contact details, headline, every role, company and date match the site, that the bullets of the two latest roles match word for word, that past roles are in the past tense, that a team size and the keywords recruiters search for appear, and that every number on the home page has a bullet behind it. Errors cost 25 points and warnings 5. A new PDF only replaces the current one when its score does not drop, and overriding that needs a reason in the commit message.

The gate earned its keep on its first day. The first batch of résumé changes from the loop described below added scope and outcomes to my latest role, and the PDF ran one line onto a third page. The checker scored it 65 out of 100, the test went red, and main stopped deploying:

$ npm run resume:check
public/steven-wilssens-resume-2026.pdf: score 65/100, 3 pages, 346 KB, 1 error, 2 warnings
  [error] 3 pages; a resume for a principal role is two at most.
  [warn] "15% increase in Windows adoption" has no role bullet behind it in src/resume.ts; a hiring manager will ask what it measures.
  [warn] "30% reduction in customer issues" has no role bullet behind it in src/resume.ts; a hiring manager will ask what it measures.

The fix was a layout one, an hour later: a compact honors line, a shorter patents note and a smaller bottom margin brought the PDF back to two pages at a score of 90. The checker was not touched. The two warnings are fair too: those numbers have been on my résumé for years, and I owe them a bullet or a deletion.

03Three kinds of readers, counted apart

I did not add a third-party analytics script. The site has its own tracker, a few hundred lines, that records page views, time on page, scroll depth, clicks and Core Web Vitals into a Cloudflare D1 database: no cookies, no stored IP addresses, nothing kept past 13 months, and nothing at all from visitors who send Global Privacy Control or Do Not Track. The privacy page says exactly this, in plain words.

The part I would not get from an off-the-shelf tool is the second table: every request from a crawler, logged separately with the bot’s name, the company behind it, its category and the network it came from, so a faked user agent shows up as a mismatch.

ReadersSinceCountWhat they went for
People27 Sep61 visitors, 134 page views, 10 countriesTwo thirds of views on the home page, then Experience, Recommendations and Patents. Desktop first, phones close behind.
Search engines12 Oct108 requestsBing most of all, then Google and Apple. The sitemap, then every page.
AI crawlers12 Oct52 requestsOpenAI’s search bot, Anthropic’s Claude for users and for training, Amazon. The robots file and sitemap first, then the home page and the Markdown copies.
Scripts and probes12 Oct348 requestsNo user agent, curl, or a bare “Mozilla”. Kept apart so they never count as readers.

Three weeks is too early for conclusions, but two things already changed what I did. AI crawlers fetch the Markdown copies of the pages, which told me the next section was worth doing. And every loading, responsiveness and stability measurement so far is rated good, on phones as well as desktops, so performance work can wait. The numbers are read on a private dashboard, behind a token: the decision and the number behind it on one screen, the same habit as at Microsoft.

04Readable by AI, on purpose

When a recruiter asks an AI assistant “who is Steven Wilssens”, the answer depends on what the assistant could read. A single-page app that renders in the browser gives it very little. So the build does the work up front.

  • Every page is prerendered to its own HTML file with its title, description and structured data: who I am, what each page is, where it sits on the site, and that this write-up is an article. Google uses the profile markup for people results.
  • Every page also exists as Markdown. The build converts the prerendered HTML into /about.md, /experience.md and so on, served with a canonical link back to the page, plus an llms.txt index and one file with every page. A model reading the Markdown gets the content without the navigation and the icons.
  • The sitemap tells the truth about dates. A page’s last-modified date is when its content changed, not when the site was last deployed, and each production deploy pings the search engines through IndexNow.

None of this is visible to a person, which is why it is easy to skip. The crawler log is what makes it worth doing: within five days, the AI crawlers were fetching the Markdown copies.

05An assistant that knows only what I published

Every page has an “Ask about Steven” button. It opens a panel where a recruiter can ask anything about my work and get a short answer with links to the pages it rests on. Like the help assistant on wilssens.com, it is restricted by what it is given, not by what it is told.

The assistant docks beside the page on a wide screen and the page moves over. The suggestions change with the page the reader is on.
  • Its knowledge is the data file plus my public notes. The system prompt is built from the same file as the site, the patents list and the dossier entries I marked public. It contains nothing I have not published.
  • Every answer starts with a hidden line the server strips: the sections it rests on, shown to the reader as links; “none”, when my notes do not cover the question, which saves it as a gap; or “out”, for anything off topic, where the reader sees a fixed sentence and the model’s text is never shown.
  • Answers are signed, so nobody can send the assistant an earlier answer it did not give. Limits are in the code: thirty questions an hour per visitor, twelve turns, a thousand characters, an optional human check, and a monthly budget counted from the provider’s own token usage. When the budget is spent the button disappears and the site keeps working.

Questions, cost, votes and the unanswered questions go to the dashboard. The assistant is a day old as I write this, so the only honest number is the cost: four questions, nineteen cents. The interesting number will be the gaps, because of what happens to them next.

06The loop: interview, review, edit, merge

The part of this site I am most pleased with is not on the site. It is a private MCP server, reachable only through a link I have, that turns Claude into a workspace for my career. Connected to the Claude app, it does four things.

  • Interview me like a sceptical recruiter. It reads what is already published, so it never asks what the site answers. It knows which roles have no numbers, which stints are short and where dates overlap, and asks one question at a time. Every answer becomes a dossier entry, private by default. Questions the site’s assistant could not answer go to the top of the list.
  • Review the site from a recruiter’s seat. A review tool reads the published pages, the résumé report and an import of my LinkedIn profile, and returns findings with why a recruiter cares and what fixes it: a leadership role with no team size, an unexplained number, a gap, a LinkedIn headline that says something else. It scores the whole, and the score is what I work against.
  • Propose résumé changes and record my decisions. At the end of a session Claude compares the dossier with the résumé and proposes concrete edits: the current text, the new text, the notes it rests on. I approve or reject each one. So far: 20 proposals, 18 approved, 2 rejected because they overstated.
  • Change the site, through a pull request. An approved batch goes to GitHub Actions, where Claude Code edits the data file on a branch, rebuilds the résumé through the gate, runs the lint, tests and build, opens a pull request, starts the preview and emails me. I read the preview and merge, from the same conversation if I like.
From a question nobody could answer to a change on the siteA visitor asksthe assistant on the siteNo answer in the notesthe question is savedClaude interviews mein my private MCP, as a recruiterThe dossier grows22 entries, 9 publicA résumé proposalcurrent text, new text, whyI approve or rejectin the conversationClaude Code editson a branch, under checksI merge the pull requestafter the previewDeploy: the pages, the PDF, the Markdown copies and the assistant change together, and I get an email when the résumé is live.The question that started it is now answered, on the site and in the assistant.
A question the assistant could not answer becomes an interview question, a dossier entry, a proposal, a pull request and a change on the site. I decide twice.
The first batch of résumé changes, as the workflow opened it: the instruction, the approved wording, the checks, and a preview link. Five files changed, seven lines each way.

The agent never pushes to production. It has a short list of allowed tools, a turn limit, and the instructions I follow myself: never invent a number, leave a question in a comment when a fact is missing. Six runs so far: two failed and made nothing, four opened pull requests, and with an earlier one, five are merged. The failures cost minutes and showed me where the instructions were vague.

The site is edited by an AI, reviewed by an AI playing a recruiter, and answered for by an AI. Every one of them is confined to what I wrote down, and a person decides before anything ships.

07Best practices

  1. One source of truth, with a test that fails when outputs drift. Generated outputs are only trustworthy if something checks them, on every build and in every pull request.
  2. Turn quality into a score, then gate on the score. “Is the résumé better?” is an argument. “Does it score at least as well?” is a build step. The findings point at the data, never at the checker.
  3. Every number needs its evidence next to it. An impact number must name the bullet that backs it, or the build warns. The rule I held my teams to, applied to myself.
  4. Count the machines too. Search engines and AI crawlers are readers with their own needs. Logging them apart turned “should I bother with Markdown copies?” into a yes with evidence.
  5. Restrict an assistant by what it is given. A prompt can be talked around; a system prompt built only from published data cannot leak what is not in it. Sign the answers, cap the spend, keep the off-topic path on the server.
  6. Let the agent open a pull request, never push to main. A branch, a tool allowlist, a turn limit, the full test suite and a human merge make its failures cheap and visible.

08What’s next

Three batches of approved résumé changes have landed through the loop, including a role that closes a gap recruiters used to ask about. Next, LinkedIn gets compared against the site after every batch, so the three tell one story, and the assistant’s unanswered questions, once there are some, set the agenda for the next interview.

The numbers in this write-up are from 17 October 2026. The dashboard will tell me when they are worth updating.

Ask the assistant something a recruiter would ask.