Engineering
How this site is actually built
This site itself is a proof point: the decisions behind it, the stack it runs on, and a changelog distilled from its real commit history. Not marketing copy about engineering discipline. The discipline itself.
Decisions
Decisions
Six calls that shaped the build
Context, the call made, and what it cost or bought. Rewritten from this repo's own ADRs and design specs.
-
Astro over Next.js
The site is mostly static, SEO-critical content read by recruiters and peers on commutes, not an app that needs client-side routing state.
Astro 5 with React islands only where interactivity is real (chat, forms, theme toggle), instead of Next.js.
Gave up Next's larger ecosystem and app-router conveniences. Bought near-zero client JS on every content page by default — a budget Next would need deliberate work to hit, since its defaults ship a client-first runtime whether a page needs one or not.
-
Git-based CMS over a hosted one
No evidence yet that a plain git-plus-editor workflow can't keep up with actual publishing cadence, before paying for a hosted CMS to solve a problem that might not exist.
Content lives as git-versioned MDX/JSON, read through one ContentRepository port, edited three equivalent ways — direct file, Claude Code, or a local Keystatic UI.
Gave up a hosted UI's polish and multi-editor collaboration. Bought a $0 CMS with the swap to Sanity or Payload staying a single new adapter file, not a page-level rewrite, if the workflow ever does hit a ceiling.
-
Workers AI first, Anthropic as fallback
The chatbot needs a model provider, and a paid API as the default breaks the $0/month budget the moment the site gets any real traffic.
A provider router tries Cloudflare Workers AI first, free and colocated with the rest of the stack, and falls back to Anthropic only if that call errors — never the other way around.
Workers AI quality trails frontier models on nuanced questions. The tradeoff: a chatbot that costs nothing at normal traffic, with the upgrade to Claude Haiku sitting behind one config change in src/config/llm.ts, not a rewrite.
-
Cloudflare's free tier as the ceiling, not a starting point
Domain already owned; every other cost (hosting, embeddings, rate-limit storage, email) had to fit inside a tier that won't get bumped by a personal site's real traffic, not just look cheap on a demo.
Pages/Workers for hosting, KV for rate limiting, Vectorize for embeddings, Workers AI for the chatbot — one provider, one free tier, for everything that isn't the domain itself.
Gave up a multi-vendor stack that might optimize any one piece slightly better. Bought a $0/month recurring bill regardless of how many features get added, because they all share the same headroom instead of each adding its own bill.
-
The hero moved from decorative to data-true
An early hero concept was a generic animated gradient mesh with an optional pointer-parallax dot grid: motion for its own sake, no connection to what the site is actually about.
Replaced it with a living-blueprint diagram generated at build time from the real competency matrix — nodes are skill domains weighted by real depth, edges are roles that touched them. Not decorative randomness.
More build complexity than a static gradient. The hero now changes only when the underlying career data changes — the diagram is a claim about real experience, not a mood board.
-
A hand-written fetch transport over the MCP SDK's own
The MCP server's Cloudflare Worker transport needed to implement the protocol's Transport interface, and the SDK ships one, StreamableHTTPServerTransport, for exactly this.
Traced that class into node_modules and found it's typed and implemented directly against Node's IncomingMessage/ServerResponse, which don't exist in the Workers runtime. Wrote FetchTransport instead: the same interface, implemented directly over the standard Fetch API.
Gave up the SDK's official, more-tested transport implementation. Bought a Worker that actually runs, verified with a real `wrangler dev` handshake, instead of one that fails at runtime reaching for a Node built-in that isn't there.
Why this stack
Why this stack
Every piece, one honest reason
No component here is the default choice. Each earned its place against the $0/month constraint.
-
Astro 5
Islands architecture ships near-zero client JS on static content by default.
-
React (islands only)
Used only where a component genuinely needs client-side state — chat, forms, toggles.
-
TypeScript, hexagonal ports & adapters
core stays framework-free; swapping an adapter never touches a page.
-
Tailwind CSS v4
Token-driven styling; every color and spacing value traces to one CSS custom-property file.
-
Cloudflare Pages + Workers
Hybrid output: pages prerender to the CDN, /api/* runs as Workers functions.
-
Cloudflare Workers AI + Vectorize
Free-tier chatbot provider and embedding index, with Anthropic as a config-swappable fallback.
-
Astro content collections (git-based CMS)
content/ is the CMS — MDX/JSON, versioned in git, edited three equivalent ways.
-
Keystatic
Local content-editing UI mirroring the same zod schemas Astro's collections already validate.
-
Cloudflare KV
Rate-limit storage for /api/contact and /api/chat, free tier, no separate database.
-
GSAP, Lenis, OGL (motion stack)
MIT-licensed; loaded lazily per page so content pages never pay for motion they don't use.
-
Model Context Protocol (packages/mcp)
Publishes the same content and chatbot to AI agents as typed tools, not scraped HTML.
Changelog
Changelog
Milestones, not commit noise
Distilled from real git history: dated, narrative, no hashes to chase.
Last updated 2026-07-10
- 2026-07-10 fix
Pre-launch polish and voice pass
Em-dash sweep across all published prose; section spacing unified site-wide via a shared Section.astro scale; hero kinetic type reserves minimum height on mobile to prevent layout jumps; contact copy, credibility framing, and writing subtitle all revised against the voice guide.
- 2026-07-10 infra
PT-BR archived for v1 launch
All /pt/* routes removed from the build by renaming the directory to _pt/ (Astro ignores underscore-prefixed directories). Files stay versioned; the language switcher is removed. Bilingual v2 restores them once the English content is fully QA'd and stable.
- 2026-07-10 content
Experience content revamp
All seven roles rewritten from an owner interview: AI/GenAI leadership added across roles, titles corrected (Finance Consultant/Business Architect; Order-to-Cash Solution Lead), PwC location updated to Amsterdam NL, Siemens dual-city display (Lisbon PT + Munich DE).
- 2026-07-10 feature
Chat launcher redesigned as avatar
The chat entry point changed from a pill button to a 56px circle showing the owner's brand glyph with a green online-status dot — consistent with the brand identity throughout the site.
- 2026-07-09 content
Projects page reframed around delivered value
Each project card now leads with its north star and concrete outcomes before exposing technical detail. Selfwright and felipetavares-dev entries gain expandable deep-dives.