Technical Case Study

How We Built folio.lu to Score 96/100 on PageSpeed

Folio Studio May 2026 12 min read

We audit other people's websites for a living. It would be embarrassing to have a slow one ourselves. This is the full technical case study of folio.lu — every major decision, the Lighthouse scores, and the three issues our own audit process found that we had to go back and fix.

No self-congratulation: the purpose of this post is to show our methodology in action on a site you can inspect yourself. Open DevTools and check the claims.

The scores

96
Performance (mobile)
98
SEO score
0.9s
LCP (mobile)
88
Accessibility

The 88 accessibility score is a known gap — colour contrast on some of the muted-text elements is below WCAG AA. We're iterating on this. The 96 performance score reflects genuine optimisation, not a simple site with nothing on it.

Decision 1: No framework, no CMS.

The first architectural decision was to ship static HTML, CSS, and minimal vanilla JavaScript. No React. No Next.js. No WordPress. No Webflow.

We evaluated Astro — it's in our tech stack for client projects — and decided against it for our own site. Astro adds build complexity and an abstraction layer that doesn't benefit a site where the content doesn't change frequently. Static HTML ships as-is, runs on any CDN, has zero server surface, and loads faster than any framework alternative because there's nothing to hydrate.

The rule we use: if a site needs real-time data, user authentication, or content that changes daily, use a framework. If it's a marketing site that updates monthly, static HTML will outperform every framework alternative on every metric that matters to clients.

The consequence: our editors (us) write HTML. For a two-person studio, this is fine. For a client who needs to update blog content, we would add a headless CMS — Sanity or Payload connect to static output without giving up the performance benefits.

Decision 2: The typography stack.

Typography is the loudest design decision on a text-heavy marketing site. We use three typefaces, each with a specific role:

Typeface
Role
Why
Fraunces
Display headings (serif, variable)
High optical size range (9–144pt), optical size axis means it looks correct at both 12px and 120px. The SOFT axis adds warmth. The italic is distinctly different from the roman — expressive, not just slanted.
Inter Tight
Body copy and UI labels
Compressed version of Inter — same neutrality and legibility at text sizes, but 8–10% narrower. More text per line on mobile without going below 16px. Works at 300–700 weight range.
JetBrains Mono
Monospace labels, eyebrows, metadata
Technical credibility signal. Used at small sizes (10–13px) with tight letter-spacing for category labels, timestamps, and data. Instantly reads as "precision" without being code-heavy.

All three are loaded from Google Fonts with preconnect and display=swap. We load all axes in a single request to avoid multiple font connections.

Decision 3: Dark mode as primary.

Most web agencies default to a white site with a dark footer. We inverted this: #0A0A0B background as the primary experience, with #39D353 (GitHub-contribution green) as the accent.

The reasoning:

  • Dark editorial design reads as premium and considered — not default. It signals we chose this intentionally.
  • The green accent on near-black is high-contrast, legible, and unusual enough to be memorable.
  • Dark backgrounds reduce LCP impact from hero images — the eye reads the text before the image loads, so a slow image is less jarring.
  • The target client — an SME owner who cares about quality — responds better to confidence than to convention.

We don't offer a light mode toggle. The site has a point of view. Clients who prefer white backgrounds have 200 other agencies to choose from.

Decision 4: CSS custom properties as the design system.

Every colour, spacing unit, and type size is a CSS custom property defined in :root. This means:

  • The entire visual language can be changed in one file
  • There is no design-system drift — a component either uses the variable or it doesn't compile
  • Dark mode (or any theme) would be a three-line CSS override, not a rebuild
  • All spacing uses clamp() with min, ideal, and max — the layout is fluid, not step-responsive

The grid lines you see throughout the site are not borders on individual elements. They are a 1px gap on a CSS Grid container with background: var(--border). Each child element has background: var(--bg). The gap exposes the container background. This technique makes every grid cell "bordered" without any per-cell border declarations — and it makes the entire grid remove its lines in one property change.

Decision 5: Structured data and schema.

Every page of folio.lu has JSON-LD structured data. The homepage has a full LocalBusiness schema with areaServed (7 Luxembourg cities), hasOfferCatalog linking to all three services, and priceRange. Service pages each have Service schema with price specifications. The services page has a FAQPage schema. Every inner page has BreadcrumbList.

This is the SEO work that's invisible to users but fully visible to Google. Rich results (FAQ dropdowns, business information in the knowledge panel) require structured data to be present and valid. Most SME websites have none of this.

The three issues our own audit found.

We ran our own site through the same audit process we use on clients. Here are the three issues it flagged, and what we did about them.

01
Fraunces loaded without latin subset
The Fraunces variable font was loaded without a subset=latin parameter. The full character set for a variable font with five axes is significantly larger than the Latin subset. Fix: appended &subset=latin to the Google Fonts URL, reducing the font payload by ~40%. This directly improved LCP by removing unnecessary network weight from a render-blocking resource.
02
Missing aria-label on case study thumbnail links
On work.html, case study cards used an <a> wrapping an image with no aria-label. Screen readers would announce "link" with no destination context. Fix: added aria-label="View [client name] case study" to each card link. This brought the accessibility score from 84 to 88 — and is a practice we now check on every client build before launch.
03
sample-audit.html CSS class scope leak
Several layout classes (.sect-head, .cta-section, .hero-actions) were defined inline in index.html's <style> block and used across other pages without being redefined. The sample audit page looked completely unstyled when loaded directly. Fix: moved all shared layout classes that weren't already in style.css into the relevant pages' local <style> blocks. This is a hazard of no-build-tool development — components don't have automatic scope, so global styles must be in a global stylesheet.

Lessons for client work.

Building and auditing our own site reinforced three practices we now apply to every client project:

  • Load fonts with preconnect. Every Google Fonts link must be preceded by two preconnect tags — one for fonts.googleapis.com, one for fonts.gstatic.com. Without them, the browser doesn't start the font connection until it parses the stylesheet, adding 200–400ms to first render.
  • Aria-label every image link. Any <a> whose only child is an <img> needs an aria-label. We added this to our launch checklist.
  • All shared CSS lives in style.css. No class should be defined in a page's <style> block if it's used on more than one page. The component model without a build tool requires discipline about this boundary.

The 96 performance score isn't a number we polished for a case study. It's the output of applying the same methodology we use on clients to ourselves. We'll keep auditing our own site — the three fixes above came from it, and there will be more.

← Previous article
What 32 Website Rebuilds Taught Us About Speed, Design, and SMEs

Want the same
audit on your site?

We run the same process on your site and send you a full report — performance scores, specific issues, exact fixes — within 24 hours. Free.