---
name: site-building
description: Bouw een statische website voor Spaicely die makkelijk te vinden, te delen en te gebruiken is, met sitemap.xml, robots.txt, llms.txt, linkvoorbeelden voor sociale media, gestructureerde gegevens en iconen. Gebruik dit altijd als je een site bouwt of verbouwt die op Spaicely komt te staan, ook als je een bestaande site (WordPress, Wix, Squarespace) naar Spaicely verhuist.
---

# Een site bouwen die mensen en machines kunnen vinden

Spaicely host wat je uploadt, zoals het is. Wat een goede site naast de pagina's nodig heeft, maak je zelf. Dit lijstje verwachten we van elke site; na elke publicatie toont `sites.versions` een rapport waarvan de `hints` zeggen wat er nog ontbreekt, elk met een `fix`.

## Eerst vragen

- **Het domein.** Sitemaps, canonical-links en linkvoorbeelden hebben volledige adressen nodig, zoals `https://bakkerijjansen.nl/over/`. Gebruik het domein dat aan de site gekoppeld is (`account.whoami`, `domains.status`). Nog geen domein? Vraag welk het wordt en gebruik dat.
- **Talen.** Eén taal, of meer in mappen (`/`, `/en/`, `/de/`).
- **De gegevens.** Naam, wat het bedrijf doet, adres, telefoon, openingstijden, accounts op sociale media. Ze komen in de pagina's en in de gestructureerde gegevens. Verzin nooit een adres, openingstijden of recensies.

## Bestanden in de hoofdmap

| Bestand | Waarvoor |
| --- | --- |
| `index.html` | De voorpagina. Elke andere pagina is een map met een eigen `index.html` (`/over/index.html`), gelinkt als `/over/`. |
| `404.html` | Voor adressen die niet bestaan, in de stijl en taal van de site, met een link naar de voorpagina. |
| `robots.txt` | Laat iedereen toe en noemt de sitemap. |
| `sitemap.xml` | Elke pagina die gevonden moet worden, met volledig adres. |
| `llms.txt` | Een korte samenvatting van de site in Markdown voor AI's, volgens https://llmstxt.org. |
| `favicon.ico`, `icon.svg`, `apple-touch-icon.png` | Het icoon in tabbladen, bladwijzers en op telefoons (apple-touch-icon is 180x180). |
| `site.webmanifest` | Naam, kleuren en iconen (192 en 512 px) als iemand de site op zijn beginscherm zet. |
| `og.jpg` | Het plaatje voor linkvoorbeelden, 1200x630, kleiner dan 300 KB. Een pagina mag een eigen plaatje hebben. |

`robots.txt`:

```
User-agent: *
Allow: /

Sitemap: https://bakkerijjansen.nl/sitemap.xml
```

`sitemap.xml`, met als `lastmod` de datum waarop de pagina echt voor het laatst veranderde:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url><loc>https://bakkerijjansen.nl/</loc><lastmod>2026-10-26</lastmod></url>
  <url><loc>https://bakkerijjansen.nl/over/</loc><lastmod>2026-10-26</lastmod></url>
</urlset>
```

Laat `404.html` en pagina's met `noindex` weg. Werk de sitemap bij als je een pagina toevoegt of weghaalt.

`llms.txt`:

```markdown
# Bakkerij Jansen

> Bakery in Haarlem since 1952: bread, pastry and cakes to order. Open Tuesday to Saturday.

- [About us](https://bakkerijjansen.nl/over/): the family and how we bake
- [Cakes to order](https://bakkerijjansen.nl/taarten/): prices and how to order
- [Contact](https://bakkerijjansen.nl/contact/): address, opening hours, phone
```

## De head van elke pagina

```html
<!doctype html>
<html lang="nl">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Taarten op bestelling | Bakkerij Jansen</title>
  <meta name="description" content="Verjaardag, bruiloft of zomaar: bestel uw taart twee dagen vooraf. Ophalen in Haarlem.">
  <link rel="canonical" href="https://bakkerijjansen.nl/taarten/">

  <meta property="og:type" content="website">
  <meta property="og:site_name" content="Bakkerij Jansen">
  <meta property="og:title" content="Taarten op bestelling">
  <meta property="og:description" content="Bestel uw taart twee dagen vooraf.">
  <meta property="og:url" content="https://bakkerijjansen.nl/taarten/">
  <meta property="og:image" content="https://bakkerijjansen.nl/og.jpg">
  <meta property="og:image:width" content="1200">
  <meta property="og:image:height" content="630">
  <meta property="og:locale" content="nl_NL">
  <meta name="twitter:card" content="summary_large_image">

  <link rel="icon" href="/favicon.ico" sizes="32x32">
  <link rel="icon" href="/icon.svg" type="image/svg+xml">
  <link rel="apple-touch-icon" href="/apple-touch-icon.png">
  <link rel="manifest" href="/site.webmanifest">
  <meta name="theme-color" content="#7a3e1d">
  <link rel="alternate" type="text/markdown" href="/llms.txt">
</head>
```

- Elke pagina heeft een eigen titel (ongeveer 50 tot 60 tekens, onderwerp voorop, sitenaam achteraan) en een eigen beschrijving (ongeveer 120 tot 160 tekens, geschreven voor iemand die beslist of hij doorklikt).
- `canonical`, `og:url`, `og:image` en de sitemap gebruiken altijd het volledige `https://`-adres op het domein van de klant, met dezelfde slash aan het eind als de links.
- Een pagina die niet in zoekresultaten hoort (een bedankpagina na een formulier) krijgt `<meta name="robots" content="noindex">` en blijft uit de sitemap.

## Gestructureerde gegevens

Beschrijf het bedrijf in JSON-LD op de voorpagina, zodat zoekmachines adres, openingstijden en logo kunnen tonen. Kies het preciezere type van https://schema.org (`Bakery`, `Restaurant`, `Dentist`, `LocalBusiness`, of `Organization` zonder winkel of praktijk), en voeg `WebSite` toe:

```html
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@graph": [
    {
      "@type": "Bakery",
      "@id": "https://bakkerijjansen.nl/#business",
      "name": "Bakkerij Jansen",
      "url": "https://bakkerijjansen.nl/",
      "logo": "https://bakkerijjansen.nl/icon-512.png",
      "image": "https://bakkerijjansen.nl/og.jpg",
      "telephone": "+31 23 123 4567",
      "address": { "@type": "PostalAddress", "streetAddress": "Grote Markt 1", "postalCode": "2011 RD", "addressLocality": "Haarlem", "addressCountry": "NL" },
      "openingHoursSpecification": [
        { "@type": "OpeningHoursSpecification", "dayOfWeek": ["Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"], "opens": "08:00", "closes": "17:00" }
      ],
      "sameAs": ["https://www.instagram.com/bakkerijjansen", "https://www.facebook.com/bakkerijjansen"]
    },
    { "@type": "WebSite", "@id": "https://bakkerijjansen.nl/#website", "url": "https://bakkerijjansen.nl/", "name": "Bakkerij Jansen", "inLanguage": "nl" }
  ]
}
</script>
```

Blogberichten krijgen `Article` (met `datePublished` en `author`), producten `Product` met `offers`, evenementen `Event`. Beschrijf alleen wat de bezoeker ook op de pagina kan lezen.

## Doorverwijzingen

Komt de site van een ander platform, of verandert een adres, zet de oude adressen dan door in een bestand `_redirects` in de hoofdmap. Eén regel per doorverwijzing:

```
# oud              nieuw                              status (standaard 301)
/over-ons.php      /over-ons/
/blog/*            /nieuws/
/winkel            https://winkel.example.nl/         302
```

- Een pad dat eindigt op `/*` stuurt alles daaronder door naar hetzelfde doel.
- Het doel is een pad op dezelfde site of een volledig https-adres.
- 301 voor blijvend (dan nemen zoekmachines het nieuwe adres over), 302 of 307 voor tijdelijk.
- Hoogstens 50 regels; de doorverwijzing gaat voor een bestand op hetzelfde pad.
- Het bestand zelf wordt niet gepubliceerd. Een regel die niet klopt, staat met zijn regelnummer in de tips van de versie.

## Meer dan één taal

- Eén map per taal, elke pagina met `lang` ingevuld en dezelfde opbouw.
- Elke versie van een pagina noemt alle versies, zichzelf ook, plus `x-default`:

```html
<link rel="alternate" hreflang="nl" href="https://bakkerijjansen.nl/taarten/">
<link rel="alternate" hreflang="en" href="https://bakkerijjansen.nl/en/cakes/">
<link rel="alternate" hreflang="x-default" href="https://bakkerijjansen.nl/taarten/">
```

- Een zichtbare taalkeuze met links, nooit automatisch doorsturen op de taal van de browser.
- `og:locale` per taal; de sitemap noemt elke versie.

## Snel, leesbaar, bruikbaar

- Semantische HTML: `header`, `nav`, `main`, `footer`, één `h1` per pagina, koppen op volgorde.
- Elke `img` heeft `alt` (leeg, `alt=""`, bij versiering) en `width` en `height`. Afbeeldingen onder het eerste scherm krijgen `loading="lazy"`. Gebruik WebP of AVIF en maak foto's zo groot als ze getoond worden, hooguit twee keer zo groot voor scherpe schermen.
- Weinig JavaScript; de site moet zonder werken. Geen frameworks voor een site van een paar pagina's.
- Lettertypen op de site zelf (`.woff2`, `font-display: swap`), niet van de servers van Google Fonts: dan gaat het IP-adres van de bezoeker naar Google, waarvoor een Duitse rechter in 2022 een schadevergoeding toekende.
- Genoeg contrast, zichtbare focus, links die zeggen waar ze heen gaan, een telefoonnummer als `tel:`-link, een adres met een link naar een kaart.
- Spaicely verkleint HTML, CSS en JavaScript al en comprimeert JPEG en PNG opnieuw bij het publiceren. Afbeeldingen omzetten naar een ander formaat doet het niet.
- Browsers bewaren CSS, JavaScript, afbeeldingen en lettertypen een dag. Verander je zo'n bestand, geef het dan een nieuwe naam (`style.v2.css`) en pas de links aan, dan zien bezoekers de wijziging meteen. HTML wordt altijd vers opgehaald.

## Formulieren

Een statische site kan toch berichten en inschrijvingen ontvangen. Spaicely handelt ze af op `https://scripts.spaicely.com`; gebruik geen formulierdienst elders.

1. **Kies de soort.** `contact` mailt elk bericht naar één adres; `signup` houdt een lijst bij (nieuwsbrief, wachtlijst) met dubbele opt-in: de bezoeker krijgt een mail en staat pas op de lijst na een klik op de link daarin.
2. **Zet het aan** met `scripts.enable`: `site_id`, `kind`, `name` (hoe de persoon het noemt), en waar het past `locale` (taal van de mails en van onze pagina's), `subject`, `recipient`, `success_url` en bij signup `confirmed_url`. Zonder `recipient` mailt een contactformulier naar wie het aanzette. Een ander adres krijgt eerst een mail om te bevestigen; tot dan heeft het formulier status `waiting_for_recipient` en verstuurt het niets, dus zeg de persoon dat hij in die mailbox kijkt.
3. **Zet de `html` uit het antwoord ongewijzigd in een pagina**, en geef het daarna de stijl van de site. Het ziet er zo uit:

```html
<form action="https://scripts.spaicely.com/f/k3v9q2x7m1c8b4n6r0td" method="post">
  <input type="text" name="_hp" tabindex="-1" autocomplete="off" aria-hidden="true" style="position:absolute;left:-9999px">
  <label>Naam <input name="name" required></label>
  <label>E-mailadres <input type="email" name="email" required></label>
  <label>Bericht <textarea name="message" rows="6" required></textarea></label>
  <button type="submit">Versturen</button>
</form>
```

   Laat `action`, `method` en het verborgen veld `_hp` staan (dat vangt bots). Je mag velden toevoegen, zoals `telefoon` of `onderwerp`, met elke naam die niet met `_` begint; ze komen allemaal in de mail. Het veld `email` wordt het antwoordadres, dus beantwoorden gaat rechtstreeks naar de bezoeker. Houd bij elk veld een zichtbaar `label`.
4. **Maak de bedankpagina's.** `success_url` (na versturen) en `confirmed_url` (na bevestigen van een inschrijving) zijn pagina's op de site zelf, volledige `https://`-adressen op het eigen domein, met `noindex` en buiten de sitemap. Zonder die pagina's ziet de bezoeker een eenvoudige pagina van ons in de taal van het formulier, met een link terug.
5. **Of blijf op de pagina.** Verstuur met `fetch` en `Accept: application/json`; het antwoord is `{"ok": true}`, of `{"ok": false, "error": ...}` met `origin_not_allowed`, `form_inactive`, `invalid`, `rate_limited` of `failed`. Zonder JavaScript blijft het gewone formulier werken:

```html
<script>
  for (const form of document.querySelectorAll('form[action^="https://scripts.spaicely.com/"]')) {
    const status = document.createElement("p");
    status.className = "form-status";
    status.setAttribute("role", "status");
    form.append(status);
    form.addEventListener("submit", async (event) => {
      event.preventDefault();
      const response = await fetch(form.action, {
        method: "POST", body: new URLSearchParams(new FormData(form)), headers: { Accept: "application/json" }
      }).catch(() => null);
      const answer = response ? await response.json().catch(() => ({})) : {};
      status.textContent = answer.ok ? "Bedankt, uw bericht is verstuurd." : "Dat lukte niet. Probeer het later nog eens.";
      if (answer.ok) form.reset();
    });
  }
</script>
```

**Een download in ruil voor gegevens** (een gids, whitepaper of prijslijst): gebruik `kind: "download"`.

- Zet het bestand in de site onder `_private/`, zoals `_private/gids.pdf`, en publiceer het met de rest. Alles onder `_private/` blijft buiten het CDN, dus via de site is het niet op te halen; alleen het formulier geeft het.
- `scripts.enable` met `kind: "download"`, `file_path: "_private/gids.pdf"` en eventueel `recipient`. Wat de bezoeker invult, gaat per mail naar die ontvanger, net als bij `contact`. De HTML heeft naam, e-mailadres en bedrijf; voeg velden toe als de persoon meer wil weten.
- `delivery: "mail"` (standaard) stuurt de downloadlink naar het ingevulde adres, zodat de gegevens kloppen; zeg op de bedankpagina dat de link in de mail staat. `delivery: "direct"` start de download meteen; met `fetch` krijg je `{"ok": true, "download_url": ...}`, zet dan `location.href = answer.download_url`.
- Een link werkt 7 dagen (direct: een uur) en hooguit 5 keer. Staat het bestand niet in de live versie, dan is de status `waiting_for_file` en geeft het formulier een foutmelding.

**Een kortingscode in ruil voor een e-mailadres**: gebruik `kind: "coupon"`.

- Eén code voor iedereen: `coupon_code: "WELKOM10"`. Een unieke code per bezoeker: `coupon_codes: ["JANSEN-001", ...]` (maak ze eerst aan in de webshop of kassa; tot 1000 per aanroep, later meer toevoegen met `scripts.configure`, `clear_coupons: true` haalt de nog niet uitgegeven codes weg).
- De ontvanger krijgt per mail wat de bezoeker invulde en welke code die kreeg, en een melding als er nog 10 codes over zijn en als ze op zijn. Zonder codes is de status `waiting_for_codes`.
- `delivery: "mail"` (standaard) stuurt de code naar het ingevulde adres. `delivery: "direct"` kan alleen met codes die iedereen krijgt; met `fetch` krijg je `{"ok": true, "code": ...}` om op de pagina te tonen.
- Codes met een periode: `dated_codes: [{"code": "WEEK45", "valid_from": "2026-11-03", "valid_until": "2026-11-09", "shared": true}]`. Met `shared: true` krijgt iedereen die in die periode vraagt dezelfde code (een code per week of per actie); zonder gaat elke code één keer uit en staat in de mail tot wanneer hij geldt. Datums zonder tijd zijn hele dagen in Nederlandse tijd. Volgorde: eerst een gedeelde code die nu geldt, dan de unieke codes, dan `coupon_code`.

**Betalen voor een download of code**: geef een download- of kortingsformulier `price_cents` (inclusief btw, minstens 50). De bezoeker betaalt via Stripe aan de eigenaar van de site, net als in de webwinkel: abonnement Winkel, een gekoppeld Stripe-account en 2% commissie. Na betaling gaan de link of code per mail naar de koper en de gegevens naar de ontvanger.

- Het formulier post dan naar `https://app.spaicely.com/forms/<form_id>/checkout`; gebruik de nieuwe `html` uit het antwoord. Een gewoon formulier gaat door naar Stripe; met `fetch` (JSON) krijg je `{"url": ...}` om te openen.
- Het verplichte vinkje `start_now` moet blijven staan: de koper wil het meteen en ziet daarmee af van het herroepingsrecht voor digitale inhoud. Zonder dat vinkje weigert de checkout.
- Zolang er iets ontbreekt, zegt de status wat: `waiting_for_plan`, `waiting_for_billing_details` of `waiting_for_stripe` (geef de persoon dan `connect_url`). `price_cents: 0` maakt het formulier weer gratis.
- De eigenaar is de verkoper. Zet op de site de bedrijfsgegevens, de prijs incl. btw en een pagina over levering en herroeping, zoals bij de webwinkel.

**Donaties**: `kind: "donation"` werkt bij elk abonnement. De bezoeker kiest een bedragknop of typt een bedrag en betaalt via Stripe op het eigen account van de eigenaar (één keer koppelen, 2% commissie). De donateur krijgt een bedankmail, de ontvanger een mail per donatie.

- `donation_amounts` in centen voor de knoppen (standaard 10, 25 en 50 euro), `donation_min_cents` en `donation_max_cents` voor de grenzen, `goal_cents` voor een doel en `ends_at` voor de laatste dag (daarna neemt het formulier niets meer aan). Er is geen vinkje voor herroeping: er wordt niets geleverd.
- Het formulier stuurt `amount` (een knop) of `amount_other` (getypt), `name`, `email`, `message` en `show_name`. Alleen met `show_name` komen naam en bericht in de voortgang.
- Voor een voortgangsbalk: `fetch(progress_url)` geeft `{"raised_cents", "donors", "goal_cents", "ends_at", "open", "donations": [{"name", "message", "at"}]}`, open voor elke origin en binnen een minuut actueel. Toon de balk als percentage van `goal_cents`, en zet duidelijk op de pagina waar het geld voor is en dat het ook naar de eigenaar gaat als het doel niet gehaald wordt.
- Niet voor beleggen, lenen of beloningen in geld; daarvoor is een vergunning nodig. Iets terug geven (een vermelding, een kaartje) mag.

Goed om te weten:

- Een formulier accepteert alleen berichten vanaf het gekoppelde domein van de site (en www daarvan). Op een voorbeeld of een lokale kopie antwoordt het `origin_not_allowed`; test het als het domein live is.
- Limieten: 10 berichten per bezoeker per 10 minuten, 100 per formulier per uur en 3 inschrijvingen per adres per uur.
- Mail komt van `no-reply@spaicely.com` met de naam van de site als afzender.
- `scripts.list` toont de formulieren van een site met hun status en opnieuw de HTML; `scripts.configure` wijzigt teksten, ontvanger of pagina's, of zet een formulier uit met `enabled: false`.
- `scripts.subscribers` geeft de bevestigde inschrijvingen, elk met een `unsubscribe_url`. Elke mail aan de lijst moet die link bevatten.
- Noem het formulier in de privacypagina: wat er verzameld wordt, dat Spaicely het afhandelt en hoe lang het bewaard blijft.
- Sessies en een winkelwagen hebben geen server nodig: bewaar ze in de browser met `localStorage`.

## Webwinkel

Met het abonnement Winkel verkoopt een site zelf. De koper betaalt via Stripe Checkout rechtstreeks aan de eigenaar; Spaicely houdt 2% commissie in. Er is geen winkelsysteem nodig: de producten staan in één bestand.

1. **Stripe koppelen.** De eigenaar van het account koppelt één keer een eigen Stripe-account. `shop.setup` geeft daarvoor `connect_url`; stuur de persoon daarheen. Tot dat klaar is, is de status `waiting_for_stripe`.
2. **Producten in `data/products.json`.** Prijzen in centen, inclusief btw. `id` is uniek en verandert niet; `active: false` haalt een product uit de verkoop zonder het te verwijderen. Zonder `shipping` vraagt Checkout geen adres (diensten, digitale producten).

```json
{
  "products": [
    { "id": "mok-blauw", "name": "Blauwe mok", "price": 1495, "image": "/img/mok-blauw.jpg", "description": "Handgedraaid, 300 ml" },
    { "id": "bord", "name": "Bord", "price": 2000 }
  ],
  "shipping": [
    { "id": "nl", "name": "Verzenden binnen Nederland en België", "price": 495, "countries": ["NL", "BE"] }
  ]
}
```

3. **Publiceer en zet aan** met `shop.setup` (`site_id`, en waar het past `success_url`, `cancel_url` op het eigen domein, `notify_email`). Het antwoord noemt de status, wat er nog ontbreekt (`problems`), het `endpoint` en een `snippet`: een koopknop als gewoon formulier.
4. **Een winkelwagen** houd je in de browser bij (`localStorage`) en verstuur je met `fetch`; het antwoord is `{"url": ...}` om naar Stripe te gaan, of `{"error": ..., "message": ...}` met een tekst voor de bezoeker:

```html
<script>
  async function checkout(items, shipping) {
    const response = await fetch("https://app.spaicely.com/shop/SHOP_ID/checkout", {
      method: "POST", headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ items, shipping })
    });
    const answer = await response.json();
    if (answer.url) location.href = answer.url; else alert(answer.message);
  }
  // checkout([{ id: "mok-blauw", quantity: 2 }], "nl")
</script>
```

Goed om te weten:

- Prijzen komen altijd uit de live `data/products.json`, nooit van de pagina. Wijzig je een prijs, publiceer dan ook de pagina's die hem tonen.
- Bestellen werkt alleen vanaf het gekoppelde domein (en www) of het adres van de site; tot 50 regels, 1 tot 99 stuks per product.
- De eigenaar krijgt bij elke betaalde bestelling een mail met de producten en het afleveradres; `shop.orders` geeft de bestellingen. Stripe stuurt de koper het betaalbewijs.
- Maak een bedankpagina (`success_url`, `noindex`) en zet in de privacypagina dat Stripe de betaling afhandelt.
- De eigenaar is de verkoper. Maak met de persoon een pagina met leveren, verzendkosten, retourneren en het herroepingsrecht van 14 dagen voor consumenten (met het modelformulier), plus de bedrijfsgegevens (naam, adres, KvK, btw-nummer), en link die in de footer en bij het afrekenen. Terugbetalen doet de eigenaar in zijn Stripe-dashboard; de commissie van Spaicely gaat dan niet terug.

## Een bestaande site verhuizen (WordPress en andere)

Wie elders een site heeft, kan die door jou hier laten nabouwen: dezelfde pagina's en adressen, als gewone snelle HTML, zonder plugins of updates. Verhuis alleen een site die van de persoon is of die hij mag verhuizen.

1. **Inventariseer.** Lees de sitemap (`/wp-sitemap.xml`, `/sitemap_index.xml` of `/sitemap.xml`). Bij WordPress geeft de REST API meestal alles: `/wp-json/wp/v2/pages?per_page=100`, `/posts`, `/media` en `/categories` (verder bladeren met `&page=2`). Per pagina krijg je `link`, `title`, `content.rendered` en, met Yoast of Rank Math, `yoast_head_json` met de titel en beschrijving voor zoekmachines. Staat de API uit, lees dan de pagina's zelf, of vraag de persoon om een export (WordPress: Extra > Exporteren, een XML-bestand). Maak een lijst van alle adressen en laat die zien voordat je bouwt.
2. **Bouw na, kopieer niet.** Neem per pagina de tekst, koppen, afbeeldingen en links over en schrijf nette HTML in een thema in de stijl van de oude site (of in een nieuwe stijl, als de persoon dat wil). Laat shortcodes, opmaak van pagebuilders, pluginscripts en tracking weg. Houd per pagina de titel en beschrijving.
3. **Afbeeldingen.** Geef de bestanden uit `wp-content/uploads/` mee als `source_url` in `sites.deploy` (hooguit 20 per aanroep); Spaicely haalt ze op en maakt ze kleiner. Neem het origineel, geen miniatuur, en houd de alt-teksten.
4. **Houd de adressen.** `/over-ons/` wordt `over-ons/index.html`, zodat links en zoekresultaten blijven werken. Wat wel verandert, komt in `_redirects` met 301 (zie Doorverwijzingen). WordPress-adressen die niet meer bestaan (`/category/*`, `/tag/*`, `/author/*`, `/feed/`, `/?p=123`) gaan naar de dichtstbijzijnde pagina.
5. **Wat een plugin deed.** Contactformulieren (Contact Form 7, WPForms, Gravity Forms) worden een contactformulier, een nieuwsbrief wordt een inschrijfformulier (zie Formulieren). WooCommerce wordt een webwinkel op het Winkel-pakket (zie Webwinkel). Een blog wordt een pagina per bericht plus een overzichtspagina. Reacties, zoeken en inloggen gaan niet mee: zeg dat, en zet bestaande reacties als tekst over als de persoon dat wil.
6. **Laat het zien voor je omzet.** Publiceer en laat de persoon de site bekijken op het eigen adres (`https://<label>.spaicely.site`, zodra de hosting betaald is). Loop hem na met de prompt `launch_check`.
7. **Dan het domein.** `sites.domains.connect` leest de huidige DNS-records van het domein en neemt over wat de persoon houdt, vooral mail (MX, SPF, DKIM), zodat de mail blijft werken. Pas als de site op het eigen domein werkt, kan de oude hosting worden opgezegd. Het domein zelf kan ook naar Spaicely verhuizen met `domains.transfer`, maar dat hoeft niet.

## Privacy en cookies

- Geen tracking zonder toestemming. Analytics, of ingesloten video's, kaarten en berichten van sociale media die cookies zetten, vragen om een cookiemelding die eerst toestemming vraagt, waarin weigeren net zo makkelijk is als accepteren. Beter: een privacyvriendelijke analytics-tool zonder cookies, een videoplaatje dat de speler pas bij een klik laadt, en een link naar de kaart in plaats van een ingesloten kaart.
- Een privacypagina zodra de site iets verzamelt: wat, waarom, hoe lang, bij wie je terechtkunt. Zakelijke sites in de EU tonen ook bedrijfsnaam, adres, KvK-nummer en btw-nummer (in Duitsland en Oostenrijk op een pagina `Impressum`).

## Als het live staat

- Elke betaalde site staat ook op een eigen adres, `https://<label>.spaicely.site` (`address` in `account.whoami` en `sites.versions`; `address_live` zegt of het werkt). Het werkt zodra de hosting betaald is en er een versie live staat, ook zonder gekoppeld domein: handig om de site te laten zien voordat het domein klaar is. Zoekmachines krijgen daar noindex, dus canonical en sitemap blijven naar het eigen domein wijzen.
- Lees de `hints` in `sites.versions` en los op wat van toepassing is.
- Stel voor dat de persoon het domein toevoegt aan Google Search Console en Bing Webmaster Tools en daar de sitemap indient. Beide controleren een domein met een TXT-record, dat jij kunt zetten met `domains.dns.update`.
- Werk na een grote wijziging `lastmod` in de sitemap bij voor de pagina's die veranderd zijn.

## Meer

- Uploaden, publiceren en terugzetten, één blok van een pagina wijzigen, de site op zijn domein zetten, DNS, mail doorsturen, mensen en rollen, het hostingpakket: de skill `site-management`, https://spaicely.com/skills/nl/site-management/SKILL.md.
- Elke tool, parameter en elk HTTP-endpoint: https://spaicely.com/developers/, en voor AI's https://spaicely.com/llms-full.txt.
- Staat een tool die daar genoemd wordt niet in je lijst, dan is je lijst oud: vraag de persoon de Spaicely-connector opnieuw te verbinden, dan laadt de actuele.
