# Deploy your site

From a folder on your laptop to a url with HTTPS: the build, picking a host, deploys on every push, your own domain and what to check once it is live.

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

Tags: deploy, hosting, dns, github, security

Slides: <https://slides.mctantwerp.be/en/extra-webdev/04-deploy>

<!-- slide 2 -->

## After this lesson you can

- Explain what happens between typing a url and seeing a page: DNS, HTTPS and the server or CDN that answers
- Tell a static site from a server-rendered site and a backend, and know what each needs from a host
- Build a Vite or Astro project and know what the dist folder is and what ends up in it
- Pick a host for a project, free tier and terms included, and deploy from GitHub on every push
- Set environment variables, a SPA fallback and a 404 page, and know what is public in your bundle
- Connect your own domain with the right DNS records, without breaking the email on it

> **Explanation:** The first chapters are theory you need to understand error messages later: what DNS does, why a static site is easy to host and what the build produces. From chapter 5 on you deploy something real. Keep a project of your own open next to this deck and do every step on it, not on an example.

<!-- slide 3 -->

## Agenda

1. What happens on a visit
2. Static, server or backend
3. The build
4. Choosing a host
5. Deploying from GitHub
6. Settings that bite
7. Your own domain
8. After the launch

> **Explanation:** Chapters 1 to 3 are the basics that every host assumes you know. Chapter 4 helps you choose, chapters 5 and 6 are the hands-on part. Chapter 7 is only urgent once you have a domain, but read it before you buy one. Chapter 8 is what separates a site that is online from a site you can hand over to a client.

<!-- slide 4 -->

## 1. What happens on a visit

> **Explanation:** Every error you will get while deploying is about one of three things: the name does not lead to the right place, the certificate does not match, or the server answers with something else than you expected. Knowing the three steps of a visit makes those errors readable.

<!-- slide 5 -->

**Deploying is putting your files on a computer that never sleeps.**

A host keeps your files on computers that are always on. DNS gives those computers a name people can type, and HTTPS proves to the browser that it talks to the right one.

> **Explanation:** Your laptop is a server too: npm run dev starts one on localhost. But it sleeps when you close the lid, it has no public name and your router does not let strangers in. Hosting is renting a computer that stays on and is reachable, and these days that is usually a whole network of computers.

<!-- slide 6 -->

### From url to page

1. Browser → Resolver, **studentclub.be?**: The browser does not know where the name lives. It checks its own cache first, then asks a resolver.
2. Resolver → Nameserver, **studentclub.be?**: The resolver asks the servers of .be (run by DNS Belgium) which nameserver is in charge of this domain, and then asks that nameserver.
3. Nameserver → Resolver, **A 185.199.108.153**: The answer: an IP address, plus a TTL that says how many seconds everyone may cache it.
4. Resolver → Browser, **185.199.108.153**: The browser now has an address. DNS is done, and has nothing to do with the rest of the visit.
5. Browser → Host, **TLS handshake**: The browser connects to that address on port 443. The host shows a certificate for studentclub.be. The browser checks the name, the date and who signed it.
6. Browser → Host, **GET /**: Over that encrypted connection the browser asks for the page itself.
7. Host → Browser, **200 index.html**: The HTML comes back. The browser reads it and asks for the CSS, the JavaScript and the images, each with a GET of its own.

> **Explanation:** The IP address in this example is one of the four that GitHub Pages really uses. The resolver is usually run by your internet provider, unless you set one like Cloudflare's 1.1.1.1 or Google's 8.8.8.8. Each step caches its answer, so the second visit skips most of the DNS part. HTTP/3 and a few other details are left out: they change how fast this goes, not who does what.

<!-- slide 7 -->

### Looking up a name yourself

```console
$ dig kdg.be +noall +answer
kdg.be.            2770  IN  A      185.135.13.159
$ dig mcp.mctantwerp.be +noall +answer
mcp.mctantwerp.be.         3600  IN  CNAME  mct-slides-mcp.b-cdn.net.
mct-slides-mcp.b-cdn.net.  35    IN  A      207.211.214.145
$ dig kdg.be MX +short
10 kdg-be.mail.protection.outlook.com.
```

A name can point at an address (`A`), at another name (`CNAME`) or at a mail server (`MX`). The website and the email of one domain can live at two different companies.

> **Explanation:** dig is on macOS and Linux. On Windows, nslookup does the same, or use an online tool like dnschecker.org. The number in the second column is the TTL in seconds: how long a resolver may keep the answer before it has to ask again. Note that a CNAME is not an address, it points at another name, and the resolver follows it until it finds an A record.

<!-- slide 8 -->

### What HTTPS adds

- **Encrypted**: Nobody on the wifi of the café or the student home reads or changes what goes back and forth.
- **The right server**: The certificate proves that the server controls the name in the address bar. A certificate for another name gives a full screen warning.
- **[Free and automatic](https://letsencrypt.org/how-it-works/)**: Let's Encrypt signs certificates for free. Your host requests and renews them for you, you never touch a file.
- **Not optional**: Browsers flag plain HTTP as Not secure, and a lot of browser APIs refuse to work without HTTPS.

> **Explanation:** The certificate does not say the site is trustworthy, only that you are talking to the server that controls this name. A phishing site can have a perfectly valid certificate. Since 2015, Let's Encrypt hands out certificates for free and fully automatically, which is why every host includes HTTPS today. Browsers mark a site without HTTPS as Not secure, and features like geolocation, service workers and the clipboard API only work over HTTPS.

<!-- slide 9 -->

### One server or a CDN

#### One server

- Your site lives in one data centre, say in Frankfurt
- A visitor in Seoul waits for every file to cross the world
- A traffic spike or a broken disk takes the site down
- Needed for PHP, a database or anything that runs per request

#### A CDN

- Copies of your files sit in data centres all over the world
- Every visitor gets them from the closest one
- A spike is spread over hundreds of machines
- Works for files that are the same for every visitor

> **Explanation:** A CDN (content delivery network) has servers in hundreds of cities. A request from Antwerp lands in Brussels or Amsterdam, a request from Tokyo in Tokyo. That only works for files that are the same for everyone, which is exactly what a static site is. Netlify, Vercel, Cloudflare and GitHub Pages all put your files on a CDN without you asking.

<!-- slide 10 -->

### Your browser shows DNS_PROBE_FINISHED_NXDOMAIN for your new domain. Where is the problem?

- A. The server of your host is down
- B. The name does not lead to an address: a missing record, a typo or an expired domain
- C. The HTTPS certificate is not ready yet
- D. Your index.html is missing

**Answer:** B. The name does not lead to an address: a missing record, a typo or an expired domain

The browser never got to a server: the lookup itself failed. So look at DNS first, with dig or dnschecker.org. A server that is down gives a timeout, a missing certificate gives a security warning, and a missing index.html gives a 404 page from the host. Each of those needs a working DNS lookup first.

> **Explanation:** NXDOMAIN means non-existent domain: the DNS system says this name has no records at all. Common causes are a typo, a domain that expired, or a record you have not created yet. A server that is down gives a timeout or a connection refused, which looks different in the browser.

<!-- slide 11 -->

## 2. Static, server or backend

> **Explanation:** This is the most important question before you pick a host. A static site runs anywhere for free. Once something has to happen on a server per visitor, your options and the price change completely.

<!-- slide 12 -->

### Three kinds of site

| Kind | What the server does | Examples |
| --- | --- | --- |
| **Static** | Sends files that were made in advance, the same for everyone | A portfolio in HTML, an Astro site, a Vite app |
| **Server-rendered** | Runs your code on every request and makes the HTML then | Astro or Next.js with an adapter, WordPress, Laravel |
| **Backend** | Keeps data, logins and secrets, and answers your frontend | A database, Supabase, a Laravel API, payments |

A static site needs nothing but a place to put files. That is why it is free almost everywhere. Code that runs per request needs a host that runs it, and that is where the limits and the bills start.

> **Explanation:** The lines blur: Astro builds static pages by default, but with an adapter it can render some pages on the server. Next.js can do both in one project. What counts for hosting is the question: does code of mine run on the server for every request, or only once, during the build?

<!-- slide 13 -->

### A single-page app is still static

- **What the host sends**: One `index.html`, a bundle of JavaScript and CSS. The same files for every visitor, so any static host works.
- **What runs in the browser**: The router, the pages and every fetch to an API. The data comes from a backend somewhere else, like Supabase or your own API.

> [!TIP]
> Static is about the files on the server, not about what the page does. A static site can be very interactive, it just cannot keep a secret.

> **Explanation:** React Router, Vue Router and TanStack Router all work this way: the server only ever sends index.html, and JavaScript in the browser decides what to show for /about. That is why a refresh on /about can give a 404 on a host that does not know this, which chapter 6 fixes. Lovable apps are built like this too: a Vite app in the browser, with Supabase as the backend.

<!-- slide 14 -->

### What a static host cannot do

- Run PHP, so no WordPress and no Laravel
- Keep a database, so no comments or accounts of its own
- Hide an API key: everything it sends, the visitor can read
- Send an email when a form is submitted
- Change a page per visitor before it leaves the server

> **Explanation:** Each of these has a static-friendly workaround that you will see later: a form service or Netlify Forms for the contact form, a serverless function for the secret key, Supabase or another service for the data. When you need more than two of those workarounds, a real server is often simpler.

<!-- slide 15 -->

### Which of these projects needs more than a static host?

- A. A portfolio built with Astro
- B. A Vite React app that reads and writes data in Supabase
- C. The WordPress site of your student association
- D. A one-page site with a contact form through Formspree

**Answer:** C. The WordPress site of your student association

WordPress builds every page with PHP and keeps its content in MySQL, so it needs a host that runs both, like a shared hosting package. The Vite app is static files that talk to a backend somewhere else, and Formspree receives the form for you. Astro builds plain HTML unless you add an adapter.

> **Explanation:** The tempting one is the Vite app with Supabase, because it has logins and data. But the app itself is static files, the data lives at Supabase. The contact form works through a form service, which is also a static setup. Only WordPress needs PHP and MySQL running on the server for every page.

<!-- slide 16 -->

## 3. The build

> **Explanation:** On your laptop you run npm run dev, which serves your source files with tricks to make editing fast. Nobody deploys that. A host runs the build, and the result is a different set of files. Knowing what goes in and what comes out saves you hours of guessing when a deploy fails.

<!-- slide 17 -->

### npm run build

```console
$ npm run build
> vite build
vite v8.3.4 building client environment for production...
✓ 9 modules transformed.
dist/index.html                  0.45 kB │ gzip: 0.29 kB
dist/assets/hero-CLDdwZDr.png   13.05 kB
dist/assets/index-CsUDhMuy.css   4.10 kB │ gzip: 1.46 kB
dist/assets/index-CAoPt-vL.js    4.05 kB │ gzip: 1.77 kB
✓ built in 248ms
```

Your `src` folder goes in, a `dist` folder comes out. Only `dist` goes online.

**[npm and Vite](https://slides.mctantwerp.be/en/extra-webdev/02-npm-and-vite)**: What npm, package.json and the Vite build do.

> **Explanation:** This is the real output of a fresh Vite project (vite 8.3, vanilla template). The build bundles your JavaScript into as few files as possible, minifies it, processes the CSS and copies the images. The gzip column is roughly what travels over the network, because every host compresses text files.

<!-- slide 18 -->

### What is in dist

*dist/*

```bash
index.html
favicon.svg
assets/
  index-CAoPt-vL.js
  index-CsUDhMuy.css
  hero-CLDdwZDr.png
```

- No node_modules, no source files, no TypeScript: only what the browser needs
- Files in public are copied as they are, like the favicon
- The hash in a name like `index-CAoPt-vL.js` changes with the content, so browsers can cache it forever
- Put `dist` in `.gitignore`. The host builds it again on every deploy

> **Explanation:** The hash in the file name changes whenever the content changes. That lets a host tell browsers to keep these files for a year: a new version gets a new name, so nobody gets a stale script. index.html itself keeps its name and is never cached for long. Do not commit dist to git: the host builds it again on every deploy, and committed build files only cause merge conflicts.

<!-- slide 19 -->

### Look at the build before the world does

```console
$ npm run build && npm run preview
> vite preview
  ➜  Local:   http://localhost:4173/
 ➜  Network: use --host to expose
```

`npm run dev` serves your source files and fixes a lot on the fly. `npm run preview` serves the real `dist`, the files your host will get. Click through every page there, and refresh on a page that is not the home page.

> **Explanation:** vite preview serves the dist folder the way a static host would. Things that work in dev but break in the build show up here: a path to an image that only worked through the dev server, an environment variable that is missing, a page that needs the SPA fallback. Astro has the same command (npm run preview). It is not meant to host anything in production, it is a check.

<!-- slide 20 -->

### Three settings every host asks for

| Project | Build command | Output folder | Node version |
| --- | --- | --- | --- |
| Vite (React, Vue, Svelte, Lovable) | `npm run build` | `dist` | The one you use locally |
| Astro | `npm run build` | `dist` | The one you use locally |
| Plain HTML, CSS and JS | None | The folder with `index.html` | Not needed |

Get them from your own `package.json`, not from a tutorial: the `scripts` block says which command builds.

> **Explanation:** The host asks for these values when you connect a repo, and it usually guesses them right for Vite and Astro. Lovable exports a Vite project, so the first row applies. A site in plain HTML has nothing to build: leave the build command empty and point the output at the folder with your index.html. When a build fails on the host but works on your laptop, compare these three values first.

<!-- slide 21 -->

### The build works on your Mac. On the host it fails with: Could not resolve './components/header.js'. The file is called Header.js. Why?

- A. The host runs an older Vite
- B. The build server runs Linux, where file names are case-sensitive
- C. You forgot to commit node_modules
- D. The host does not support JavaScript modules

**Answer:** B. The build server runs Linux, where file names are case-sensitive

On macOS, header.js finds Header.js. On Linux those are two different names, so the file does not exist. Match the case exactly in every import and every image path. Committing node_modules is never the fix: the host runs npm install itself, for its own operating system.

> **Explanation:** macOS and Windows treat Header.js and header.js as the same file by default. Linux, which runs almost every build server, does not. So the import works on your laptop and fails on the host with Could not resolve. Fix the import to match the file name exactly, or rename the file with git mv, because git on macOS may not notice a change in case only.

<!-- slide 22 -->

## 4. Choosing a host

> **Explanation:** Hosts change their free plans every year or two, and the internet is full of comparisons that are out of date. So look at what to compare, not only at today's numbers: what is free, what happens when you go over, whether a client site is allowed, and who owns the account.

<!-- slide 23 -->

### Four hosts for a static site

- **[GitHub Pages](https://docs.github.com/en/pages)**: Built into GitHub. A site per repo, deployed with GitHub Actions. No server code, no preview per pull request.
- **[Netlify](https://docs.netlify.com)**: Deploys from Git, preview per pull request, forms and functions built in. Free plan on credits.
- **[Vercel](https://vercel.com/docs)**: The makers of Next.js. Same idea as Netlify, with the best support for Next.js. Free plan for personal use only.
- **[Cloudflare Workers](https://developers.cloudflare.com/workers/static-assets/)**: Static files on the Cloudflare network, with code next to them when you need it. Requests for static files are free.

> **Explanation:** All four put your site on a CDN, give you HTTPS for free and deploy from a GitHub repo. GitHub Pages is the simplest, with the fewest features. Netlify and Vercel feel alike: a dashboard, previews, functions. Cloudflare asks a bit more setup and is very generous with traffic. Cloudflare itself now says to start new projects on Workers instead of Pages: Pages keeps working, but new features go to Workers.

<!-- slide 24 -->

### The free tiers side by side

| Host | Free | Watch out for |
| --- | --- | --- |
| **GitHub Pages** | Public repos, sites up to 1 GB, 100 GB traffic a month (soft) | Not for an online business or a shop |
| **Netlify** | 300 credits a month: a production deploy is 15, a GB of traffic 20 | Out of credits: all your projects pause |
| **Vercel Hobby** | 100 GB transfer, a million function calls a month | Personal, non-commercial use only |
| **Cloudflare** | Static files free and unlimited, 100,000 Worker requests a day | Max 25 MiB per file |

Checked in October 2026, and these change every year. Read what happens when you go over, not only what is free: the pricing pages of [GitHub Pages](https://docs.github.com/en/pages/getting-started-with-github-pages/github-pages-limits), [Netlify](https://www.netlify.com/pricing/), [Vercel](https://vercel.com/docs/plans/hobby) and [Cloudflare](https://developers.cloudflare.com/workers/platform/pricing/).

> **Explanation:** Checked on 14 October 2026 on the pricing and limits pages, which are all in the links of this deck. Netlify counts in credits: 300 a month on the free plan, a production deploy costs 15 and every GB of traffic 20. Twenty production deploys a month and you are out, before a single visitor. Deploy previews are not counted. When the credits run out, every project in your team pauses until the next month. Vercel Hobby also cannot build from a repo that belongs to a GitHub organisation, which matters for group projects.

<!-- slide 25 -->

**A client is not a hobby.**

Vercel Hobby is for personal, non-commercial use, and a site you get paid to build counts as commercial. A client site goes on a plan that allows it, in an account the client owns.

> **Explanation:** The Vercel fair use guidelines spell it out: commercial use is any deployment used for financial gain of anyone involved in any part of its production, including a paid developer writing the code. Ads count, donations do not. GitHub Pages forbids running an online business or a shop on it. Netlify and Cloudflare do not forbid commercial use in their plan descriptions, and their staff say so on the forums, but read the current terms yourself. Either way, create the account in the name of the client: then the site stays theirs when you are gone.

<!-- slide 26 -->

### A classic shared host

#### What you get

- PHP and MySQL, so WordPress or Laravel run
- Email boxes on your domain in the same package
- A certificate from Let's Encrypt, included
- Often a local company, with support in Dutch or French

#### What you miss

- No build on every push: you upload the files yourself
- No preview url per change
- No rollback button, unless you kept the old files
- You share the server with other sites

> **Explanation:** Combell is the best known Belgian host and also a registrar for .be, so you get domain, mail and hosting from one company. Its web hosting packages list PHP up to 8.5, MySQL 8, Let's Encrypt certificates and FTP accounts (checked October 2026). There are many others, Belgian and European. Look for PHP and MySQL versions, SSH or SFTP access, backups and where the data centre is. For a WordPress site of a student association this is still the normal choice.

<!-- slide 27 -->

### Uploading with SFTP

```console
$ npm run build
$ sftp studentclub@ssh.your-host.be
Connected to ssh.your-host.be.
$ cd www
$ put -r dist/*
Uploading dist/index.html to /www/index.html
Uploading dist/assets/ to /www/assets
```

> [!WARNING]
> Plain FTP sends your password unencrypted. Always pick SFTP (or FTPS), and upload the contents of `dist`, never your source folder.

> **Explanation:** FTP sends your password and your files without encryption, so anyone on the same network can read them. Use SFTP, which runs over SSH: your host lists the server, the port and the user name in its control panel. FileZilla or Cyberduck do the same with a window if you prefer that. The folder for your site is often called www, public_html or httpdocs. A shared host can also run a GitHub Action that uploads over SFTP on every push, if you want the automation back.

<!-- slide 28 -->

### Drill: which host

**1. Your own portfolio, built with Astro, in a public repo. You want it online tonight.**

- **GitHub Pages with GitHub Actions** ✓
- A shared host with PHP and MySQL
- A server you rent and manage yourself

Static, personal and in a public repo: GitHub Pages is free and Astro has an official guide for it. Netlify, Vercel or Cloudflare would work just as well. PHP and your own server are overkill for HTML files.

**2. A static site for a bakery, built by you for a fee. The owner wants no monthly costs.**

- Vercel Hobby on your own account
- **A free plan that allows commercial use, in an account of the bakery** ✓
- GitHub Pages in your personal repo

You get paid to build it, so Vercel Hobby is out. Whatever the host, the account belongs to the client: you will not be there in three years, the site will. Check the terms of the free plan before you promise free.

**3. The WordPress site of your student association, with events and a members area.**

- Netlify
- GitHub Pages
- **A shared host with PHP and MySQL** ✓

WordPress runs PHP on every request and keeps its content in MySQL. None of the static hosts run that. A shared host like Combell does, with mail on the domain in the same package.

**4. A group project in a GitHub organisation of your class. You want a preview per pull request, for free.**

- Vercel Hobby, connected to the organisation repo
- **Netlify or Cloudflare, connected to the organisation repo** ✓
- GitHub Pages, which makes a preview per pull request

Vercel Hobby cannot build from a repo owned by a Git organisation. GitHub Pages has no previews at all. Netlify and Cloudflare both build previews for pull requests on their free plan.

**5. A festival site with a 12 MB promo video. You expect 40 GB of traffic in festival week.**

- Netlify free plan
- **Cloudflare Workers with static assets** ✓
- Your laptop and a tunnel

On Netlify, 40 GB is 800 credits, almost three times the free 300, so the site pauses in the middle of the festival. Cloudflare serves static files without counting them, and 12 MB is under its 25 MiB limit per file.

> **Explanation:** There is not always one right host, but there is always a wrong one. For every case, ask four questions: is it static, is it commercial, how much traffic, and whose account is it? Read the explanation after every answer, also when you got it right.

<!-- slide 29 -->

## 5. Deploying from GitHub

> **Explanation:** Uploading by hand works once. The second time you forget a file, the third time you upload the wrong folder. Every modern host builds from your Git repo: the branch main is what is live, and git push is the deploy button. Your repo becomes the single source of truth.

<!-- slide 30 -->

### Push to deploy

1. You → GitHub, **git push origin main**: The only thing you do. Everything after this happens without you.
2. GitHub → Build, **webhook: new commit**: GitHub tells the host that main has a new commit. You gave it that right once, when you connected the repo.
3. Build → GitHub, **clone, npm ci, build**: The host fetches your code, installs the packages from package-lock.json and runs npm run build in a clean container.
4. Build → CDN, **publish dist**: Only a successful build goes live. The CDN switches to the new files in one go.
5. Build → GitHub, **status: deployed**: A green check next to the commit on GitHub, or a red cross with a link to the log when the build failed.

> **Explanation:** The webhook is a small HTTP request that GitHub sends to the host when something is pushed. The host then builds in a clean container: it clones your repo, runs npm ci (which installs exactly what package-lock.json says), and runs your build command. Only when that succeeds does it switch the live site to the new files, all at once. A visitor never sees half a deploy.

<!-- slide 31 -->

### Connecting a repo

1. **Sign in**: With your GitHub account, at the host of your choice.
2. **Import**: Pick the repo. Give access to that repo only, not to all of them.
3. **Check**: Build command, output folder and Node version. The guess is usually right.
4. **Deploy**: A url like studentclub.netlify.app, live in a minute.

The docs: [Netlify](https://docs.netlify.com/deploy/create-deploys/), [Vercel](https://vercel.com/docs/git), [Cloudflare Workers Builds](https://developers.cloudflare.com/workers/ci-cd/builds/).

**[Git and GitHub](https://slides.mctantwerp.be/en/extra-webdev/01-github)**: Commits, pushes and pull requests, if git is still new.

> **Explanation:** When the host asks for access to GitHub, choose Only select repositories instead of All repositories: it needs one repo, not your whole account. The first deploy gives you a url on the domain of the host, like studentclub.netlify.app. That url keeps working after you add your own domain. On Cloudflare this is called Workers Builds, under Workers and Pages, Create application, Import a repository.

<!-- slide 32 -->

### Settings in your repo

*netlify.toml*

```toml
[build]
  command = "npm run build"
  publish = "dist"

[build.environment]
  NODE_VERSION = "24"
```

- The same values as in the dashboard, but in git, next to your code
- A setting in the file wins from the dashboard
- Vercel reads `vercel.json`, Cloudflare `wrangler.jsonc`
- [File-based configuration](https://docs.netlify.com/build/configure-builds/file-based-configuration/) in the Netlify docs

> **Explanation:** Settings in a file are in git: you see who changed them and when, they travel with a fork, and a new team member does not have to click through a dashboard. A setting in the file wins from the same setting in the dashboard. Vercel reads vercel.json, Cloudflare reads wrangler.jsonc, and for GitHub Pages the workflow file is the configuration.

<!-- slide 33 -->

### GitHub Pages: the trigger

*.github/workflows/deploy.yml*

```yaml
name: Deploy to Pages
on:
  push:
    branches: ['main']
  workflow_dispatch:

permissions:
  contents: read
  pages: write
  id-token: write

concurrency:
  group: 'pages'
  cancel-in-progress: true
```

Runs on every push to main, or by hand. It may read the repo and write to Pages, nothing else. A newer push cancels a deploy that is still running.

> [!NOTE]
> First, in the repo: Settings, Pages, Build and deployment, Source: **GitHub Actions**.

> **Explanation:** This is the first part of one file, .github/workflows/deploy.yml, and the next two slides are the rest. It is the workflow from the Vite docs, shortened a little (the docs pin every action to a commit hash, which is safer, with the version as a comment). workflow_dispatch adds a Run workflow button in the Actions tab, handy to deploy again without a commit. The id-token permission lets the deploy step prove to Pages that it really is this repo.

<!-- slide 34 -->

### GitHub Pages: the build

*.github/workflows/deploy.yml*

```yaml
jobs:
  deploy:
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: lts/*
          cache: 'npm'
      - run: npm ci
      - run: npm run build
```

A fresh Ubuntu machine fetches your repo, installs Node and your packages, and runs the same build as on your laptop. The `environment` puts the live url on the summary of every run.

> **Explanation:** lts/* means the newest Long Term Support version of Node, which keeps you on a supported version without editing the file. If your project needs a specific version, put that number instead. cache: npm keeps the downloaded packages between runs, so the second deploy installs faster. npm ci fails when package.json and package-lock.json disagree, which is exactly what you want on a build server: commit your lock file.

<!-- slide 35 -->

### GitHub Pages: the publish

*.github/workflows/deploy.yml (end of steps)*

```yaml
- uses: actions/configure-pages@v6
- uses: actions/upload-pages-artifact@v5
  with:
    path: './dist'
- id: deployment
  uses: actions/deploy-pages@v5
```

The contents of `dist` are packed and handed to Pages. The whole file is on [Deploying a static site](https://vite.dev/guide/static-deploy#github-pages) in the Vite docs, the Astro version on [Deploy to GitHub Pages](https://docs.astro.build/en/guides/deploy/github/).

> **Explanation:** Version numbers checked on 14 October 2026: checkout v7, setup-node v7, configure-pages v6, upload-pages-artifact v5 and deploy-pages v5. Dependabot can keep them up to date for you. For an Astro site you can replace the whole job with withastro/action@v6, which detects your package manager and builds for you: see the Astro guide for GitHub Pages.

<!-- slide 36 -->

### A project site lives in a subfolder

A repo called `portfolio` ends up on `yourname.github.io/portfolio/`. Your build has to know that, or every link to a script points at the wrong folder.

*vite.config.js*

```js
import { defineConfig } from 'vite'

export default defineConfig({
  base: '/portfolio/',
})
```

*astro.config.mjs*

```js
import { defineConfig } from 'astro/config'

export default defineConfig({
  site: 'https://yourname.github.io',
  base: '/portfolio',
})
```

**[Astro](https://slides.mctantwerp.be/en/extra-webdev/05-astro)**: The site and base settings of an Astro project.

> **Explanation:** A repo named after your account (yourname.github.io) becomes a user site on the root of that domain, and needs no base. Every other repo becomes a project site in a subfolder. Without base, the HTML asks for /assets/index.js on the root of the domain, where nothing lives: you get a white page and 404s in the console. Remove base again when you connect your own domain, because then the site lives on the root.

<!-- slide 37 -->

### A preview for every pull request

- **[Netlify](https://docs.netlify.com/deploy/deploy-types/deploy-previews/)**: A deploy preview per pull request, with the link as a comment. Not counted in your credits.
- **[Vercel](https://vercel.com/docs/deployments/environments)**: A preview deployment per push to a branch, with a url per branch and per commit.
- **[Cloudflare](https://developers.cloudflare.com/workers/ci-cd/builds/)**: Preview builds for every branch that is not production, each on its own version url.
- **GitHub Pages**: None. One site per repo, and it is always the live one.

> **Explanation:** Previews are the best reason to pick Netlify, Vercel or Cloudflare over GitHub Pages for a client project. You work on a branch, open a pull request, and the host posts a link on that pull request. The client clicks around on the real thing before it goes live. On Vercel, preview links can be protected so only members of your team see them; on the other hosts anyone with the link can open it, so do not put real customer data on a preview.

<!-- slide 38 -->

### A failed build keeps the old site

```console
$ npm run build
vite v8.3.4 building client environment for production...
✗ Build failed in 20ms
[UNRESOLVED_IMPORT] Could not resolve './components/header.js' in src/main.js
Failed during stage 'building site': Build script returned non-zero exit code: 2
```

Visitors keep getting the previous deploy. Fix the error, push again, and the next build replaces it. Reproduce it locally with `npm ci && npm run build`.

> **Explanation:** This is the real error from the case-sensitivity problem of the build chapter. The host keeps the last successful deploy online, so a broken build never breaks your live site: it only means your change is not there yet. Open the log and scroll to the first red line, not the last one. Most hosts mail you on a failed deploy, and GitHub puts a red cross on the commit.

<!-- slide 39 -->

## 6. Settings that bite

> **Explanation:** These are the settings that make a deploy go wrong after it succeeded: the build is green, the site is up, and still something is broken or leaks. Most of them you set once per project. Do it at the start, not on the evening before the deadline.

<!-- slide 40 -->

### Pin your Node version

*.nvmrc*

```bash
24
```

*package.json*

```json
{
  "engines": {
    "node": "24.x"
  }
}
```

Run `node -v` on your laptop and put that major version in both files. A build that works on Node 24 and fails on 22 is a vague error you do not want to debug through a deploy log.

> **Explanation:** Node 20 reached end of life on 30 April 2026. Node 22 and 24 are supported LTS versions, and 26 becomes LTS at the end of October 2026. Hosts pick a default that is not necessarily yours: Vercel and Cloudflare Workers Builds default to 24, the older Cloudflare Pages build image to 22. Netlify and Cloudflare read .nvmrc or .node-version, Vercel reads engines in package.json. Putting both in costs nothing.

<!-- slide 41 -->

### Environment variables on the host

| Host | Where |
| --- | --- |
| Netlify | Project configuration, Environment variables |
| Vercel | Settings, Environment Variables, per environment |
| Cloudflare | The settings of your Worker, with separate build variables |
| GitHub Pages | Repository secrets and variables, read by your workflow |

Your `.env` stays out of git, so the host never sees it. You type the same names and values in its dashboard.

**[Accounts and secrets](https://slides.mctantwerp.be/en/general/07-accounts-and-secrets)**: What a secret is, why .env sits in .gitignore and what to do when a key leaks.

> **Explanation:** Every host lets you set a value per environment: production, previews and local development (Vercel calls them Production, Preview and Development). Use a test key for previews, so a preview never touches real payments or real mail. Changing a value does not change the running site: on every host the new value is only used from the next deploy, so redeploy after you edit one.

<!-- slide 42 -->

### Everything in the bundle is public

*src/weather.js*

```js
const key = import.meta.env.VITE_WEATHER_KEY
fetch(`https://api.weather.example/today?key=${key}`)
```

*dist/assets/index-D8k2xq.js*

```js
fetch(`https://api.weather.example/today?key=4f9c2e81b7a0`)
```

The build pastes the value into the JavaScript. Anyone opens DevTools and copies your key. Only names with `VITE_` (Vite) or `PUBLIC_` (Astro) get there, and only because you added the prefix. See [Env variables and modes](https://vite.dev/guide/env-and-mode).

> **Explanation:** The Vite docs say it in so many words: VITE_ variables should not contain sensitive information such as API keys, because their values are bundled into your source code at build time. Astro does the same with the PUBLIC_ prefix. The prefix exists so you do not leak a variable by accident. Adding it is you saying: this one may be public. Some keys are meant to be public, like a Supabase publishable key or a Google Maps key restricted to your domain. A secret key never is.

<!-- slide 43 -->

### Keeping a key secret

*netlify/functions/weather.mjs*

```js
export default async () => {
  const key = process.env.WEATHER_KEY
  const res = await fetch(
    `https://api.weather.example/today?key=${key}`
  )
  return Response.json(await res.json())
}

export const config = { path: '/api/weather' }
```

The key stays on the server. Your frontend calls `fetch('/api/weather')` and only gets the weather back.

That is a [serverless function](https://docs.netlify.com/build/functions/get-started/): code that runs per request, without a server for you to manage.

> **Explanation:** This is a Netlify function: a file in netlify/functions, deployed with your site, that runs on a server only when someone calls it. It reads WEATHER_KEY with process.env, which never ends up in the browser. Vercel and Cloudflare have the same idea with slightly different syntax. Free plans count the calls, so a function that is called on every page view adds up. Protect it too: anyone can call /api/weather, so do not let it do anything expensive for strangers.

<!-- slide 44 -->

### Your fetch to the OpenAI API only worked after you renamed OPENAI_API_KEY to VITE_OPENAI_API_KEY. What did you just do?

- A. Fixed it the right way, Vite needs the prefix
- B. Put your secret key in the JavaScript of every visitor
- C. Nothing, the key is encrypted in the build
- D. Made the key available on the server only

**Answer:** B. Put your secret key in the JavaScript of every visitor

The prefix is what makes Vite paste the value into the bundle. It worked because the key is now public. The build does not encrypt anything, it minifies. Revoke the key, make a new one, and call the API from a serverless function that reads the key with process.env.

> **Explanation:** This happens a lot with AI APIs: the fetch works, the demo works, and a week later the key has been scraped and the bill is a few hundred euros. Bots scan public JavaScript for keys. Revoking the key is step one, moving the call to a function is step two.

<!-- slide 45 -->

### Refresh gives a 404

#### The visitor asks

```http
GET /events/fuif
```

A link inside the app works, because the router in the browser handles it. A refresh or a shared link asks the server for that path.

#### The host has

```bash
index.html
assets/index-CAoPt-vL.js
```

No file called `events/fuif`. The fix: answer every unknown path with `index.html`, and let the router decide.

> **Explanation:** Locally this works because the Vite dev server answers every unknown path with index.html. Most static hosts do not: they look for a file called events/fuif or events/fuif/index.html, find nothing, and send their 404 page. Astro does not have this problem by default, because it builds a real HTML file for every page. It is a problem of single-page apps with a router in the browser.

<!-- slide 46 -->

### The SPA fallback per host

| Host | File | Rule |
| --- | --- | --- |
| Netlify | `public/_redirects` | `/* /index.html 200` |
| Vercel | `vercel.json` | `{ "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }] }` |
| Cloudflare | `wrangler.jsonc` | `"not_found_handling": "single-page-application"` |
| GitHub Pages | None | No rewrites. Only a 404.html trick. |

The address bar keeps `/events/fuif`, the host quietly sends `index.html` with status 200, and the router in the browser shows the right page. The details are in the [Netlify](https://docs.netlify.com/manage/routing/redirects/rewrites-proxies/), [Vercel](https://vercel.com/docs/frameworks/frontend/vite) and [Cloudflare](https://developers.cloudflare.com/workers/static-assets/routing/single-page-application/) docs.

> **Explanation:** This is called a SPA fallback, or a rewrite: the url in the address bar stays /events/fuif, but the host sends index.html. On Netlify the _redirects file goes in public, so Vite copies it into dist. GitHub Pages has no rewrites at all. The usual trick there is to copy index.html to 404.html after the build: it works for visitors, but every page then answers with status 404, which search engines do not like. For a single-page app, that is a reason to pick another host.

<!-- slide 47 -->

### A 404 page of your own

*src/pages/404.astro*

```astro
---
import Layout from '../layouts/Layout.astro'
---
<Layout title="Not found">
  <h1>This page does not exist</h1>
  <a href="/">Back to the home page</a>
</Layout>
```

- Astro builds this to `dist/404.html`. In Vite, put a `404.html` in `public`
- Netlify and GitHub Pages pick up 404.html by themselves
- Cloudflare needs `"not_found_handling": "404-page"`
- Check it with a url that does not exist, on the live site

> **Explanation:** A real 404 page keeps the visitor on your site: a link to the home page and a search or a menu beats the grey default page of the host. It also matters that it answers with status 404 and not 200, so search engines drop the url instead of indexing a page that says not found. You can check the status in the Network tab of DevTools.

<!-- slide 48 -->

### Task: Put your project online

With a Vite, Astro or plain HTML project of your own, in a GitHub repo. Stuck? Every slide links to the docs, and all links are behind the book icon.

1. Connect the repo to GitHub Pages, Netlify, Vercel or Cloudflare, and get a first deploy
2. Pin the Node version in .nvmrc and package.json
3. Add a SPA fallback if your project has a router, and a 404 page of your own
4. Open a pull request with a small change and look at its preview (not on GitHub Pages)
5. Done when a refresh on a subpage works, a wrong url shows your 404 page, and a push to main goes live without you touching the host

> **Explanation:** Start with the host you want to learn, not the one with the nicest homepage. If your project is a single-page app, test the refresh on a subpage first, because that is where most first deploys break. Stuck on the build? Run npm ci && npm run build locally: if it fails there, it is not the host. The slides Connecting a repo, Pin your Node version and The SPA fallback per host have the details.

<!-- slide 49 -->

## 7. Your own domain

> **Explanation:** A domain makes a site look finished, and it is the part of a deploy that people are most afraid of, because a mistake takes hours to show up. With the right order of steps and a low TTL, it is a quarter of an hour of work and some waiting.

<!-- slide 50 -->

### Buying a domain

- **[At a registrar](https://www.dnsbelgium.be/en/register-domain-name/find-registrar)**: DNS Belgium manages .be, but you buy it from a registrar, like Combell or one of the others on their list. Prices differ per registrar.
- **[Check if it is free](https://www.dnsbelgium.be/en/search)**: The search of DNS Belgium tells you if a .be is still available. First come, first served.
- **Per year**: You rent a name, you never own it. Switch on auto-renew, or your site and your email vanish on the expiry date.
- **In the name of the client**: The client is the registrant, with their own account. You get access, you do not hold the keys.

> **Explanation:** DNS Belgium runs .be, .brussels and .vlaanderen, but you cannot buy a domain from them: you go through a registrar, which sets its own price. .eu is run by EURid, with its own rules on who may register. A domain is rented per year. Switch on auto-renew and keep the payment card up to date: an expired domain takes your site and your email offline, and someone else can register it.

<!-- slide 51 -->

### The records you will touch

| Record | Points at | For |
| --- | --- | --- |
| `A` | An IPv4 address | The site, on the bare domain |
| `AAAA` | An IPv6 address | The same, over IPv6 |
| `CNAME` | Another name | The site on `www`, pointing at your host |
| `MX` | A mail server | Email to you@yourdomain.be |
| `TXT` | A piece of text | Proof you own the domain, and mail rules (SPF, DMARC) |
| `CAA` | A certificate authority | Who may issue certificates for your domain |

> **Explanation:** You do not need all of these on day one: an A or CNAME for the site, and the mail records that your mail provider gives you. TXT records collect verifications from every service that wants proof you own the domain: Google, Microsoft, GitHub. A CAA record lists which certificate authorities may issue certificates for your domain. If one exists and Let's Encrypt is not on it, your host cannot get a certificate.

<!-- slide 52 -->

### Apex and www

#### studentclub.be

- The apex: the bare domain, without anything in front
- No CNAME allowed here, so you use A and AAAA records
- Or an ALIAS or flattened CNAME, if your DNS provider has one

#### www.studentclub.be

- A subdomain like any other
- A `CNAME` to the name your host gives you
- Pick one of the two as the real address, and redirect the other

> **Explanation:** The DNS rules (RFC 1034) say a name with a CNAME may have no other records. The apex always has others (NS, and usually MX), so a CNAME there is not allowed. Some DNS providers work around this with an ALIAS, ANAME or CNAME flattening: you type a name, they answer with the addresses behind it. Add both the apex and www at your host, and it redirects one to the other for you.

<!-- slide 53 -->

### Pointing a domain at GitHub Pages

| Type | Name | Value |
| --- | --- | --- |
| `A` | `@` | `185.199.108.153` |
|   |   | and `.109.153`, `.110.153`, `.111.153` |
| `AAAA` | `@` | `2606:50c0:8000::153` to `8003::153` |
| `CNAME` | `www` | `yourname.github.io` |

Then add the domain under Settings, Pages, Custom domain, and tick **Enforce HTTPS**. The full list is in [Managing a custom domain](https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site/managing-a-custom-domain-for-your-github-pages-site).

> **Explanation:** These are the values from the GitHub Pages docs, checked in October 2026. Never copy DNS values from a blog post: hosts change them, and Vercel even gives every project its own. Netlify uses an A record to 75.2.60.5 (or an ALIAS to apex-loadbalancer.netlify.com) and a CNAME to your-site.netlify.app. Vercel shows the exact values in the domain settings of your project. Cloudflare Workers only take a custom domain whose DNS is at Cloudflare, and then make the records for you. GitHub also recommends verifying your domain with a TXT record in your account settings, so nobody else can claim it for their Pages site.

<!-- slide 54 -->

### TTL and propagation

```console
$ dig yourname.github.io +noall +answer @1.1.1.1
yourname.github.io.  3600  IN  A  185.199.108.153
yourname.github.io.  3600  IN  A  185.199.109.153
```

`3600` is the TTL: every resolver may keep this answer for an hour. A change only reaches someone once their cached copy expires. That waiting is what people call propagation.

> **Explanation:** There is no wave that rolls over the internet. Every resolver that looked up your name keeps the old answer until its TTL runs out, and then asks again. With a TTL of 3600 that is at most an hour, with 86400 a full day. So lower the TTL to 300 a day before you switch, and raise it again afterwards. Your own laptop and browser cache too: test with dig @1.1.1.1 or with dnschecker.org, which asks resolvers all over the world.

<!-- slide 55 -->

### The certificate comes by itself

1. **DNS**: Your records point at the host.
2. **Proof**: The host shows Let's Encrypt that it controls the name.
3. **Certificate**: Issued for your domain, renewed before it expires.
4. **Enforce**: Every http:// request is redirected to https://.

Stuck on a certificate error for more than a day? Check for a typo in the records, and for a `CAA` record that leaves out Let's Encrypt. See [From 90 to 45 days](https://letsencrypt.org/2025/12/02/from-90-to-45).

> **Explanation:** The host proves to Let's Encrypt that it controls your domain, usually by serving a file on it, which only works once your DNS points at the host. That is why the certificate comes after the DNS change, and why it can take a while: GitHub says up to 24 hours before Enforce HTTPS can be ticked. Let's Encrypt certificates last 90 days today and are going to 45 days by 2028. That is no problem when the host renews them, and a reason never to install one by hand.

<!-- slide 56 -->

### Do not break the email

| Record | Name | What breaks without it |
| --- | --- | --- |
| `MX` | `@` | All mail to the domain bounces or disappears |
| SPF (`TXT`) | `@` | Mail you send lands in spam |
| DKIM (`TXT` or `CNAME`) | `selector._domainkey` | Mail you send fails its signature check |
| DMARC (`TXT`) | `_dmarc` | Receivers do not know what to do with failures |

> [!WARNING]
> Moving DNS to another provider? Copy every record first. The website is only two of them.

> **Explanation:** This goes wrong when you move the nameservers of a domain, for example to Cloudflare or to the DNS of Netlify. The new provider starts with an empty zone or with a guess, and the mail records of the old one are gone. Before you switch, export or screenshot every record at the old provider. DKIM is sometimes a TXT and sometimes a CNAME, depending on the mail provider. Mail that bounces for a day is a bad first impression on a client.

<!-- slide 57 -->

### You moved the nameservers of a client domain to Cloudflare. The site works, but mail to info@ bounces. What is the most likely cause?

- A. The certificate does not cover email
- B. The MX records were not copied to the new nameservers
- C. The TTL is too low
- D. Cloudflare blocks email by default

**Answer:** B. The MX records were not copied to the new nameservers

The nameservers answer every question about the domain, mail included. If the new zone has no MX record, nobody knows where to deliver the mail. A certificate for the website has nothing to do with mail, and a low TTL only makes changes arrive faster. Copy MX, SPF, DKIM and DMARC before you switch.

> **Explanation:** Changing nameservers moves the whole zone, not just the website. Cloudflare tries to import the existing records when you add a domain, but it can miss some, and other providers start empty. The website works because you added those records yourself. Compare the old and the new zone record by record.

<!-- slide 58 -->

### Task: Connect a domain

With a domain you own, or a subdomain you are allowed to use. Stuck? The slides Apex and www, Pointing a domain at GitHub Pages and TTL and propagation, and the domain docs of your host.

1. Write down every existing record of the domain before you change anything
2. Add the domain at your host and copy the records it gives you, nothing more
3. Make www and the apex both work, with one redirecting to the other
4. Done when https:// works on both, http:// redirects to https://, and the mail records are unchanged

> **Explanation:** No domain of your own? Prices differ per registrar, so compare a few from the DNS Belgium list before you buy, and look at the renewal price, not only the first year. Or ask the student association or a family member who has one, and use a subdomain like test.theirdomain.be, so you never touch their apex or their mail. Lower the TTL first, then change the records. Check with dnschecker.org, not with your browser, which caches.

<!-- slide 59 -->

## 8. After the launch

> **Explanation:** A site that is online is not yet a site you can hand over. Someone will fill in the contact form, someone will ask how many visitors there were, and one day a deploy will break the home page. This chapter is about those moments.

<!-- slide 60 -->

### A form without a server

- **[Netlify Forms](https://docs.netlify.com/manage/forms/setup/)**: One attribute on your form, and Netlify keeps the submissions. Only on Netlify.
- **[Formspree](https://formspree.io/plans)**: Works on every host. The form posts to them, you get a mail. 50 submissions a month for free.
- **Your own function**: A serverless function that receives the form and sends a mail through a mail API. Most work, most control.

> [!WARNING]
> A form collects personal data. Say on the site what happens with it, and add spam protection before the bots find it.

> **Explanation:** Netlify Forms works by adding the attribute data-netlify='true' to a form in your HTML: Netlify detects it during the deploy and stores every submission, with a mail notification if you want one. On the credit-based plans submissions are free. Formspree works on any host: the action of your form points at their url. Free means 50 submissions a month, which is fine for a portfolio. A function of your own is the most work, but you decide where the data goes.

<!-- slide 61 -->

### Counting visitors without cookies

- **[Plausible](https://plausible.io/privacy-focused-web-analytics)**: Hosted in the EU, no cookies. Paid after a 30 day trial, or free to host yourself.
- **[Umami](https://umami.is/pricing)**: Open source, no cookies. The free cloud plan covers one site and 100,000 events a month.
- **[Cloudflare Web Analytics](https://developers.cloudflare.com/web-analytics/)**: Free, no cookies or local storage. Works on any host.

Google Analytics sets cookies, so it needs consent before it loads. The cookie-free tools say they need no banner.

> [!WARNING]
> The Belgian Data Protection Authority still asks consent on [its own site](https://www.gegevensbeschermingsautoriteit.be/burger/cookie) before it loads Plausible. Whatever you pick, name it in your privacy statement.

> **Explanation:** Google Analytics sets cookies and sends data to the US, so in Belgium it needs consent first, with a banner. Plausible and Umami say they use no cookies and need no banner, and Cloudflare Web Analytics stores nothing in the browser either. Note that the Belgian Data Protection Authority (GBA) asks consent on its own site before it loads Plausible. The vendors' claim is not the regulator's ruling. Mention whatever you use in your privacy statement, and ask the client how careful they want to be.

<!-- slide 62 -->

### Know when it is down

- **[An uptime monitor](https://uptimerobot.com/pricing/)**: UptimeRobot or Better Stack checks your site every few minutes and mails you when it does not answer. Free for a handful of sites.
- **The expiry dates**: The domain renewal in your calendar, with the card that pays for it. A lapsed domain is the most common outage that has nothing to do with code.
- **The usage page**: On Netlify and Vercel, a free plan that runs out pauses your site. Look at the usage page after a busy week.
- **The deploy mails**: Leave on the mail for a failed deploy. It is the only sign that your last fix never went live.

> **Explanation:** An uptime monitor requests your url every few minutes from somewhere else and mails you when it fails. UptimeRobot's free plan has 50 monitors at a five-minute interval, and their help pages say it may be used commercially; Better Stack has 10 monitors for free (both checked October 2026). Point it at a page that matters, like the home page or the form, not only at a file that never changes.

<!-- slide 63 -->

### Rolling back a bad deploy

| Host | How |
| --- | --- |
| Netlify | Open an older deploy, click **Publish deploy** |
| Vercel | **Instant Rollback** on the deployment page. Hobby: only to the one before |
| Cloudflare | Deployments, an older version, **Rollback** |
| GitHub Pages | `git revert` the bad commit and push |
| Shared host | Upload the old files again, if you still have them |

Roll back first, so visitors get a working site in a minute. Then fix it in git, so the next push does not bring the bug back.

> **Explanation:** A rollback on the host does not change your repo: main still holds the broken commit, and on Netlify the next push publishes again over your rollback. So roll back first to stop the damage, then fix or revert in git. On Vercel Hobby you can only go back to the deploy right before the current one, and new deploys do not go live until you click Undo Rollback. On GitHub Pages, the rollback is a git revert.

<!-- slide 64 -->

### Task: Ready for a client

On the site you put online in the first assignment. Stuck? The slides of chapter 8, and the docs behind the book icon.

1. Add cookie-free analytics and mention it on a privacy page
2. Add a contact form that works on your host, and test it with a real submission
3. Set up an uptime monitor that mails you
4. Push a visible change, roll it back on the host, then revert it in git
5. Done when you can say where every visitor, form submission and alert ends up, and you have rolled back once

> **Explanation:** Take the project of the first assignment. The checklist is what a client expects without saying it. For analytics, pick Umami or Cloudflare Web Analytics if you do not want to pay. For the rollback, push a visible change (a different heading), roll it back on the host, and then revert it in git, so you have done it once before it matters.

## Glossary

- **Deploy**: Putting a new version of your site online. With a host connected to Git, a push to main starts a build and publishes the result. (slide 30)
- **DNS**: Domain Name System: the system that turns a name like kdg.be into an IP address, through resolvers and the nameservers of the domain. (slide 6)
- **Nameserver**: The server that holds the records of a domain and answers questions about it. Moving the nameservers moves every record, mail included. (slide 6)
- **TLS certificate**: A file signed by a certificate authority that proves a server controls a domain name. It makes HTTPS possible. Hosts request and renew them for you. (slide 8)
- **CDN**: Content delivery network: copies of your files in data centres all over the world, so every visitor gets them from nearby. (slide 9)
- **Static site**: A site made of files that are built in advance and are the same for every visitor. Any host can serve it, usually for free. (slide 12)
- **Build**: The step that turns your source code into the files a browser needs: bundled, minified, with hashed names. For Vite and Astro the result lands in dist. (slide 17)
- **dist**: The output folder of the build in Vite and Astro. Only this folder goes online. It stays out of git, the host builds it again. (slide 18)
- **Preview deploy**: A separate deploy of a branch or pull request on its own url, so you and the client can check a change before it goes live. (slide 37)
- **Environment variable**: A setting outside your code, like an API key, set in .env locally and in the dashboard of the host online. A change only counts from the next deploy. (slide 41)
- **Serverless function**: A small piece of code that the host runs per request, without a server you manage. The place for a secret key in a static site. (slide 43)
- **SPA fallback**: A host rule that answers every unknown path with index.html, so the router of a single-page app can handle a refresh or a shared link. (slide 46)
- **Registrar**: The company you rent a domain from, per year. For .be that is one of the registrars of DNS Belgium, which sets no price itself. (slide 50)
- **A record**: A DNS record that points a name at an IPv4 address. AAAA does the same for IPv6. (slide 51)
- **CNAME record**: A DNS record that points a name at another name, like www at your-site.netlify.app. Not allowed on the bare domain. (slide 52)
- **Apex domain**: The bare domain without anything in front, like studentclub.be. It uses A and AAAA records, or an ALIAS when the DNS provider has one. (slide 52)
- **TTL**: Time to live: how many seconds a resolver may cache a DNS answer. Lower it before a change, so the change arrives sooner. (slide 54)
- **MX record**: A DNS record that says which server receives the mail of a domain. Forget it when you move DNS, and mail bounces. (slide 56)
- **Rollback**: Putting an earlier deploy back online from the dashboard of the host. Fast, but your repo still holds the bad commit until you fix or revert it. (slide 63)

## Links and sources

### In the deck

- [Let's Encrypt: how it works](https://letsencrypt.org/how-it-works/) (slide 8)
- [GitHub Pages](https://docs.github.com/en/pages) (slide 23)
- [Netlify](https://docs.netlify.com) (slide 23)
- [Vercel](https://vercel.com/docs) (slide 23)
- [Cloudflare Workers: static assets](https://developers.cloudflare.com/workers/static-assets/) (slide 23)
- [GitHub Pages limits](https://docs.github.com/en/pages/getting-started-with-github-pages/github-pages-limits) (slide 24): 1 GB, 100 GB a month and the prohibited uses
- [Netlify pricing](https://www.netlify.com/pricing/) (slide 24)
- [Vercel: Hobby plan](https://vercel.com/docs/plans/hobby) (slide 24)
- [Cloudflare Workers: pricing](https://developers.cloudflare.com/workers/platform/pricing/) (slide 24): Requests to static assets are free and unlimited
- [Netlify](https://docs.netlify.com/deploy/create-deploys/) (slide 31)
- [Vercel](https://vercel.com/docs/git) (slide 31)
- [Cloudflare Workers Builds](https://developers.cloudflare.com/workers/ci-cd/builds/) (slide 31, slide 37)
- [Netlify: file-based configuration](https://docs.netlify.com/build/configure-builds/file-based-configuration/) (slide 32)
- [Deploying a static site](https://vite.dev/guide/static-deploy#github-pages) (slide 35)
- [Astro: Deploy to GitHub Pages](https://docs.astro.build/en/guides/deploy/github/) (slide 35): site, base and withastro/action@v6
- [Netlify](https://docs.netlify.com/deploy/deploy-types/deploy-previews/) (slide 37)
- [Vercel: environments](https://vercel.com/docs/deployments/environments) (slide 37): Preview deployments per branch and per commit
- [Vite: Env variables and modes](https://vite.dev/guide/env-and-mode) (slide 42): VITE\_ variables are bundled into your source code
- [Netlify Functions: get started](https://docs.netlify.com/build/functions/get-started/) (slide 43)
- [Netlify: rewrites and proxies](https://docs.netlify.com/manage/routing/redirects/rewrites-proxies/) (slide 46)
- [Vercel: Vite](https://vercel.com/docs/frameworks/frontend/vite) (slide 46): The rewrite for a single-page app
- [Cloudflare Workers: single-page applications](https://developers.cloudflare.com/workers/static-assets/routing/single-page-application/) (slide 46)
- [At a registrar](https://www.dnsbelgium.be/en/register-domain-name/find-registrar) (slide 50)
- [Check if it is free](https://www.dnsbelgium.be/en/search) (slide 50)
- [GitHub Pages: managing a custom domain](https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site/managing-a-custom-domain-for-your-github-pages-site) (slide 53): A, AAAA and CNAME values, Enforce HTTPS
- [Let's Encrypt: from 90 to 45 days](https://letsencrypt.org/2025/12/02/from-90-to-45) (slide 55)
- [Netlify Forms](https://docs.netlify.com/manage/forms/setup/) (slide 60)
- [Formspree plans](https://formspree.io/plans) (slide 60)
- [Plausible: privacy-focused web analytics](https://plausible.io/privacy-focused-web-analytics) (slide 61)
- [Umami pricing](https://umami.is/pricing) (slide 61)
- [Cloudflare Web Analytics](https://developers.cloudflare.com/web-analytics/) (slide 61)
- [GBA: cookies](https://www.gegevensbeschermingsautoriteit.be/burger/cookie) (slide 61): The Belgian DPA asks consent before it loads Plausible
- [UptimeRobot pricing](https://uptimerobot.com/pricing/) (slide 62)

### Sources

- [Vite: Deploying a static site](https://vite.dev/guide/static-deploy): dist, vite preview, base and the GitHub Pages workflow
- [Astro: Environment variables](https://docs.astro.build/en/guides/environment-variables/): Only PUBLIC\_ variables reach client-side code
- [Astro: Pages](https://docs.astro.build/en/basics/astro-pages/): 404.astro builds to 404.html
- [GitHub plans](https://docs.github.com/en/get-started/learning-about-github/githubs-plans): Pages in public repositories on GitHub Free
- [GitHub Pages: configuring a publishing source](https://docs.github.com/en/pages/getting-started-with-github-pages/configuring-a-publishing-source-for-your-github-pages-site)
- [GitHub Pages: verifying your custom domain](https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site/verifying-your-custom-domain-for-github-pages)
- [GitHub Pages: securing your site with HTTPS](https://docs.github.com/en/pages/getting-started-with-github-pages/securing-your-github-pages-site-with-https)
- [Netlify: credit-based pricing plans](https://docs.netlify.com/manage/accounts-and-billing/billing/billing-for-credit-based-plans/credit-based-pricing-plans/): 300 credits on Free, 15 per production deploy, 20 per GB
- [Netlify: paused projects](https://docs.netlify.com/manage/accounts-and-billing/billing/resume-paused-projects/)
- [Netlify: manage dependencies](https://docs.netlify.com/build/configure-builds/manage-dependencies/): NODE_VERSION, .nvmrc and .node-version
- [Netlify: manage deploys](https://docs.netlify.com/deploy/manage-deploys/manage-deploys-overview/): Publish deploy as a rollback
- [Netlify: configure external DNS](https://docs.netlify.com/manage/domains/configure-domains/configure-external-dns/)
- [Netlify: environment variables](https://docs.netlify.com/build/environment-variables/get-started/)
- [Netlify Functions: environment variables](https://docs.netlify.com/build/functions/environment-variables/)
- [Netlify Forms: usage and billing](https://docs.netlify.com/manage/forms/usage-and-billing/)
- [Vercel: fair use guidelines](https://vercel.com/docs/limits/fair-use-guidelines#commercial-usage): Hobby is non-commercial personal use only
- [Vercel: limits](https://vercel.com/docs/limits)
- [Vercel: Deployment Protection](https://vercel.com/docs/deployment-protection): Standard Protection keeps previews for Vercel users with access
- [Vercel: Instant Rollback](https://vercel.com/docs/instant-rollback)
- [Vercel: Node.js versions](https://vercel.com/docs/functions/runtimes/node-js/node-js-versions)
- [Vercel: adding a domain](https://vercel.com/docs/domains/working-with-domains/add-a-domain): A CNAME per project
- [Vercel: environment variables](https://vercel.com/docs/environment-variables)
- [Cloudflare Workers best practices](https://developers.cloudflare.com/workers/best-practices/workers-best-practices/): Use Workers instead of Pages for new projects
- [Cloudflare Workers: limits](https://developers.cloudflare.com/workers/platform/limits/)
- [Cloudflare Workers: rollbacks](https://developers.cloudflare.com/workers/configuration/versions-and-deployments/rollbacks/)
- [Cloudflare: DNS record types](https://developers.cloudflare.com/dns/manage-dns-records/reference/dns-record-types/)
- [Cloudflare: Time to Live](https://developers.cloudflare.com/dns/manage-dns-records/reference/ttl/)
- [Cloudflare: CNAME flattening](https://developers.cloudflare.com/dns/cname-flattening/)
- [RFC 1034: Domain names, concepts and facilities](https://www.rfc-editor.org/rfc/rfc1034): Section 3.6.2, no other data next to a CNAME
- [DNS Belgium: register a domain name](https://www.dnsbelgium.be/en/register-domain-name)
- [DNS Belgium: FAQ](https://www.dnsbelgium.be/en/faq): Registry for .be, .brussels and .vlaanderen, prices set by registrars
- [Combell: web hosting specifications](https://www.combell.com/en/hosting/web-hosting/specifications)
- [Node.js releases](https://nodejs.org/en/about/previous-releases): Node 20 end of life on 30 April 2026
- [Cloudflare: free, privacy-first analytics](https://blog.cloudflare.com/free-privacy-first-analytics-for-a-better-web/)
- [Better Stack pricing](https://betterstack.com/pricing)
