Nobody builds this for me.
The portfolio of someone who builds things for a living should, itself, be well built.
1999
Building sites by hand since
Next.js 16
App Router · Turbopack · TypeScript
1 vault
Single source for site, /ai and llms.txt
Claude Code
Pair-programmed, not outsourced
The habit started in 1999.
Every version of this site, back to that year's raw HTML, came out of my own hands. Agencies started offering portfolio-building as standard work somewhere along the way; that offer never got taken up. Hiring it out was never really the temptation.
Five eras, five completely different tools, and the same person behind the keyboard in every one of them. This site's content now lives in an Obsidian vault, the same second brain that runs the rest of my work, with Claude Code doing real pair-programming rather than acting as a chatbot on the side.
A showcase built by someone who builds products for a living makes its own argument before any case study loads, and that argument only holds if the thing is actually well made under the hood. Everything below this line is the evidence for that, laid out for whoever wants to check it.
Text and numbers first. Screens after.
The product case studies here run on real prose and real numbers already; the screens and galleries behind them are still being built. That ordering was deliberate: finish what actually carries a hiring or investment decision, then layer in the visual polish.
If a screenshot would settle something a paragraph can't, asking directly beats waiting for the gallery to ship. The work behind these numbers is real whether or not a screenshot is live yet.
A documented voice, not a vibe.
The copy across this site follows a written guide, not instinct rewritten each time. Identity before job title. A point of view before the evidence that backs it. A number wherever an adjective was about to do the work instead.
One rule did more editing than any other: no more than two consecutive sentences opening with "I + verb" in English, or "Eu + verbo" in Portuguese, a pattern that reads as flat and repetitive even when each sentence is fine on its own. A full pass through all six case studies went back and cut exactly that, along with an overused contrast pattern ("not X, it's Y") that had crept into four of them. This paragraph followed the same rule while describing it.
One serif for a point of view, one sans built to be read.
Two typefaces, chosen for opposite reasons. Atkinson Hyperlegible carries the body copy: designed by the Braille Institute with letterforms disambiguated for low-vision reading, chosen here because running text has to be read without effort, not admired. DM Serif Display carries the opposite job, reserved for wherever the page needs to state a position instead of transmit information.
One accent color, one radius token, one spacing scale: the same restraint applied to color and space as to type. Two of the grays used for captions and dividers fall short of WCAG AA on their own and are restricted to exactly that, borders and low-emphasis labels, never running text. The full audit, tokens and all, is live at /design-system.
One markdown vault, every projection.
The content pipeline is the part that actually took engineering, more than any animation. A single note in a personal knowledge vault turns into a human-readable page, a machine-readable route, and an entry in llms.txt, all from the same file.
Write
Every case, page and decision starts as a note in an Obsidian vault, the same second brain that runs the rest of my work.
Sync
A sync script copies finished markdown from the vault into this repo's content/ai/ folder, one file per case or page.
Split
Each file generates two projections from a single source: full prose for /ai and llms.txt, a curated sections field in the frontmatter for the human case page.
Render
Next.js reads that same file with gray-matter to build both the human route and the machine-readable one. Nothing gets typed twice.
Ship
Commit, push, Vercel deploys. Editing a live case study means editing one markdown file, not three.
Next.js on Vercel, nothing exotic.
No framework built in-house, no clever infra to show off. The interesting decisions live in what didn't get added: no headless CMS, no database, no auth layer for a site that has nothing to log into.
App Router, server-rendered by default. No client bundle ships unless a page genuinely needs interactivity.
Next's Rust-based bundler, replacing Webpack in dev and build. Faster cold starts, the main reason to run it.
Timeline-based entrance animations, hand-tuned per page. No animation-library defaults, every easing curve was picked on purpose.
No headless CMS, no database. Every case and page is a .md file with frontmatter, versioned exactly like the code that reads it.
Continuous deploy from main. Push is the deploy button; there is no staging environment between a commit and production.
Built to be read by crawlers and by models.
Search engines are one audience. Language models fetching the raw page are a different one, with different needs, and the site is structured for both instead of assuming they overlap.
Explicitly allows 12 named AI crawlers, not just the wildcard. The default already permits them; naming each one out loud makes the intent checkable, not just implied.
Raw markdown routes, in English and Portuguese, with no nav, no CSS, no JS. This page has all three; /ai deliberately has none of them.
Every human route plus every /ai/{en,pt}/* page and case, cross-linked with hreflang so engines read the two languages as alternates, not duplicates.
On home and /about, real fields only, sourced from the same facts as the rest of the site. No invented ratings, no schema the page can't back up.
Every markdown file carries its own last-edited date, surfaced in llms.txt so a crawler can tell what actually changed.
Named in robots.txt:
GA4 for people, a Sheet for machines.
GA4 tracks human visits site-wide, standard client-side script. But /llms.txt and /ai/* are force-static: nothing runs per-request in production, so GA4's JS never fires for whatever actually reads those routes, which is mostly curl, LLM tools and crawler bots, not browsers.
A proxy intercepts exactly those two path patterns and fires an async POST before letting the static response through. That request authenticates as a Google service account by hand-signing its own JWT, no SDK dependency, and appends a row (timestamp, path, user-agent, referer, IP) to a Sheet. A daily job buckets the results by bot signature and writes a summary back into the vault. Two logging systems, because GA4 genuinely can't see the audience the other one is built for.
Next 16 renamed middleware.ts to proxy.ts. Matches /llms.txt and /ai/:path*, nothing else.
Fire-and-forget, appends to Google Sheets via a hand-signed service-account JWT.
Covers every human page. Never fires on the two AI-facing routes above.
Curious about the operating system behind all of this?
See how I work