# Performance and SEO

Core Web Vitals, images, fonts, scripts and caching. Then how search engines crawl and index your site, and the HTML that helps them.

Multimedia & Creative Technology · 3.1 Extra Webdev · Self-study · version 0.1.0 · 2026-10-14

Tags: performance, seo, images, html

Slides: <https://slides.mctantwerp.be/en/extra-webdev/08-performance-and-seo>

<!-- slide 2 -->

## After this lesson you can

- Explain what happens between typing a url and a usable page, and what slows it down
- Measure LCP, INP and CLS with Lighthouse, PageSpeed Insights and DevTools, and tell lab data from field data
- Fix the usual suspects: heavy images, late fonts, render-blocking scripts, third-party tags and missing cache headers
- Explain how Google crawls, indexes and ranks a page, and write the head of a page that is ready for it
- Set up sitemap.xml, robots.txt, canonicals, noindex, redirects, structured data and Open Graph without hiding your own site
- Use Search Console to see what Google sees

> **Explanation:** The first half is performance, the second half SEO. They overlap more than you think: a slow page loses visitors before Google even matters, and Google uses speed as one of its signals. Keep your own site open in a second tab and check every slide against it as you go.

<!-- slide 3 -->

## Agenda

1. Why speed matters
2. What happens when a page loads
3. Core Web Vitals and measuring
4. Images, fonts and video
5. CSS, JavaScript and caching
6. How search works
7. On-page SEO
8. Technical SEO
9. Search Console, content and AI

> **Explanation:** Chapters 1 to 5 are performance, 6 to 9 SEO. Already comfortable with Lighthouse? Skim chapter 3, but do the drill at the end of chapter 5. Never shipped a sitemap or a canonical? Then chapter 8 is the one to read slowly.

<!-- slide 4 -->

## 1. Why speed matters

> **Explanation:** Performance is easy to postpone: the site works on your laptop, so it feels done. This chapter is about who pays for a slow site, and why your own machine is the worst place to judge it.

<!-- slide 5 -->

### Who notices

#### Your visitor

- Waits on mobile data, on a phone that is slower than your laptop
- Leaves when nothing shows up
- Loses their place when the page jumps

#### Your client

- Pays for every visitor that leaves before the contact form
- Pays the hosting bill for every megabyte
- Hears it from their customers

#### Google

- Uses Core Web Vitals in its ranking systems
- Puts relevance first, always
- Measures your real visitors, not your laptop

> **Explanation:** Google's own documentation is careful here: Core Web Vitals are used by the ranking systems, but relevance always wins. A slow page with the best answer still ranks above a fast page with a bad one. Speed mostly matters when several pages are equally relevant, and for what visitors do once they land. Companies publish case studies on web.dev about conversion going up after speed work. Read them with some care: they are the success stories.

<!-- slide 6 -->

### Your laptop is not the user

|   | You, while building | A visitor |
| --- | --- | --- |
| **Network** | localhost or campus wifi, no latency | 4G on a train, every request takes a round trip |
| **Device** | A recent laptop with a fast CPU | A mid-range phone that is several times slower at JavaScript |
| **Cache** | Everything already downloaded a hundred times | First visit, nothing cached |
| **Screen** | A wide monitor | A phone, where the hero image is the whole first screen |

A page that feels instant while you build it can take seconds for a first-time visitor. That is why you measure, and why you measure on a simulated phone.

> **Explanation:** Chrome DevTools can simulate a slower device: in the Performance panel you set CPU throttling to 4x or 6x and the network to Slow 4G. It is still a simulation. The real test is a cheap Android phone on mobile data. If you have one in a drawer somewhere, keep it for testing. Also turn off the cache in the Network panel, otherwise every reload comes from your disk.

<!-- slide 7 -->

## 2. What happens when a page loads

> **Explanation:** Every fix in this deck makes sense once you know the order in which a browser does things. This chapter is that order. You will recognise it later in the waterfall of the Network panel.

<!-- slide 8 -->

### A page load

1. Browser → Your server, **GET /**: First a DNS lookup, a connection and a TLS handshake. On mobile that alone can take hundreds of milliseconds. The time until the first byte of HTML arrives is TTFB.
2. Your server → Browser, **index.html**: The browser reads the HTML from top to bottom and builds the DOM. A preload scanner reads ahead and starts fetching every file it finds in the markup.
3. Browser → CDN, **style.css**: CSS in the head is render-blocking: the browser paints nothing until it has arrived and is parsed. Otherwise you would see the page without styles first.
4. Browser → CDN, **app.js**: A script without `defer` or `async` stops the parser. It waits for the download and runs the script before it reads one more line of HTML.
5. CDN → Browser, **CSS and JS arrive**: Now the browser can work out the styles and paint the first text and colours. That moment is First Contentful Paint.
6. Browser → CDN, **hero.avif**: The image is in the HTML, so the preload scanner found it early. Had it been a CSS background, the browser would only find it now, after the CSS.
7. Browser → CDN, **font.woff2**: Fonts are discovered last: only when the CSS says visible text uses them. Until then the text is invisible or shown in a fallback font.
8. CDN → Browser, **hero.avif painted**: The biggest element in view is on screen: that is LCP. From now on every click waits for the main thread, and that is what INP measures.

> **Explanation:** The preload scanner is the reason most images start downloading early: while the main parser waits on a script, a second, simpler scanner reads ahead in the HTML and starts fetching every src and href it finds. It cannot see what is only in CSS or created by JavaScript. That is the whole reason a hero image as a CSS background, or one inserted by a slider script, loads late.

<!-- slide 9 -->

### What blocks the first paint

| In the HTML | Blocks | Why |
| --- | --- | --- |
| `<link rel="stylesheet">` | Rendering | Without CSS the page would flash unstyled |
| `<script src>` | Parsing and rendering | A script can change the HTML, so the parser waits |
| `<script defer>` | Nothing | Runs after the HTML is parsed, in order |
| `<img>` | Nothing | Loads in parallel, but is often the LCP element |
| `@font-face` | The text that uses it | Invisible text until the font arrives, unless font-display says otherwise |

Every render-blocking file in your head is a round trip before the visitor sees anything. Fewer and smaller is the whole strategy.

> **Explanation:** Moving scripts to the end of the body was the classic fix before defer was reliable. It still works, but defer in the head is better: the browser discovers the file earlier and starts the download right away, while it keeps parsing. The media attribute trick also works: a stylesheet with media=print does not block rendering on screen.

<!-- slide 10 -->

## 3. Core Web Vitals and measuring

> **Explanation:** Before you fix anything, measure. Otherwise you spend an afternoon on something that was never the problem. This chapter gives you the three numbers Google cares about and the tools to get them.

<!-- slide 11 -->

### Core Web Vitals

| Metric | Measures | Good | Poor |
| --- | --- | --- | --- |
| **LCP** | Loading: when the biggest element in view is painted | ≤ 2.5 s | > 4 s |
| **INP** | Responsiveness: how fast the page reacts to input | ≤ 200 ms | > 500 ms |
| **CLS** | Visual stability: how much content jumps | ≤ 0.1 | > 0.25 |

Measured on real visits, at the 75th percentile, for mobile and desktop separately. Everything in between is "needs improvement". The thresholds are on [web.dev/articles/vitals](https://web.dev/articles/vitals).

> **Explanation:** The 75th percentile means: three out of four visits have to be at least that good. One fast visit on your laptop says nothing. Each metric is judged separately for mobile and desktop, and a page passes the assessment only when all three are good. Google also shows older metrics like First Contentful Paint and Time to First Byte. They help you find the cause, but they are not Core Web Vitals.

<!-- slide 12 -->

### Largest Contentful Paint

The LCP element is usually your hero image, a video poster or the biggest heading. Its time splits into four parts, and each part has its own fix.

| Part | What takes time | Typical fix |
| --- | --- | --- |
| Time to first byte | The server builds and sends the HTML | Static hosting, a CDN, caching |
| Resource load delay | The browser only finds the image late | An img in the HTML, fetchpriority, preload |
| Resource load duration | The image itself downloads | AVIF or WebP, the right size |
| Element render delay | Downloaded, but not yet painted | No blocking scripts, no fade-in on the hero |

More in [Optimize LCP](https://web.dev/articles/optimize-lcp).

> **Explanation:** Element render delay is the sneaky one: the image is downloaded, but the browser still waits, for a script that hides the hero until it has run, for a font, or for a slider that fades it in. Fade-in animations on the hero look nice in a design review and push your LCP back by the length of the animation. On a text-heavy page the LCP element is often a heading or a paragraph, and then the web font is usually the cause.

<!-- slide 13 -->

### Interaction to Next Paint

Every click, tap and key press counts. INP is the time between the input and the next frame on screen, and a page gets the score of roughly its slowest interaction. JavaScript that keeps the main thread busy is nearly always the cause.

- Ship less JavaScript, so there is less to run
- Split long tasks, so the browser can paint in between
- Show feedback first (a pressed state, a spinner), do the heavy work after

INP replaced First Input Delay as a Core Web Vital on 12 March 2024. Details on [web.dev/articles/inp](https://web.dev/articles/inp).

> **Explanation:** The browser draws a new frame roughly every 16 ms, but only when the main thread is free. A long task is anything over 50 ms that runs in one go. Typical culprits on a portfolio: a big animation library that initialises on load, a tag manager with five tags, or a click handler that rebuilds a whole list. Lighthouse cannot click, so it shows Total Blocking Time as a stand-in: the total time the main thread was blocked during load.

<!-- slide 14 -->

### Cumulative Layout Shift

You start reading, an image above loads, and the text jumps away. CLS adds up how much visible content moves without the visitor asking for it.

| Cause | Fix |
| --- | --- |
| Images and videos without dimensions | `width` and `height` on the element |
| Embeds, ads and iframes that resize | Reserve their space with min-height or aspect-ratio |
| A banner or message inserted above the content | Overlay it, or reserve the space from the start |
| A web font that is wider than its fallback | font-display: optional, or a fallback with matching metrics |

More in [web.dev/articles/cls](https://web.dev/articles/cls).

> **Explanation:** CLS only counts shifts the visitor did not cause. Content that moves within half a second after a click, like an accordion opening, does not count. A cookie banner that pushes the page down does count, which is why a banner that overlays the page at the bottom is the safer design. The score is the impact of the shift (how much of the screen moved) times the distance it moved.

<!-- slide 15 -->

### A visitor taps the menu button. Nothing happens for 600 ms, then the menu opens. Which metric suffers?

- A. LCP
- B. INP
- C. CLS
- D. None, it happened after the page loaded

**Answer:** B. INP

INP measures every interaction during the whole visit, not just during loading. 600 ms is above the 500 ms threshold, so this is poor. LCP is about the first big paint, CLS about content jumping. The cause is almost always JavaScript keeping the main thread busy when the tap comes in.

> **Explanation:** The pressed state matters: when the button visibly reacts within 200 ms, the interaction feels fast, even if the real work takes longer. That is why showing feedback first is one of the main INP fixes.

<!-- slide 16 -->

### Lab and field data

#### Lab

- One test run, on a simulated phone and network
- Lighthouse, the Performance panel of DevTools
- Repeatable, so you can compare before and after
- Cannot click, so INP becomes Total Blocking Time

#### Field

- Real Chrome visitors, over the last 28 days
- The [Chrome UX Report](https://developer.chrome.com/docs/crux) (CrUX)
- What Google uses for Search, at the 75th percentile
- Only for sites with enough traffic

> **Explanation:** CrUX only has data for pages and sites with enough Chrome visitors, and Google does not publish the exact threshold. A fresh portfolio usually has none, and that is normal. Then Lighthouse is all you have, and that is fine for fixing things. Do not panic when lab and field disagree: Lighthouse simulates one slow phone, while field data mixes every visitor, including the ones on fast laptops.

<!-- slide 17 -->

### Lighthouse

Built into Chrome DevTools (the Lighthouse tab). It loads your page on a simulated mid-range phone with a slow connection, gives a score from 0 to 100, and lists what to fix. The score is a weighted mix of lab metrics, so look at the metrics and the list, not only at the number.

```console
$ npx lighthouse https://example.com --view
Opens the report in your browser when it is done
```

See the [Lighthouse docs](https://developer.chrome.com/docs/lighthouse/overview).

> **Explanation:** Browser extensions slow Lighthouse down and mess up the score, so run it in an incognito window or a separate Chrome profile. Run it two or three times: the score moves a few points between runs, that is noise. Lighthouse is also available as a CLI, handy for a site that is not online yet: point it at your local preview server. Recent versions group many of the old audits as insights, so screenshots in older tutorials look different.

<!-- slide 18 -->

### PageSpeed Insights

- **Top: field data**: What real Chrome visitors experienced over the last 28 days, from CrUX. Only when your site has enough traffic. This is what Google sees.
- **Below: lab data**: A Lighthouse run on Google's servers, with the score and the list of fixes. Always there, also for a brand new site.

Paste any public url in [pagespeed.web.dev](https://pagespeed.web.dev). That includes the sites of your competitors and the site of your client before you start.

> **Explanation:** The origin view shows the field data of your whole domain, the url view only that page. A small site often has origin data but no data per url. The lab part of PageSpeed Insights runs on Google's servers, from the US or Europe, so it can differ a bit from Lighthouse on your own laptop. Search Console shows the same field data per group of similar urls, more on that in the last chapter.

<!-- slide 19 -->

### The Network panel

1. **Disable cache**: The checkbox at the top, so every reload is a first visit.
2. **Throttling**: Slow 4G or Fast 4G in the dropdown, to feel what a phone feels.
3. **Waterfall**: When each file starts and ends. A long chain one after another is your problem.
4. **Size and filters**: Sort by size, filter on Img, JS or Font. The heaviest files are your first targets.

All of it in the [Network panel docs](https://developer.chrome.com/docs/devtools/network).

> **Explanation:** The Size column shows two numbers per file: what went over the network (compressed) and the size of the file itself. A big difference means compression works. Right-click the column headers to add the Priority column: there you see fetchpriority at work. Right-click a request and choose Block request URL to see how your page does without a third-party script.

<!-- slide 20 -->

### The Performance panel

Open it and you see LCP, CLS and INP of your own visit, live, without recording anything. Click the LCP value and DevTools highlights the element.

- Set CPU throttling to 4x or 6x and the network to Slow 4G before you measure
- Record a page load to see what the main thread does, task by task
- Layout shifts are marked in the recording, with the element that moved

See the [Performance panel docs](https://developer.chrome.com/docs/devtools/performance).

> **Explanation:** The live metrics show INP for your own clicks, so you can click around your own site and see which interaction is slow. In a recording, long tasks get a red corner, and a click on one shows which function took the time. You do not need to understand everything in a performance trace. Find the LCP element, find the biggest shift, find the longest task. That covers most sites.

<!-- slide 21 -->

## 4. Images, fonts and video

> **Explanation:** Images are the heaviest part of most portfolio and client sites, so this is where most of your gains are. Fonts and video come next. Nearly everything in this chapter is a few attributes in your HTML.

<!-- slide 22 -->

### Image basics first

Raster versus vector, which format for which image, exporting at the right width and compressing it: that is all in the General course.

**[Images and assets](https://slides.mctantwerp.be/en/general/06-images-and-assets)**: Formats, compression, 2x screens, srcset basics and alt text, with a format lab.

> **Explanation:** If formats, resolution, 2x screens and compression are new to you, go through that deck first: this chapter builds on it and does not repeat it. What it adds here is the link with LCP and CLS, and the attributes that tell the browser which image is important.

<!-- slide 23 -->

### Modern formats

```html
<picture>
  <source srcset="studio.avif" type="image/avif">
  <source srcset="studio.webp" type="image/webp">
  <img src="studio.jpg" alt="The studio at night, three screens on" width="1600" height="900">
</picture>
```

AVIF is usually the smallest, WebP a close second, and both are far lighter than a jpg of the same quality. With `<picture>` the browser picks the first format it understands.

> **Explanation:** The browser takes the first source it supports and ignores the rest, and the img at the end is the fallback that is always there. Every current browser supports both AVIF and WebP, so the jpg fallback is mostly for old devices and some email clients. Your width, height, alt and loading go on the img, not on the source elements. Astro, Next and most image CDNs generate this markup for you.

<!-- slide 24 -->

### srcset and sizes

```html
<img
  srcset="card-400.avif 400w, card-800.avif 800w, card-1600.avif 1600w"
  sizes="(min-width: 1024px) 33vw, 100vw"
  src="card-800.jpg"
  alt="Poster for the student show"
  width="800" height="1000"
  loading="lazy">
```

1. Three versions of the same image, with their real width in pixels (w).
2. How wide the image will be on screen: a third of the window from 1024px on, the full width below that.
3. A fallback for very old browsers, and the alt text that describes the poster.
4. The ratio, so the browser reserves the space before the image arrives.

> **Explanation:** Without sizes, the browser assumes the image fills the whole width of the screen (100vw) and picks a file that is far too big for a card in a grid. sizes is a guess you give the browser before the CSS has loaded, so it does not have to be perfect, just close. The browser also multiplies by the pixel density: a 360px slot on a 3x phone needs a file of about 1080px wide.

<!-- slide 25 -->

### The LCP image

```html
<!-- The hero: never lazy, and first in line -->
<img src="hero.avif" fetchpriority="high" alt="..." width="1600" height="900">

<!-- Everything further down the page -->
<img src="project-3.avif" loading="lazy" alt="..." width="800" height="600">

<!-- A hero as a CSS background: tell the browser in the head -->
<link rel="preload" as="image" href="hero.avif" fetchpriority="high">
```

`loading="lazy"` on the hero delays the one image that counts for LCP. See [Fetch Priority](https://web.dev/articles/fetch-priority).

> **Explanation:** Do not put fetchpriority high on every image: then nothing has priority. One or two images at most. Preload is for an LCP image the preload scanner cannot see: a CSS background or an image a script inserts. For an img that is in your HTML anyway, fetchpriority alone is enough. A carousel where every slide is an LCP candidate is a common LCP problem on client sites, and a reason to argue against the carousel.

<!-- slide 26 -->

### Width and height against CLS

*HTML*

```html
<img src="team.avif" alt="..."
     width="1200" height="800">
```

*CSS*

```css
img {
  max-width: 100%;
  height: auto;
}

iframe.video {
  width: 100%;
  aspect-ratio: 16 / 9;
}
```

The browser turns `width` and `height` into a ratio and reserves that space before the image arrives. Your CSS still decides the real size.

> **Explanation:** The CSS height: auto is essential: without it, the height attribute wins and your image gets stretched when the width shrinks. Modern browsers use the attributes only for the ratio. For an iframe, like a YouTube or Google Maps embed, there is no automatic ratio, so you set aspect-ratio in CSS yourself.

<!-- slide 27 -->

### You add loading="lazy" to every image on your portfolio, the hero included. What happens to LCP?

- A. It improves, fewer images load at the start
- B. It gets worse, the hero now waits until layout is done
- C. Nothing, lazy loading only affects images below the fold
- D. It improves, but CLS gets worse

**Answer:** B. It gets worse, the hero now waits until layout is done

A lazy image only starts loading once the browser has done layout and knows it is near the viewport. For the hero that means a late start for exactly the element LCP measures. Lazy loading does not skip images in view, it just starts them later. Keep it for everything below the first screen.

> **Explanation:** Lighthouse flags this one explicitly, and it is one of the most common mistakes: a site adds loading=lazy to every image with a find and replace. Lazy loading is great, just not above the fold.

<!-- slide 28 -->

### Web fonts

```css
@font-face {
  font-family: 'Satoshi';
  src: url('/fonts/satoshi-variable.woff2') format('woff2');
  font-weight: 300 900;
  font-display: swap;
}
```

The browser only fetches a font when visible text needs it, so fonts arrive late. Without `font-display` many browsers hide the text for up to three seconds. `swap` shows the fallback right away. More in [Best practices for fonts](https://web.dev/articles/font-best-practices).

> **Explanation:** FOIT is the flash of invisible text, FOUT the flash of unstyled text. swap gives you FOUT: the text is readable at once, and the font swaps in when it arrives, which can shift the layout a little. optional gives the font a very short window and otherwise sticks to the fallback for that visit. That avoids the shift, at the cost of sometimes not seeing your font on a first visit. For body text swap is the usual choice, for a decorative display font optional is worth a try.

<!-- slide 29 -->

### Fewer and smaller fonts

- Only woff2. Every browser supports it, and the older formats are bigger
- One variable font instead of six files for six weights
- A subset: only the characters you need (Latin, no Cyrillic or Greek)
- Self-host, instead of a stylesheet from Google Fonts
- Preload only the one font your hero text uses

```html
<link rel="preload" href="/fonts/satoshi-variable.woff2" as="font" type="font/woff2" crossorigin>
```

> **Explanation:** A German court ruled in January 2022 that a site loading Google Fonts from Google's servers passed the visitor's IP address to Google without consent, which broke the GDPR. Self-hosting avoids that discussion and is usually faster too, because there is no extra connection to another domain. google-webfonts-helper and Fontsource give you the woff2 files. crossorigin on the preload is required, even for your own domain, otherwise the font downloads twice.

<!-- slide 30 -->

### System fonts

```css
body {
  font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
}
```

Zero bytes, no layout shift, and the text appears with the first paint. The font that loads fastest is the one already on the device. Stacks by style on [modernfontstacks.com](https://modernfontstacks.com).

> **Explanation:** A system font stack loads nothing: San Francisco on a Mac and an iPhone, Segoe UI on Windows, Roboto on Android. GitHub's interface uses one. It is a design choice, so talk about it with the designer or the client. For body text a good system font is often fine, and a brand font for the headings only is a common middle ground. modernfontstacks.com lists stacks by style, from a humanist sans to a monospace.

<!-- slide 31 -->

### Video

```html
<!-- A short loop instead of a GIF -->
<video autoplay muted loop playsinline poster="loop.avif" width="1280" height="720">
  <source src="loop.mp4" type="video/mp4">
</video>

<!-- A long video further down: nothing loads until play -->
<video controls preload="none" poster="talk.avif" width="1280" height="720">
  <source src="talk.mp4" type="video/mp4">
</video>
```

`muted` and `playsinline` are required for autoplay on phones. For a YouTube embed, use a [facade](https://github.com/paulirish/lite-youtube-embed) that loads the player on click.

> **Explanation:** An animated GIF of a few seconds is often several megabytes, and the same clip as mp4 is a fraction of that. A YouTube embed loads hundreds of kilobytes of JavaScript before anyone presses play. A facade shows only the thumbnail and a play button, and loads the real player on click: lite-youtube-embed by Paul Irish is the classic. For video files on your own site: compress to H.264 at 1080p or lower, and host big files on a CDN or a video host, not in your git repo.

<!-- slide 32 -->

## 5. CSS, JavaScript and caching

> **Explanation:** This chapter is about the files you write yourself and the ones others make you add. The fixes are mostly a word in a tag or a header on your server, but deciding which scripts to remove is a conversation with your client.

<!-- slide 33 -->

### defer, async and module

|   | Blocks the parser | Runs | Order |
| --- | --- | --- | --- |
| `<script src>` | Yes | As soon as it is downloaded | In order |
| `<script defer>` | No | After the HTML is parsed | In order |
| `<script async>` | No | As soon as it is downloaded | Whichever arrives first |
| `<script type="module">` | No | After the HTML is parsed | In order, deferred by default |

Your own scripts get `defer` in the head. Details on [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script).

> **Explanation:** A script with type=module is deferred by default, which is why a Vite project never needs defer. async is for scripts that do not care about the page or about each other, like analytics. Do not use async for your own code that needs the DOM or another script: it runs whenever it arrives, sometimes before the HTML is done.

<!-- slide 34 -->

### Render-blocking CSS

- One stylesheet, minified and compressed, instead of five small ones
- No `@import` inside CSS: each one is another round trip after the first file
- Tailwind and most build tools strip the CSS you do not use
- CSS for print gets `media="print"` and blocks nothing on screen
- Very large sites inline the CSS for the first screen in the head, for a portfolio that is rarely worth it

> **Explanation:** The Coverage tab in DevTools (Cmd or Ctrl+Shift+P, then type coverage) shows per file how much CSS and JavaScript was actually used on this page. A framework like Bootstrap often shows ninety percent unused. That is not all waste, other pages may need it, but it is a hint. A chain of @import rules is extra slow: the browser only discovers the second file after the first has arrived.

<!-- slide 35 -->

### JavaScript costs more than its bytes

An image of 200 kB only has to be decoded. A script of 200 kB has to be downloaded, parsed, compiled and run, on the same main thread that handles every click.

| You reach for | The browser already has |
| --- | --- |
| jQuery for a class toggle | `element.classList.toggle()` |
| An accordion plugin | `<details>` and `<summary>` |
| A modal library | `<dialog>` with `showModal()` |
| A carousel library | CSS `scroll-snap` |
| A date library to format one date | `Intl.DateTimeFormat` |

> **Explanation:** On a mid-range phone, the same JavaScript can take several times longer to run than on your laptop. A bundle analyzer like rollup-plugin-visualizer for Vite shows which package takes how much space in your bundle. Often one library is half of it. Ask for every dependency: what does it do that the browser cannot do by itself any more?

<!-- slide 36 -->

### Third-party scripts

Analytics, a chat widget, a cookie banner, a booking tool, ad pixels. Each one is code from someone else's server, which you cannot optimise, and which can change tomorrow. A tag manager like Google Tag Manager makes it easy for marketing to add another one without a deploy.

> [!WARNING]
> Make a list of every third-party script on the site, with who asked for it and what it costs. Then ask the client which ones they still use. The fastest script is the one you remove.

> **Explanation:** Some cookie banners themselves weigh more than the rest of the page. Privacy-friendly analytics like Plausible or Umami are a few kilobytes and often need no cookie banner at all. For a chat widget, load the script on the first click on a button that looks like the widget, the same facade idea as with YouTube. Use request blocking in the Network panel to measure exactly what one script costs: block it, reload, compare.

<!-- slide 37 -->

### Cache headers

```http
# index.html: check with the server on every visit
Cache-Control: no-cache

# /assets/app-BRk2x9aQ.js: keep it for a year, never ask again
Cache-Control: public, max-age=31536000, immutable
```

A returning visitor should only download what changed. The HTML gets checked every time, everything with a hash in its name stays in the cache for a year. See [HTTP caching](https://web.dev/articles/http-cache) and [Cache-Control on MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control).

**[Deploy your site](https://slides.mctantwerp.be/en/extra-webdev/04-deploy)**: Where these settings live on your host.

> **Explanation:** no-cache does not mean do not cache. It means: keep it, but ask the server every time whether it changed. The server answers 304 Not Modified when nothing changed, which costs a round trip but no download. no-store really means never keep it, for private pages. Most static hosts set sensible headers for hashed assets themselves; check in the Network panel under Headers what yours sends.

<!-- slide 38 -->

### Hashed filenames

```console
$ npm run build
dist/index.html                    1.12 kB
dist/assets/index-C8f3kQ1a.css    14.80 kB │ gzip:  3.91 kB
dist/assets/index-BRk2x9aQ.js     61.42 kB │ gzip: 22.07 kB
```

The build tool puts a hash of the content in the name. Change the file and the name changes, so the HTML points at a new url and every browser fetches the new version. That is what makes a cache of a year safe.

> **Explanation:** This is why you never cache style.css for a year without a hash: change one colour and returning visitors keep the old file until the cache expires. The hash is calculated from the content, so a file that did not change keeps its name and stays cached across deploys. Vite, Astro, Next and most build tools do this by default. On a hand-written site you can add ?v=2 to the url, but that is easy to forget.

<!-- slide 39 -->

### CDN

A content delivery network keeps copies of your files on servers around the world, so a visitor in Sydney does not wait for a server in Antwerp. Static hosts like Netlify, Vercel, Cloudflare Pages and GitHub Pages put your whole site on one.

- **Closer**: Shorter round trips, so a lower TTFB and faster files.
- **Compressed**: Brotli or gzip on HTML, CSS, JS and SVG.
- **Cached**: Your cache headers decide how long each copy lives.

> **Explanation:** Compression matters as much as the CDN: text files like HTML, CSS, JavaScript and SVG shrink a lot with gzip or Brotli. Check the content-encoding response header in the Network panel. Images and video are already compressed, so compressing them again gains nothing. Most static hosts also serve over HTTP/2 or HTTP/3, which handles many small files over one connection.

<!-- slide 40 -->

### Drill: which fix

**1. The Largest Contentful Paint image was lazily loaded.**

- Lazy load even more images
- **Remove loading="lazy" from the hero, add fetchpriority="high"** ✓
- Convert the hero to PNG

The hero is the LCP element. Lazy loading makes it wait for layout. Without lazy and with a high priority it starts downloading as soon as the preload scanner sees it.

**2. Image elements do not have explicit width and height. CLS is 0.24.**

- Add loading="lazy"
- Preload every image
- **Add width and height to every img** ✓

Without dimensions the browser reserves no space, and the text jumps when the image arrives. The attributes give it the ratio up front. Lazy loading and preloading change when it loads, not the jump.

**3. Render-blocking requests: three Google Fonts stylesheets and slider.js in the head.**

- **defer on the script, self-host one variable font with font-display: swap** ✓
- async on the stylesheets
- Inline the whole slider library in the HTML

defer takes the script out of the critical path. One self-hosted woff2 replaces three stylesheets on another domain. async does not exist for stylesheets, and inlining a library makes the HTML itself heavier.

**4. Use efficient cache lifetimes: /assets/app-3f9a1c.js is cached for 10 minutes.**

- max-age of a year on index.html too
- **Cache-Control: max-age=31536000, immutable on the hashed assets** ✓
- Rename the file every day

The hash changes whenever the content does, so a year is safe for that file. index.html has no hash: cache it for a year and visitors keep the old version, pointing at old assets.

**5. Reduce unused JavaScript: 300 kB from a chat widget and a tag manager.**

- Minify the chat widget yourself
- Move both scripts to the bottom of the body
- **Load the widget on the first click, and remove the tags nobody uses** ✓

You cannot minify code from someone else's server, and moving it down still downloads and runs all of it. A facade only loads the widget when someone wants to chat, and a tag nobody uses is just cost.

**6. Properly size images: the hero is 4000 px wide, shown at 400 px on a phone.**

- **srcset with a few widths, and sizes** ✓
- width: 100% in CSS
- Convert it to SVG

CSS only scales the image after it has downloaded all 4000 pixels. srcset and sizes let the phone pick a file of about 800 or 1200 px. A photo as SVG makes no sense.

> **Explanation:** These six are the warnings you will see most on portfolio and client sites. Lighthouse words them a little differently between versions, but the fix stays the same. Go through them once now, and come back to this slide when you work on your own site in the assignment.

<!-- slide 41 -->

### main.js uses a function from gallery.js, and both work with elements on the page. How do you load them?

- A. Both async
- B. Both defer, gallery.js first
- C. Both without attributes at the end of the head
- D. gallery.js async, main.js defer

**Answer:** B. Both defer, gallery.js first

defer keeps the order and waits until the HTML is parsed, so the elements exist and gallery.js has run before main.js. With async, main.js can run first and crash. Without attributes in the head, both block the first paint, and the elements do not exist yet when they run.

> **Explanation:** You will see the order problem with async in practice when a script uses a library that is loaded with async too: it works on a fast connection and breaks at random on a slow one. Those bugs are hard to reproduce, so stay with defer for your own code.

<!-- slide 42 -->

## 6. How search works

> **Explanation:** Now the other half. SEO has a reputation for tricks and secrets. Most of it is plain: let Google fetch your pages, make sure it understands what each page is about, and make pages that answer what people search for.

<!-- slide 43 -->

### Crawl, index, rank

1. **Crawl**: Googlebot fetches a url, renders it in a recent Chrome, and follows the links it finds.
2. **Index**: Google stores what the page is about, picks one canonical url for duplicates, and decides whether to keep it at all.
3. **Rank**: For every search, Google picks and orders the indexed pages that answer it best. Relevance first, then many other signals.

Google explains each step in [How Google Search works](https://developers.google.com/search/docs/fundamentals/how-search-works).

> **Explanation:** Googlebot finds new pages mostly through links, from your own pages and from other sites, and through your sitemap. Indexing is not guaranteed: Google skips pages it sees as duplicates, as thin, or as low quality. A new site can take days to weeks before it shows up. Search Console tells you per url which step it reached, more on that in the last chapter.

<!-- slide 44 -->

**Crawlers read HTML.**

Your content, your links, your title and your meta tags should be in the HTML the server sends. A site that only builds its content with JavaScript in the browser makes every crawler work harder, and some of them do not run JavaScript at all.

> **Explanation:** Google does render JavaScript, but rendering costs it more and can happen later than the crawl. In a study by Vercel at the end of 2024, the crawlers of OpenAI, Anthropic and Perplexity did not run JavaScript at all. A React app that only renders in the browser is an empty div to them. Astro, Next, Nuxt and most static site generators send real HTML, which solves this.

<!-- slide 45 -->

## 7. On-page SEO

> **Explanation:** On-page SEO is everything inside your own HTML. None of it is exotic, and most of it also makes your site better for screen readers and for the person who shares your link.

<!-- slide 46 -->

### Title

```html
<title>Branding for De Koffiebar, Antwerp · Lena Peeters</title>
```

- Unique for every page, and about that page
- The most specific words first, your name or brand at the end
- Written for a person scanning ten results, not stuffed with keywords

Google's rules in [Influencing your title links](https://developers.google.com/search/docs/appearance/title-link).

> **Explanation:** Google calls what it shows in the results a title link, and it builds it mostly from your title element, but it can rewrite it with your h1 or with link text from other sites when it thinks your title does not fit. A title that is the same on every page (Home, or just your name) is the most common reason for that. There is no fixed limit, the results cut it off by pixel width, so put the important words first.

<!-- slide 47 -->

### Meta description

```html
<meta name="description" content="Logo, menu cards and a new website for a coffee bar in Antwerp. Case study with sketches, the final identity and the results after six months.">
```

Not a ranking factor. Google sometimes shows it under your title, when it describes the page better than a piece of your text. A clear, unique description gets more people to click. See [Control your snippets](https://developers.google.com/search/docs/appearance/snippet).

> **Explanation:** In 2009 Google wrote that it sometimes uses the description for snippets but does not use it in ranking, and nothing it published since says otherwise. The keywords meta tag is ignored completely, so leave it out. There is no length limit for the description either; Google cuts the snippet to fit the screen. One or two sentences that say what the visitor will find is enough.

<!-- slide 48 -->

### Headings and semantic HTML

```html
<header>…<nav>…</nav></header>
<main>
  <article>
    <h1>Branding for De Koffiebar</h1>
    <h2>The brief</h2>
    <h2>Sketches</h2>
    <h3>Logo</h3>
  </article>
</main>
<footer>…</footer>
```

One `h1` that says what the page is, then `h2` and `h3` as an outline. The elements `header`, `nav`, `main` and `article` tell a crawler and a screen reader which part is the content.

**[Accessibility](https://slides.mctantwerp.be/en/extra-webdev/07-accessibility)**: The same structure, read by a screen reader.

> **Explanation:** Google's starter guide says literally that it does not matter for Search if your headings are out of order. For screen readers it matters a lot: many users jump through a page by heading. So write a logical outline anyway, for people. A div with an onclick is not a button for a keyboard, a screen reader or a crawler, and a styled span is not a heading.

<!-- slide 49 -->

### Internal links

#### Crawlable and clear

```html
<a href="/work/de-koffiebar">
  Branding for De Koffiebar
</a>
```

#### Not a link, or says nothing

```html
<span onclick="go('koffiebar')">
  Read more
</span>

<a href="/work/3">Here</a>
```

> **Explanation:** Crawlers follow a elements with an href, and nothing else. A link that only exists in a click handler is invisible to them. The link text tells Google, and a screen reader user, what the target is about. Link from your homepage to your best projects, and between related projects: pages without any internal link to them are hard to find, for people and crawlers.

<!-- slide 50 -->

### Descriptive urls

| Better | Worse |
| --- | --- |
| `/work/de-koffiebar-branding` | `/project.php?id=37` |
| `/en/contact` | `/Contact_Page_FINAL.html` |
| `/blog/image-formats-2026` | `/blog/2026/10/14/post-1` |

Lowercase, words with hyphens, no ids or file extensions when you can avoid them. And keep them stable once the site is online.

> **Explanation:** Changing urls on a live site loses what they built up unless you redirect the old ones, more on that in the next chapter. So pick a structure before you launch. Google says keywords in the url alone have hardly any effect on ranking. A readable url still helps: it shows in the results and in shared links, and people trust what they can read.

<!-- slide 51 -->

### Alt text

```html
<img src="koffiebar-menu.avif" alt="Menu card in two colours, hand-drawn coffee cups in the margin">
<img src="divider.svg" alt="">
```

Describe what there is to see, as you would on the phone to someone. A decorative image gets an empty alt. The General deck on images has more examples.

> **Explanation:** Alt text is first of all for people who cannot see the image. Google Images uses it too, together with the text around the image and the file name. A file named koffiebar-logo-sketch.avif says more than IMG_4032.avif. Decorative images get an empty alt, so a screen reader skips them instead of reading the file name aloud.

<!-- slide 52 -->

### You rewrite the meta descriptions of all your pages. What can that change?

- A. Your position in Google
- B. How many people click your result
- C. How fast Google indexes the page
- D. Nothing, Google ignores the tag

**Answer:** B. How many people click your result

Google does not use the description for ranking, so your position stays the same. When it shows your description as the snippet, a clear one makes more people click. 'Ignores the tag' is wrong too: that is the keywords meta tag, which Google really does ignore.

> **Explanation:** This is why tools that score your meta description length miss the point a bit. Write it for the person scanning the results. If Google keeps rewriting it, that usually means it does not match what people search for on that page.

<!-- slide 53 -->

## 8. Technical SEO

> **Explanation:** Technical SEO is where a single wrong line can take a whole site out of Google. The good news: most frameworks and static hosts generate these files for you. Your job is to know what is in them and to check them before launch.

<!-- slide 54 -->

### sitemap.xml

```xml
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://example.com/work/de-koffiebar-branding</loc>
    <lastmod>2026-09-30</lastmod>
  </url>
</urlset>
```

A list of every page you want in Google, with full urls. Point to it from robots.txt and submit it in Search Console. See [Build a sitemap](https://developers.google.com/search/docs/crawling-indexing/sitemaps/build-sitemap).

> **Explanation:** Astro has @astrojs/sitemap, Next has a sitemap file convention, WordPress makes one by default. Only list urls you want in Google, with the canonical version of each. lastmod only helps when it is true: Google only uses it when it is consistently accurate, so do not set it to the build date on every page. A sitemap is a hint, not a guarantee of indexing.

<!-- slide 55 -->

### robots.txt

*https://example.com/robots.txt*

```text
User-agent: *
Disallow: /admin/

Sitemap: https://example.com/sitemap.xml
```

> [!WARNING]
> robots.txt controls crawling, not indexing. Google calls it "not a mechanism for keeping a web page out of Google": a blocked url can still show up in the results when other sites link to it. To keep a page out, use noindex or a password.

See [Introduction to robots.txt](https://developers.google.com/search/docs/crawling-indexing/robots/intro).

> **Explanation:** The classic launch disaster: a staging site with Disallow: / gets copied to production, and the new site disappears from Google over the next weeks. Check robots.txt on launch day. A staging site you want to keep out of Google gets a password, not a robots.txt rule: the password stops everyone, robots.txt only stops polite bots. AI crawlers have their own user agents, like GPTBot and ClaudeBot, if you want to treat them differently.

<!-- slide 56 -->

### noindex

```html
<meta name="robots" content="noindex">
```

*Or as a response header, also for a PDF*

```http
X-Robots-Tag: noindex
```

Keeps a page out of the results. Google has to crawl the page to see it, so never block that same page in robots.txt. See [Block indexing with noindex](https://developers.google.com/search/docs/crawling-indexing/block-indexing).

> **Explanation:** This is the trap: a page with noindex that you also block in robots.txt stays in the index, because Google is not allowed to fetch the page and never sees the noindex. Use noindex for a thank you page after a form, internal search results, a page for one client only. Remember that noindex is not security: the page is still public for anyone with the url.

<!-- slide 57 -->

### Your old portfolio page /old-work still shows up in Google. You add Disallow: /old-work to robots.txt. What happens?

- A. It disappears from Google within a day
- B. It can stay in the results, Google just stops fetching it
- C. Google deletes the page from your server
- D. It gets a 404 in the results

**Answer:** B. It can stay in the results, Google just stops fetching it

robots.txt only stops crawling. The url can stay indexed, often without a description, because Google can no longer read the page. Remove the page and return a 404 or 410, redirect it with a 301, or add noindex and keep the page crawlable until it has dropped out.

> **Explanation:** Removing a page from Google fast is possible through the Removals tool in Search Console, but that only hides it for about six months. The noindex, or deleting the page, is the real fix.

<!-- slide 58 -->

### Canonical

The same page often lives at several urls. Google picks one to show, and you can tell it which.

```text
https://example.com/work/koffiebar
https://www.example.com/work/koffiebar/
https://example.com/work/koffiebar?utm_source=linkedin
```

```html
<link rel="canonical" href="https://example.com/work/koffiebar">
```

See [How to specify a canonical URL](https://developers.google.com/search/docs/crawling-indexing/consolidate-duplicate-urls).

> **Explanation:** Google calls a redirect a strong signal and rel=canonical a strong signal too, but both are signals: Google can choose another url as canonical when the rest of your site disagrees. So be consistent: link internally to the canonical url, list only canonicals in your sitemap. Use an absolute url in the canonical. A canonical on every page, pointing at itself, is a sensible default and protects you against ?utm parameters.

<!-- slide 59 -->

### Status codes and redirects

| Code | Means | Use it for |
| --- | --- | --- |
| `200` | OK | Every normal page |
| `301` | Moved permanently | A new url, http to https, www to non-www |
| `302`, `307` | Moved temporarily | A short campaign, a temporary page |
| `404`, `410` | Not found, gone | A page that no longer exists, with a helpful 404 page |
| `503` | Temporarily unavailable | Maintenance, so Google comes back later |

How Google handles each one: [HTTP status codes](https://developers.google.com/search/docs/crawling-indexing/http-network-errors).

> **Explanation:** A soft 404 is a page that says not found but returns 200: Google flags those in Search Console. On a redesign, make a list of every old url (your old sitemap, or Search Console) and redirect each one to its new counterpart with a 301. Redirecting everything to the homepage is treated as a soft 404. A 503 with a Retry-After header tells Google maintenance is temporary, so it does not drop your pages.

<!-- slide 60 -->

### hreflang

*In the head of both versions*

```html
<link rel="alternate" hreflang="nl" href="https://example.com/nl/werk/koffiebar">
<link rel="alternate" hreflang="en" href="https://example.com/en/work/koffiebar">
<link rel="alternate" hreflang="x-default" href="https://example.com/">
```

For a site in Dutch and English. Every version lists all versions, itself included, otherwise Google ignores the tags. `x-default` is for everyone else. See [Localized versions](https://developers.google.com/search/docs/specialty/international/localized-versions).

> **Explanation:** The most common mistake is the return link: the NL page points to the EN page, but the EN page does not point back, and then Google ignores both. Use language codes like nl and en, or with a region like nl-BE and nl-NL when the content really differs per country. Also set the lang attribute on the html element, for screen readers and for translation tools.

<!-- slide 61 -->

### Structured data

```html
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Event",
  "name": "Open studio night",
  "startDate": "2027-03-12T19:00+01:00",
  "location": {
    "@type": "Place",
    "name": "De Koffiebar",
    "address": "Antwerpen, Belgium"
  }
}
</script>
```

JSON-LD describes your page as data: this is an event, on this date, at this place. Google recommends this format, and it can turn into a rich result with the date and place under your link.

> **Explanation:** Google's required properties for an Event are name, startDate and location, the rest is recommended. Use the vocabulary of schema.org and only describe what is visible on the page: markup for content the visitor cannot see goes against Google's guidelines. Most CMSes and SEO plugins generate the common types. Check yours in the Rich Results Test, linked on the next slide.

<!-- slide 62 -->

### Rich results today

- Markup makes a page eligible, it does not guarantee a rich result
- Google has dropped many types in the last years, including How-to and FAQ for most sites
- Still in the [search gallery](https://developers.google.com/search/docs/appearance/structured-data/search-gallery): Article, Breadcrumb, Event, Local business, Organization, Product, Recipe, Review snippet, Video and more
- Validate with the [Rich Results Test](https://search.google.com/test/rich-results) before you deploy

> **Explanation:** In 2023 Google stopped showing How-to rich results and limited FAQ results to a few authoritative sites, and since 2025 it has dropped several more types to simplify the results page. Structured data still helps Google understand the page, but plugins that promise rich results for everything are behind. The search gallery is the list to trust.

<!-- slide 63 -->

### Open Graph

```html
<meta property="og:title" content="Branding for De Koffiebar">
<meta property="og:description" content="Logo, menu cards and website for a coffee bar in Antwerp.">
<meta property="og:image" content="https://example.com/og/koffiebar.jpg">
<meta property="og:url" content="https://example.com/work/koffiebar">
<meta property="og:type" content="article">
```

Not for Google, but for every place your link gets shared: LinkedIn, Teams, Slack, WhatsApp. These tags decide the card with the image and the title. Protocol on [ogp.me](https://ogp.me).

> **Explanation:** og:image needs an absolute url with https, a relative path does not work. 1200 by 630 pixels is the common size. Platforms cache a preview per url, sometimes for days, so after a fix use the LinkedIn Post Inspector or the Facebook Sharing Debugger to fetch it again. Without og tags, a platform guesses from your title and the first image it finds, often the logo.

<!-- slide 64 -->

## 9. Search Console, content and AI

> **Explanation:** Everything so far you can check yourself. This chapter is about what Google actually does with your site, about the content itself, and about what changes with AI in search. The last part is moving fast, so it stays short and sticks to what Google itself says.

<!-- slide 65 -->

### Google Search Console

| Report | Tells you |
| --- | --- |
| **Performance** | The searches you show up for, with clicks, impressions and position |
| **Pages** | What is indexed, and why the rest is not |
| **URL inspection** | What Google saw on one url, and a button to request indexing |
| **Sitemaps** | Whether Google could read yours |
| **Core Web Vitals** | Field data for groups of similar urls |

Free, at [search.google.com/search-console](https://search.google.com/search-console). Your client's site belongs in there from launch day.

> **Explanation:** Verify the domain property through a DNS TXT record at your domain registrar: then it covers every subdomain and both http and https. Data only starts on the day you verify, so do it on launch day. Bing Webmaster Tools does the same for Bing, and it can import your Search Console setup. DuckDuckGo and Yahoo use Bing results too.

<!-- slide 66 -->

### Content and E-E-A-T

Technical SEO makes sure Google can read your page. Whether it ranks depends on the content: does it answer what someone searched for, better than the other results? Google asks you to write for people first, and to show who wrote it and why they know.

- **Experience**: You did it yourself
- **Expertise**: You know the subject
- **Authority**: Others refer to you
- **Trust**: The page is honest and accurate

See [Creating helpful content](https://developers.google.com/search/docs/fundamentals/creating-helpful-content).

> **Explanation:** Google's SEO starter guide lists 'E-E-A-T is a ranking factor' under the misconceptions. It is a framework from the guidelines for the people who rate search results, which Google uses to evaluate its own systems. For a portfolio, experience is your strongest card: process, sketches, what went wrong and how you fixed it. That is content nobody else can write, and that is exactly what generic AI text lacks.

<!-- slide 67 -->

### AI search and llms.txt

AI Overviews, AI Mode in Google, ChatGPT search and Perplexity all start from pages they can crawl and index. Google says it plainly: you do not need new machine readable files, AI text files or markup to appear in its AI features.

- What counts is the basics: crawlable HTML, clear content, a snippet Google can use ([AI features](https://developers.google.com/search/docs/appearance/ai-features))
- [llms.txt](https://llmstxt.org) is a proposed Markdown index of your site for language models. Google Search does not use it
- Several AI crawlers do not run JavaScript, another reason to send real HTML

> **Explanation:** llms.txt was proposed in 2024 and it is still a proposal, not a standard. Google Search does not use it. Some AI tools and coding agents do read it, and documentation sites publish one. For a portfolio or a small client site it is optional: an hour of work at most, with little proof that it does anything. What you can control: let the crawlers you want in through robots.txt, and send real HTML.

<!-- slide 68 -->

### A client asks what extra you need to build so their site shows up in Google's AI Overviews. What do you answer?

- A. An llms.txt file in the root
- B. Special schema.org markup for AI
- C. Nothing extra: indexable pages that are eligible for a snippet
- D. A separate sitemap for AI crawlers

**Answer:** C. Nothing extra: indexable pages that are eligible for a snippet

Google's documentation says there are no additional requirements: the page has to be indexed and eligible to be shown with a snippet. So the work is the normal work: crawlable, indexable, clear content. llms.txt is not used by Google Search, and there is no special AI markup.

> **Explanation:** Agencies sell GEO (generative engine optimisation) as a new field. Some of it is useful, like making sure your facts are stated clearly on the page. Much of it is the same SEO under a new name. Be sceptical of anyone who promises a place in an AI answer.

<!-- slide 69 -->

### Task: Measure your own site

On a site of your own that is online. Stuck? Lighthouse, PageSpeed Insights and both DevTools panels each have a slide with a link to the docs.

1. Run PageSpeed Insights on your homepage and one project page, and note whether there is field data
2. Run Lighthouse in DevTools on mobile, in an incognito window: write down the score, LCP, CLS and TBT
3. Find your LCP element and your biggest layout shift in the Performance panel
4. List the five heaviest files in the Network panel, with Disable cache and Fast 4G on
5. Done when your repo has a performance.md with these numbers, the LCP element and the five files

> **Explanation:** Measure the page that matters most, usually the homepage, plus one project page, because those often have the heaviest images. Run Lighthouse three times and take the middle score. If PageSpeed Insights shows no field data, write that down too: it tells you your site has little traffic so far, which is normal.

<!-- slide 70 -->

### Task: Make it faster

Same site, same pages. Use the drill and the slides of chapters 4 and 5 when you do not know which fix fits.

1. Fix the LCP image: AVIF or WebP, srcset and sizes, fetchpriority="high", no lazy loading
2. Give every img a width and height, and lazy load everything below the first screen
3. Fonts: woff2 only, at most two files, font-display set, or a system font stack
4. defer on your own scripts, and remove or delay one third-party script
5. Done when LCP in Lighthouse mobile is under 2.5 s, CLS under 0.1, and performance.md has before and after

> **Explanation:** Start with the LCP image, it is usually the biggest win for the least work. Then fonts, then scripts. Measure after every change, so you know which change did what, and commit them separately. If your host does not let you set cache headers, write down what it sends, and move on.

<!-- slide 71 -->

### Task: An SEO pass

Same site. Stuck? Chapter 7 for the head of your pages, chapter 8 for the files and the structured data.

1. A unique title, meta description and one h1 on every page, and a canonical pointing at itself
2. A sitemap.xml with only canonical urls, and a robots.txt that blocks nothing by accident and names the sitemap
3. Open Graph tags with an absolute og:image, checked in the LinkedIn Post Inspector
4. JSON-LD for one type that fits your content, valid in the Rich Results Test
5. Done when Search Console is verified, the sitemap shows Success, and URL inspection shows your homepage

> **Explanation:** View the source of your page (Cmd or Ctrl+U), not the Elements panel: the source is what a crawler without JavaScript gets. Indexing can take days, so verifying Search Console on day one matters. If your site has only one language, skip hreflang. For structured data, pick one type that really fits: Organization or Person on the homepage is not a rich result, but Event, Article or Product can be.

## Glossary

- **Core Web Vitals**: Three metrics from Google for how a page feels to a real visitor: LCP for loading, INP for responding to input and CLS for things jumping around. Measured at the 75th percentile of real visits. (slide 11)
- **LCP**: Largest Contentful Paint: the moment the biggest image or text block in view is painted. Good is 2.5 seconds or less, poor is more than 4. (slide 12)
- **INP**: Interaction to Next Paint: how long it takes after a click, tap or key press before the screen shows a reaction. Good is 200 ms or less. Replaced FID in March 2024. (slide 13)
- **CLS**: Cumulative Layout Shift: a score for how much visible content moves while the page loads. Good is 0.1 or less. Images without width and height are the classic cause. (slide 14)
- **Lab data**: Measurements from one test run on a simulated device and network, like Lighthouse. Repeatable and good for debugging, but not what your visitors experience. (slide 16)
- **Field data**: Measurements from real visitors in Chrome, collected in the Chrome UX Report (CrUX) over 28 days. What Google uses, and only available for sites with enough traffic. (slide 16)
- **Render-blocking**: A file the browser has to download before it can paint anything: CSS in the head, and a script without defer or async. Every one of them delays the first paint. (slide 9)
- **srcset**: An attribute with several versions of one image and their widths. Together with sizes, the browser picks the smallest file that is still sharp on that screen. (slide 24)
- **fetchpriority**: A hint to the browser on how important a file is. fetchpriority="high" on the LCP image makes it start downloading before the other images. (slide 25)
- **font-display**: A descriptor in @font-face that decides what happens while the font loads. swap shows the text in a fallback font right away, optional only uses the font when it is there fast. (slide 28)
- **Cache-Control**: The HTTP header that tells the browser and a CDN how long they may keep a file. A year for files with a hash in their name, a check on every visit for HTML. (slide 37)
- **CDN**: Content delivery network: servers all over the world that keep a copy of your files, so a visitor gets them from nearby. Most static hosts include one. (slide 39)
- **Crawling**: A bot like Googlebot fetching your pages and following the links on them. Crawled is not the same as indexed: Google decides afterwards whether to keep a page. (slide 43)
- **Meta description**: A summary of the page in the head. Not a ranking factor, but Google sometimes shows it under your title, and a good one gets more clicks. (slide 47)
- **robots.txt**: A file in the root of your site that tells crawlers what they may fetch. It controls crawling, not indexing: a blocked url can still show up in Google. (slide 55)
- **noindex**: A meta tag or X-Robots-Tag header that keeps a page out of the search results. Google has to be able to crawl the page to see it. (slide 56)
- **Canonical URL**: The one url you want in Google when the same content lives at several urls. Set with <link rel="canonical">, a strong signal but no command. (slide 58)
- **JSON-LD**: Structured data as a block of JSON in a script tag, with the vocabulary of schema.org. The format Google recommends, and the way to get rich results. (slide 61)
- **Open Graph**: Meta tags like og:title and og:image that decide how a shared link looks in LinkedIn, Teams, Slack or WhatsApp. og:image needs an absolute url. (slide 63)
- **E-E-A-T**: Experience, Expertise, Authoritativeness and Trust: what Google's quality raters look for in content. No ranking factor in itself, a good description of what Google tries to reward. (slide 66)

## Links and sources

### In the deck

- [Web Vitals](https://web.dev/articles/vitals) (slide 11): The three metrics, the thresholds and the 75th percentile
- [Optimize Largest Contentful Paint](https://web.dev/articles/optimize-lcp) (slide 12): The four subparts of LCP
- [Interaction to Next Paint (INP)](https://web.dev/articles/inp) (slide 13): 200 and 500 ms
- [Cumulative Layout Shift (CLS)](https://web.dev/articles/cls) (slide 14): 0.1 and 0.25, and the usual causes
- [Chrome UX Report](https://developer.chrome.com/docs/crux) (slide 16): Where the field data in PageSpeed Insights comes from
- [Lighthouse overview](https://developer.chrome.com/docs/lighthouse/overview) (slide 17)
- [pagespeed.web.dev](https://pagespeed.web.dev) (slide 18)
- [Network panel docs](https://developer.chrome.com/docs/devtools/network) (slide 19)
- [Performance panel docs](https://developer.chrome.com/docs/devtools/performance) (slide 20)
- [Optimize resource loading with the Fetch Priority API](https://web.dev/articles/fetch-priority) (slide 25)
- [Best practices for fonts](https://web.dev/articles/font-best-practices) (slide 28)
- [modernfontstacks.com](https://modernfontstacks.com) (slide 30)
- [facade](https://github.com/paulirish/lite-youtube-embed) (slide 31)
- [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script) (slide 33)
- [Prevent caching problems with HTTP caching](https://web.dev/articles/http-cache) (slide 37)
- [Cache-Control on MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control) (slide 37)
- [In-depth guide to how Google Search works](https://developers.google.com/search/docs/fundamentals/how-search-works) (slide 43)
- [Influencing your title links](https://developers.google.com/search/docs/appearance/title-link) (slide 46)
- [Control your snippets in search results](https://developers.google.com/search/docs/appearance/snippet) (slide 47): When Google uses the meta description
- [Build a sitemap](https://developers.google.com/search/docs/crawling-indexing/sitemaps/build-sitemap) (slide 54)
- [Introduction to robots.txt](https://developers.google.com/search/docs/crawling-indexing/robots/intro) (slide 55): Not a mechanism for keeping a page out of Google
- [Block indexing with noindex](https://developers.google.com/search/docs/crawling-indexing/block-indexing) (slide 56)
- [How to specify a canonical URL](https://developers.google.com/search/docs/crawling-indexing/consolidate-duplicate-urls) (slide 58)
- [HTTP status codes](https://developers.google.com/search/docs/crawling-indexing/http-network-errors) (slide 59)
- [Tell Google about localized versions of your page](https://developers.google.com/search/docs/specialty/international/localized-versions) (slide 60)
- [Structured data search gallery](https://developers.google.com/search/docs/appearance/structured-data/search-gallery) (slide 62): The rich result types that are still supported
- [Rich Results Test](https://search.google.com/test/rich-results) (slide 62)
- [ogp.me](https://ogp.me) (slide 63)
- [search.google.com/search-console](https://search.google.com/search-console) (slide 65)
- [Creating helpful, reliable, people-first content](https://developers.google.com/search/docs/fundamentals/creating-helpful-content) (slide 66)
- [AI features and your website](https://developers.google.com/search/docs/appearance/ai-features) (slide 67): No special files or markup needed for AI Overviews and AI Mode
- [The /llms.txt file](https://llmstxt.org) (slide 67)

### Sources

- [Largest Contentful Paint (LCP)](https://web.dev/articles/lcp): Thresholds and which elements count
- [INP is officially a Core Web Vital](https://web.dev/blog/inp-cwv-launch): Replaced FID on 12 March 2024
- [Understanding page experience in Google Search results](https://developers.google.com/search/docs/appearance/page-experience): Core Web Vitals are used by the ranking systems, relevance still comes first
- [SEO Starter Guide](https://developers.google.com/search/docs/fundamentals/seo-starter-guide): Headings out of order, keywords in urls, E-E-A-T is no ranking factor
- [Google does not use the keywords meta tag in web ranking](https://developers.google.com/search/blog/2009/09/google-does-not-use-keywords-meta-tag): Also: the description meta tag is not used in ranking
- [Introduction to structured data markup](https://developers.google.com/search/docs/appearance/structured-data/intro-structured-data): Google recommends JSON-LD
- [The rise of the AI crawler](https://vercel.com/blog/the-rise-of-the-ai-crawler): Which AI crawlers render JavaScript, December 2024
- [The Website Specification](https://specification.website): A checklist of what a modern site needs, with sources
