Felipe Tavares — brain glyph brand mark Felipe Tavares

Why I Built This Site the Way I Did

The reasoning behind felipetavares.dev: a $0 constraint, a framework-free core, one sanctioned model call, and a site built for machine readers as much as human ones.

5 min read

Most personal sites start with a template and end with a plugin problem. I wanted the opposite: a site whose own construction shows the discipline I write about. This is the reasoning behind felipetavares.dev, including the parts I got wrong.

The rules I set

One hard constraint and three rules did most of the design work.

The constraint: $0/month recurring, domain already owned. Everything has to fit inside free tiers with enough headroom that a personal site never hits a limit. That single line ruled out hosted CMS seats, ruled out a pay-per-request LLM API as the default path, and pointed at Cloudflare, which gives away generous compute.

The rules that followed:

  • No visual feature is load-bearing. If an animation never fires, the content still reads.
  • Content is data with one source of truth. Every surface renders from the same records.
  • A model call has to justify itself. Determinism is the default; AI is a deliberate exception.

The stack the rules produced

The site runs on Astro 5, static-first, deployed to Cloudflare Pages. React exists only as islands, small components that genuinely need interactivity: the theme toggle, the menu, the chat widget, the accessibility panel. Article bodies, navigation and the projects grid ship as plain HTML with almost no client JavaScript. The design spec caps total JS on content pages at 50KB, and islands load lazily.

The few routes that need a server run as Workers functions on the same deployment: /api/chat, /api/contact, and /api/subscribe. One codebase, one deploy, two rendering modes chosen per route.

Styling follows the same one-source rule. A single token file drives dark and light themes through CSS custom properties, and Tailwind v4 maps those tokens into utilities, so component markup never hardcodes a color. Change a token and both themes update everywhere.

A core with no framework imports

The part that surprises people: src/core/ is pure TypeScript, entities, ports and services, with zero framework imports. The same hexagonal discipline I would use on a much larger system.

Content is the reason. Pages never read Astro collections directly; they call a ContentRepository port, and one adapter knows content lives in git as MDX. If editing ever outgrows git, a hosted CMS becomes a new adapter plus a one-line config change, with zero page changes. The chatbot’s LLM provider sits behind the same kind of seam: Workers AI today, swappable through one config file.

This is not free. A page calling getCollection() directly would be simpler for exactly one content source. I keep the port because I have rebuilt enough systems to know which version gets expensive later.

And the boundary is enforced, not hoped for: architecture tests fail the build if any core file imports a framework, and they run on every commit alongside the rest of the suite.

One sanctioned model call

The site’s first architecture decision record sets one rule: the chatbot is the only LLM call in the codebase. It answers from a retrieval index built at deploy time, currently 87 content chunks, scored lexically, running on the Workers AI free tier, rate-limited.

Everything else that looks like it wants a model is a deterministic function instead:

  • Search scores matches with a hand-rolled algorithm, no embeddings.
  • Social images are generated at build time from each page’s own metadata.
  • The publish kit turns one article into Substack HTML, a LinkedIn post, a Reddit framing note and an X thread, using templates, not generation.
  • The redaction gate is a regex scanner that blocks confidential figures from ever reaching a public page. A model would be slower and worse at an exact-match problem.
  • The blueprint graph on the projects page uses a seeded random generator, so rebuilds are byte-identical.

The reason is cost and trust in equal parts. Deterministic functions are free, reproducible and testable. The one model call earns its place because language understanding is the feature.

Built for machine readers too

A growing share of this site’s visitors are not people. Coding agents, AI screeners and chat assistants read it, so the site treats them as first-class readers:

  • an MCP server publishes the content as typed tools, so an agent gets structured JSON from the same core services the HTML renders from;
  • llms.txt gives crawling agents an index, and every page has a markdown twin;
  • an AGENTS.md at the repo root tells coding agents where to start.

Nothing here is a second copy of the truth. The MCP tools, the markdown twins and the HTML pages all render from the same content records, which is the one-source rule doing its job.

The newsletter took the same shape

The newest piece follows the same pattern. The subscribe form posts to /api/subscribe, which validates the address, rate-limits by IP, and records every signup in a Cloudflare KV namespace as an audit trail. When Resend credentials are present, the address also syncs to a Resend audience. No email is sent until there is something worth sending, and the Substack link sits beside the form for readers who prefer it.

Under the hood it is ports and adapters again: a subscriber-store port, a KV adapter, a Resend adapter. Removing or swapping the email provider is a config change, which is exactly the test a new dependency has to pass to enter this codebase.

Guardrails live in the pipeline

Quality here is not a review step, it is the pipeline:

  • 118 unit tests, including the architecture boundary tests;
  • the redaction gate, on every build;
  • a post-deploy smoke suite that checks the live site after each release;
  • type checking as a merge gate.

If a rule matters, something executable enforces it. That sentence is also a fair summary of what I write about professionally.

What I would still change

Three honest items. Keystatic still runs in local mode only, so publishing from a phone needs a GitHub App I have not set up. The visual identity churned more than the architecture did, and I wasted words describing a design that got replaced before launch; the lesson is to pin decisions in writing early and describe visuals late. And the writing section deserves a better reading experience than the rest of the site’s layout gives it, which is work in progress.

Worth taking away:

  • A hard constraint is a design tool. The $0 rule made more decisions than taste did.
  • Ports cost an afternoon and save a rewrite. The newsletter proved the pattern a second time.
  • Make determinism the default. One sanctioned model call, everything else reproducible.
  • Machine readers are readers. Serve them structured data from the same source of truth as everyone else.

Newsletter

Get new articles by email

Enterprise architecture, AI strategy, and data platforms — straight to your inbox, no spam.

Or Subscribe on Substack (Launching soon)