Appearance

System follows your phone’s setting. Default.

Data layer
Date
Read
8 min
Views

I stopped renting my website. Two years ago, that would have been bad advice.

Jul 13, 2026 · 8 min read ·

I rebuilt my site with coding agents: content, analytics and orchestration as code in one repo, on commodity infrastructure. What it cost, and where it got uncomfortable.

01 · The symptom
“The monthly fee was never the real cost of a website platform. The exit is.”

The monthly fee was never the real cost of a website platform. The exit is. Leave a site builder where the templates, content, and analytics are fused to the platform, and you rebuild from scratch, migrate content by hand, and watch your tracking history die in the move. That’s the rent. Not the invoice. The switching cost.

To be fair, before I go further: headless CMS products already solve content portability, and if you’re happy where you are and never plan to leave, the rent is fine. What changed is that building it yourself went from bad advice to a real option for a small team, because coding agents have changed the build-and-maintenance math. When I rebuilt my website, I wanted to test exactly how far that goes. Not to prove a thesis. To find the limits. Here’s what I ended up with, what it cost, and where it got uncomfortable.

02 · What I own

What “own” actually means

First, honesty about the word “own.” I didn’t stop renting. The site runs on Vercel, Cloudflare, Google Cloud, and GA4. All vendors. What changed is which layer I rent: commodity infrastructure with low switching costs instead of a product layer with high ones. The content model, the components, the analytics events, the schema markup, and the pipelines. All of it is code in my repo, and any of those vendors can be swapped without a rebuild.

CLOUDFLARE EDGE VERCEL GOOGLE CLOUD WHERE THE DATA SHOWS UP Visitor's browserone loader, zero vendor scripts DNS + proxyserved first-party ZarazGA4 as a managed componentconsent enforced at the edge Worker: analytics-ingest5-event allow-list Worker: intelligenceInngest durable stepseach step retries alone Next.js sitestatic + ISR renderingJSON-LD on every pageediting UI in the frontend Cloud RunCMS API MongoDBcontent store Cloud Run functionssitemap.xml, llms.txt,llms-full.txt from content Google Analytics 4Google's attribution model BigQueryfirst-party events, permanent+ flattened GA4 export SlackMonday digest, hourly alerts Loops (planned)email sequence on lead_submit pages serves the site APIs sitemap, llms.txt 5 events server-side GA4 hits daily export streaming insert read-only queries digest + alerts planned: enrich the contact, then follow-ups that stop when the lead replies content and pages analytics events and data planned
CONTENT AND PAGES Visitor's browser Cloudflare: DNS + proxy, first-party Vercel: Next.js, static + ISR Cloud Run CMS API + MongoDB Cloud Run functions: sitemap + llms.txt ANALYTICS Visitor's browser 5 events Zaraz at the edge, consent enforced GA4attribution model Ingest Worker5-event allow-list BigQueryfirst-party events, permanent Worker: intelligenceInngest durable steps Slackdigest + alerts Loops (planned)follow-up emails
The full stack: gray arrows are content and pages, blue is the analytics flow, dashed is planned.
03 · Content

Strategy first, and a CMS that’s just an API

I started with strategy, not code. Before any mockup existed, I used Claude Design, Anthropic’s Labs tool for design systems and mockups, to work through the ICP, mission, vision, values, value proposition, StoryBrand, and the jobs-to-be-done for each service. The design system and mockups came out of that work, and the code came out of the mockups. The site argues something because the strategy existed before the components did.

The CMS is an API, not an admin panel. Content lives in MongoDB behind a Cloud Run API. There’s no editing interface bolted onto the backend. Editing happens in the frontend, which talks to the same APIs the agents use. Cloud Run functions generate sitemap.xml, llms.txt, and llms-full.txt from the content itself, so those files can’t drift the way manually edited files do. The first SEO audit caught exactly that failure mode: an llms.txt still selling a product that no longer existed.

About llms.txt, since I’ll get asked. It’s a markdown map of your site for AI crawlers, and right now, the evidence says almost nobody reads it. Ahrefs analyzed 137,000 domains and found that among sites publishing the file, 97% got zero requests in a month. Google has said its systems don’t use it. The one group that does fetch it, per the same study, is AI coding agents. Claude Code out-fetched every AI search bot. I publish it anyway, because generating it from the CMS costs nothing and version control covers the risk. That’s the shape of bet I like: cheap if it never pays off, already in place if AI search adoption arrives. There’s one early signal it might: Chrome’s Lighthouse now runs experimental agentic-browsing audits, llms.txt among them, and the site passes all three. I’d rather be early and wrong than late.

04 · Analytics

Analytics at the edge, data in my warehouse

Analytics runs at the edge, not in the browser. No gtag.js, no Google Tag Manager, no vendor script on the page. Cloudflare Zaraz runs GA4 as a managed component inside Workers; the browser ships one small first-party loader, and consent is enforced at the edge, which is the cleanest GDPR posture I could find for a DACH audience. I track five events, and each one answers a business question. github_click tells me whether the open-source projects pull people into the repos. skool_click tells me whether the site feeds the community. lead_submit covers contact form submits and booking clicks. newsletter_signup is what it says. scroll_read fires when someone gets through 75% of a blog post. A repo or community click from a pricing or services page counts as high intent. Five events someone actually reads beat thirty that nobody does.

Every event flows through two pipes into BigQuery. The GA4 native export gives me Google’s attribution model. The second pipe is the one I care about: Zaraz posts each event to a Cloudflare Worker on my own subdomain, which validates it against an allow-list and streams it into a table with a schema I designed. GA4 deletes event-level data after 14 months on a standard property. Mine is permanent, first-party, and immune to whatever Google renames next. This is exactly the layer that dies in a platform migration, and this one can’t, because it was never inside the platform.

On top of the warehouse, a second small Worker runs Inngest. This is the architecture decision I’d defend anywhere, because the obvious alternative is a plain cron job, and a cron fires and forgets. If the BigQuery query succeeds and the Slack post fails, you lose the digest or re-run everything. Inngest wraps each step so it runs once, keeps its result, and retries alone on failure. Two jobs run today: a Monday morning Slack digest comparing the last seven days against the prior seven, and an hourly batch that flags high-intent signals, posted to the team channel and to my DMs. Nobody has to remember to open a dashboard.

The next piece is already half-built. A lead_submit event lands in the pipe today. The planned sequence subscribes to it: enrich the contact, then run a follow-up email sequence through Loops, where a step can sleep for days at no cost, and the whole sequence cancels the moment the lead replies or books. A drip campaign is a short function in my repo instead of another subscription. Planned, not live. I can say that with a straight face because the event is already in the warehouse and the runtime is already deployed.

Net new cost of the analytics and orchestration layer, at my traffic: $0.

05 · SEO

The numbers, and the SEO loop

The numbers are mobile-first because mobile is what Google indexes: Lighthouse 97 performance, 100 accessibility, 100 best practices, 100 SEO. Largest Contentful Paint lands at 2.4 seconds on a simulated mid-range phone, just inside Google’s “good” threshold, with zero layout shift and zero blocking time. The desktop run shows LCP at 1.1 seconds. And full disclosure: the site’s real-user Core Web Vitals data is empty because it’s still below Chrome’s traffic threshold. The lab numbers are real, but nobody should claim rankings from them. Including me.

The SEO road is the part worth stealing, and I didn’t walk it by hand. The audit came from claude-seo, an open-source Claude Code plugin (github.com/AgriciDaniel/claude-seo) that fans out specialist agents across technical SEO, content, and schema, then hands back a prioritized fix list where every recommendation carries a check you can run from the command line. It scored the site 70 out of 100, and Search Console showed 16 of 117 URLs indexed, on 12,730 impressions and 8 clicks. The causes were not exotic. A JSON-LD template doubled the origin URL across roughly 100 blog posts, which broke BlogPosting and BreadcrumbList schema everywhere. The blog list was client-rendered, so Google got zero post links in the initial HTML. Three service pages shared the same incorrect meta description. The fixes covered the schema templates (BlogPosting, BreadcrumbList, Person, Organization), server rendering with ISR caching for the blog list, distinct descriptions, security headers, sitemap lastmod dates, and IndexNow.

Getting from 70 to 100 was not me clicking through reports either. I gave Claude Code a goal, literally: /goal, SEO score 100, with ultracode turned on and access to the PageSpeed API and Lighthouse. Then it looped. Audit, fix, deploy, re-measure, for a few hours, until the evaluator confirmed the score held. My part was reviewing what had changed. Its part was the grinding.

The loop also produced my favorite lesson because an SEO fix broke the SEO score. One fix put the IndexNow key into robots.txt as a directive, which it isn’t. The key file at its own URL is the entire mechanism. Lighthouse dropped the site from 100 to 92 for one invalid line. Deleting the line brought it back. A fix can be the bug, and that’s exactly why the loop re-measures after every change instead of trusting the deploy. Indexation stands at 17 of 117 and climbing on Google’s clock, not mine. If I told you the fixes worked in days, I’d be making it up.

06 · What broke

Where it broke

Could I have built this before coding agents? No, and I won’t pretend the agent part was smooth. Two examples. The code that tags outbound links for tracking worked in development and did nothing on the live site. No error, no warning, just zero click data. I caught it by having Claude Code drive a real browser against the live page and check its own work, then replaced the clever version with a simpler script that can’t fail silently. Second: Google Analytics showed no events arriving, and the culprit turned out to be my own security header, the Content-Security-Policy, which lists which servers a page is allowed to talk to. Mine didn’t include Google’s, so the browser blocked my own tracking. None of that is agent magic. It’s the normal mess of work that crosses frontend code, security settings, DNS, and cloud accounts. Fixing it usually takes four specialists and a ticket queue. Here, one session held the whole picture. My job stayed what it has always been: define what good means, review what comes back, and refuse to ship anything I can’t defend.

07 · The value

Where the value landed

I built this to test what today’s tooling makes possible for a small team, and to find where it breaks. Some of it broke, and I’ve told you where. And now that it’s live, I’ll be honest about where the value actually landed. It wasn’t the rent I stopped paying, which was never much. It’s that the data is permanent, and the next thing is small.

Every capability from here is a short function on infrastructure that already exists, not another subscription and another migration someday. So here’s what I want back. If you’ve built or are rebuilding your own stack with coding agents, the site, the CMS, the analytics, any layer of it, tell me what worked and what didn’t. And if you tried and it went sideways, that’s the more useful story. Share it.

08 · What it saved
$0
net new cost for analytics and orchestration
100
Lighthouse SEO score, mobile
Reactions
Subscribe

Get the next MCP-gap post in your inbox. Every two weeks, no fluff.

More on the Data layer rung

Nothing else on this rung yet. Next up the ladder.

Comments

Comments

Loading comments…

    Leave a comment

    Not shown publicly.
    Work with
    Done-for-you buildsWork with PromptMetrics →