Web platform · marketing site + content platform · 2025—26
Running a company's public web: fast, measurable, private by default, and readable by AI
Two codebases make up the public face of a B2B SaaS company: a Django marketing site and a WordPress platform behind the blog and learning hub. I was the primary engineer on both — about two-thirds of everything merged on the site and roughly 70% on the content platform since mid-2025.
The problem
A slow site you couldn't trust an experiment on, and a blog that buckled under its own traffic
The marketing site was visually dated ahead of enterprise evaluation cycles, and its templates were copy-pasted rather than composed — every new page multiplied the maintenance surface. Worse, every A/B test flickered the original page before swapping in the variant, which quietly polluted the result of anything being measured.
The content platform had the opposite problem: lots of real traffic, and the plumbing to match it — search that took seconds per keystroke, sitemaps that pinned the servers, and pages that broke after deploys.
Architecture
Two origins behind one CDN, each built to run as interchangeable replicas
Both applications sit behind the same CDN and run as several stateless containers. That one constraint — no request may depend on which replica served the last one — shaped more decisions than anything else: where generated files live, how caches expire, and how deploys roll out.
A component system
Rebuilding the site in 21 days so a new page is an arrangement, not a copy
The rebuild moved the site onto a modular component system: versioned SCSS layers (core and derived components), a library of template partials, and per-page asset bundles, so pages only ship the CSS and JS they use. It went live in three weeks; 81 new page templates followed over the year on the same foundation, and Core Web Vitals improved by more than 20% across the site along the way.
Consent & experiments
Privacy by default, and experiments decided before the first byte
A consent gate in front of every tracker
More than twenty third-party scripts — analytics, ad pixels, session recording, chat — used to load for everyone. I put one gate in front of all of them, against a hard compliance deadline:
- 1Every tracker ships inert — a script tag the browser won’t execute — labelled with its consent category.
- 2A bootstrap sets every consent signal to denied before anything else runs.
- 3When a visitor allows a category, only the scripts in that category are activated. Pixels that inject themselves check consent in code before firing.
Server-side variants
Homepage and signup experiments are assigned on the server and stored in the session, so the variant is decided before the page renders and there’s nothing to swap. That removed the flicker — and the asterisk from every result that came after it.
Readable by AI
Serving the same content as HTML to people and Markdown to machines
LLM crawlers were digging content out of full HTML pages. I built a machine-readable layer on both platforms: per-page Markdown and JSON exports at predictable URLs, an llms.txt index, and a link rel="alternate" on every page pointing agents to the clean version.
Exports live in the database
With several replicas, a file written while serving one request is invisible to the others. Exports are stored centrally and skipped when a content hash says nothing changed.Opt-in per page
Otherwise any crawler could conjure exports for every candidate URL just by guessing paths.Never publish a truncated index
If the curated part of llms.txt fails to load, the build falls back to the last good file instead of shipping half an index.Caching that agrees with negotiation
Negotiated Markdown is non-storable and varies on Accept, so the CDN can never hand Markdown to a browser. Conditional requests return 304s via strong ETags.
The export service is the most heavily tested code on the site — 101 tests — because it’s the part where a subtle bug is invisible to humans and very visible to machines.
Content platform at scale
Making WordPress fast without fighting it
- 1Search without booting WordPress. Each keystroke cost about 1.7 s — of which the database query was 0.4 ms; the rest was WordPress starting up. Typeahead now reads a static JSON index, built in pages and rebuilt on content change behind a 60-second debounce, with a CLI command to rebuild on demand.
- 2Sitemaps that don’t pin the CPU. Thousands of daily sitemap hits each rebuilt the sitemap from scratch. It’s now cached in the shared database and at the edge, and invalidated by a fingerprint of everything it depends on — posts, terms, authors, settings, redirects.
- 3Deploys that don’t break cached pages. HTML is cached for weeks, but every build deleted the old hashed assets. Assets now load under stable names with the hash as a version query, so old pages keep their styles.
- 4Layout that doesn’t jump. Reserving the header’s real height in CSS, instead of setting it on DOM-ready, took desktop CLS from 0.029 to 0.006 — it removes the race rather than shortening it.
- 5Lighter pages. Images are sized at render time rather than upload time (fixing URLs already stored in the database); lab page weight fell from about 5 MB to 1.1 MB, and real-user vitals sit at LCP 2.0 s, INP 78 ms, CLS 0.03.
On top of the performance work, I rebuilt the blog home page from the design file — hero, topic chips, card grid, eBook and video rails — with every section editable from its own admin screen and every chip’s cards shipped with the page, so filtering costs no request.
Incidents & lessons
Three that shaped how I ship
A sort order that took the blog down
- What broke
- Database CPU hit 99.9% and connections climbed from about one to 226; the blog and learning hub went down.
- Why
- Ordering home-page cards by a date stored in post metadata couldn't use an index — and with nine topic queries per uncached render, it saturated the database.
- The fix
- Reverted only the ordering, kept the per-post date reads, and wrote up the root cause.
Run EXPLAIN on production-sized data before shipping a query. A sort key needs to be indexable, not just correct.
An invisible sheet over the whole page
- What broke
- After a release, almost nothing on the page was clickable.
- Why
- A new modal class collided with an existing site-wide one and overrode its hidden state, leaving a transparent full-viewport overlay.
- The fix
- Namespaced the class — and verified by hit-testing every anchor on the page: 1 of 144 had been clickable before, all of them after.
Verify the fix the way the bug was experienced, not the way it was caused.
Conditional requests that never returned 304
- What broke
- The CDN and AI agents re-downloaded unchanged exports in full on every cache expiry.
- Why
- Two different causes on two stacks: compression middleware weakened the ETag on one; the framework escaped the client's If-None-Match header on the other.
- The fix
- Compared ETags correctly on both, so unchanged content now revalidates in a few bytes.
Check what your framework does to input before you compare it.
Practices
How the platform stays safe to change
Stateless replicas
Nothing that matters is written to local disk; caches are TTL-bounded because a save signal only reaches one worker.Approval-gated production
Every merge deploys to a test environment; production rolls out behind a manual approval with the new revision verified.Linting before commit
Strict static analysis runs as a pre-commit hook on the Django codebase.Measured performance
Audit and pixel-diff tooling with saved baselines, and a real-user INP beacon, so performance claims come with numbers.Security upkeep
Security headers on both origins, error output locked off at the server level, and runtime upgrades for published CVEs.Root causes written down
Incidents end with a commit message or backlog entry explaining the cause, not just the fix.
Built with
- Django
- Python
- PostgreSQL
- AWS
- CloudFront
- WordPress
- PHP
- MySQL
- Docker
- Node.js
- VWO
- Jenkins
- Ansible