How This Site Is Built
Architecture, decisions, trade-offs. Why this site is fast, simple, and doesn’t break.
Last reviewed: 15 May 2026
1. Why No Framework
The web has gone all-in on frameworks. Next, Nuxt, SvelteKit, Astro. They’re powerful, and they’re overkill for a twelve-page tradie marketing site. A framework’s job is to manage complexity. If your site doesn’t have much complexity, you’re paying the framework tax for nothing: bundler config, dependency updates, build steps, hydration overhead, deploy gotchas.
This site is mostly static content with a contact form. That’s HTML’s home turf. A framework would mean a Node toolchain, a build step before every deploy, and a JavaScript bundle the browser has to parse before rendering. The plain-HTML version loads the first byte and starts rendering immediately. There’s no hydration step because there’s nothing to hydrate.
The honest trade-off: I can’t reuse components the way a framework would let me, so the nav and footer markup is repeated across every page. Repeating fifteen lines of nav markup is cheap. Compiling a framework is expensive. I made the swap.
2. The CSS Architecture
The whole site uses one stylesheet: style.v12.0.0.css. About 88KB. The file has two parts.
The first line is the minified base. Variables, reset, typography, layout primitives, every component style, every media query, the reduced-motion block. All on one line. That part is built once, doesn’t change often, and survives across many iterations of the site.
The bottom of the file is readable. Each release adds its CSS in plain multi-line form so diffs stay reviewable. The Lead Engine block, the focus-visible rules, the form callout, the author bio, the in-page TOC, the light-mode fallback. Each was a deliberate addition, written for humans to read.
It’s a compromise. The minified base is efficient on the wire but hard to edit. The readable section is easy to evolve but takes a few more bytes. It works because changes happen at the bottom and the minified base is essentially stable.
3. Version Stamps and Cache Strategy
Every static asset is versioned in its filename: style.v12.0.0.css, nav.v12.10.js, hero-desktop.v11_2.webp. The filename embeds the version it was last regenerated at.
The _headers file says every versioned asset gets Cache-Control: public, max-age=31536000, immutable. That’s one year in the browser cache, never re-validated. When I change a file, I change its version in the filename, and every HTML page that references it gets updated. The old asset stays cacheable. The new asset is a different URL.
HTML pages themselves don’t use long cache: they get a short max-age with stale-while-revalidate so the CDN serves fast while still refreshing in the background. The combination means most repeat visits load almost nothing from origin, and rare HTML changes still propagate within hours.
Every HTML file also carries an SCD_DEPLOY_CHECK comment near the top stamping the deploy version. View source on any page and you see the version that’s live. No browser cache mystery.
4. The Deploy Pipeline
GitHub repo. Cloudflare Pages reads the main branch on push. The build step is: nothing. There is no build step. Cloudflare serves the files exactly as they sit in the repo. Deploy time from git push to live is usually under thirty seconds.
This is the part of the static-HTML approach that’s easy to underestimate: there is no thing to break in deploy. No node_modules. No version skew between local and production. No “works on my machine.” The files in the repo are the files on the server. If it’s right locally, the deploy will look the same.
5. Performance Trade-Offs
Google Tag Manager is deferred. The dataLayer initialises synchronously in gtm-init.v1.js so any inline push (the payment_confirmation_view on the thank-you page, for example) lands in the queue. But the actual gtm.js fetch waits for first user interaction or a 3-second requestIdleCallback timeout, whichever comes first.
That trades sub-second analytics fire for a noticeably lower Total Blocking Time on the initial pageview. A pageview that nobody interacts with may take up to three seconds before GTM lands. For a lead-gen site where visits without interaction don’t convert, that’s a fair swap.
Fonts are self-hosted: Inter and Big Shoulders Display, woff2 only, preloaded for the weights that show above the fold. No flash of unstyled text. No third-party DNS lookup. No consent-banner friction with Google Fonts.
Images are WebP. The hero photo is masked to its content edges in CSS rather than baked into the image, which means the same file renders cleanly across viewport widths without a hard pixel cutoff.
6. Security Posture
Content Security Policy is enforcing as of v12.0.1. No ’unsafe-inline’ in script-src or style-src. Every script has a src attribute pointing at a versioned file in /assets/. Every style is in the stylesheet.
This took effort. The site used to have inline <script> blocks, inline <style> blocks, inline event handlers, and inline style="" attributes. Each one would have been a CSP violation under enforcement. The foundation release externalised inline scripts into four versioned files and stripped every inline style. A later pass caught and externalised one straggling inline onclick handler that had survived the first sweep.
The rest of the headers: HSTS with includeSubDomains, X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin-when-cross-origin, and a Permissions-Policy locking down geolocation, microphone, and camera plus the FLoC interest-cohort opt-out. The bar is an A or A+ grade on securityheaders.com and Mozilla Observatory.
7. Accessibility Decisions
Keyboard-operable everywhere. Skip-to-content link as the first focusable element on every page. A 3px solid orange focus ring on anything you tab to. ARIA state on the nav dropdown, mobile menu, and FAQ accordion. role="status" aria-live="polite" on the form-success div, role="alert" on the form-error paragraph. Per-field validation on the contact form with aria-invalid toggling and aria-describedby linking inputs to their error messages. lang="en-AU" site-wide so screen readers pronounce Aussie place names correctly.
Reduced motion respected: the parallax hero, the H1 word reveal, the stats counter, the process flow draw, the in-page TOC smooth scroll. All sit still if your system asks for less motion.
The full statement, including what isn’t done yet, lives at /accessibility.
8. Forms Without a Framework
The contact form is a plain HTML form posting to Formspree. Validation runs on blur and on submit, in a small versioned JS file. On success, Formspree redirects to /thank-you. On failure, the form shows a top-level error message and a per-field span next to whichever input needs attention.
There’s no JS validation library, no React, no controlled inputs. The browser already has form controls. The JavaScript layer just adds the polite error treatment, the field-level guidance, and the dataLayer push.
9. Analytics Stack
Two analytics sources running in parallel. They measure different things.
Google Tag Manager (with GA4 inside) handles traffic source attribution and conversion events. It’s gated behind the deferred-load described above.
Cloudflare Web Analytics handles real-user Core Web Vitals: Largest Contentful Paint, Interaction to Next Paint, Cumulative Layout Shift. Measured from actual visitors on actual devices, not lab simulations. The cookieless beacon needs static.cloudflareinsights.com allowlisted in the script-src CSP directive, which it is. The data lets me see whether real users are seeing the same performance Lighthouse shows in CI.
10. Structured Data
Every service page has a Service JSON-LD block declaring provider, area served, and offers. The homepage has a rich ProfessionalService block with offer catalog, opening hours, geo coordinates, and ABN. The about page has a Person schema for me. The case study has BreadcrumbList and CreativeWork. The FAQ section on /websites has a matching FAQPage schema with the same questions and answers as the visible content.
Cross-page entity graph: the ProfessionalService block on the homepage carries an @id of https://steelcapdigital.com.au/#organization. Every other page has a stub Organization block referencing the same @id. Search engines join them into one entity rather than treating each page as a separate organisation.
11. Continuous Integration
A GitHub Action runs Lighthouse CI against the live site after every push to main. Six URLs are tested: homepage, websites, lead engine, the case study, about, and contact. The thresholds are 0.95 across performance, accessibility, best practices, and SEO. A failed run flags the commit red on GitHub. The workflow waits 60 seconds before testing so Cloudflare Pages has propagated the deploy first.
12. What I Didn’t Do
No CMS. Content lives in the HTML files. To change a price or add a paragraph I open the file in an editor and commit. For a site I personally maintain, this is faster than a CMS would be. If the site grew past fifty pages or other people needed to edit it, this decision would flip.
No blog yet. Coming. The plan is plain Markdown that compiles to HTML at deploy time. There’s a version of this site that stays framework-free even with a blog. There’s also a version where adding the blog is the moment I’d grudgingly accept a static site generator. I haven’t decided which.
No A/B testing. No personalisation. No chat widget. These all have value when there’s enough traffic to drive them. With current volume the data would be noise.
No third-party fonts from Google’s CDN. No analytics from anyone but Google and Cloudflare. No ad pixels. Every third-party connection the site makes is named explicitly in the Content Security Policy. If it’s not in the CSP, the browser blocks it. That’s the point of the policy.
Closing Note
Plain HTML, one stylesheet, one or two JavaScript files per page, a global CDN, a year of immutable caching on assets, a thirty-second deploy. Boring on purpose. The boring is what makes it fast.
If you’re a tradie reading this, the takeaway is: your website doesn’t need to be complicated to work well. If anything, the opposite. If you’re a fellow developer or a judge: the codebase is open enough that every decision here is verifiable from view-source and the response headers. Have a poke around.