Right, so you think you finally finished your website. You think you can go live. Maybe you have the source code, the Wordpress instance all ready to go, the backup ready to deploy to your server.

And hopefully you gave up FTP 🤦.

Because the cool kids around the block use containerization: they deploy images with their code all ready to go. One click, boom. Done.

But you cannot go live yet.

In fact, nobody ever can.

If you want to launch your website, you might as well do it right. So let me go through what a 2025 website should carry, starting with what you almost certainly forgot.

Favicons

Yep, that pesky little icon in your browser’s tab list. That one. Easy to miss, glancing over it, thinking a single .png upload will do.

Actually, not at all.

Different operating systems want different sizes, and some people pin your site to a home screen. A bigger .png saves nobody. The resulting HTML looks something like this:

<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="manifest" href="/site.webmanifest">
<meta name="msapplication-TileColor" content="#da532c">

Wow, so many sizes 😲. Which explains why you reach for a generator instead of doing it by hand. I still come back to RealFaviconGenerator. Upload 1 .svg, click generate, paste the markup into your <head>.

One and done ✅. Just never change your logo.

One more, new enough that most generators skip it. An SVG favicon can answer the browser’s colour scheme by itself, with a media query inside the SVG. 1 file instead of a light one and a dark one:

<style>
	@media (prefers-color-scheme: dark) { path { fill: #fff; } }
</style>

Somebody will paste your URL into Slack, or iMessage, or LinkedIn. What shows up?

If you answered “the URL”, you forgot your Open Graph tags:

<meta property="og:title" content="The page's title">
<meta property="og:description" content="One sentence about it.">
<meta property="og:type" content="website">
<meta property="og:url" content="https://example.com/page">
<meta property="og:image" content="https://example.com/preview.png">
<meta property="og:image:alt" content="What the preview picture shows">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">

It does seem like a lot. Most modern CMSes handle it for you. Wordpress does it when you set a Feature Image.

3 mistakes I see constantly:

  1. A relative og:image. It needs the scheme and the host. A crawler never visits your page, so it has nothing to resolve /preview.png against.
  2. The wrong type on an article. A blog post wants og:type: article, with article:published_time, article:modified_time and article:author. Leave it as website and you threw away the date and the byline.
  3. Passing an object where a string goes. If your framework hands you an image object and you interpolate it into the tag, you get no error. You get [object Object]. And you will never notice, because nobody ever looks at the tag.

Paste your own URL into Slack before you launch. Every time.

Compression

Turn it on. That covers the whole section. But check it, because “my host does it” and “my host does it” mean 2 different things.

curl -sI -H "Accept-Encoding: br, gzip" https://example.com | grep -i content-encoding

Nothing comes back? You ship uncompressed HTML, CSS and javascript to every visitor. Brotli beats gzip on text by roughly 15% to 20% and every serious host supports it. Images and video arrive compressed already, so leave those alone.

Responsive Images

The single biggest thing you can do for someone on a phone on a train.

<img
	src="/photo-800.jpg"
	srcset="/photo-400.jpg 400w, /photo-800.jpg 800w, /photo-1600.jpg 1600w"
	sizes="(min-width: 60rem) 55vw, 100vw"
	width="1600"
	height="900"
	alt="What the photograph shows"
	loading="lazy"
	decoding="async">

Every attribute there does a job:

  • srcset offers the browser the widths you actually generated.
  • sizes tells it how wide the image will render on the page. Get this wrong and it downloads the biggest one every time. Everybody forgets this one.
  • width and height reserve the space before the file lands, which stops the page jumping around under someone’s thumb. Set them even when your CSS overrides them.
  • loading="lazy" on anything below the fold. Never on the picture at the top, which you want fetched immediately.
  • alt describing the picture, not repeating the heading beside it. An empty alt="" answers correctly for decoration, and nobody should call that a cop-out.

Writing all of that by hand every time invites mistakes, so let the framework do it. Astro generates the whole thing from the widths you name:

---
import {Picture} from "astro:assets";
import getImage from "@utils/images";

/** The widths a picture spanning the viewport gets generated at. */
const FULL_BLEED_WIDTHS = [640, 1024, 1600, 2400];

const source = getImage(photo.image);
---

<Picture
	src={source}
	alt={photo.caption ?? ""}
	formats={["avif", "webp"]}
	widths={FULL_BLEED_WIDTHS}
	sizes="(min-width: 60rem) 55vw, 100vw"
	loading="lazy"
/>

WCAG 2.2

This can include things like:

  1. Contrast: can they actually see the text? Did you get fancy and overlay text on an image? If you did, did you check it against the brightest pixel behind it, at every screen size? Checking against your page’s background colour tells you nothing with a photograph in the way.
  2. Font sizes: can they read it in a moving car?
  3. Visible focus: :focus-visible states. When someone tabs through your page, can they see where they landed?
  4. alt text: not just for <img> and <picture> but for <video> and <audio> too. Do you have transcripts? People on the go need those as much as people with impairments do.
  5. Target size: 24 by 24 CSS pixels for anything other than a link inside a sentence. New in 2.2, and it catches nearly everybody.

A lot to take in. So how do you keep track?

  1. Lighthouse for a report you can hand to somebody.
  2. axe from Deque, whose browser extension finds far more than you expect. Work the errors 1 by 1. Give yourself a few solid days.
  3. Firefox’s accessibility inspector, the best of the built-in ones.

Then check the 2 things no tool can. Tab through the whole page. Turn a screen reader on.

Dark Mode

You probably already know about prefers-color-scheme. And you owe nobody a dark mode; you can set color-scheme: light and move on.

But what about someone reading your website outside on a bright day?

Bet you never thought of that. When I visited Tunis, basically everyone wore sunglasses. I tried looking at my phone at full brightness and could not see a thing.

Now imagine that on a website with only a light mode.

Meet WCAG 2.2 already and you will probably get by unscathed. But if your site runs at all fancy, with small text and images under text, take it outside. Can you read it? If not, add a dark mode.

The easy way in 2025 means never writing 2 of everything. light-dark() holds both values in 1 declaration:

:root {
	color-scheme: dark light;
	--surface: light-dark(#efe9dd, #0b0b0d);
	--ink: light-dark(#1c1a16, #f4f3f0);
}

1 declaration per colour, no second block to forget about. Which describes the actual failure mode of the 2-block approach: a token added to one and missed in the other throws no error. It quietly falls back to the wrong scheme and still renders.

Lesson: use custom properties. Declare color literally everywhere and you only made more work for yourself.

Valid HTML

Did you remember to run your site through the W3C validator? If not, do it right now. You might find your HTML conforms to no spec at all.

  • More than 1 <main>? Technically you can. Screen readers and Google get confused. Have 1.
  • Duplicate id attributes? An aria-labelledby pointing at the second one silently reads the first.
  • Headings skipping a level, like an <h2> followed by an <h4>. Screen reader users navigate by heading, and a gap reads as a missing section.
  • Interactive elements nested in each other. A <button> inside an <a> breaks the spec and does nothing you expect.
  • lang on <html>. 1 attribute. Without it a screen reader might read your English page in a French voice.

Preload And Prefetch

People mix these up constantly.

  • preload means “I need this for this page, fetch it now, high priority.” The font your headline uses. The hero image. Use it sparingly, since preloading everything achieves the same as preloading nothing.
  • prefetch means “they will probably go here next, grab it when you sit idle.” The next page in an obvious flow.
<link rel="preload" href="/fonts/display.woff2" as="font" type="font/woff2" crossorigin>

You cannot skip that crossorigin on a font preload. Leave it off and the browser fetches the file twice, once for the preload and once for the actual use, because the 2 requests go out in different modes.

Critical-Path CSS

The browser paints nothing until it parses every stylesheet in the <head>. Which explains render-blocking, and why a 200KB stylesheet hurts more than a 200KB image.

3 options, in the order I would try them:

  1. Ship less CSS. Astro and Tailwind both strip what you never used. Look at what you actually deploy first. You might have no problem to solve.
  2. Inline what the first screen needs, load the rest asynchronously. Real gains, real maintenance cost, since the inlined copy goes stale the moment somebody edits a component.
  3. Split by route so a page loads only its own styles.

Measure before doing any of it. Critical CSS on a site shipping 12KB of stylesheet solves a problem you never had.

Font Loading

  • Do your fonts load from a CDN? Load them from your own domain instead. Faster even on HTTP/3. And no Google Fonts, which hands a third party a log of your visitors.
  • font-display: swap? Look at your performance tree for that pesky FOIT and FOUT. You will probably just see it.
  • Fallback stack? Without one you get a flash of unstyled text.
  • Tried size-adjust and ascent-override on the fallback so both fonts take up the same space? That stops the page reflowing when the real one lands.
  • Loading 4 weights when the design uses 2? Every one costs a separate file.

Or do what this site does: use the system font stack, ship nothing, and get your character from scale, weight and spacing.

And Then

Open your site on a phone you never built it on, on a network you do not control, and read it.

That catches what the other 12 miss.