DEV Community

Cover image for Redesigning my TYPO3 portfolio – what actually moved the Lighthouse scores
stefan andres
stefan andres

Posted on

Redesigning my TYPO3 portfolio – what actually moved the Lighthouse scores

Homepage of stefan-andres.at – light hero with the headline "Design that lets your brand speak"

I recently rebuilt my personal site, www.stefan-andres.at, from scratch. It runs on TYPO3 13.4 with the Bootstrap Package, and the goal was simple: a light, calm design – and a site that scores well where it matters for users, not just in screenshots.

This isn't a "look at my new website" post. It's a list of the things that actually changed the numbers, with code you can copy.

Where I ended up (mobile, PageSpeed Insights):

  • Accessibility: 100
  • Best Practices: 100
  • SEO: 100
  • Performance: 96 (88 at launch, 95 after the font and contrast fixes, 77 after adding Analytics – more on that below)

1. Self-host your fonts – and get the file names right

The biggest single render-blocking item was Google Fonts: one CSS request to fonts.googleapis.com, then a chain to fonts.gstatic.com for every weight. Roughly 750 ms on a throttled mobile connection.

I downloaded the .woff2 files, put them in fileadmin, and wrote one @font-face per weight:

@font-face{font-family:"Plus Jakarta Sans";font-weight:400;font-display:swap;src:url(/fileadmin/Daten/fonts/PlusJakartaSans-Regular.woff2) format("woff2")}
@font-face{font-family:"Plus Jakarta Sans";font-weight:700;font-display:swap;src:url(/fileadmin/Daten/fonts/PlusJakartaSans-Bold.woff2) format("woff2")}
@font-face{font-family:"Plus Jakarta Sans";font-weight:800;font-display:swap;src:url(/fileadmin/Daten/fonts/PlusJakartaSans-ExtraBold.woff2) format("woff2")}
/* … 500, 600 and the italic display face */
Enter fullscreen mode Exit fullscreen mode

Two lessons:

  • Check the file names. My first version pointed to a single plus-jakarta-sans-latin.woff2 that didn't exist – the site silently fell back to system fonts.
  • Remove the old @import. It was still sitting at the top of the main stylesheet and kept loading Google Fonts in parallel.

Then preload only the fonts visible above the fold:

page.headerData.5 = TEXT
page.headerData.5.value (
<link rel="preload" href="/fileadmin/Daten/fonts/PlusJakartaSans-ExtraBold.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/fileadmin/Daten/fonts/PlayfairDisplay-Italic.woff2" as="font" type="font/woff2" crossorigin>
)
Enter fullscreen mode Exit fullscreen mode

Bonus: no third-party font requests means one less entry in the privacy policy and a simpler Content-Security-Policy.


2. Load page-specific CSS and JS only where it's needed

My site has ten free browser tools (contrast checker, favicon generator, minifiers, …). Their CSS and JavaScript were loaded on every page – including the home page, where none of them run.

TypoScript conditions make this a one-liner. All tools live below one page (uid 174):

[174 in tree.rootLineIds]
  page.includeCSS.tools = fileadmin/Template/css/cust03-tools.css
  page.includeJSFooter.customtools = fileadmin/Template/js/tools.js
  page.includeJSFooter.customtools.defer = 1
[END]
Enter fullscreen mode Exit fullscreen mode

tree.rootLineIds matches the page itself and everything below it, so new tools are covered automatically.


3. Load Google Analytics only after consent

Adding GA4 the "standard" way dropped my performance score from 95 to 77. gtag.js is ~177 KB and Lighthouse flags most of it as unused.

But my cookie banner is opt-in anyway – until someone clicks "Allow", GA doesn't collect anything. So why load it?

<script>
window.dataLayer=window.dataLayer||[];
function gtag(){dataLayer.push(arguments);}
gtag('consent','default',{'ad_storage':'denied','ad_user_data':'denied','ad_personalization':'denied','analytics_storage':'denied'});

function loadGA(){
  if(window.gaLoaded)return; window.gaLoaded=1;
  gtag('consent','update',{'analytics_storage':'granted'});
  var s=document.createElement('script');
  s.async=1; s.src='https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX';
  document.head.appendChild(s);
  gtag('js',new Date()); gtag('config','G-XXXXXXX');
}

// returning visitors who already agreed
if(document.cookie.indexOf('cookieconsent_status=allow')>-1){loadGA();}
// first-time visitors clicking "Allow"
document.addEventListener('click',function(e){if(e.target.closest('.cc-allow')){loadGA();}});
</script>
Enter fullscreen mode Exit fullscreen mode

cookieconsent_status and .cc-allow are the defaults of the cookie banner shipped with the Bootstrap Package – check your own cookie name in DevTools → Application → Cookies.

Result: Lighthouse (which never clicks "Allow") doesn't see GA at all, real visitors who consent are tracked normally, and it's cleaner from a GDPR point of view. Score back to 96.


4. Accessibility: contrast was the whole story

The audit flagged two things: contrast and heading order.

Contrast

My brand orange #df532e with white text sits at about 3.6:1 – below the WCAG AA minimum of 4.5:1 for normal text. Instead of changing the brand colour, I added a second token just for text and button backgrounds:

:root{
  --sa-orange:#df532e;       /* decoration: dots, lines, borders */
  --sa-orange-a11y:#c74523;  /* text and filled buttons: ~4.9:1 on white */
}
Enter fullscreen mode Exit fullscreen mode

The less obvious trap: grey on dark. My footer used the same muted grey as the light sections. Darkening that grey (to fix light backgrounds) made the footer worse. Dark backgrounds need their own, lighter muted tone.

Heading order

Two culprits:

  • A card in the hero used an <h3> right after the <h1> – visually fine, semantically a skipped level. It's now a <p> with a class.
  • The Bootstrap Package constant defaultHeaderType = 5 turned every content element without an explicit header type into an <h5> – including my footer. Setting it to 2 fixed that globally.

5. Small things that added up

  • Images as WebP with explicit width/height → CLS dropped to 0.
  • fetchpriority="high" on the first visible image, loading="lazy" on the rest.
  • One CSS custom-property system (--sa-ink, --sa-gray, --sa-shadow-card, …) instead of hard-coded values. That made the contrast fixes a two-line change.
  • Mobile menu on iOS: a position:fixed menu inside a header with backdrop-filter rendered invisible in Safari. Anchoring it with position:absolute; top:100% relative to the header fixed it.

Tools & Apps section – ten free browser tools and three apps for iPhone & Mac

What's on the site now

Besides services and a portfolio, the site doubles as a small toolbox:

  • 10 free browser tools – palette & contrast checker, favicon generator, image optimizer, SEO check, CSS/JS/SVG minifiers, a clamp() calculator, JSON formatter, a TYPO3 Fluid ViewHelper generator and a client-briefing generator
  • 3 apps for iPhone & Mac – no account, no ads, data stays on the device
  • A glossary of print, web and video terms

If you work with TYPO3 or just want a quick contrast check, have a look: 👉 stefan-andres.at/tools

I'd love to hear what you'd improve – or what moved your Lighthouse scores the most.

Top comments (1)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.