How We Hit 100/100/100/100 Without a Framework
We rebuilt the Pharoah product pages to a perfect Lighthouse score using static HTML, variable fonts, and a refusal to ship JavaScript we don't need.
The honest answer to "should we rewrite it?"
When our product pages were sitting at 58 in Lighthouse, the loudest voice in the room said: rewrite it. Astro, Next, SvelteKit, Qwik — pick a framework, ship a build pipeline, add hydration, and the score will sort itself out. We've heard that pitch a hundred times. We've also shipped the kind of sites it produces. So we said no.
What we did instead was smaller, slower to talk about at a meet-up, and more effective. We read the audit. We changed eleven files. The pages are now 100 / 100 / 100 / 100 on Lighthouse mobile. This is what we did, and why none of it required a framework.
Static-first, by which we mean actually static
Each product page is a single .html file on disk. There is no runtime. The "build" is a 90-line Python script that pastes a shared header, a product body, and a shared footer into a template, runs the result through an HTML minifier, and rsyncs the output to the VPS. The whole pipeline takes longer to describe than to run.
The hosting is a £4/month box running nginx. The server sends a text/html response, gzip-compressed, and that is the end of the story. Time to first byte is in the single-digit milliseconds because there is no application server to wake up. There is no Node process. There is no SSR. There is HTML, sent over the wire, and a browser that knows what to do with it.
If you are a small team shipping a marketing surface that changes once a quarter, a framework is overhead you are paying for someone else's imaginary use case. The web has shipped documents over HTTP for thirty years. We are, once again, doing that.
Self-hosted variable fonts, properly subset
Typography was the single biggest line item in the old build. We were pulling three weights from Google Fonts, in two families, with separate files for italic, and we were doing it from a third-party origin with a render-blocking <link rel="stylesheet"> in the head. Lighthouse docked us for it, and it was right to dock us.
The fix was boring. We picked one variable font for body and one for display, hosted both on our own origin, and preloaded the woff2 with a single line in the head so the file starts downloading before the parser sees the CSS that references it. The three things that did the work were:
- Self-hosted on our own origin. No third-party handshake, no extra TLS negotiation, no Google Fonts CSS to parse before paint.
- font-display: swap on the
@font-face— text renders in the system fallback immediately, and the variable font swaps in when it lands. - Subset to Latin only. We are a UK company. The character set we serve is the character set our customers read. The woff2 dropped from roughly 140 kb to 38 kb.
One file, two families' worth of weights, on our own origin, in the critical path only as long as it needs to be. Nothing here is novel. Most of it is in the spec. None of it required JavaScript.
Deferred hero media, and an LCP we can defend
The previous design had a 240 kb MP4 playing on a loop in the hero of every product card. It was the LCP element. It was also the reason the LCP was 3.4 seconds on a throttled Moto G4 simulation, which is a generation of phone that real people still carry.
We replaced the video with a hand-rolled inline SVG, fewer than 80 lines, animated with CSS keyframes. The browser parses the markup, paints the shape, and the animation runs on the compositor. The LCP is now the product title in body text. It paints at 0.6 seconds on the same throttled profile.
For the one place we still ship moving image — the SOLAI demo reel, which genuinely needs to be a video — we serve it as <video preload="none" poster="..."> with no autoplay, no playsinline trickery, and a click-to-play control that is a real <button>. The poster image is 18 kb. The video does not start downloading until the visitor asks for it. The first paint is honest.
Honest heading structure, because the audit was right
The old product page had three <h1> elements (one per product card) and no <h2> in the body content at all. It looked fine. It was, technically, a heading structure that a screen reader would have to untangle and a search engine would quietly discount.
The fix was to admit the page has one topic — the Pharoah product line — and a handful of sub-topics underneath it. The page now uses the structure it always should have:
- One
<h1>: the line title. - One
<h2> per product: SOLAI, GhostWire, Secure OS, Fusion. - One
<h3> per feature block inside a product.
That is the entire SEO and accessibility fix. The headings are in source order, in semantic order, and they match the document outline. Crawlers and assistive technology see a document that means what it says. We have no clever workaround, no <h2> hidden with clip-path for "SEO juice", no aria-hidden games. The structure is the structure.
What we measured, and what we did not
Lighthouse mobile, on a throttled Moto G4 with simulated 4G, gives us 100 / 100 / 100 / 100 across Performance, Accessibility, Best Practices, and SEO on every product page. LCP sits at 0.6 s, CLS at 0, TBT at 0. The lab numbers are the lab numbers; they are not the whole story, but they are a useful baseline.
We also looked at 30 days of real-user data from visitors who actually loaded /products/. Median LCP is well under the 2.5 s "good" threshold. The p75 LCP is 1.4 s. Bounce rate on the page dropped, time on page went up, and we did not run a formal A/B test because we are a small team and we do not have the apparatus to run one honestly. The signal was strong enough without one.
What we did not do is also worth recording. We did not adopt a meta-framework with islands architecture. We did not write a single line of hydration code. We did not "future-proof" the page for a product catalogue that does not exist yet. We fixed the eleven things the audit asked us to fix, in the order it asked us to fix them, and we stopped.
If you are staring at the same audit
The temptation to reach for a framework when the score is bad is real, and it is usually wrong. A framework is a tool for shipping an application. A product page is a document. Treat it like one, and most of the audit resolves itself: serve HTML, host your own fonts, do not autoplay video, and write headings that mean what they say.
The Pharoah product pages are live in their current shape and they are doing the job. If you are working on a similar surface and want a second pair of eyes on the audit, reply to this post or get in touch — we read every message, and we will tell you honestly whether the fix is eleven file edits or something bigger.