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.
- 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
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.
- 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.
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.
| Readers | Since | Count | What they went for |
|---|---|---|---|
| People | 27 Sep | 61 visitors, 134 page views, 10 countries | Two thirds of views on the home page, then Experience, Recommendations and Patents. Desktop first, phones close behind. |
| Search engines | 12 Oct | 108 requests | Bing most of all, then Google and Apple. The sitemap, then every page. |
| AI crawlers | 12 Oct | 52 requests | OpenAI’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 probes | 12 Oct | 348 requests | No 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.mdand so on, served with a canonical link back to the page, plus anllms.txtindex 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.
- 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.
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
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.