sarthak bhardwaj.
0%
Skip to content
← work

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.

Web platform high-level architectureVisitorsbrowsers · forms · searchAI agents & crawlers.md · .json · llms.txtCDNedge cache · invalidated on publish and on deployorigins · stateless containersMarketing siteDjango · several replicasContent platformWordPress · blog / learn / guidesleads · integrations dataMySQL + in-process cacheTTL-bounded across workersMySQL per installshared uploads volumeConsent platformgates every tracker by categoryCRMleads via a server-side proxyA/B testingvariant fixed before renderMedia CDNimages sized at renderCI/CDbuild → container registry → rolling deploy · production behind approval
High-level architecture. Third-party scripts are loaded by the browser but only run once the consent platform allows their category; the CRM is only ever reached through a server-side proxy.

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:

  1. 1
    Every tracker ships inert — a script tag the browser won’t execute — labelled with its consent category.
  2. 2
    A bootstrap sets every consent signal to denied before anything else runs.
  3. 3
    When 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

  1. 1
    Search 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.
  2. 2
    Sitemaps 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.
  3. 3
    Deploys 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.
  4. 4
    Layout 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.
  5. 5
    Lighter 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