fix: vercel

This commit is contained in:
2026-06-09 18:02:04 +02:00
parent ee69da1665
commit c023192113
85 changed files with 10266 additions and 3008 deletions

View File

@@ -0,0 +1 @@
,sharp-zealous-planck,claude,21.05.2026 07:44,file:///sessions/sharp-zealous-planck/.config/libreoffice/4;

View File

@@ -0,0 +1 @@
,sharp-zealous-planck,claude,21.05.2026 10:59,file:///sessions/sharp-zealous-planck/.config/libreoffice/4;

View File

@@ -0,0 +1,455 @@
# Les 13 — Docenttekst (Autocue)
**Online les — letterlijk voorleesbaar. Slide-nummers staan tussen `[SLIDE N]` zodat je weet wanneer door te klikken.**
**Lesvorm:** online, demo-driven. Cursor + browser side-by-side delen.
**Duur:** 180 minuten met 1 pauze van 15 minuten.
> **Voor de les klaarzetten:**
> - Cursor open in lege werkmap
> - Browser tabs open: github.com, vercel.com, pokeapi.co
> - Terminal klaar
> - Cursor Pro/Business actief (voor Background Agent)
> - GitHub + Vercel ingelogd
> - Webcam check + scherm delen ingericht
---
## BLOK 1 — Welkom + Terugblik (10 min)
`[SLIDE 1]`
Welkom bij Les 13. Vandaag is een belangrijke les, want we verlaten voor het eerst je laptop.
Tot nu toe draaiden al jullie projecten op `localhost:3000`. Dat is prima om te leren, maar in de praktijk wil je dat anderen je werk kunnen zien. Klanten. Collega's. Werkgevers. Vandaag zetten we die stap.
We gaan drie dingen leren die je voor elk professioneel project nodig hebt. Eén: data ophalen van externe websites en APIs. Twee: je code automatisch online zetten via Vercel. Drie: efficiënt werken met Cursor agents.
Aan het einde van deze les heb je een live URL die je aan iedereen kunt delen.
`[SLIDE 2]`
Even kort terug. In de afgelopen lessen hebben we gewerkt aan AI-features. Les 11 was de Vercel AI SDK, Les 12 was Tool Calling. Allemaal mooi, maar steeds lokaal.
Vandaag stappen we even uit dat AI-pad. Geen LLM's. Geen API keys voor AI. We gaan terug naar de basis: hoe ziet een professionele Next.js-workflow eruit van localhost tot productie?
We gebruiken een eenvoudige Pokédex als demo-app. Waarom Pokémon? Omdat de PokéAPI gratis is, geen API key vraagt, en je krijgt direct visuele resultaten. Perfect om je te focussen op de workflow, niet op de data.
`[SLIDE 3]`
Dit is de planning voor vandaag. Honderdtachtig minuten.
We beginnen met theorie. Drie blokken van een kwartier: externe APIs in Next.js, Vercel met preview-omgevingen, en Cursor agents.
Dan vier live demo's. In demo één bouwen we de Pokédex met Composer. In demo twee deployen we naar Vercel en zetten we environment variables in. Na de pauze: demo drie met de Background Agent om een feature parallel te bouwen. Demo vier: GitHub Actions om CI in te richten.
Na elke demo bouw je het zelf na in je eigen repo. Vragen tussendoor altijd welkom — typ ze in de chat.
---
## BLOK 2 — Theorie: Externe APIs (15 min)
`[SLIDE 4]`
Wat is een externe API. In simpele woorden: andere mensen of bedrijven hebben data of functionaliteit, en die stellen ze beschikbaar via HTTP. Jij doet een fetch-request, krijgt JSON terug, en kunt daar dingen mee doen.
Een paar voorbeelden van APIs die je in projecten zult tegenkomen.
PokéAPI — onze demo van vandaag. Pokemon-data, geen key nodig.
Open-Meteo — gratis weerservice, ook geen key.
GitHub API — informatie over repositories, gebruikers, issues.
Tavily — web search. Die heeft wél een API key nodig.
Stripe — betalingen. Twee keys: een publishable en een secret.
Voor APIs met een key onthoud je één regel: nooit, ooit, in je code committen. Altijd in environment variables. Hier komen we straks bij Vercel uitgebreid op terug.
`[SLIDE 5]`
Dit is misschien wel de belangrijkste slide van vandaag. Drie manieren om een API te fetchen in Next.js. Static, server, of client. Welke je kiest hangt af van je use case.
Eerste optie: **static**. De fetch gebeurt tijdens `next build`. Eén keer, en daarna is het HTML. Snel, want geen server-werk per request. Goed voor data die zelden verandert: een blog-post, een marketing-pagina, een lijst van producten.
Tweede optie: **server, oftewel dynamic**. De fetch gebeurt bij elke request opnieuw. Altijd vers, maar trager. Goed voor data die per gebruiker anders is, of die altijd up-to-date moet zijn.
Derde optie: **ISR**, Incremental Static Regeneration. Een mix van de twee. Wordt gecached zoals static, maar elke X seconden mag Next.js de cache vervangen. Het beste van beide werelden. Voor onze Pokédex de juiste keuze voor de detail-pagina's: snel, maar elk uur kijkt Next.js of de PokéAPI iets nieuws heeft.
Vierde optie: **client-side fetching**, met useEffect. Dit doe je alleen als de data live moet, of als je user-interactie nodig hebt. Belangrijk: bij client-side staat je API key in de browser. Dus alleen voor publieke APIs of voor data die geen geheim is.
Kijk naar het code-blokje onderaan. Het verschil tussen die drie server-modes is letterlijk één optie in je fetch-call. Verder verandert je code helemaal niet. Dat is de kracht van Next.js: je kiest het render-gedrag per fetch.
Vandaag gebruiken we static voor de Pokédex-lijst en ISR voor de detail-pages.
---
## BLOK 3 — Theorie: Vercel + Cursor + GitHub (15 min)
`[SLIDE 6]`
Korte sidetrack — straks komen we terug op Vercel. Even Cursor: twee soorten agents.
Composer is de agent die je al kent. Cmd+I openen, prompt typen, hij maakt aanpassingen in je editor. Synchroon. Jij wacht, hij werkt, jij kijkt, je accepteert de diff. Pair programming.
Background Agent is anders. Die draait in Cursor's cloud. Je geeft een opdracht, hij gaat aan de slag, hij maakt een branch, hij pusht naar GitHub, hij opent een Pull Request. Allemaal terwijl jij iets anders doet. Asynchroon dus.
Het verschil komt straks duidelijk in de demo. Voor nu: onthoud dat je twee modes hebt. Eén voor naast je, één voor namens je.
`[SLIDE 7]`
Nu Vercel. Dit is het hart van de les. Drie omgevingen, drie scopes — en het is cruciaal dat je dit begrijpt.
Productie is de live versie. Wordt gedeployed elke keer dat je naar de `main` branch pusht. URL is `je-app.vercel.app`. Dit ziet iedereen.
Preview is wat je krijgt voor élke andere branch. Push naar `feature/search` en je krijgt automatisch een URL terug. Iets als `je-app-git-feature-search-jouwnaam.vercel.app`. Ook publiek bereikbaar, maar alleen mensen met de link.
Development is je lokale `pnpm dev` op localhost. Alleen jij ziet dat.
Nu de belangrijke vraag: hoe ga je daar mee om als je verschillende databases, of verschillende API keys hebt per omgeving? Want dat wil je. Je wilt absoluut niet dat een experimentele preview-branch in je productie-database gaat schrijven.
Bekijk de tabel met environment variables. In Vercel ga je naar Project, Settings, Environment Variables. En daar zet je per variabele drie keer een waarde. Voor productie. Voor preview. En voor development.
Een paar voorbeelden. Je `DATABASE_URL` is voor productie je echte database. Voor preview een staging-database. Voor development je lokale Postgres. Drie totaal verschillende waarden, één variabele-naam.
Je `OPENAI_API_KEY` is in productie de echte key met hoge limit. In preview een test-key zodat experimenten je niet failliet maken. In development je persoonlijke key.
Voor Stripe heel concreet: productie is `sk_live_...`, preview en development zijn allebei `sk_test_...`. Een experiment dat per ongeluk een betaling doet komt nooit op een echte creditcard.
Onderaan staat één killer-combo: Cursor Background Agent maakt een Pull Request, Vercel ziet die PR en bouwt automatisch een preview URL, die URL komt automatisch in een comment op de PR. Reviewer klikt, opent in de browser, test live. Geen lokale setup nodig. Dit is voor mij persoonlijk een van de redenen dat ik altijd met Vercel werk.
`[SLIDE 8]`
GitHub Actions, snel. Waarom: voordat je iets merget, weet je zeker dat de code lint en bouwt op een schone machine. Niet "het werkt op mijn laptop", maar "het werkt op een verse ubuntu". Belangrijk omdat Background Agents soms lint-errors maken — CI vangt dat op.
Kijk naar het code-blok. Een minimale workflow. Bij elke pull request en bij elke push naar main: checkout, pnpm install, pnpm lint, pnpm build. Duurt ongeveer twee minuten op een kleine app.
In demo vier gaan we dit bouwen.
---
## BLOK 4 — Wat we vandaag bouwen (5 min)
`[SLIDE 9]`
Dit is wat we vandaag bouwen. Een Pokédex.
De stack: Next.js 16 met TypeScript en Tailwind. Geen Shadcn vandaag, we houden het simpel. PokéAPI als data-bron, geen API key. Cursor voor de code. GitHub voor de versie-controle. Vercel voor de deploys.
Vier features die we bouwen. Een lijst met de eerste 151 Pokémon. Detail-pages per Pokémon. Een zoek-functie. Type-filter chips.
De eerste twee bouwen we met Composer, op de `main` branch. De andere twee met de Background Agent op feature-branches.
Aan het einde heb je: een productie-URL, twee preview-URL's, drie Pull Requests, een werkende CI-pipeline, en een geverifieerd begrip van static versus ISR.
---
## BLOK 5 — LIVE DEMO 1: Pokédex met Composer (30 min)
`[SLIDE 10]`
Tijd voor de eerste demo. Open Cursor, lege werkmap, terminal openen.
Eerste commando, typ mee:
```bash
pnpm create next-app pokedex --typescript --tailwind --app
```
Beantwoord de vragen: geen src directory, gebruik Turbopack, default alias is prima.
Stap in de map: `cd pokedex` en open in Cursor met `cursor .` of File → Open Folder.
Nu het magische moment. Druk op Cmd+I — of Ctrl+I op Windows — voor Composer. Type deze prompt letterlijk over:
> Bouw een lijst-pagina met de eerste 151 Pokémon van PokéAPI. URL is `https://pokeapi.co/api/v2/pokemon?limit=151`. Gebruik Tailwind cards in een grid met de naam en sprite van elke Pokémon. De sprite staat op `https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/{id}.png` waar {id} het index-nummer is. Gebruik een static fetch — dus géén revalidate optie meegeven.
Composer gaat aan de slag. Je ziet diffs verschijnen in `app/page.tsx`. Lees ze met me mee. Accepteer alle wijzigingen.
Start lokaal:
```bash
pnpm dev
```
Open `localhost:3000`. Daar zou nu een grid van 151 Pokémon moeten staan. Werkt het? Mooi.
Tweede prompt in Composer:
> Voeg een detail-pagina toe op `/pokemon/[id]`. Fetch van `https://pokeapi.co/api/v2/pokemon/{id}`. Toon de official artwork — die staat in `sprites.other["official-artwork"].front_default`. Toon ook de types als chips. Gebruik ISR met `revalidate: 3600`. Zorg dat de cards op de homepage linken naar `/pokemon/[id]`.
Wacht op de diffs. Accepteer ze. Open een random Pokémon. Werkt? Goed.
Belangrijke uitleg terwijl studenten kijken:
Kijk naar het verschil tussen `app/page.tsx` en `app/pokemon/[id]/page.tsx`. De homepage gebruikt gewoon `fetch(URL)`. De detail-pagina gebruikt `fetch(URL, { next: { revalidate: 3600 } })`. Dat is het hele verschil tussen static en ISR. Eén optie.
Open de browser Network tab — F12. Klik door wat detail-pages. Je ziet dat er geen calls vanuit de browser naar PokéAPI gaan. Alles is server-side. Dat betekent: als PokéAPI morgen omvalt, blijft jouw site werken tot de revalidate triggert.
Optioneel — en als de tijd het toelaat — voeg een derde route toe met `cache: "no-store"`. Bijvoorbeeld een "random pokemon" pagina. Zo zien studenten alle drie de modes naast elkaar.
Commit alles in de terminal:
```bash
git add . && git commit -m "Initial Pokédex met Composer"
```
---
## BLOK 6 — LIVE DEMO 2: Vercel deploy + env vars (20 min)
`[SLIDE 11]`
Tijd om dit live te zetten.
Open github.com in een nieuwe tab. Klik "New repository". Naam `pokedex`. Public. Geen README, geen gitignore, geen license — die hebben we al lokaal.
Klik Create. GitHub laat een URL zien. Kopieer die.
Terug in de terminal:
```bash
git branch -M main
git remote add origin https://github.com/JOUWNAAM/pokedex.git
git push -u origin main
```
Refresh github.com. Daar staat je code.
Open vercel.com. Log in met GitHub. Klik "Add New" en kies "Project". Vercel toont een lijst met je repositories — kies `pokedex`. Klik "Deploy".
Wacht zestig tot negentig seconden. Je ziet de build logs voorbij komen. Aan het einde: live URL. Klik. Daar staat je Pokédex op het internet. Live.
Nu het belangrijkste stuk: environment variables.
Open `app/page.tsx` lokaal in Cursor. Voeg ergens bovenaan in de return een regel toe:
```tsx
<p className="mb-4 text-sm text-zinc-500">
Omgeving: {process.env.NEXT_PUBLIC_APP_ENV ?? "onbekend"}
</p>
```
Push naar main:
```bash
git add . && git commit -m "Toon huidige omgeving"
git push
```
Vercel deployt automatisch opnieuw. Maar de regel toont "onbekend" — omdat we de variabele nog niet hebben ingesteld.
Ga in Vercel naar je project → Settings → Environment Variables. Klik "Add new".
Variable name: `NEXT_PUBLIC_APP_ENV`. Value: `production`. **Belangrijk:** vink ALLEEN Production aan. Klik Save.
Tweede keer toevoegen. Zelfde naam, value `preview`, en vink ALLEEN Preview aan. Save.
Ga naar de Deployments tab en redeploy de laatste deploy. Of push een kleine wijziging om automatisch te triggeren. Wacht. Open productie-URL — staat nu "production".
Nu een feature-branch. Terug in Cursor terminal:
```bash
git checkout -b chore/banner
# kleine wijziging — bv een spatie ergens
git add . && git commit -m "Banner aanpassing"
git push -u origin chore/banner
```
Open de Vercel deployments tab. Daar verschijnt een nieuwe deployment voor `chore/banner`. Klik de preview URL. Daar zie je "preview" staan, niet "production". Twee URL's, twee waarden, één variabele.
Voor `.env.local` zou je `NEXT_PUBLIC_APP_ENV=development` zetten — dat doen ze in het huiswerk.
Belangrijke regel: voor secrets — echte API keys, database passwords — vink je in Vercel **alleen** Production aan. In Preview gebruik je een aparte test-key. Dit voorkomt dat experimenten je productie-data raken.
---
## BLOK 7 — Pauze (15 min)
`[SLIDE 12]`
Vijftien minuten pauze. Ik mute mijn microfoon en zet de camera uit. Tot zo.
---
## BLOK 8 — LIVE DEMO 3: Background Agent + preview (25 min)
`[SLIDE 13]`
Welkom terug. Nu het stuk wat veel mensen voor het eerst zien: de Cursor Background Agent.
Open Cursor. Druk Cmd+Shift+P voor het command palette. Type "Background Agent". Eerste keer: Cursor vraagt of je je repository wilt verbinden aan de cloud. Accepteer in de browser, autoriseer GitHub.
Nu kun je een Background Agent starten. Type deze prompt — letterlijk over:
> Maak een nieuwe branch genaamd `feature/search`. Voeg bovenaan de homepage een zoekbalk toe. Wanneer iemand typt, filter dan de Pokémon-lijst op naam. Gebruik client-side state met useState. Push de branch naar GitHub en open een Pull Request met titel "Add search bar" en een korte beschrijving in de body.
Klik start. De agent gaat aan de slag.
Wat je nu kunt doen — en dit is het hele punt: iets anders doen. Een tweede Background Agent starten met een andere taak.
Nieuwe Background Agent, prompt:
> Maak branch `feature/type-filter`. Op de homepage, naast de zoekbalk: chips om te filteren op type. Type-data moet je per Pokémon fetchen via de detail-endpoint. Push de branch en open een PR.
Beide agents lopen nu parallel. Klik op de eerste in het Background panel — je ziet wat hij doet. Bij vragen kan hij je stoppen voor input.
Wacht drie tot vijf minuten. Pull-Requests verschijnen op GitHub.
Open de eerste PR — "Add search bar". Onderaan zie je een comment van Vercel met de preview URL. Klik. Test de zoekbalk live in de browser. Werkt? Goed.
Open de code-diff in de GitHub UI. Lees mee. De agent heeft toegevoegd: een useState voor de query, een input bovenaan, een filter op de lijst. Logisch.
Als de code goed is, klik "Merge pull request". Push gaat naar `main`. Vercel deployt naar productie. Binnen een minuut staat de zoekbalk live.
Hetzelfde voor de tweede PR. Review, test op preview-URL, merge.
Wat hier net gebeurde: in vijf minuten zijn er twee features autonoom gebouwd door agents, getest op aparte preview-URLs, en gemerged naar productie. Zonder dat ik één regel code zelf heb geschreven.
---
## BLOK 9 — LIVE DEMO 4: GitHub Actions CI (15 min)
`[SLIDE 14]`
Laatste demo. We voegen CI toe. Lint en build moeten slagen voor elke PR.
Open Composer in Cursor, Cmd+I, prompt:
> Voeg een GitHub Actions workflow toe op `.github/workflows/ci.yml`. Bij pull request én bij push naar main: checkout, setup pnpm met `pnpm/action-setup@v4`, setup node 22 met `actions/setup-node@v4` en `cache: pnpm`, install, run lint, run build.
Composer maakt het bestand. Accepteer.
Push de wijziging:
```bash
git checkout -b chore/add-ci
git add . && git commit -m "Add GitHub Actions CI"
git push -u origin chore/add-ci
```
Open de PR op GitHub. Onder de PR zie je de "Checks" tab — de workflow draait. Wacht ongeveer twee minuten. Groene vinkjes — goed.
Nu de demo van een falende CI. Introduceer een typo in de code. Open `app/page.tsx` lokaal, verander een opening tag in iets fouts — bijvoorbeeld `<dvi>` in plaats van `<div>`. Commit en push naar dezelfde branch.
CI draait opnieuw, faalt. Rode X. Open de logs in GitHub — zie de TypeScript error.
Fix met Composer:
> Repareer de TypeScript build error in `app/page.tsx`.
Composer fixt. Commit, push. CI draait. Groen.
Laatste stap: branch protection. Ga naar GitHub repo → Settings → Branches → Add rule. Branch name pattern: `main`. Vink aan: "Require status checks to pass before merging". Selecteer de CI-workflow. Save.
Vanaf nu mag niemand — ook jij niet — naar main mergen zonder groene CI.
Merge de PR. Klaar.
---
## BLOK 10 — Composer vs Background (10 min)
`[SLIDE 15]`
Vandaag hebben we beide gebruikt. Wanneer kies je welke?
Composer is je pair-programmer. Je zit erbij, hij maakt aanpassingen, je accepteert per file. Goed voor: snelle wijzigingen tijdens je werk, complex refactor waar je wilt meekijken, onbekend gebied waar je niet wilt dat een agent vastloopt.
Background Agent is je delegate. Je geeft een welomschreven taak, gaat iets anders doen, krijgt later een PR met preview URL. Goed voor: features met heldere specs, parallel werken aan meerdere features, tijdens een meeting iets voorbereiden.
Mentaal model: Composer is "samen werken". Background Agent is "uitbesteden".
Twee waarschuwingen voor Background Agent. Een: hij kan vastlopen op rare config — debuggen is moeilijker omdat je er niet bij was. Twee: hij verbruikt credits, dus Cursor Pro of Business is wel nodig.
Praktische tip: te vage prompts geven matige PR's. Wees specifiek. Noem de bestanden, beschrijf de UX, geef voorbeelden van wat je wilt. Hoe meer context, hoe beter de output.
---
## BLOK 11 — Lesopdracht + Huiswerk (10 min)
`[SLIDE 16]`
Tijd voor jullie eigen werk. Twee delen.
**Lesopdracht — doe je nu, dertig minuten:**
Een. Eigen Pokédex-repo opzetten met dezelfde stappen als de demo: `pnpm create next-app`, Composer prompt voor de lijst, Composer prompt voor de detail-page.
Twee. Push naar GitHub.
Drie. Verbind met Vercel, deploy. Open de productie-URL en stuur de URL in de chat — ik wil zien dat het werkt.
Vier. Voeg de `NEXT_PUBLIC_APP_ENV` environment variable toe met een aparte waarde voor production en preview. Verifieer dat het werkt op beide URLs.
**Huiswerk — voor volgende les:**
A. Eén feature toevoegen met de Background Agent. Mijn voorstel: zoekbalk, type-filter, favorieten-systeem, of vergelijk-twee-pokemon. Open de PR, krijg de preview URL.
B. GitHub Actions CI toevoegen met lint en build.
C. Branch protection op `main` instellen.
D. Schrijf een `DEPLOY.md` in de root van je repo met: productie-URL, één preview-URL, en een reflectie van ongeveer driehonderd woorden over Composer versus Background Agent. Wanneer werkte welke beter? Geef één voorbeeld waar Background verbluffend goed of juist slecht was.
Bonus voor wie wil: vervang de Pokédex door een andere externe API. Open-Meteo voor weer, GitHub API voor jouw eigen repositories, of een free API naar keuze.
Inleveren via Teams, voor volgende les.
---
## BLOK 12 — Afsluiting (5 min)
`[SLIDE 17]`
Vandaag hebben we veel gedaan.
We hebben externe APIs leren fetchen vanuit Server Components. We hebben drie rendering modes gezien: static voor de lijst, ISR voor detail-pages, en kort genoemd hoe je dynamic of client-side zou doen. Dat is een knop die je per fetch omdraait.
We hebben Cursor Composer gebruikt voor synchrone wijzigingen, en Background Agent voor twee parallel-features. Verschillende tools voor verschillende momenten.
We hebben naar Vercel gedeployed. Productie op main, preview per branch. En het belangrijkste: environment variables per omgeving. Een variabele kan drie totaal verschillende waarden hebben, één per scope.
En tenslotte GitHub Actions CI. Zekerheid dat je code lint en bouwt voor de merge.
Volgende les is les 14: Agents. Nog een stap verder dan tool calling. LLM in een loop met tools, autonoom twintig tot vijftig stappen. We bouwen een research-agent from scratch met `ToolLoopAgent`, `stopWhen` en `prepareStep`. En we bespreken wanneer agents wel of niet de juiste keuze zijn.
Dat was 'm. Bedankt voor jullie aandacht. Vragen?
---
## VEELVOORKOMENDE FOUTEN — voor mezelf
| Probleem | Oplossing |
|----------|-----------|
| Vercel deploy faalt op build | Check Build Logs — meestal type-error of missende env var |
| Background Agent doet niks | Repo niet gekoppeld aan Cursor cloud — Cmd+Shift+P → Repository Settings |
| Preview URL toont oude versie | Hard refresh (Cmd+Shift+R), of Vercel cache invalidate |
| Env var komt niet door | Redeploy na toevoegen var — bestaande build pakt 'm niet automatisch |
| `process.env.NEXT_PUBLIC_*` undefined | Moet beginnen met `NEXT_PUBLIC_` om in browser te werken |
| GitHub Actions faalt op pnpm | `actions/setup-node` MET `cache: pnpm` — anders geen lockfile-aware install |
| Branch protection blokkeert PR | Eerst CI groen zien voor je merge |
| Composer 'kan repo niet vinden' | Cursor verbinden met die specifieke repo via Cmd+Shift+P |
---
## TIMING-CHECK
| Blok | Cumulatief | Duur |
|------|-----------|------|
| 1. Welkom + Terugblik | 010 | 10 min |
| 2. Theorie Externe APIs | 1025 | 15 min |
| 3. Theorie Vercel + GH | 2540 | 15 min |
| 4. Wat we bouwen | 4045 | 5 min |
| 5. Demo 1 — Composer | 4575 | 30 min |
| 6. Demo 2 — Vercel + env | 7595 | 20 min |
| 7. Pauze | 95110 | 15 min |
| 8. Demo 3 — Background Agent | 110135 | 25 min |
| 9. Demo 4 — GitHub Actions | 135150 | 15 min |
| 10. Composer vs Background | 150160 | 10 min |
| 11. Lesopdracht + Huiswerk | 160170 | 10 min |
| 12. Afsluiting | 170175 | 5 min |
| Buffer voor vragen | 175180 | 5 min |
Totaal: 180 minuten.

View File

@@ -0,0 +1,313 @@
# Les 13 — Huiswerk
## Background Agent + CI + branch protection + DEPLOY.md
**Vak:** AI-Assisted Development
**Opleiding:** NOVI Hogeschool Utrecht
**Deadline:** Voor Les 14 — MCP servers
**Inleveren:** GitHub repo URL + Vercel productie URL + DEPLOY.md
---
## Doel
Bouwt voort op de **lesopdracht** (Pokédex op Vercel productie). In dit huiswerk:
- Voeg een feature toe via **Cursor Background Agent** → preview URL
- Voeg **GitHub Actions CI** toe (lint + build)
- Activeer **branch protection** op `main`
- Schrijf **`DEPLOY.md`** met reflectie
> Niet klaar met de lesopdracht? Eerst die afmaken — het huiswerk heeft die als startpunt.
---
## Onderdeel A — Feature met Background Agent (verplicht)
Voeg een feature toe via **Cursor Background Agent**, niet via Composer.
### Stappen
1. **Open Background Agent** in Cursor:
- `Cmd+Shift+P` → "Open Background Agent"
- Eerste keer: verbind je GitHub repo aan Cursor cloud (volg flow)
2. **Schrijf een specifieke prompt.** Vage prompts → matige output. Voorbeeld:
> Maak een nieuwe branch `feature/search`. Voeg een controlled input toe bovenaan `app/page.tsx`. Filter de Pokémon-lijst case-insensitive op naam terwijl gebruiker typt. Houd Tailwind-styling consistent met bestaande cards. Push de branch en open een PR met titel "Add search bar". Voeg in de PR body een korte beschrijving van wat je gedaan hebt.
3. **Wacht** (~3-5 min). Volg in Cursor panel wat agent doet.
4. **Open PR op GitHub.** Vercel zet een comment met preview URL. Klik die — feature werkt live.
5. **Review:**
- Code-diff op GitHub
- Live test op preview URL
- Eventueel: comment achterlaten of nieuwe prompt aan agent
6. **Merge PR** (of laat open tot je CI hebt — zie B).
### Feature-ideeën (kies één — of bedenk eigen)
- **Search bar** — filter Pokémon op naam
- **Type filter** — chips per type (fire, water, ...) om te filteren
- **Sort** — op nummer of naam (asc/desc)
- **Favorites** — localStorage-based, ster-icoon per card
- **Pagination** — 20 per pagina i.p.v. 151 in één keer
- **Eigen externe API** — voeg bv. weather widget toe (Open-Meteo, geen key)
### Eisen
- [ ] Feature gebouwd via Background Agent (niet Composer)
- [ ] Branch `feature/...` op GitHub bestaat
- [ ] PR open of gemerged
- [ ] Vercel preview URL werkt
- [ ] In `DEPLOY.md`: prompt + agent-resultaat reflectie
---
## Onderdeel B — GitHub Actions CI (verplicht)
Voeg een CI workflow toe die lint + build draait op elke PR.
### Stappen
1. **Via Composer (of handmatig)** maak `.github/workflows/ci.yml`:
```yaml
name: CI
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm lint
- run: pnpm build
```
2. **Push naar een nieuwe branch** `chore/add-ci`:
```bash
git checkout -b chore/add-ci
git add .github/workflows/ci.yml
git commit -m "Add CI workflow"
git push -u origin chore/add-ci
```
3. **Open PR** op GitHub → tab "Actions" → workflow draait → groene check
4. **Merge** naar main
### Test je CI
- Maak een typo in code (bv. `<dvi>` in plaats van `<div>`)
- Push naar nieuwe branch + open PR
- CI faalt → rode X
- Fix met Composer of Background Agent → push → groene check
### Eisen
- [ ] `.github/workflows/ci.yml` in repo
- [ ] CI draait succesvol op merge naar main
- [ ] Minstens 1 voorbeeld van rode CI (screenshot of beschrijving in DEPLOY.md)
---
## Onderdeel C — Branch protection (verplicht)
Bescherm `main` zodat niemand direct kan pushen.
### Stappen
1. GitHub repo → **Settings → Branches → Add rule**
2. Branch name pattern: `main`
3. Check: **Require a pull request before merging**
4. Check: **Require status checks to pass before merging** → select `check` (van CI workflow)
5. Check: **Do not allow bypassing the above settings** (optioneel maar aanbevolen)
6. **Save changes**
### Test
```bash
git checkout main
echo "test" >> README.md
git add . && git commit -m "direct push"
git push
```
Verwacht: `! [remote rejected] main -> main (protected branch hook declined)`.
Goed — protection werkt.
### Eisen
- [ ] Branch protection actief op `main`
- [ ] PR's vereisen groene CI voor merge
- [ ] Direct pushes naar main worden geweigerd
---
## Onderdeel D — `DEPLOY.md` (verplicht)
Schrijf in repo-root.
### Sectie 1 — URLs
```markdown
- Productie: https://pokedex-XXX.vercel.app
- Preview (feature/search): https://pokedex-git-feature-search-XXX.vercel.app
- GitHub repo: https://github.com/USER/pokedex
```
### Sectie 2 — Background Agent prompt
Plak je exacte prompt + wat de agent uiteindelijk leverde:
```markdown
**Prompt:**
"Maak een nieuwe branch ..."
**Resultaat:**
- Branch: feature/search
- PR #4 — Add search bar
- Files changed: app/page.tsx, app/components/SearchBar.tsx
- Build time: 2m 14s
```
### Sectie 3 — Composer vs Background reflectie
Maximaal 200 woorden over:
- Wanneer werkte Composer beter voor jou?
- Wanneer Background Agent?
- Eén voorbeeld waar de agent verbluffend goed was
- Eén voorbeeld waar het matig was (en wat je deed)
### Sectie 4 — CI workflow
- Link naar `.github/workflows/ci.yml`
- 1 screenshot van een groene CI run
- 1 screenshot of beschrijving van een rode CI run + hoe je het fixte
### Sectie 5 — Eén observatie
Iets wat je opviel deze week:
- Preview deployments — wat was makkelijker dan verwacht?
- Branch protection — hoe veranderde je workflow?
- Vercel logs / Cursor Background panel?
- Iets anders?
### Vorm
- Max 600 woorden totaal
- Concrete links, screenshots, prompts
- Mag wat informeel
---
## Bonus (optioneel)
### Bonus 1 — Eigen externe API
Integreer een 2e externe API in je Pokédex (of als losse pagina):
- **Open-Meteo** (weer, geen key) — bv. "Pikachu's geboorteplaats Pallet Town weer"
- **GitHub API** — embed repo-stats van je eigen pokedex-repo
- **Quote API** — random quote bovenaan elke detail-pagina
### Bonus 2 — Environment scoping
- Voeg een `NEXT_PUBLIC_APP_NAME` env-var toe
- Production scope: "Pokédex"
- Preview scope: "Pokédex (Preview)"
- Development scope (.env.local): "Pokédex (Dev)"
- Toon de naam in header → zie verschil per environment
### Bonus 3 — Twee parallelle Background Agents
Start 2 features tegelijk:
- `feature/sort` — sorteer op naam of nummer
- `feature/types` — type-filter chips
Documenteer in `DEPLOY.md` of parallel werken meerwaarde gaf, of dat het lastig was.
### Bonus 4 — Custom domain
Voeg een custom domain toe via Vercel (gratis subdomeinen via vercel.app blijven werken). Niet verplicht — leuk om eens gedaan te hebben.
---
## Inleveren
1. **GitHub repo URL** in Brightspace
2. **Vercel productie URL** in Brightspace
3. **`DEPLOY.md`** in repo-root
4. **Updated `.github/workflows/ci.yml`** in repo
5. **Minstens 1 merged PR** van een Background Agent
---
## Beoordeling
| Criterium | Punten |
|-----------|--------|
| A — Background Agent feature werkt + preview URL | 3 |
| B — GitHub Actions CI werkt + bewijs van rode + groene run | 2 |
| C — Branch protection actief op main | 1 |
| D — DEPLOY.md compleet met 5 secties | 3 |
| Productie URL werkt | 1 |
| **Totaal** | **10** |
Voldoende = 6+. Bonus telt mee bij twijfelgevallen.
---
## Tijd-indicatie
| Onderdeel | Tijd |
|-----------|------|
| A — Background Agent feature | 40 min |
| B — GitHub Actions CI | 25 min |
| C — Branch protection | 5 min |
| D — DEPLOY.md schrijven | 30 min |
| **Totaal** | **~1,5-2 uur** |
---
## Veelvoorkomende valkuilen
| Probleem | Oplossing |
|----------|-----------|
| Background Agent niet beschikbaar | Cursor Pro nodig — gebruik trial of vraag credits |
| Agent maakt branch niet | Prompt explicieter: "Push branch en open PR" |
| Vercel preview URL leeg / 404 | Preview-deploy nog aan het builden — wacht 1-2 min |
| CI faalt op `pnpm install` | `frozen-lockfile` flag aan — commit je `pnpm-lock.yaml` |
| Branch protection blokkeert merge | CI moet eerst groen zijn — wacht of fix lint-errors |
| Background Agent kost te veel credits | Maak prompts korter, beperk scope per agent |
---
## Tips
- **Specifieke prompts** voor Background Agent — vague = matig
- **Lees PR description** die agent schrijft — staat vaak goede uitleg in
- **Vercel logs zijn je vriend** — bouwfout? Logs lezen meestal de oplossing
- **Houd DEPLOY.md bij tijdens werken** — screenshots maak je dan in flow, niet aan eind
- **CI failure ≠ slechte agent** — CI vangt typo's, agent loopt soms net naast
Volgende les: MCP — Model Context Protocol. Een vector-DB met PostgreSQL pgvector. We gaan een tool bouwen die semantic search doet — handig om in een agent te gebruiken. Tot dan!

View File

@@ -0,0 +1,194 @@
%PDF-1.4
%<25><><EFBFBD><EFBFBD> ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 4 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
5 0 obj
<<
/Contents 15 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
6 0 obj
<<
/Contents 16 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
7 0 obj
<<
/Contents 17 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
8 0 obj
<<
/Contents 18 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
9 0 obj
<<
/Contents 19 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
10 0 obj
<<
/Contents 20 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
11 0 obj
<<
/Contents 21 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
12 0 obj
<<
/PageMode /UseNone /Pages 14 0 R /Type /Catalog
>>
endobj
13 0 obj
<<
/Author (NOVI Hogeschool Utrecht) /CreationDate (D:20260607134605+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260607134605+00'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Les 13 Huiswerk) /Trapped /False
>>
endobj
14 0 obj
<<
/Count 7 /Kids [ 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R ] /Type /Pages
>>
endobj
15 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2335
>>
stream
Gatm<8TWXr'#*s=oOTMp+p88i0S``km&nK)4H:aeYK@Z=KI#7cWK<@8RD\phY5ps4[FioP*2b%o"M4JGo^W"k./ksmR4\Ns:>ke8#)pH1#0\GY9YS;tM>+n44QMVH!"kBOQii2!rWi,O.j50i)"X,GU_/`O&&)RBb#e_cF]$($W"mN_GRR+WdRa4",RZpVm]@9Nj;_1s,RD;4GJ1C[C@bmI>3jPZmQV&r)9d\Q;M@mp$_dVJ9,RX$p%pasY3)`U(KFJN+)S..7131e7rWW85&6V"BiPB+eZC=$eP_\"F-p4MjFu/BORJ*Xpjtuio"V+-R"3NK]X^u69dfZZRHJ,_C`qqcFQu=naK;;4?mLU3JXc2&a1W&9/Nl<*3]Btcn0'-KLAK01rLjq<Ih!Xpg]M&<clc_BD*JdK&[<1C1reNmQ;cp8;K)'9n>nB(6@3FR=-e.Hbp`)1cF7/,'SFb:Gdu(Q8]_-Fk85_d7#aTQ4<8St:o&p.&3GpYPr+RcFroC@:73'Kf\M1tmRCkCNV:No20LQ&+9>u8otYsl!u>CPCiW5LFfFm,,O,PW[U<K_RkB=)`Y<'JhuY)ZOjYZG6eLiOG8&$O.:A#91IXVm-oWJ-BA*D$q_h#ak&n'a1H:k6<l%FRGE*co3[rM]XQlfcBq&0MNkPI3n`B(daYGrH0rQSM,mQ5<35Hnji,^j9LI]VTNH<HCC-Z`>k;MWT9'DQ`^@)#EFhpVtW3h>^&5t,MhijNg>=K.*MD;?B4&2'c[UgO6:YY._#l,k[mo>McH)ZfKfdPJhg55)L$qro><E0JTc@R[o0$c`?[]ALhAQ`?bRg1WeqC#&#AUuJ7<I#S6W>]rgk>U5d?;E9b<u#2\Gg=kWcO0Z]r,Q^Hmm)46Z:TM9SH.qPR/Y-4"_/&.FhO^2pDl[T-Ne1@>[e#oaiSu5(9u,;I7eJS#m]omZ'2Bp5E*38LNOBH*DUY4mfi^[I[M[%\'QJN6K)gX/YkhLdQBm@%P6b9lPueQLOkQASV)j)Img:7"*XKLn._SdIP4+;?98'\ENR_o3f-a=D3n'N.Fhu6?W0%[qkV6W'Gg3$$Ri\q'Genu/0q*tV!'TcY5sr<&pGV;2@2B/1WG3Q1Rq_JE:4Z/Ph3`*Z+4]PpCe8m,aBiQi+RQ<DAgg[e/L/h>HR?t\AcnC6FDD=6(s`^Q.:^7BVUKcCI=jI&TguePc::$l??7+Wp!CLl8aFaQ(!>'--T?:Y0d>gV;O7-i8(t^.MpBWD<Ve2<B87c\4"kTF8F*;V?S!j.nK1(Xa[b2\6]396XEpYf<bF'+"^KN4#&8XA498>-XVNP6<\VP5)EB_J63khN<2I@$:"/MA5JYHh+ota49eprp#TS?905eUoT0SRK$Bh8F(=tIabc^DD@#i8T_2&<EY1+J)6auLdD4#q_INA,oe39X7%G)fW.NlPg)F`B3uA(\Q=oUU,\Fn,)6;QU1GM0rbIQcL6jSXN:(s)&_%m[7\U!pr+Q)ZB,f^/b&os4s_JREpBEM>VboV/A)IYF+'fi/2eam9%8VEcFO<6u/4k+=0f,2Pe^9u'tohZHI<LInhd!Ba*5qd;aBias4o*Ki-mH9jX34277Vs5?:R'5_*]hNbS[ap:08UIqj?@H,>ZhEL7.tdO1N;j"+%Z5OTYM&+XqBW6&VmrU*ooCN=#?<!9Kc;S*DZGE3RNo%Pea]@Z.;]+*7TQY[-UZNOT*2,Q!NCGdcu6A@lR()^<1;-RFea%C=K_'>j`hDd&n(\F^r"+qpRe>AMK&VhjACZ(hc@]9Ejp7nUpQ-[0LYq4+9j2Yo`%AZ?<h79?sC9pBo0dc=E2f)W=N(-qQ=RUS#\Wqj<"C#1fZ%g!Ao:2^g;14dOJ2U/>(2V<)Oj(ft_q7g63Y;:-eM4@)&^fY4QmDNN*j18YQM:DK&gf](3KqLP;2bp;mP"(Uj2gl^7OQ4&r<%`^`XRdiK`X65Ir("#>b`!09_>];PLshr"Fs/$WdN7S)ES;8'1UlL2Urok?6&Ode@`M_]^d_*/nK0?Wpr&0,c;$CAV)EYc.s'\,E]:)?J^GYNNn[+D-gSa8)XhH%8&8(7K#8F$DsAg?MJ<nCVP"VD&g+daEY^F%'^WT[oreotjro'IU[Feb*\>mpi2J3Z5VReo9#cQ#=A7+'Rc;5an=k\AqBZcKMb<Mf,=aJW@JP.;cRoCIcK+\3k8&N0J?03UJmi3>g)X)_J+f+"%JUf$0bCfK*KqOF91*,8Ut&^556oMrL8r#$G09Tm&u_q$tgg:rNX1a(#uPHB[?e[0!QT$l&_cC-!KLDPLXlXmEl.lt8IV>/_HQ9j\@QmVL4m:l[YrrGKPSRq~>endstream
endobj
16 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1035
>>
stream
Gb!#[gN)"%&:N^lp5u.>Z$_i6Q8D0sRC"+ha\6>l&S<aI=E7sRmGD!Z5Y8IrkQ<8Y^k8cFF3^r^(^c:Oe*8a2!%rS:G"mP!cl3#YM%@9]\E^JN[YPm6OWOSnR,c.<OOu'jT[T3[+=(C^`Y=dR&Jk3#1J/LB)7Xiq'[[1C_W\?qJOH1F0cp[r6--p&oc=EFf;A75&;K_uoaO)c_M_"<TX-?<_ZmGkd"^%"Wok%N##PB6%,,(VT5NUWV-#!QV5E`q1aZ]7IFjJ:r3gZQ'V[9@KM.iq<94uiErs#scoWh#Xi:%JJG>]L4cb)J"],_IAtiD#T%%bl4;F$3],'VHMRn`\\dd*/EsW3oh\O+9?H_G\?UgrpUjR>k.TC.JBO-=Yh`?Jb20(M%fF^Kh.W;rf"^ZU(U#2FIMq%_o<u7>^ffsb:EujL;du^jmUeZ'CQG7/WVeR>ueNV1L$uGB39D13l-LZh3*OBM&G+V7O:66MC!MtaXEbuE:?O9kh-#Rdgb'cX[P;*KUY#>0De?5i4&lXE$\n^e6\kG[l3CqH%S`gnYrp_7iG5'EK$$s@-Cj9jL+;p*MJDA:Tb$.jMB"7p'*pu==4RUBtLR):]Z^l2@g0'_EdVADXOZdkMU<?dH'#!FKV1l`p456`,q"1%QR]"dUn,eC6_+mJU&W'E_C@""%UV=rp+SC%')q"0-i,6Gu]q*br>EGB!$.+3SOP=Cp9X*2"4#Asl,la[]Pt4aRriu]n#j:$umD9:5%ei!+c=\u4?sucACkZ&K@>\Y!gVXDX/#*J]B?`V`gXn,Y.eg""clt,>IX'\Sr7M$.bBf(l]iLW)_<LRLNS-[V"#X9k@4jNEn77\s23UBe\$`W)3,L+k^dj[OW^+8<k20cG+3=7EF(oK;<)Z"+KTlT8P;"r%!=Z1dS8WT3gCEEL)0Wbu7Wqj:[n5!$:5ZshD!W36kQuB`;ogiA"YSa'lQ)3#ans5&r5!Y$VcsIo=5CLcmj&j$Ygi%-1@Pcu\`GjjMqPIR7/I4$1r[B=gbC(Jo`l3#&Dm~>endstream
endobj
17 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1666
>>
stream
GauHK=`<%a&:WfGQq&HQ<dSh1BbWl$-9r'=:kKO+;,GbG@a%4%+ZKAmSL#SFFsJdsN(AOkX`hGSB'1EAMJq'n7k)7F3Qtec?l\<q^a,$rIg<1>hR,k`i`c1DKV9UJ5Q]nN:oq;<Hpq#M!sXKp@^H8P(bm?k^emjBVP155q:K1B[%c8F+GT^7W,L`#;4OCr8UR@d]CJ%4`mG0Mg9F#6m0CIH#MU-HTaF@>o<f,#'IFncIXVZCns(f%Fe0uMHWA^3?,6^-)P4X54tFgdXR$R+g9"*?9kq$"&^8#*r3=^.^mDnc;G_c\s.(`m$$]Chjc(jp>28^g'k\\G!RN2Ve3/uS4/SGb+rk-XSih!gi5lfZPuNikgaDK+\!D8D'2#)=XDtT$,mi%X%p/]-T#g6Kn:hh#*)sB]i4d!^%U6/\:Ieg-&0\mgIH!!ZQh>jV%Z#:3,*2FK')^.>g^/1:$nRIP[FYPHHag_]]lq6@7'@eH(EWaO"ba.2EruhjL%.mtZf_G*&Cqooh<P&J2>@42hr7:(Hkp,JhST:MY27Y9GSX;!SsAg)/R1uX[b)i[7c)+ZZ)CJ"m@rTdW""g0iNjk*S&D.`)A6gM`<mrait,PXkpA'#!Vn%C_eb`q<P3&k1C_5P+KN"Uf8J<V?<gb_q+Wl-@e)BNc>G?ua@uep:aMQ\AJ@kZ'^r2Ai&1Uo21b8jBMqW@h3Nh6/b,X1:9\*jC'Z;N<"E'SIS]%tQ,`;?$QH$^%FKDFfn+:?g.`T?D?Za1+"h050n[5T%n:nO/o47#"7<YejEk\=gOiob+D@L,*fes)1QG&bj.4Ok9P%XIcS1Rnf;;;0)F=UC92hFQNU.F<\?"?mYc3iIT>Fpd[]IIO\N:bqmj%oL7@6MZ>FrZ<Bc(K4O,,Sep*?3cE]p:\;Bfftn#;?c6[Xhe-[oNk<l>MBX5lpf-q?f_-(C^_O[;+VeX20=0huh/7o<p\'`A1-U<]cbRY2EIrWn=uTJ8"]5d5ZR7`f-$%CGCX"&,C@h$JgQR@5u;jE:-;.n?f\2?Za,"'D%/PKm@IBkaVoo5lKP^o1%rnqY-`DQl<a(_d8"YJ%E<HXW<o@s=T^BlP[A_O"BfO.Sq[J"#Il4*i)#+As,qJ(+a2'1[Z10uE1FG*[)n)FXEi$#T*<6m;E5Fi\9:+c$o:71N)pj0Fr(3%M$`8ROTm/FrgW2GNc+l7u-$=GddP7_'bB8UP?slMS9+*gU0_EO$SMUI(AsYIYe>o637^@'"%ao43ka$"*&dqKohCp>[O^]is.f]<D).Nm2f-B^`pFGO1_=*s@WYD)%NZ@)Bu^L0-Cg\[C&=%1WW:fkoqo/C]c8@=bg=0C15p/uMPTdjK,6S,:2cB3""NF8)Xc<q[pTCpX^^/H62ipR6PMNdC-uV<[@na4Y=7-1FR&BsQO6mELjgEq.4RlWT[f(7;W$`sK'o`+nf_<o;6@YN(,[F&b.acCTg=(&G;_Q:!GI0AEqqI7*,FZ-a.8I].@9'pgg?5-9"-aI0\B.LPbYI_&oj+6A;S7m=h'Tha50p_8MCo@OK/..#3J?ie<'ktQUV([98P[b(1>JVg47=bL3g6!6^?/[tL#T5WF5e!;7<#+i-j-^o95$fAQcC[l,$_)BVtKsf#8[UIb<'XSt\q!uu2]8qH)FEo.r_##32XoWG~>endstream
endobj
18 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1812
>>
stream
Gb"/'gN)%,&:N_CD#KGDWJqH38:l:]a0D2h1JZsi8iiNS%ho1=N-UOW88A1=$^(SUMe(L<1MNbQ`h\p9m[hC,pOEror6X_YPQJM^_lL6\."hL)-mp#2B5lY8Xt@LQk&V>p,A\`85QXIU5r#f'E'<1a/>+,%T*V%k(R,1;#[fX<pSe[WK[EGN!qAG.(EA7(Xs^kDo,ZgFL8HtN4YO<.^[Y*5pB'>3hP$G3(R<qbg41q^*DUG9qC_D=kl()Oo-p3j@p)9b4AXn!.V(A5E64IX#H)nRAJph,%cKt(AWfhfRAh"LrrW.cg/^^.G?i!*gV9`f9U7pa317bI65EY1hXb\+B-Hc)U(8*jV'_6\T),l<N`I$oCEm^W(`D:OO*cslbU)qaOX1e7M/@=]OYP7Bie7.LlUosoR)h$<]V0pTXicG$*#H+mF??iN@iSSX,'7dLT3)f"de9AO'\UJuAD+($$2uVR2NnT/%e@]0=B/8`:VHJ*hWj1$ZI^=qacF`9"bb<S0``!]3l2W^RN6S&L!Vm)\$$hN?DTk/m&kf_Le@hDg`-+I'O_bO?s;V`7Ol\>\EqIO0hl-$]ZV*%AVDLJP82[[3(V;Sk1Xo)*`Y%C+&t:kb&%*4c)t'QCL!3MXP24bAIpQRG,:Fk!jS8VTs^H2e(#>K_UD\&<HWVPXRqY0c%\$Omcpj\['&D'e_agE]q8eW1)jUuRHCaJ)Ce+V"20anWAki`qH:LTZFW7;%\]79Ei!7I`)a+K%C!=-%'Y(Ye!mA,>g1&a_DG4a(>1mI@@XG#j_Y0cZN2KCBqPq`1:l1p$[FmnYu?<0SL"Rpn+BA?$3kO8UGS5><oNQeS9uhHq$&hS<cF?S<3m2]/M09M\=dDuW231ld(o8<an%BGfiAZr)^&a"L%m'O/6<B08\T_k?Q(:E-)_,&p)ou;.%2a"#4#<_H5f+t)UaZi_MBOIL=G:EN=WL7RGQjWDem%[O.0],3b+9s2fgfIdi=(qf^4dNR;3hlMPD:',cpEgWPs;#AqWX>=<.2V:2E@R2L<=([bm@+jd=,(8,:*$T$&Wa93FL]k#W8(<pnFKOR^+W:f[B[dhH/Fc==IdB",3*;\:Sc]CiS"hkFh'mq!sY"BFsYqh[q\-pt<&'$p)`)DR7P^";kD\m;W1RHccMG;Rq!F-5MFYbQ/kGMr.0\2ZZ3H?W'2iHc4IUT+X10csj0,k1.<<5.F-MO2!jG(s+ZYi&2fj^`#;b2mcF;,;In!-+Rj:!,W1f70+*,"/$1^MNMeZH`CL^DAXo:55>!$bA!HIoKk,'m+:_1laSIkW&9Y]C.^32r\C>.m#X2_93h'N<4>rI=J)W8F]uCKYCs^.HI9lPXE+)&`hDr3"!",*P0hQUX5Q=4>-_\^XObCj!'hXKYp`UkBTq0PBFg,I7&6T6?gqF!+a8U(`)dV>3*Y!_j(TIO(C'$]XP-GO+a%6s.>%\l"TqV+$L,U*M8t;rsa>$j%dEap[9FJ*hAh!=`rBO/+Qs4_E@fQ6H5rs9=_Xn[[)3cZ$]@E&U!tR#((L>o[-Q@`OXNGThcJ;Wu7t;pf)A&s0jKWH'&VM<CeeZs6fIW0$['8ZP&$e$TX:0iZV"QHV*\?-FjZ0b0!FA]'UNUl5fICN39C+cE\T5N+JkC=%K5.**Xhh:G3gQ8U(F0:F%0j0aL"\,#g87o4R/S,-dBkYKPdqi\7u`I^,6a;Jb[20T$NH2J9Y\/du?31'A>io('cbj(.I9Fg`;aV[hi4,0<J\^qB"<[ZI8[)Y\2bC+@]malmr@qnIkg-8@@R,sJKC$.V9j=o>X9.tI:O'q_3^~>endstream
endobj
19 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 972
>>
stream
Gb!Sj9lJ`N&A@Zck"*pDn_.c*l(q##7#_ngM*fZ!(Lmuq@!:e;J`P^4CkqJ0R^PhXJd"JQBAT,'o>VaiHLpDX*WqpUTQS)%]?odE(JI*W$I1_mmg\C`DCBm?SohWR$`r`\0/.*-JKM+hmC##G-Y+tH+=(St[O&G*/e!"2#,-P%i5!4F$)IVK\KAn#kIeh:f;6uKq*EBa56$F7QLNTZGXP%Ul\gAB5PQJ&AQ2GZL\mbUSM;rcJ%<1E:HCo`&%'X@CLJn<'[V!Y&5Fk8SQMB7!Yb?b1X,N&N2ejhc4E%WAZiH9_Xj2bS8^:`'jBN:I%"YjNb282]oLN77Gt?Q\tPkd+:[$*PJ/9?)0FW4DV(\a[Q%8kggsD"k9YAY5'!;jCWIM--.^Y$]80TI]%Q1R\,gRiUrSnL\`@P:TYC+&kF9<&77n^A3/uopC@kIOE0=+=5O?Wcno7uregf:XBQ9DRD0h"`!])[HI+n$6ED&/9LR>atZ/=\$Nf#BADA*c/UB6/U$"D=Gg<"cLKctCO_@GXqY]nQ;=XL6VM@p'PFWIp*9a]-LN/J,/>W#\tQS@$'eBdf8D^e`YjiIIF^904K!Yn-.=IKtWBs4qCMf$ENHPkqLHdHY?K;e-%(GLa]OG0@EZMeC8?LshlGdVkZ;DQpEQDq58BlnqQ5Bm84NTK(-Y^h1G?f98VShHX!4aVW@Hfn\\k6c=WU7%-6m"3rZ5i6R2D$'[ojVk\NogGoM?)n.Y[9c5lg>FO"3LSnQf+6tqK<=H=RcZ`<jJ/I^k1O]ACG;IUd_;)=o^Hj6"o\toGp_uY'<-3V';LI`*a7*q9>qO]80qr49"YO!HK?[W%S_mBj>s]qK!VG]HK28B&h2$8Er=NPkn6S!<T>V:[/`?5@6[%lYNH0?%XX4<HHr[bm^*kHYB$aq2/.='Bn&2i,#*)U%*l4(lRWTfg3iTGfVO_Y%hdoI;HU@2)&b^>F#s%m~>endstream
endobj
20 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2107
>>
stream
Gb"/'>BcPr&:WeDbY,Fu[S`+t+M(??R^Un5[Vj[\m[fV`H8&B9@R((#h<_?&&R`EK1n42QA&mB#D_BIc1BRdo5IC=t@XsY+*5/16)>%itEl+N"!G?i@c61>>0"6C6_R)E0`3](XA0W<"gEDTt4:8rIM/YIo6&?/E!/C)MQb&8M\9VLZ+Kf4cJccV7G(d1[h]`P?.%1h(EP0it[=1HCk7OWC%b7Yp#_jR;D,c.&EFMi#[@n\[_8f>YcJcGTaZLCj'u<n4a^AAh2.J6YT-#-;H'fCg#]Jct7OjC/;Z$n#>NiiKZ9%d9Z3e!6G8^EIk!1MC57R4pbU+'BcQ@jt%o*An1HKZ0jKMfdf+c0OO?pP.j+`8Z&JfT#"0o[Y;Oo`?,(mGE1qDi*A"k$`$$@9>]ji_8"@dGC:T0pc6T)>8*9Z#%?sBuVdjVH1&YO`6o>mU"PdV+I#0:VV"GYTE&`H0RA3IIXULSRWN/_jVkte_\7rS(_.<hs_7(kfGCZZ5`jGXD<Lhd<$P\YHn=VjCT2m48apeH\7&[m_G)6p/r)g;Vg6m^;%Xq+K8KIW*>])Hiq:2'@&@l_e/\22A"nNmC9f.9p09mA;Q$Ugqjm+=KBIF6GG_pYUnZOHY^C[/K(.X%)qKsW9]>9do(Y_$Rtd9"jW)_Ki29DmA0K.9jG_N@NB8W_g:R3`K+HfsV(:T7"t5KH0k=7@gnhG4B[J.T$GaNmM^Yf+k/`Br.N>f!kY,+4.Q8i-W9*iLnh7d,\iB7KThp9%.Cc;21rpNg12*e-/%REUc=_C1Sh:r;#^1H_Hg/*G$F?=N'Co?otVUV(,Sp/6Amd8W@%d<PT+M'#D\R@5Hi36&("keOfmIPK+.;6I2+?Gb^-\5-\MgGqLG2ug]OI'[T`Vg4tF(ol4!':=J;I$%PSUGZUHY:.pH,hoXraj:"MUE>_]K]fGuUUNHI+.Xbk(,m-%lq9=#8g.>*81se>'AKc.S)J7G?CO9UE^T)E<eK"."/AGAh"FpVhiS5!ob>58a]8k]=kA)(Ua@iU6ITN0.Z\"PRjtqZ]E!o<P:R]5M`gY7\V$0;-aAUn^bY(mQ;oaDBr6X6TEsZRRE(r<4R3%1n].'lk,`dsl#.1F[7tuC%Psr=?"r@k/E.2ELcs%XX*!RkVMC,I@QYYNbuH[4M:=XP$:8bTY@iehQ1Yigfafe(EL)"Y'5ZhEX>_;gfa)H]?,Ff]5U-0/ed;p`N#]/o6V8;<[V0])8UK4F&lX%u^f[V0nnHp>c;ZW"AO5Llgejn)7*08S_p>q5G+@]N[S&hq]P5aYgf9BnI+&qll-:;d=?/!G@?J#0flJ28m'==7I%sPcE7m]2f<Y0lU?O3hec[l]oBL$*@a>Z^j"2nm9Ipp_s(o^LV?q.bcJ[7>k3\8P]Fs6AIYii/55oD91G?mU9_)mEB;(i2)W%Aq%;Tf%LN:Z)4i^,=p[fHtRgr_+&*,jcc-,TC:!3;6R;o`4Adukjai*JSFDkr0jsRuIkEeL>\u,a>VjX#;Jnc::9oeV,cegR">+uO,S46CcA00BbCS<8L_^9XdhH!um30r<,.8?,Bg]LeWghKBfL4*(sI^B?[)G89"0MO.6j8U\HGsX1m8VFVU),J#\'1aWmq9C*@bSuk(gBc5o&cAo)3O+Aegr26?RbC;Be.U#V46,!ZN\JWlrW%;W$lojE'W1`'+jNEbY#EG";(?cQin0OsF6O.0eR9C9Jr["2V.[3&2B'Y@(i"q/JA);.^-5"DX?JS4H"I@"i6fOVAj*$f2+r?"(rMt!+>KH2XfpE2rq0N.Et:h+p6JV$0!FN?MPD4t$r\2!)fh3ZXYcDe?aBMPAMh;P2,S@]R:^n)R>+S`@Tj)Pg8!q=eS.a#2n[F98VjW/cnAtJah[YW70B#fb75@3OJlSiFXS036E-j%^5q>2W$n@X.HeilBbUbr.C7Xq]Jn*UM_W:G.`J%:jms5&VJ`O"P%/m(=S7ZkThH\R%mKR==R1m2%qY;=k.Z:u9Fi#Z@BtKAoQZ!s=J/>YP;EJLOSD[P+X=2/@8T3JN)VJSpO6Cb[0Z)72i,so,%XeS2,3J>F,1QGL%k25k21[*gZ7<^%E2'\f@m8l'9%DALiJ>9~>endstream
endobj
21 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 830
>>
stream
GarnTgMWKG&:O:SS<li^g2(rC;lQde2JF@<c)#<X8pS6,->N]V!BX2n^9cbMNh[N2H=5mZR@,i<OtY?Mdh4X0iq,Q'3LJ*c4Q0^0_>/3FG9i]o#sBBINMILT&@$##B_#"(57g-I`!VV8721(;+sis3No4k](oe,V@=C=:&7H^!aFf;5+%3r7GZP$._l5fX#=3_S^You3HEd`6m$QpS1DkJ;9i5+of+V'XToWoEUEA+^=1C(I[HXG@"b(S'V2lW<*%/]"l=A\Y]'Uo`2d0-glB8184f/T\O);$_I*_J>kK<9Ore+H'(RHZ.5#JX+E,UnE<sc`,TNc=!)lA2LLrbo(%td*]Rh:FL94e?I"4@ZnTkiC9N[j"A_9c_LJdq.'moj59q/3DmIAAql8_6H!9V3]mm9l0;FpY74EkFPn%BTL][rJ\Y9_n>66Z.:+VsO5VGf9&\<24Y:AT);rZ&Bp1>Jq$;,3Es.=YC:GY^,2R@]_p,Z>\YTp0P87(Tt*0m7W5"BA`Dgh4('*kh;`0?K\4&/3IJEr\&F79m,:cA$l'E_0^7pB04lhOeW-rLA4nMWooh:KXrmLh6g?(e)2cKQ[*(Agl?C".N>1h:@].Y1WlNBH(W/e%S.7BES]CnA+;Igm^/sTBgU7'Z_/8NQu?u--#u?j@AUSf%^GH$8Z=s`neTPt;.2am2qeb>O.Vije(h%90:5k.JtAq[5T.u!L#G1o%RDbmMAPIRAodi?PSn(ZJPIjRW`n%dT^!aO9c]Cm!pM0rjCAV#K^fFb5N-MX*?RQ.N3\C@[/s]03AW'85[Q5^L1/uXX3Ir^Fk^7?r<"3n44O~>endstream
endobj
xref
0 22
0000000000 65535 f
0000000061 00000 n
0000000112 00000 n
0000000219 00000 n
0000000331 00000 n
0000000436 00000 n
0000000641 00000 n
0000000846 00000 n
0000001051 00000 n
0000001256 00000 n
0000001461 00000 n
0000001667 00000 n
0000001873 00000 n
0000001943 00000 n
0000002236 00000 n
0000002334 00000 n
0000004761 00000 n
0000005888 00000 n
0000007646 00000 n
0000009550 00000 n
0000010613 00000 n
0000012812 00000 n
trailer
<<
/ID
[<9e0017c2fae42cc449cacedd8dc86476><9e0017c2fae42cc449cacedd8dc86476>]
% ReportLab generated PDF document -- digest (opensource)
/Info 13 0 R
/Root 12 0 R
/Size 22
>>
startxref
13733
%%EOF

View File

@@ -0,0 +1,186 @@
# Les 13 — Lesopdracht
## Pokédex opzetten + eerste deploy naar Vercel
**Vak:** AI-Assisted Development
**Opleiding:** NOVI Hogeschool Utrecht
**Duur:** 30 min in-class
**Status:** Tijdens de les afronden — daarna huiswerk uitbreiden
---
## Doel
Aan het einde van deze opdracht heb je een **werkende Pokédex** op `pokedex-{jouwnaam}.vercel.app`, gebouwd met Cursor Composer, gepushed naar GitHub, en automatisch gedeployed via Vercel.
---
## Vereisten
- Node 20+, pnpm, git
- **Cursor** geïnstalleerd (cursor.com) — Pro account is fijn maar niet verplicht voor lesopdracht
- **GitHub** account
- **Vercel** account (signup met GitHub kan)
---
## Stap 1 — Nieuwe Next.js app
```bash
pnpm create next-app@latest pokedex \
--typescript --tailwind --app --no-src-dir --import-alias "@/*"
cd pokedex
```
Open in Cursor:
```bash
cursor .
```
---
## Stap 2 — Eerste feature met Composer
Open Composer: `Cmd+I` (Mac) of `Ctrl+I` (Win/Linux).
**Prompt:**
> Bouw een Pokédex-startpagina op `app/page.tsx`. Haal de eerste 151 Pokémon van PokéAPI (`https://pokeapi.co/api/v2/pokemon?limit=151`) als Server Component. Toon ze in een grid van cards (Tailwind, ~4 per rij op desktop). Elke card heeft: naam (capitalized), pokeapi sprite (`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/{id}.png`), en pokemon nummer. Make sure cards have hover effect. Gebruik `next/image` voor de sprites.
Accepteer de diffs. Run lokaal:
```bash
pnpm dev
```
Open http://localhost:3000 — Pokémon-grid zichtbaar.
> **Tip:** als sprites niet laden, voeg PokéAPI's domain toe aan `next.config.ts` (Composer kan dit voor je doen — vraag het hem).
---
## Stap 3 — Detail-pagina (optioneel als tijd)
**Composer prompt:**
> Voeg detail-pagina toe op `app/pokemon/[name]/page.tsx`. Toon: grote sprite, naam, types als chips (met type-kleur), stats (HP, attack, defense, etc) als progress bars, en height + weight. Link de cards op de homepage naar `/pokemon/{name}`.
Test in browser.
---
## Stap 4 — GitHub repo
Maak een nieuwe repo op GitHub:
- Naam: `pokedex`
- Public
- Geen README/license (we hebben al bestanden)
Lokaal:
```bash
git init
git add .
git commit -m "Initial Pokédex with Cursor Composer"
git branch -M main
git remote add origin https://github.com/JOUW-USER/pokedex.git
git push -u origin main
```
Check op GitHub: code is daar.
---
## Stap 5 — Vercel verbinden
1. Ga naar https://vercel.com — login met GitHub
2. **Add New → Project**
3. Kies je `pokedex` repo
4. Vercel detecteert Next.js — laat alle defaults staan
5. Klik **Deploy**
Wacht ~60-90 seconden. Vercel toont:
```
🎉 Your project has been deployed
https://pokedex-{jouwnaam}.vercel.app
```
Klik de URL. Werkt? Klaar.
---
## Stap 6 — Test een tweede deploy
Wijzig de titel van de homepage (kun je via Composer):
> Verander de h1 van "Pokédex" naar "Mijn Pokédex".
Push:
```bash
git add .
git commit -m "Custom title"
git push
```
Check Vercel dashboard — nieuwe deploy verschijnt direct. ~30s later: live.
---
## Eisen
- [ ] Repo `pokedex` op GitHub, public
- [ ] Minimaal 1 feature toegevoegd met Composer
- [ ] App werkt lokaal (`pnpm dev`)
- [ ] Vercel productie URL werkt
- [ ] Tweede push triggert automatische deploy
- [ ] Geen errors in Vercel build logs
---
## Tijdsindeling (30 min)
| Stap | Tijd |
|------|------|
| 1 — Next.js setup | 3 min |
| 2 — Composer eerste feature | 10 min |
| 3 — Detail-pagina (optioneel) | 5 min |
| 4 — GitHub repo + push | 5 min |
| 5 — Vercel deploy | 5 min |
| 6 — Tweede deploy test | 2 min |
---
## Veelvoorkomende problemen
| Symptoom | Oplossing |
|----------|-----------|
| `next/image` klaagt over PokéAPI domein | Voeg `images.remotePatterns` toe in `next.config.ts` (vraag Composer) |
| `pnpm: command not found` | Installeer: `npm install -g pnpm` |
| Vercel build error: TS-error | Composer: "fix de TypeScript build error" |
| Cursor Composer niet zichtbaar | Cmd+I — als niets gebeurt, herstart Cursor |
| Push naar GitHub: permission denied | Setup SSH key of gebruik HTTPS met PAT |
| Vercel deploy mislukt | Check build logs in dashboard — meestal env-var of dep-issue |
---
## Klaar? Verder met huiswerk
Het huiswerk bouwt voort op deze setup:
- Feature branch met **Background Agent**
- GitHub Actions CI workflow
- Branch protection op `main`
- `DEPLOY.md` met reflectie over Composer vs Background
Zie `Les13-Huiswerk.md` of `Les13-Huiswerk.pdf`.
---
## Tips
- **Lees Vercel build logs** als iets faalt — meestal staat de oplossing er letterlijk in
- **Push klein en vaak** — kleinere commits = makkelijker rollback
- **Composer is voor pair programming** — accepteer per file, niet blind alles
- **Sla `pokedex` url op** — die heb je nog nodig voor huiswerk

View File

@@ -0,0 +1,156 @@
%PDF-1.4
%<25><><EFBFBD><EFBFBD> ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 4 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
5 0 obj
<<
/Contents 13 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 12 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
6 0 obj
<<
/Contents 14 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 12 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
7 0 obj
<<
/Contents 15 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 12 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
8 0 obj
<<
/Contents 16 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 12 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
9 0 obj
<<
/Contents 17 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 12 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
10 0 obj
<<
/PageMode /UseNone /Pages 12 0 R /Type /Catalog
>>
endobj
11 0 obj
<<
/Author (NOVI Hogeschool Utrecht) /CreationDate (D:20260607134605+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260607134605+00'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Les 13 Lesopdracht) /Trapped /False
>>
endobj
12 0 obj
<<
/Count 5 /Kids [ 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R ] /Type /Pages
>>
endobj
13 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2127
>>
stream
Gb"/'=``(P&:XAWkb<s7i>A9+Q=QRp\Z=?_9,:jr`UP@5'9@!H%tWQjs1U',Ou"o)fHj=(;RRlE?[#`1?amt(ifA66F9V0g5!4/5J8s4+";[.3p><`FrSU=6\E#,uJ-\k<b^m^Lr]trH'G*h[@idGQ<"Z6!#W3i1`u6q_;i3$&%3448(_A!mdNK1H,,s\THpK%@-orW9Pk!o65sK5:MhrF\6T+aj"N(gf0Zp)HOAH^l#kh=RgO&\Z%D4<S9TpNaAIa3u[7QE`=E(G$l'qLtVTG:X/E+/8C@\Z6RA.A"q^8Utho_DGLHHhE%b5k&\#'0Q!,lGVX:kj;4upcC"P<kC"+Ji69_(JP.2Yp%,]Gn!rTJgNH)i&e;?*IAjOf7pVp`;IXFSfPqmM/7E"0eiTi&bp>i-oc:b2g+``V#WEl)'W)GFK#_'$tp:le)ZYV`#O7ZQ)/r,B!Uk_HuoJ4iV(em+m8*8O&*Nhc,[r-u.rJWN4_WU3q&WbJ9Bm5uFZ3FT)2g?om)G1T)E)YLuHZgAMZ+q\=RH,Iu@,8Oe%N+$=JI5F'4M)(k9(j%;m7*NT-1!5ms>_#-U.93YIiir14M(>2/KT7Na7ht#]ka9;Y=X=XTI<iC8&R1]B:f`(6[P\4N!+TaP3`qh#0>Bl:,W0s)F_^It_Zb`B90!hAdfD)r,>$^A0*1HHO'`)kUn.A;n_?*rK$aH1[&j9lcS"EnefO^6#EXrsB:k@9a6/\YC)*LFfai(.Cj$lsC3hoo%UFefO:fgjlhY?HXp_sPKtu?cMR?abXI,o2dPR:nn3[D-Cm0%u>f249I2fH3n[_NW_e_P!=dl'NGD."=,rr'M!^$6t=ebL3d3#Quk02>QYg1sTg19u-9:HG2=uidH,#!A&).:@_rI@_S!S^3=O&stZeo?Cu'ne@1B`_uU54/oE:alYE-+J;00O$i4*97;a-#FgMZcZlp9>!5d3o]P*;FX!F-XJF$D^+J"KUJCl>*^!m%t^&3d.'-8\Tl:&`/`]0.F]Q<G`2&K*<u.;g?G$B0mp\G7<$imb:Qph9OgfeRisXXaIFODF6p]IEh`OQ)a1H4Rm,2IfIl&J17pNB+6`=U4HH1<%O>`LKq#5Yh8q0&H^YXfCg&M1qBJ0E^kk?p/\l\,Z$f0X*SS`#E<"Ed0i[86_8lB/-ft15)0kfbq:\\"8ZM&!*16oq?c'rF^6q,gf1fDcZZ9'2NN)<<C>BjS4*:u2.ViCp%itep:Y'Bn9%j]3/f`Bi4:_7W,m2JWlN]p.2,RI">o4P)Q^A:nI:W^2csO-cALOgP^oZ6S\j"@&R)*L8-[f51UMOh3I+f6nA(&0ci<(FlXu(0)(lSO(GM&:L].m'3Yp2[@a6,Qmpm1`Be,`Do`>08Pf:I:7LWRi#M<.+LYurlecI]:HdlS1(7BU$cfGWZUN>7Jum*jCKYl6FshC&)(9nJ)YNkut#Ub?B+iM3.bS,8'NC'%c28"j/e)l.lB.6/`E$_?Du\S&aC\hN[4o(BD[QZ](a\Q<5OF8Ff<<(443Ls%DGs*@RUr-X,d=Vp7!^+5QD*9%fQ+>P2K7oR+f=jaTJSn"J(KZ,M0As'H*/acnR^TUI_^AXh.)qs25jHfa9R[3848iuDJUK]G>LFeo/n2i2=7;T9)@U7ZG18G7:9o9tWH!r$2a:Bbrr"mc<.adj[9_;H"=89)%VY;jP4u9EU^:!#)9C8ajC.F(!B@7e<=f5XAiNYXkG@0D7U#.c_ZNR\@Q+"^7e5#XA^1nrNUh\VYa"`bk#-!Df1('p,%+=Qt<Hgj^K.AhgEZ2G/K^oT<(J=#8C[/a7QXsGiHQ/p8FdVX"nW,b;"8D3=b0aLd0lH6nLiheuG26t(K![EBmc4a\YW8l;P8me$4'%]P=R84F)t=%HpO_uX&7@\?^K7:(l`=i<,K]L4XDRJ>Z12"^o;5:GbH/]QL1P%X;Y)GRi6INVp[;?>MrU8Yj2Fbo\f>k*I+%X<UX)7lY&[)Lj)-IF78pZg;rT]KMYAt#U-HCR7,MA3<mdeOEE@7YF/WboNZ0'.]W6!i2%"X<6`q/6Y'm`^5'XE!J/>QLP8(p=4PZOTf(mT/T<mP)gT4M,>MRuiWGS+=.<!WD:N#%@BMQ0bEWa.m?-mRY~>endstream
endobj
14 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1391
>>
stream
Gb!#Z95iiK&AII3b[jX!88,_*8HF\mg[94e/@sNcD(iFR&oB.b7'37E5n<eH4/<ou*_gN@,KCL4br;En50f5j`JaQ\ZikWgK&oK,N5KB>9G.Z4cgY?XHj^3]4H^j!&Q=g2+Gk&ectn:@6:Fkj'*^1N8/t3_0dC\.`5'bQMHC"@Jb=#uJ4,8aYlk]G:rV^f]c`2RF=g(_<:\OfLbEr1,t&3i@@6uu_=.ah][VTa,bZcJ6<KMWq!H73(\#Oa9Ufq%/`=sX1$Q-!/']A!^GnApgUuoI#/?P=.;W:YW]Pg_%BPS\Y8P(8P41k/O7>H((75(ACn3G0K"fa(][FT.=C3ed,FIIXYY$0oWDFc\!F_3=$BmB-II^[l!mEFY+g*1h"u;)4KMQAP+Cj[F%ndQdMGQm>q+<b5P6TJS6SbGKbX5*?+]2,E(r7(FmN7lAQrXd`0I[&mU,\bC9FkP'i%#9#+d`<Y;lkWMiGB(6Bs8&Z]%kho@/3Ybi`/ZD4Qp=q>s]1L\,q%2&Y`c2h0XKq#KB_^Bm1m[pSVl+S$)19ZSgsX?K01jY.Z29CLY4^$'@!'\]=ZEFTImrkQ[:U/7dU)Be.j%hV7kf`S'R5@73GS+bp?-mCKkl>:s\Jeoa_c]>S-gkC*T.Ik("Ems[Z;YN&795X_l4eO9jqQtC7Tpt:t@X\elt<Ybdc0T]@"3L_AAI,(]=ZM&E[+\3k<n'S<S/"IK#H'Or9G6p-QC%<40fZ^_7cIQ&e"?mM]j,mbcbdr-G.3)p`$A?I'ZnC<PO^]@m?/uMC]/i!(+[OK+:#;9O^c*)c"P%?lRcY&+a3bQ7?#%T0D,SU$YNEF,c$*!Pqehgg:)IneD@CdFgodM@s*7kY-qYGlq*IdP6L4if3)<IOG:4Z5V(>uW>6;Rs<`%Eh$LI,Amh`ZmOWk)Fs+A`Dpq3P5rT"B6cA3t"TpjoQX!9St'a4!iA4%BgIp=hmP,PFm6pC<_0cu/?Pl#APYhaAJi-_C2=F@)F[;O%FL+dACMlgj.1I3H1HAgU)s,/B^D"e+uqpb)aLE'OF[FPf_=+O&)&hDNaaEQ5?.j&m0\hMcddgjeMY?eb0WtHjZ4[g%lS7"DgKq$;5^LM;SL60\k_OT"1__RqH!CWmh3o6cJMaK3A.`_Qs66/s]6\mtUiIUutqD_F+_V`C1"n,-M4aS!pI1l'H-?pSg\Hmu^%.t3kd@K9m5CQG$YYcq"p>j[OC!8!2M6"t%m`Y@G24elVkF2'VWMcMdI[/NV2s3q"'o$5iPe+mUjP;Aa`As7T^>u[ujedYsB;R_@]eu:aj8@7jAkVMN[K\RSFB?VhPuR'sK2%Bu?(f/oZbmlk]L-bp^^ZSOMi)W1)b!2ds2D_"H@qKp_uK;<i4f$qJq!d_"+6s~>endstream
endobj
15 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2009
>>
stream
Gb"/'=``=U&:WfGf^Y/r=qE;mIV?c]SYM$_b/nbj50sufdH21,ZBJnKhqeMIOGXRj)3<4$=;mB,!daq)Ib5qKMB>n!%hjKN-SU"fJ8ps(!%*PUa`BG3=<B-3*UZN=M"%A43=oQ]5:C,E$FW;ATDK$Zl4VF.+?`1VchDtM4P+-s'FI#dYS[r'TroboGg(=nkK80<*6us=,dub.hp-0=o<`j.EE_hXc/sr,E!0s/10TAWPhE2KP6A=$]R9GfSn'WWL@ZsSiOC@:33V7nEu4PBNqUXO.P!B1"*L<s/5K5KmD.ck-4dJcJUY]eiP'E#oflM(0EHF?nnA[QeZXR-PQi67\mFF.G01f]3pT\)#XMbOO'<FV'"BomOH+E*Z+*Ms>9f%XmLi-g2ir$_*@2#5*Bo_?h3cPE1;%$:@p5-0q)H[/_<;Z9).OZM"<fhYh8^;KVlYA=N?W,=KGNsgM"-7?ZCPgK(doEKF0!<Gr\W@oI\cL4"5"c'$^&960hSsr\,_80KkGc*[`QA"&CrE(\(?1[Vq1;MIT=/D`+D2BG=hhh.`g$-)@?_L$DMd02i'HHcLNI-;PiB:SQ4lmHtq$%P7j>Q1@T;J[*@<%g17L4F8KC3jB8Gha:#Y]p6LYCD[mRVk_.GnA/)qu$oZ]APVj:4jU#kB:+YY@:%fN/Ce9]QM7pu,KL;]6mSMnc@m[4<jV!n?[]EJO18"?sXm)][m]q"tn\@[l<W>GHdIk/gA(iH.]e'[bYE+8.^:*Y1B_pJ!+Xt412hG@k88Xc7bY"?a_JSf5/lOQn/chfk'P'3,Gs9N[F-U;#pocMFeDFaUTL`(3,1dK.VdB_h$I0RmP:TB$?rgg'Jhf!R9'nq`"N`8qT6'(t+>+h-7sTpk9iP+c8*@-\'IPuS\(flO5FT#r4(T.+NAa2\XAEI7Va42J_H]Mc,fa9MZG'K#HZuS@2G2;8D'07EBrRF%n)miJoJ5VI$8;k[75*"hZ7;tmgi?22q^:WGP]Z:eOKNCjpMj.q=e&qAO_62l+FSlsO4OqY6`5(#<7C/`L10s-#gOTQeDaBV`\]jugNua9mV-rNUNEM"[Gjs;p+GEadl7\Y@A[!2D[uLPNXj)X:hcoMC=\[`H--^`T!Wo8D!p$&OL0F16D!K[de<'OOR^+W1hiF$L9uqnp<%&eCVus-kD#4ma(rK8U'dVO\tNNuVV!N+V%CjE$)UX'_10A/5u.fn,f.YPJZYh`0FD96eY!4$UM:8L(Ia`,clMpdG;pJIqU6WZ_`<[9+:4e7%Wnm%(J)5aWp$Ce,/UYok#I\5HfHhESM*_E3^<W(jmi5qn_:r?j#.:dCt6*+cN%f0?^0d6U,2A"Vq7gN2Agc-IhM]LM;HngdG#,X^JdS"@^o7RBp;BC9p"SN_Z;#H>-rb6q9i+qj/*FVjs/oCXpVoU7r&h#lt/SN[-]A*96WLU*X>CsFA]5tcFRVAiiGlk/F&[Vksbo>kO0]d^.+'pV*"?,qa`Iqe0U'&Ki^]dVS_VaB]Qip,M%Z?Ic$/KMm2b9;dH(><YPmBX-r'oq7X]sf8,cC?bC6Y@\88lVc6OP?@Joq=kNj6#k2el@tc-k"H+*9$#Cu@oTkU--";4\Lqe"`j66D5)0hhF3;\*BjQi<#47:c(``eW]CcSruodd"k*BNa?PUam68!8li/*mVpGqm,)KW3Z!LY%nk&>.Z7)qLBYs"``Hbg`$.D$T_TF:qkhRNJEH1_SLmUV3c=f9f2Get:WiIY4E+%d!f#e%/,<eeh<6odcuD->U/'o]7W.>j=6J?V,TP"F7_]T&TS`DK0W..+ZLj^&.>hk60YBik5*sSmfn\C\0U1Oi2;PD*JE=9bqd;-42c2@K206/'uqO/eoS;L%m,c1<e>[(j@@Jkj4p4^NY;[WfR]d[P+Aj1f5V^CG7!.GfSnFi0C;"6V\>oL1)+WBbc8T>;WIj(c9o4pXJ"=Wc`0Go7g4uLJr`D<)LEo@rN$UYgmp/>>9sdFVLCC>mJ$MbTC6k#FUDDE<~>endstream
endobj
16 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 627
>>
stream
GasId9lJN8'YO<EN-Zt=qJ&*P5IctF7AU?i'RJaO&pL[DCmH@6Ol)2jSHT/$'uqtVj70de!"Vd4n#QTI,e1E#&/*(uKHc-2*q'a8K9=[3W#-4O*]XOI;?*<BYleH0g.<Z"\KP`&PSI`2Z]H?V(h(d94C'WA17B/_5ZYhWl9FhadSbh%'ua@ght&4E[\r:Z:[A/-Ym7M;6;3/<:a8D6`$RrOiV^U6a#gr;GJ;=Z\h<N`COB0)p`cQNp"e_Pg[<hAkBQ<"Hs%OAJ*:2^q1dt)heRkJUa#&f3Q8c-/OY7GeAo3n([+ZZDLY3N2HUJUf,79*M/3`W9Wql*0bR_N*CO,O;>a`W(/uUuLU]^jPVJrk9<;[Hnih%&/QL3:'fXm'S_%CgM8aH_P2LA2RU_Yn$kHt>fM.8>#"p4,Xcnp)E%JWsbuZU5mZfN@Ot":ZiB$)(Vcc5?<j9DGI2bn"FW8.+0'6[lT589*a@RO4U52JgV&`1;gNQ3[*;&uOcT*aodCXG@am,l5_e"P.S*.5\.s1ZkoOn2YRE$3,ptQYb22AVu3VkgoZ(Lla#AF8'E09s@Oa@,SD[N+ba.mQ.?Qr;#ibP)C3T4Vh>EijbS(B(_0:kp@4>=!HCW,V/~>endstream
endobj
17 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2100
>>
stream
Gb"/'=``=U&:WfGfZ?fJ:1>FNG_,9Ma#WP<9q4aoV]EP!,Xg$,$&4-"35<08?D;7-X?DQ8i^;]+I(P;Bm'd?0-N;Hi1X#i*:#Y1q$C&AK#1OtX9TS0"eG*bijtb8+J6:U6Hj7NuoFI1kOb;[,f4R8NDL-t_2V=N4c@$02kBS-ATG0),DGVIICO4b?_PuR9pWe/E\;[+pRlIS:hk&`ja0g4WEEhg,c/oPQE!1$;l;9.&R+1XGQtESoiT'/7*6MA_iEXM>^8Oq_Es638H#R`0G>M:2V*kH)`0El)Obm=+KBWP5T5.R3I%/fBq6mbiIk3(B(>tN1U,(r+:P(RJ")nHHT5\'t0UK2iH6t#*AU<R$Xc%(!\2`9L?/((W?k-m'qe)nE4CXa;+$=&G*G#oL\SV&Zl,JBGrsUP/Vc6%VO*hr144%JAZl.=CcohY3_<s?A.))P$QY$b=p/45p>\]D\9s8(h\Mn#+H"bE]3j/*nj8PD6FSsVhlk800aM;ZB,`GZ6!C^7O41o_U+8CQ:AsmRe&+JBnh^@bC^<MI$I>ltsr3o6DndW>%"4]9RhY1@dZ[U^:eV_sF=hc>rq&D(Hl7#p=AtU;"Xk'I0Uqs/,q9BlN]G20:8ttVkMN/UrK'$tR3*c^f/.)];]G-M'#-rdRM)nn3mm=9=:Qoam=:U]n*mM-iXF&XoB9FWF&>V9r-3CTjA>Q)X`>*hiO9l)oAQHlEP[!KrgrHaB.I1;`*Y\c_Mt0b3.PJ3CB0/OgA.XTuZY*K9ZhWRK[ZknfdnW=LMA1qG7Z1(h!ks%GDJ\-<(Gd/ZY,/4HfepGY\DqOp<HCuJ'$$_G&1j'i!gOl3$?TU:b+!6K5,+/MZLPU>3Jau*AHbX8XXh^68K)$1`ml*?T5Md>ak(O:IaMEmPC"C6Hi5&1^!l$dWffIWBp-N(ZRccq,eoVLf$L\@r@T!X$=KCdnn$@`a6^OuD&9)kA)$=h&78MUB^2_gDAM[<0kB0i!3jL5EUC/'NSq+M<o+.HeS8^S6ju,3eW-,tT\*(fRi$m?C%k!&NsA$jf1u;\@Ebne./]5s5rJZW5)=-7>Z=N+[2>1dcF.gcXWNHFbbd=,]nig4!$Ou$2S-2&9N[3?c#(l_O#@o!_VPP5,jAX_F<G3XZ;?D%Kh,:R^cI#uPd=)Sj4.Z+?<gF^U"cj1HVlTFLm/Z`Wta?'c'V6uS)s+Cf/`s2j=LEO2nD/$9:QGT-rFjH!]qQSlf(n^3j&=?rth'U3+j!R&;L.e<Hl;";[Ro$D\*/Vq[AT=GE9I4@Bu<)/DJ&X'\gm)K_8g\bJi$()<7cN6HpUr=JrYhYG<^&AL;G'^6S7O("mg:a\8`GLiL^DcUY%+[@Ve9-t#1[XT5P;T+TlOI'eM&5HkZ6S1qLq$708lIc"'GCDeCiDY-HC',=OcnH2pm:$jr]cV&($@f_p1&B"q:("1#,2U2OT7hB)oCs$2"7P3+=G+VSu<MU=s<8o0"8sFUt]2MuaCLJKQq(.AZW^cbOGC',tat*qk#]cnP-5Us7?'f`;DA&/G;F-bg?47`eKMGR*pq!okLZr?Q^=k3F"m?eH+.g]>D_SjR`$?mt68`FP7Yp4"U9%n9jQSeC0)g21cII1ON+gR(AC6=1SknL;kQ>Y8>3$g.E"SijhG-TnR;*MnLI.5.JjC`)j<`;hAFUUlM6:;q1-("\E<C4cLXYM0Uct&'3LE"#W\?.9W=!RQ;AJjRDIupnkA+!4798UVHo7Le$`mAo"do%'%u1M/1b&'"1":5,[um_fg7D)Fa5UL0*'@GX,ih5#]Q'pBeDFDBT"P8Hh'G*gLH$*bdM<EJc#/E<O[Atdn?ppZBKj[EcOf7.f5o#mVjPHlfKsu7k?'\TT2>h2JG;j)DA/F"bb9:1,dRGt-qJJtg8sC.$Z4EUZK[nF9Lq%VAu:*L5;3V5^Zg>#_Mt5KB"gecI5Xt.O4M0c0`%pmF@\"6gGp6IJoeK4<hS\Ol%?cBHl#MV^#,HRSX2V`c>-RdK((NJFf'_B=QF5+X"OuM?,]k6qIm>Gigk66d\RTLlWk&^]QnjEr:+]g2+MY_R&0YU+Ri/NoKbXJpC:Eh8.j+fL@YI#rWT?e_sd~>endstream
endobj
xref
0 18
0000000000 65535 f
0000000061 00000 n
0000000112 00000 n
0000000219 00000 n
0000000331 00000 n
0000000436 00000 n
0000000641 00000 n
0000000846 00000 n
0000001051 00000 n
0000001256 00000 n
0000001461 00000 n
0000001531 00000 n
0000001827 00000 n
0000001911 00000 n
0000004130 00000 n
0000005613 00000 n
0000007714 00000 n
0000008432 00000 n
trailer
<<
/ID
[<6d65c6032a302e19cbfdfbf2e67cdcbe><6d65c6032a302e19cbfdfbf2e67cdcbe>]
% ReportLab generated PDF document -- digest (opensource)
/Info 11 0 R
/Root 10 0 R
/Size 18
>>
startxref
10624
%%EOF

View File

@@ -0,0 +1,517 @@
# Les 13 — Lesstof
## Externe APIs + Cursor agents + Vercel deploy
**Vak:** AI-Assisted Development
**Opleiding:** NOVI Hogeschool Utrecht
**Vorige les:** Les 12 — Tool Calling
**Volgende les:** Les 14 — Agents
---
## Inhoud
1. [Externe APIs in Next.js](#1-externe-apis-in-nextjs)
2. [Server- vs client-side fetching](#2-server--vs-client-side-fetching)
3. [Environment variabelen](#3-environment-variabelen)
4. [Cursor agents — Composer vs Background](#4-cursor-agents--composer-vs-background)
5. [Vercel deployments](#5-vercel-deployments)
6. [Preview deployments per branch](#6-preview-deployments-per-branch)
7. [GitHub Actions CI](#7-github-actions-ci)
8. [Branch protection](#8-branch-protection)
9. [Werkstroom — feature van begin tot productie](#9-werkstroom--feature-van-begin-tot-productie)
10. [Productie-overwegingen](#10-productie-overwegingen)
---
## 1. Externe APIs in Next.js
Een externe API is een HTTP-endpoint van iemand anders waar je data of functionaliteit kunt ophalen. Voor de meeste apps die je gaat bouwen heb je er minstens één nodig.
### Wat valt eronder
- **Data-APIs** — PokéAPI (Pokemon), Open-Meteo (weer), GitHub API (repos), TMDB (films)
- **AI-providers** — OpenAI, Anthropic, Tavily (web search)
- **Payment** — Stripe, Mollie
- **Auth** — Clerk, Auth0, Supabase Auth
- **Email** — Resend, Postmark, SendGrid
### Drie smaken qua authenticatie
- **Geen key** — PokéAPI, Open-Meteo. Direct fetchen.
- **API key in URL of header** — Tavily, TMDB. Key beheren als env-variabele.
- **OAuth flow** — Google, GitHub. Complexer, vaak via libraries.
### Basis-pattern in Next.js
```typescript
// Server Component
async function getPokemon(name: string) {
const res = await fetch(`https://pokeapi.co/api/v2/pokemon/${name}`);
if (!res.ok) throw new Error("Niet gevonden");
return res.json();
}
export default async function Page({ params }) {
const data = await getPokemon(params.name);
return <div>{data.name}</div>;
}
```
Drie dingen om te onthouden:
- Server Components mogen `async` zijn — `await` direct in component
- Errors moet je zelf afvangen (`!res.ok`)
- `fetch` in een Server Component **wordt automatisch gecached** door Next.js
---
## 2. Vier manieren om een externe API te fetchen
Next.js geeft je per fetch-call de keuze hoe je data wilt renderen. Voor 95% van je werk komt het neer op drie server-modes plus client-side. De code verandert minimaal — meestal één optie in je `fetch()`.
### 2.1 Static (build-time)
```typescript
// app/page.tsx — Server Component
async function getPokemon() {
const res = await fetch("https://pokeapi.co/api/v2/pokemon?limit=151");
return res.json();
}
```
- Gefetched tijdens `next build`, daarna **HTML voor altijd**
- Geen server-werk per request → snelste optie
- Data ververst alleen bij een nieuwe build (= nieuwe deploy)
- **Goed voor:** marketing-pagina's, blogs, productlijsten, alles dat zelden verandert
- **Niet voor:** prijzen die per minuut updaten, user-specifieke data
### 2.2 ISR — Incremental Static Regeneration
```typescript
const res = await fetch(URL, { next: { revalidate: 3600 } });
```
- Eerste request: zelfde als static (HTML uit cache)
- Na 3600 seconden: volgende request krijgt nog steeds cache, maar achter de schermen wordt de pagina vernieuwd
- **Goed voor:** Pokédex detail-pages, een nieuws-feed die niet realtime hoeft, productdetails
- **De sweet spot tussen snel en redelijk vers**
### 2.3 Dynamic (request-time / SSR)
```typescript
const res = await fetch(URL, { cache: "no-store" });
```
- Bij elke request opnieuw gefetched op de server
- Altijd 100% vers, maar trager (geen cache)
- **Goed voor:** dashboards met live data, user-specifieke views, anything per-request
- **Let op cost:** elke request = serverless function call
### 2.4 Client-side (`useEffect`)
```typescript
"use client";
import { useEffect, useState } from "react";
export function LivePokemon({ name }) {
const [data, setData] = useState(null);
useEffect(() => {
fetch(`/api/pokemon/${name}`).then((r) => r.json()).then(setData);
}, [name]);
return data ? <div>{data.name}</div> : <p>Loading...</p>;
}
```
- Fetch gebeurt in de browser na hydration
- Goed voor user-interactie, real-time, infinite scroll
- **Cruciaal:** API-keys staan **in de browser** als je hier rechtstreeks naar een externe API roept. Alleen voor publieke APIs, of via een eigen `/api/...` proxy-route.
### Spiekbriefje
| Mode | `fetch()` optie | Wanneer |
|------|----------------|---------|
| Static | `fetch(URL)` (default) | Data verandert per deploy |
| ISR | `{ next: { revalidate: N } }` | Data verandert traag |
| Dynamic | `{ cache: "no-store" }` | Data verandert per request |
| Client | `useEffect` + fetch | User-interactie / live |
| Tag-based | `{ next: { tags: ["x"] } }` | Triggered revalidation via `revalidateTag()` |
---
## 3. Environment variabelen — server-only vs client-exposed
### Twee soorten variabelen
| Type | Voorbeeld | Waar beschikbaar | Veiligheid |
|------|-----------|------------------|------------|
| Server-only | `OPENAI_API_KEY`, `DATABASE_URL` | Alleen op de server | **Geheim** — nooit zichtbaar voor de browser |
| Client-exposed | `NEXT_PUBLIC_APP_ENV`, `NEXT_PUBLIC_APP_URL` | Server + client bundle | **Publiek** — iedereen kan ze zien in DevTools |
**Regel:** alles met `NEXT_PUBLIC_` prefix wordt ingebakken in de browser-bundle. Zonder prefix = server-only.
```typescript
// ✅ Veilig — server-side gebruik
process.env.OPENAI_API_KEY
// ❌ NOOIT
"use client";
const key = process.env.NEXT_PUBLIC_OPENAI_KEY; // staat in de HTML response
```
### 3.1 Vercel — drie omgevingen, drie scopes
Op Vercel heb je per project drie environments. Bij elke env-var kies je in welke scopes hij wordt geïnjecteerd:
| Scope | Wordt gebruikt bij | URL-patroon |
|-------|---------------------|-------------|
| **Production** | Push naar `main` | `je-app.vercel.app` |
| **Preview** | Push naar elke andere branch | `je-app-git-{branch}-{user}.vercel.app` |
| **Development** | Lokaal (`pnpm dev`) | `localhost:3000` |
### 3.2 Per-omgeving andere waarden
In **Vercel → Project → Settings → Environment Variables** zet je per variabele drie keer een waarde:
| Var | Production | Preview | Development |
|-----|------------|---------|-------------|
| `DATABASE_URL` | productie-DB | staging-DB | lokale DB |
| `OPENAI_API_KEY` | echte key | test-key (lage limit) | jouw key |
| `STRIPE_KEY` | `sk_live_...` | `sk_test_...` | `sk_test_...` |
| `NEXT_PUBLIC_APP_URL` | `je-app.com` | preview-URL | `http://localhost:3000` |
**Hoe in te stellen:** in de UI: Add new → vink alleen de juiste environment(s) aan → Save.
**Belangrijke veiligheidsregels:**
- Productie-secrets (echte API keys, prod-DB-password): **alleen** Production vinken
- Preview krijgt een test-variant, anders kunnen experimentele branches je productie raken
- Development: `vercel env pull` om Vercel's dev-vars naar je `.env.local` te syncen, of zelf invullen
### 3.3 Verandering pakt pas door bij nieuwe build
Bestaande deploys zien een nieuwe env-var niet automatisch. Na het toevoegen of wijzigen: **redeploy** (Deployments → ··· → Redeploy) of push een nieuwe commit.
---
## 4. Cursor agents — Composer vs Background
Cursor heeft twee soorten "agents". Niet hetzelfde, niet uitwisselbaar.
### Composer
- Lokaal in je editor (`Cmd+I` / `Ctrl+I`)
- **Synchroon** — jij wacht, agent werkt, jij ziet diffs
- Multi-file edits in één keer
- Terminal-commands met approval
- Voor: pair programming, snelle changes, exploreren
### Background Agent
- Runt in **Cursor's cloud sandbox**
- **Asynchroon** — jij geeft prompt, gaat iets anders doen
- Maakt zelf een branch + commit + opent PR
- Kan tests draaien, dependencies installeren
- Verbonden via Cursor cloud (eenmalige setup)
- Voor: welomschreven features, parallel werk, PR-prep
### Aanroep
```
Cursor Composer: Cmd+I (Mac) of Ctrl+I (Win/Linux)
Cursor Chat: Cmd+L (kan switchen naar Agent mode)
Background Agent: Cmd+Shift+P → "Open Background Agent"
Of via Slack-integratie
```
### Mentale model
- **Composer = pair programming.** Jij + AI samen aan dezelfde plek tegelijk.
- **Background Agent = delegeren.** Je geeft taak, gaat iets anders doen, komt terug als-ie klaar is.
### Wanneer welke
| Scenario | Tool |
|----------|------|
| Snel iets aanpassen tijdens coden | Composer |
| Refactor over 5 files — wil zien wat hij doet | Composer |
| Onbekend gebied / leren | Composer |
| Welomschreven feature, can-be-async | Background Agent |
| Parallel werken aan 3 features | 3x Background Agent |
| Tijdens vergadering PR voorbereiden | Background Agent |
### Hoe schrijf je een goede Background Agent prompt
Een Background Agent ziet je niet — je moet specifiek zijn.
**Slecht:**
> "Voeg search toe."
**Goed:**
> "Maak een nieuwe branch `feature/search`. Voeg een controlled input toe bovenaan `app/page.tsx`. Filter de Pokémon-lijst op naam terwijl gebruiker typt. Case-insensitive. Gebruik Tailwind. Houd het stijl-consistent met bestaande cards. Push de branch en open een PR met titel 'Add search bar' en korte beschrijving in PR body."
Specifiek: bestandsnamen, gedrag, edge cases, output-formaat (branch + PR + titel).
---
## 5. Vercel deployments
Vercel is hosting platform door Next.js team (zelfde bedrijf). Voor Next.js apps de simpelste deploy.
### Eerste deployment
1. Push je repo naar GitHub
2. `vercel.com` → Add New → Import Git Repository
3. Kies repo, klik **Deploy**
4. ~60-90s later: live URL
**Geen config nodig.** Vercel detecteert Next.js automatisch.
### Build settings (als je ze nodig hebt)
- Framework: Next.js (auto)
- Build command: `pnpm build` (auto)
- Output directory: `.next` (auto)
- Install command: `pnpm install`
- Root directory: alleen aanpassen als app in subfolder zit (monorepo)
### CLI alternatief
```bash
pnpm i -g vercel
vercel login
vercel # preview deploy van current dir
vercel --prod # production deploy
vercel env pull # download .env.local van Vercel
```
---
## 6. Preview deployments per branch
Hier komt de magie. Vercel maakt automatisch een preview deploy voor **elke branch en elke PR**.
### Hoe werkt het
```
git push origin main → Production deploy
(je-app.vercel.app)
git push origin feature/search → Preview deploy
(je-app-git-feature-search-user.vercel.app)
PR open → Vercel comment in PR:
"🚀 Preview: <URL>"
```
### Wat krijg je hiermee
- **Reviewers** klikken op de URL in de PR, testen de feature live
- **Background Agents** maken PR → Vercel zet preview neer → klikbaar
- **Stakeholders** zien een feature voor merge, niet alleen screenshots
- **Geen "werkt op mijn laptop"** — alles draait op Vercel infra
### Belangrijk om in te stellen
- **Environment scoping:** preview-deploys mogen geen productie-keys gebruiken — gebruik aparte env vars per scope
- **Database:** in productie wijs je naar prod-DB, in preview kun je naar test-DB wijzen — meer dan een Vercel-feature, eigen architectuur
### Comment-bot
Vercel installeert een GitHub App. Die plakt een comment op je PR met:
```
🚀 Preview Deployment: https://...
✅ Build successful — 1m 23s
View build log
```
Per push naar de branch wordt deze comment ge-update.
---
## 7. GitHub Actions CI
Een **CI** (Continuous Integration) check draait automatisch op je code. Minimaal: lint + build.
### Waarom
- Voor merge weet je: code lint clean, types kloppen, build slaagt
- Cursor Background Agent maakt PR → CI draait → groene check of feedback
- Voorkomt dat broken code in main komt
### Minimaal workflow
`.github/workflows/ci.yml`:
```yaml
name: CI
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm lint
- run: pnpm build
```
### Wat gebeurt er
1. **Trigger:** PR open of update, of push naar `main`
2. **Runner:** Ubuntu container op GitHub infra
3. **Stappen:**
- Checkout code
- Install pnpm + Node 20
- Install dependencies
- Run lint (faalt op fouten)
- Run build (faalt op TypeScript errors)
4. **Status:** groene check of rode X op de PR
### Tijdsbudget
Voor een kleine Next.js app: 1-2 minuten. Voor grote apps met tests: 3-10 minuten.
### Caching
Met `cache: pnpm` herbruikt GH Actions de node_modules-cache tussen runs — scheelt 60-80% tijd.
---
## 8. Branch protection
Standaard kan iedereen pushen naar main. Voor productie wil je dat niet.
### Branch protection rules (op GitHub)
1. Repo settings → Branches → Add rule
2. Branch name pattern: `main`
3. Check: **Require a pull request before merging**
4. Check: **Require status checks to pass before merging** → select CI workflow
5. Check: **Do not allow bypassing the above settings**
### Effect
- Niemand kan direct naar `main` pushen — moet via PR
- PR kan pas merge als CI groen is
- Force-push naar main geblokkeerd
- Background Agents werken automatisch via PRs (zo bedoeld)
### Voor solo-projecten
Beetje overkill, maar oefenen is goed. En het voorkomt 'fluitje van een cent' bugs naar main.
---
## 9. Werkstroom — feature van begin tot productie
De volledige flow zoals je 'm in dit vak gebruikt:
```
1. Idee voor feature
2. Background Agent prompt (specifiek!)
3. Cursor opent branch + maakt commits + opent PR
4. GitHub Actions CI draait — lint + build
5. Vercel zet preview deploy neer — URL in PR comment
6. Jij/team reviewt: code-diff + live preview
7. Eventueel: commentaar in PR, Background Agent retried
8. Merge PR → main
9. Vercel productie-deploy is automatisch
10. Klaar — live op je-app.vercel.app
```
Tijdsindicatie voor één feature: 10-30 minuten end-to-end (afhankelijk van complexiteit).
### Wat als CI rood is
- Klik op rode X in PR → naar GitHub Actions logs
- Lees error: lint-error, build-error, of test-failure
- Twee opties:
- **Composer:** open feature branch lokaal, fix met Composer, push
- **Background Agent:** nieuwe prompt: "fix de CI error in deze PR — lees de logs en repareer"
### Wat als preview niet werkt
- Open Vercel dashboard → project → Deployments → klik op preview build
- Build logs lezen
- Vaak: env-var mist (Vercel preview scope leeg)
- Of: dependency die op Vercel niet beschikbaar is
---
## 10. Productie-overwegingen
### Costs
| Service | Free tier | Wanneer betalen |
|---------|-----------|-----------------|
| **Vercel** | Hobby tier — gratis voor persoonlijk gebruik | Commercieel of >100GB bandwidth |
| **GitHub Actions** | 2000 min/maand gratis (public repos unlimited) | Bij grote teams of veel runs |
| **Cursor** | Hobby (gratis), Pro ($20/mnd), Business | Background Agents zitten in Pro/Business |
| **PokéAPI** | Volledig gratis, geen rate-limit issues | n.v.t. |
### Performance
Vercel doet veel automatisch:
- Edge CDN voor static assets
- ISR (Incremental Static Regeneration) voor `revalidate`
- Image optimization via `next/image`
- Automatische gzip/brotli
Voor sub-1s page loads: meestal niks doen.
### Observability
- **Vercel Analytics** — page views + Core Web Vitals (gratis tier beperkt)
- **Vercel Logs** — server-side console.log zichtbaar
- **Sentry / LogRocket** — externe tools voor error tracking
- **GitHub Actions logs** — bewaard 90 dagen
### Security
- API keys NOOIT in client (geen `NEXT_PUBLIC_`)
- `.env.local` in `.gitignore` (vooraf checken!)
- Rotate keys regelmatig — vooral na PR's van Background Agents
- Vercel environment scoping om dev/prod te scheiden
### Rollback
Als productie kapot is:
1. Vercel dashboard → Deployments
2. Vorige werkende deploy → "Promote to Production"
3. Klaar, ~10 seconden — geen rebuild nodig
---
## Bronnen
- **Next.js fetching:** https://nextjs.org/docs/app/building-your-application/data-fetching
- **Next.js caching:** https://nextjs.org/docs/app/building-your-application/caching
- **PokéAPI:** https://pokeapi.co/
- **Cursor docs:** https://docs.cursor.com/
- **Cursor Background Agents:** https://docs.cursor.com/background-agent
- **Vercel docs:** https://vercel.com/docs
- **Vercel preview deployments:** https://vercel.com/docs/deployments/preview-deployments
- **Vercel environment variables:** https://vercel.com/docs/environment-variables
- **GitHub Actions:** https://docs.github.com/en/actions
- **GitHub branch protection:** https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches

View File

@@ -0,0 +1,271 @@
%PDF-1.4
%<25><><EFBFBD><EFBFBD> ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 5 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/Contents 19 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 18 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
5 0 obj
<<
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
6 0 obj
<<
/Contents 20 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 18 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
7 0 obj
<<
/Contents 21 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 18 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
8 0 obj
<<
/Contents 22 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 18 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
9 0 obj
<<
/Contents 23 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 18 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
10 0 obj
<<
/Contents 24 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 18 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
11 0 obj
<<
/Contents 25 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 18 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
12 0 obj
<<
/Contents 26 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 18 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
13 0 obj
<<
/Contents 27 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 18 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
14 0 obj
<<
/Contents 28 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 18 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
15 0 obj
<<
/Contents 29 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 18 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
16 0 obj
<<
/PageMode /UseNone /Pages 18 0 R /Type /Catalog
>>
endobj
17 0 obj
<<
/Author (NOVI Hogeschool Utrecht) /CreationDate (D:20260607134605+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260607134605+00'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Les 13 Lesstof) /Trapped /False
>>
endobj
18 0 obj
<<
/Count 11 /Kids [ 4 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R 12 0 R 13 0 R 14 0 R
15 0 R ] /Type /Pages
>>
endobj
19 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1911
>>
stream
Gatm;gN)"=&:N/3oLl3SA3H?=Y5<se:?#GYiZof&#?RaT(dLQ(La6rF?b\b!)]'aB/3j4\c#i]Jkru#aJE!&dmq;`@0n_K()))Q<#r%k$0YDt;U%QkYguY-p!"hC0!='5CrISj#V;Sl%%=#\(V]P@k)f2,Yh[b+m\Ba]5#FU7e.uOGu>jD,C&>(tcn*[:o`8qtEgVe*f@!@#6T<H-?Lg*<'@?qulL,2p8Wj+$i6CHjo0O2EHLQC>qiOt]&4IR^a^t0codB%j[c93"LT+CKTc3lXe^Z8QFC:TF?nIgTN0RT^FD:l=7:)j[\g]!2VJI0,a]MOg?ltn_Adh(/dSPQbn[3:l<J\$FF05q&c7H]D00!8D\[c`me9Hh5<5WEoZSW2HLIDO[C$^&sJ(__Q`%b#4JCG$2?$_XcE=A[:XS2F@lCZBSI<R.$`['`A-B9>V;N!-nrq:1P?ME,@p/3IO]I%Lebc7r=P8K&[pj)iqa5a0(V2mIFb:nKdtMk(+'XN"Ca7:U6?%JJokD6;4:g&K'pNB+pcb\&L]RrF,d==\NNl$$8:Dr.3!<u25k(9a`L,D38<R4#KE%n!/B$T9fnAFA!O6>!3M_<l@TYMjMejXS/4hddMEA@jH&F"kKOU=3=3!mYmmkj2N@6DC2.RAmiL1^l0BAnnu9MHH/f/7_WT%B!<e3QMotaE9$U?n@Y$qOKiR=QnMg`.WiP3ME,PLURqG>1D?1pCJRI"r2(/Y[1,!?@m^NcSc#:6>Dna+=\Uj;A&X#c`Nj!UC&^To`"Kjc*hIR9k)lF-uQq"+?hCk)3<g"qD=GN*d41r+R9Lc_ESCMhas2"NM<Ur[Vm,iE-EX:[E;nJ4-/M$&'Wj`3Zi#)>,HKs4!8Nh>`-X>Xcb-3jjV$A]%@$qR"CZ93n3>6)(#[P6Pl-MYTOLFN[5sVa"d8ZnJE+T"S6p.%S?)'0p6%;ht`ke:HP]uVK#H(\<$R$__g``Cm!uhqsDbnGBiYo0YIu)YhbrtGn_.kni1auV`"7uH81T8CHrJl&rDJW'bD\];YO9s2GqTO7Q-7Q\W60gAWmM2*MV)-^p"hN2MW?.Dms>G6X'eA&E.s6RoF"mJ(h!!`aGV<#A;Cu:AIUY]FiBGq^Y4>(O5_%Qq0IETAn_$RQ.6q,UbaN8h!IkFfabP<!m/SElV]u<@Vj^%qDKc/(;M6WY)q^%@bWpR>mJ*A83kk]=#g>9o=PJY;VuY46GqOD]#@>P"Sb=4Q$b@=$c7qpjuKu1UpDBV+Q_.C;#SCBNE`Y[Tcp&7R/#S'?JFmF'.UO-ZR[^*b01aA7:e?>c)3MC:j<l?,r@*[?SHfbi3T;nN`m*F!Q%,FD,l2WIbX^rsXD4I_E8%^AJ"IL!#t0.$I)+nQ]k:&p=pP7\us[,)k[59Wdg&#?pR.mD#qTn;^\h%)Lc'['me;26,6]X1'B[0!!h/f9re9lr`!(@V=Ze<^X,G41W:[-8I,QQoLil9VWJ5F_1*V#5T78Eg3]cq;+TSMi7>1&Id$E8oqMTL'O!g-]*.7";R051ZZXZNJpnX1>^cOMp`OK>=`6.$9\,<cc]TrKQ5_<P(`39%j*;3;NbG#RF(c*[oEWM_pmggN0c)AS^,`"I#t)W-U$4o'[LU*OJ&Os?C[',$6odcZt]1m\)HUT9!VI)D64k$Lh,bB";L;*C;\T.5^hnZN%T);8\g0Hi7oPQlqqZd_IT8APMHIBm:tNb/O;7_9fY5gIV(?P9gc73Lb6kp_;ltpl1BPKO#sq1f/Ird$dWP._sN*>8Fe_c7&9m+?ongOGWh]?aWPrAfE(+kPi;?:F!P8^.<Gf^dr7b&"3cm,nQfbu]Z?=@PWVf%4mOeUl?P]240?ul:f"tFN:p-3A*^0^ENW-a*]1HB-e4gn;4i<tfM#7=\%^`1K(U3Nd"HW~>endstream
endobj
20 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2006
>>
stream
Gb!;d>BA7Q'RnB336UYU?s:O409TI]B?IXsB"8T]4QQUo;%g@HAS``2l1DaZO`,5\*>jO)9i='1o]_n7*1`!:=SSWPru_Mj5(p`[$C&AG!n8A'$t$ooYkPOqR)c_s!jRGjq[A:!Rm4uk]`_T$GWt^J#`BA4*H2--6MLU!Y2c$ma:q=>oI(:==^3pO"EcldXf]'h`u&^]&D-OOeb-=t_MI@gYEf[;XPO5;1MEmiMeY>lU+UXu(dn>j]?d\=V4O\LQt[SWV]o'UXFLmm*0+>G*p%[7(MXB!C7uiB`[WDY_sd_^:X!,cI)f[67Ifg+^K*?k)t9",H;_SNa_@,5?lbd3aCtb;Z!'tA$"DX)7pG=*]OrHD_KJ&^K=Js,;Veck;3,^+=cV^)0i?8;#j!,nNk!?(T\#4_%2G*GW-68&H+jY<[S<e_XT6U2J-%Y';30_?FSV]^!a4nC6-i_,&`K'WaEhj,7S!'C2sZ_mAR:Yd#;;&)Pg8S,;7f?=ACMes:hBeu>[+_41`>]5L!Y_>eS>EI/+J&)f("_i:`oZ0*ra9(Ts@)&dFkQ?_?A_ql[iC&qn0c*?L&&-]=["!"K(;N/.\gnA3u-QG;dE7<<?pG0n_P8*-sUbSfOhASE_dW?m-0h<@j=gi(%?=>Y1@"Kn?Q\,bY04k"/C&3g!9AW3Cfq3A/eF9L^KTN<]@->%]Ni&1`HI#@s:c0Fj?K3nNu(!bU`QHr1d"@=hF@*(IA@&`6sP-'7>13$n<tfh@DN3ooY9g6+D8d+33"dR?dmc]sC-.bVCQ)baLSFZ-\NT,Zs,$pe#82k0IA&SbIX+7(Sb<g]_gq]j8Z<[%!h\J,QR]@T)Oj"kM8cQ&_-%ulpA4"U'hf3q?[Xr7]r+2GKf-&6D<U%AAUL;h@33L&RA2oj7Rast1B^Q+CfYO<_>o0Ji07;._$Akn<Q8#Y`N;TpneL&n;+(L_El6ZH`:3lRT%^/OPXH.++S24ZrI=dZ2kGb_#^<4`kCKmN`[RO>?KUWlD[:cX9P"*FCPLh1aT!$Cb.Bbl`\KR(<a8@b)uITDOkc7kjPZ+S!trcKhZcANQD-k'Xk_Zju1EWl9EYk7ukl)@6jQg3^'UDAF@&<"=*-E7>`S\4YMc`YC`aG-ZN5(.ZmP5%^BN*r.>WM3B-X9nf[ld"$SA]Ug/Y[g9?LC9N(nm"Uu-SN73;1EeC#6lN;^=pfKWRfNh2$o5W^H,+7FfhqOXHMWj/4'Y?Gb-&r.UQgtCCG/ekGQ:Lk7W-*_c.DKXP7),1!_/)ZEb5geAB':qlf]s)5q@!Y[JcX.U;#*_g0eE[:<([&XHS0*,6ETO'%F]]-F&<'D1!5arMZgO-WW0)"nQ3gX(IL/uhLF"RQI]/\d[5Jd^!%fiiu1bIpSJ+dP/k93LTR95U!()k.0Rr6br'VOkXPIRu;;)8bsmErEfSdm^^\HIqqS::<'Cm:lmB"ng'h1;?D4[e,<R.PQ[X==9km,%L9[Em2>@FS$DfpRkuJj`@Vg#p@SsHXeRs_quT?A6;@7;1B)`2<7qiN<8T\<YT9IF>-6_/=D5<ci^oh(9AKA5mMnr)G0D3lp8'b69Vf?DtNh.h=H%6mGs1;IYdrFrt!V7kY!2=<+&dhW%0jL17aF$O5>Vk5l)67S"&Kd??gQdgNE!F<q)M,k#l0g_J[V[^DF]WM;nlX8q=P0F'VPb?I[3T50Ir&BCi-a<7e+p$LG)GEZPt"i[&5XW^cqk87a..*:ENILM[]B1FDO88(mYpma:^?$C;U9mcB#Y))i(DmM#D4*%q6$2&hc.GL$J*@%YR,lRJ[!,G)g-LhDlJ)uhid=*d+.PSMadcP>S+*<Z#?)N/o)8(W$t^VHS\mqhr1$`o?6&lqA&%^@oCeX=p)126!8C[(LrGd0D7rGEA#k%?+iS/A(WRTq99VP4C<a[2;r;Ff%C%<Y'B-"?NAJ^dKh-4\0lZ"@4A0hq2Ol6(&BZZ0E"'HL\5Op<cO-%X"oH44<TXD$'-Y[n[U^&J-0#X3V~>endstream
endobj
21 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2085
>>
stream
Gb!#\>Ar7S'RnZ;3%.hO.:SaA,`9am%)&A9ZDL((p7X*87A4QEZK+W`QQjM#h]ZQ.E\;Oa%`+;N&h3B5(S8et0qe<bbl;nln9bLof*iLOi>@qP4;]-UX.f.0+^FNPo7=E7\fh:k](uYhM3EKDL^8:I5gk2(^]R@f,%W0Rj1ZXD!>Gm\:E0Qa&2]Ii&BT;C`JsgK&Y,V0O8)%pfCuK@P?PY]Lm<.^B)RKm\:UiAZDaor..[N_L,8&,4oT[3Dt8`7j(Y*_n97m2GZqq>o)P@&p7IU];HE&s?E0hooD)'keIE#6@/*.5lJR"1-U+Q>Ndmis?nUB6XR^??q_"<_=TiUIk<8[K!>9dVBL!k`>oMX(Cim2IU1:"b#uA@\iBF5Zm9JrGJ5GOQc5`1O<?>S"i8#9c=VEl?%W@_F9LIL4&D)V"=DX>SCTIT^7#VI2pSl(".;L#'$Uh+]fV,D_.1WGBX3K4SNodN%F\GKnhtlOAYBB3!i"\Y__P;0_N"R`\GC^p?.t),lN-ER$YhI@RY8r"!n3b'2ip2D.+i`mfrWcFS<RHS5$#uDA4S?nY']W=rnMJ?7gS.#*C89eC('U+#BMXQSpqq?O;+8Z8'76cfhkqah-o^,+-lRj9CThk_RW-2u-bM?bk'00gkJ!K[*=kl[NU;r^&;!`>#2L0MXGIrZa-a(/.1DH3$]+7c3@f''B4;WZ%I=Ki[0%G3JWu_GZj!5,/00t\[=PctY4f?U[(!LKV$itW"+"0FL*Yi=$OC%V#c0PjhI$?2Hp3q\kA$;qK?m[W;dTb#BVJ$t?JJkJ)Ba-`l['H])CR)MB/,>]=^37u"qt"MX"d+-NB,^DbKX]E,b?A02eW22Ss:$kit@qMoM:]NFVp0cRo+S#?Ira='S*C9Wg]9$"(Pds:Ipudn1n*dC\2P3>/RDE]:5KWT*RB;(ASRuF]%Pb[8>$p9hqHUm=?c9Ke*'icc/gE<J[k]'Ute%QNSr(%JFJuO3QuNL8OB?X7_1H(.G*k7F"NmnZ;f9<U[]V<Yp.Mg[ru))X>)L$N[BA\g[b(o=R%_WQOr&X95n\H?e$KU?6G/g8okeQ7=`g,pG1Wh:]u#5h-GSJif79Y2V8(+WX^TrP7"#T=m=>F7gPKdc>4L5s>OTbb_;%GKQ0.b(hdSQ$"at?::[$^ncddUkE:l_->+kN5Q0KEVaWt[Bf43f8!GJrFLOc'VdTI4,6]%c1%n]R&LEHdj(ejZ'^Jn2Au=@2ZB5'R7^D3N-@7/rVVD3pNO#\c]t%X]d]jKDl%T`K&3CRbEOLM+N]cr0g^43f<6W=03-$&]7;M(Eh)N+Q;g_Ep?.1`ra(cTCr0\h>HWF3`k5u!e98YJbIGk#nNW[$W)G,.3fO_8Ea@7i0gaBJ=rPl6(`Lj3=rPl6ck_SKZ6#CQ0Fo6u(7H<<BMkcEkC'_6oeCh1#QmrQT4Bk+M[EZJmXI)8X16m6[i[*djk.Jk76Z>oKo4mWYXn+o5`IfD<`/LYn8%"J_?dW@kpT[a9:CrJ+=VlBmDN^ucS`Z*F.GdsWa=]0(F2k;=ieW!BtBQQXH>SXrIa/4klj14\0mo$L;/-oo]nR4=t?C5_Wq1m2-$+2oSQhE_NLO.@+Ug:`EKffX)kH6[B!T3M=XLU2q(S%K`0A1H#T_f`&M=XTI4+Xp?)ZNH$"pLQ4U>dQll6mm#L+A85lqah*Gj7a7q(Z8VTqOF!X\VI2NdiF;MqgMR0&u?.5,I(3@`a2q2ij#+=(Cm.#qL%KKLNJj#jD5h,$^fAC[hf*lG^W,!6('F,cn^8hLp%n*R6LH`YIU<<*T42SnSnmFb:B[Q?6`k?Ug=,920_poO&O^buupmA3"eGlMq1PR?+H30N+(hpqu>HgbR!QVZ:5[bXZ@f=]3)@[VPqDbFg;>i15`j9-`2=!;mRVn+UO-sm6B>]DZF-T5JUd-u:oq&,3S>$ee?#TB3Cg4"[qHLhkj5\$#2ZMnI7@]u"!r=JLU4pn0.Z#DK=U[<d.P_1;ZeU19'V0kSR3)'[I"CAs1%_NIXsTMO&>b\3p?"emnbm'Uh8s?S>Bu\?^IB+594'oU@ht;JP+:'Fn!Yler!Q#deee~>endstream
endobj
22 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 704
>>
stream
Gas1\fl!<6'Rf^W*6hCpFZd)e?LjD^N>a6<2qjTn+"6Y1/H[G`-rQ=[mq!B(1hcquI(B!AEu.IT\n1W]G/3DHMfT3(OK9Q^f17Oa2IX?<=%6=5K1A[)?@i!P]hkh6J_4r%Q'ZI,Eb@8:PMA1eTIA)hBi,&8:7)0n7TcS)SW&Oa:Z<U:PTjke@q5GOnUj$Wb4FBsl:>Oe&/]]6+fQ&2dTjlG`^k84\pFDL!q49SW3]3$k4mA(+knhU.V3UC0gD`)D;Bk\]Erd*05l$-BTA5Q6up^r*T1_8GYIZLj'RNZ1'0%M;m(H!&*\CEUsK?++/u,a+JT.7`bHumo[F8gV3'P1_8`l9Hs89o($6tm]>oqGC\qS%[[_%Zg;(feG/S?nKfufQ]hcWp0?^ISZ0n:Y2UB(B?$CX7.gMtL_.Y!jBp1@LEphkpR)$YYPqb"Ma_<PG*TEu5VU(3<=>sCMh%.<\4G*#K=E$4Ca?e]eAuQ2\bY;VRf<nUlZH_t([`Vt9&$e%HKN$)qAX.sp\!u>bH&Uuk/8u9&?!>U"@!Qi7<p=JX/<?DFPs&[mE>pA"*+olORo<LkE0o_Ala.f9OV%X*'/PbWI/\dZ2PD<JcqI`=%b]%$MYbqrU)+FoauN13od%_3Zma+7dlS@!F7%TZkLZNqq@qh!2s%6I.uW*TK*7$gIa<M>24_Le&DOL8FE#%p!m'UG&c~>endstream
endobj
23 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1519
>>
stream
GasIg?$Dc-&:N_C9_5'U8*g*TQM0seBp;)dl_q8OIN5!?K@!f=7Nd=7fD5*1?!tJY[OfP91,F8ipYM<L5k2bYs'u6$IL\ji)))QlCLE;rR%M0)mbN/A#<\57&>?%Y306[u5:BT>%XpZpLdqjf%>bE<8B$B_OTLPl@E:b`2-dof(m$GEToqYTo)f_Tk:]BA3DM/_*sBu4rs#)Nn0/K%kR_A)`F*AqR\X1f+UM_/kehcMUXLk4;ts=nFHrqc,(m)2;CCcN>]Ln&nT9.`<GAki9DG1S3\.WXSaYFPr!)<PbZqjV;KCbA]Vk9qF9P><?E.:N?&+uiX#,mRn&>)9f<bu$n-3Ps'bQ%P\/2>$\0l6(P`UG)@,l7!e@0<Ho6<eSQ)Zd_<F[<]$No6-nQVL$F(9I4'OrJS)iDupo"i9\XL<*Kg49A-[eH31n1Es4;AY/#=V?d=-o-0h+<+s[7PFAO>C2Z7k#63sd7;(p9AY2tL*"#[A!CrO\,_8'L%:Mb1==hm_Wk]]IdP)bD#B+Yn)G?VYR#OO,lT(HVe(u0-G_kuds.QBSKB%C&5brP]Da(c3[QDaij=N?6%jLJDn1JIR9o.E-Td"s['^`:S7P7;LNj%S^"2EPd]-pC[j#2E&l'g(c`^qfhk*Sl]n1+#YLENm':a\GrD8YBqDE+-`<AMt]5(,J93WqUK\$U=nRNKf28Y>aSlF3!G!iEo"GBAcp.&(tPB^u(@Ju9S0H8/oq1qcQBS/)l;V&o4E$H4"HSEYQQ0Q&[>qk!WA]Up`*,(,8naEdJD?n96+L9ds6emYl`#0'co+F5^CoMQ!"R!AOlZ-7kfcuGaYL>%.%2Z*(64a_XQ/X^=[O`&m<CH"snms^>4e>t#c_adY:]>W&L`[8eVN4#L[Z\.*,#g/Fe&fSi%$njGp(+Y>5siD5mJ#bWdZ@ugb3.$qYPW=pF42hE?W,t8%W:qaquZgJq]p)8`bZ/RDQcbYeLj<'V)`pls)EY8WS6?Bq?NC&P`aNWHqX,Bep9grakW56'@5Kco(/paoW"JDbNM9ZM9\B\l:mNXgGE9N-t:dRV3HBJYI/]a;:+QHbbaXn%@N2IaG3G[A@atep$CBl8Tc^>Q`ljb7G8M>2Gg,F5h,5]3Ne<i@FH+1X@:d1rY=V,f&a+j(S',&8n36G']h[ICtUVBf!^:Q)OV,1o5!pBb$oXgm*al>V6gdQq[\"B'os+'QTW!:9q8rUkKl,9'0Q/*5fF\9%(d?:2OO,#eIMlb29*\hC*5E?7DkjYo43iOk=:o1<<n%3rc=(Y4b,%t;Eo+sF3n\!PN"K\1UnFXC600%S6`il!6WPC!6L&]1]D8g]i\8<WeTOG=4kEn663nqD]\H-U".,Z@k^)XE,INn=<41C#a<K4EnGn7B4,99c2^Z+"]KQ[BTE5fiF+R\Sr\d```!3!VRSoNg(^q"S_\$b3cJc'hS@!uQIFSWN-8o(i84ST9t;\@dp&nrgR8q_CcIfOg3;SaAQk&SZ6%oZ(VBJ<M#t?i!QG3#BE~>endstream
endobj
24 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1952
>>
stream
Gb!;$=`<%Qpg(Yo6\0,h'/QEoMX%DHBt@d,dbAPEr5)"3X]U?Oa[t)k^Q5sS[OI)M[1/%iaR;=7JDunqs'Z9(F:>(n))*#L;d_4t@=/BrO0+HYZ+986*Y:h<-a8.)Nqn3s=W)ISnA#MN#jN)^]AO#cKEq_5Gg?&O%pHgAZ,M7gSGL)+,+-CSh`C3;DVL!$,p)6ck4s`q@E8f^Dl?#YlQ62G*NW\LKBft"1=,]&SAN[s=77g#O*mg^E1Z5!r*=%Pc:aPW[L[!eUU-\NMc*"59\lWm?`#;'+2R^j`fCS3!IWM,M21/o_nT.S0F.)EPkhZjqc_;)9EH<OH%Rkh!HG^pe>ImX%eW[8#6f!*-lCpQguXW$[R]Uk\!C.):FLA)@*KJ3/se8agVXc0h%NuJ)@i-Bb^euE!o+T:d(sm-]r**J>mQS.G+;YChm35-m/YND95)_EM?G:IOj2jFN</4927G')1>c:.VpC@i<6!+<BZstZ1.I*PR-al@\;p&HV)!7f(W'oADa"EqGm-DTp,7;.&M-2Z?%[X@dRC<)cHqd_paM251&hF&o5pB:9#F@(:M%&'l$Sab?SW37d*>%<=$lji[sXF46!UrUX^0W,g(H/BC0ErLWtZ7nY"P$;Zk7O#WtO<VT2dY+e-TYt^5C+g\0t<s-)Dr('^hh17fsY?8p$!P:4],d\WP<HXIY\f]GP%Qea$7OSK-uKTXEqd@(^6-\_#m8&3?'CK_bUR,16*IL6g]Tg'1Z3kt)no(5#5nO>;AHK51W?MG)IYjJYChqV5d[*1a"TnHGH"H)["fMrnjamqD9*06mVQGCMA]ESXIrn,CGG\^'fJNiCY#2;Cf_gaBN"_QD5oS=&uq`bM8n3\:iGm"kOK\]&\]Q=$_V1*00MUfMeM$t/(>X?q0]O"sh9""o1>R,/pg4g$lcVQnPu-HTd4k%Q<OW.5SI.$mSthoOjiDth?,2f\O4A*ak2b#ea$1Xe*J7V=)FA1BmiL3&bk20&c`dk]%QdOO"GjNYD8lT*t:L"\]s_:`Y<lIchk?/,=dO;luMdTTn"oY%^aN%*t/FjBoqJ0pkr1`V3@^5ARK;n^=<*O*j3+MF9[gTbm%'a\QXkn9cV9M1P(_@VCu.U><(mkE5j2P8dP/RM&Z.)?TYTWeL/dLp*ka'dn&>iHl5R^DY^mJe_mgl]jrA-Sk[:ihbb103'76jbr4<#3KrUm.U<(l*9gSKB>4]N=qZ2f-<)//u'Pg.<Y^W8U]&nI-pS<N_VmCS;gqU"j+$RUQr2YtrVPWarZg-B0RQ2Yp+IK'3@)GI5k7q=Q2U$2A^;.;(Ebq$uK?8/Z"Pe+oKYQVm+C)"c-X!EJ=K@V]Z/4]Z=.^0b5'FDH`p$>L(E9ANkV-e,!lE(.-\6EB#*';'9jX[':0!=<SW]%<m3qlJK#;:)EfW.&3E?XJW*CYAlc4YG4`MOk(,e(U>\\`nl2qd0S[9)`p6qeIlARtL#=meCSBC"m8`!W:H,_urG<3-bZ&[-8uXj[5Q/?H1[`e!^O/5iF]@GAMUHa'=oFr"d=!6<moG.W49%S>-g3fO3:L#GcX@^4aEorL!JU$HZoUnt\SBDB;Tsj4?!]%3E1!^^$>J*umbu4q/"SnTd!o!m\C\_2dNV<*8+EZ.d[JGI<Y%MU(3G=q@gd%#^*C?=?:$<EN_kMO3I?$r<^5B"U#X$40H/_QiSd?8oB,F*KqB+<9#s1P`OZ.YbPu&C,Xd3"GI05*?*o%V^9C)Fu#84FBkm%J;rfkR:l0+!*d]4iHuu`<uuc+K,Z;S0"+P$$s'=YQs6ME)L'<I<A8@@>F5"oGfcjIP<mh])((rgT`@rPE8>G0u0T#U>=(galmnE%<f3@^M['JA?ur)5I%p`L9h8h*`I.UJh@V6qT8u-M-ACV2DAs9/Rh$Je]_Rl*@Olc_3J-bm&Al;@$.@s6g<-;qc]mm&7GGE/[u]h~>endstream
endobj
25 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1559
>>
stream
Gau`S9lo&I&A@[:m,ZO'R0k9+O9F=%e&H8dC#do^Ep3`O+UJbfW$GrnrUjIc9<p.;DEH<NZ63Tlmk&]!D]oHO^1d,+('&9NE.1B0176@<El)8]!T$Pa5<Qe,(@r).+T*?j5a&q]6+QInKFs%RQ;A-A9LR*U$VH(C">F'sa#&NK_>K9_J]*Zm$P#SO0G;cso`9md6Y]B5-qYPan,*jE]nAeEGN`Z97PM3Bh51D+J.nQ0E<t"pc@5TakrnrX(TL)of&lI+?)i!L_s`Q'E:JBt;,t3-<<b[d4-ePpThPDB9NNorj1#Tg3G0U*pE&=D:_h=WE2[M5Ddfj265p7$!R@kcdiKoHSYL2?L^8Eh\Sr^iD299p%sR7?GJ*+*/o",7KF>uVB5*<&-1"UF<Y'usWJ;=ETfAis0&*lg')NA=(a$'L?G-g2a)"<+dZ0fB2"mu;5Dkt!rr>#jUU^6cVFGOXd9R6l9u1:Nr$F5$/(fLH76j5k'[CQqN%DA"?>KY'85j$3q%Sb]^sm;hH:A7`1P@;n$'!(nX1A?]bska!1/R::VG%/m`F6uS%d/1CCi#rjj6"GG[jU3*c"1)5''pXiAI*@Jj&u2XaCM^V%X'mg5;g$VP;Ooa$8SYW*5&ri*V,R&Ji73_btP7)a^CfS)Gg,gjJj3UB"*#D,E*SJc_q]b8kY$+@:66K]e.bnO@h<sWJR_E9fg/C!POc_H;WBJ\pL\AWBh-SWH@:SZBTWp$/MQnpZPK^6-int:j*\l(O.BES,$c"/XXk<bZ(Rd!j6J"cJ!;nOk&JLP`5"[8_$W3^HsppJd&)E%/WQAF:s-S"ZKL($"5cK:9d$FmP0LO7e;WIDTH<#;H-=h^#K;R)%?MH,%"K<W@&:49o5-8K<#m'\pG!n'41:kd&EqeZt\8h,B%[ZYjPdLi9VI"`tKQai75&0@,0DA/:8l8!(+(jWLdn57[oYd05qDRIh8kJor/M1db)'WG+6*Mr9XkoJs`brBikZ6BbFi?$tJ&!S3hQNcY/.+#o"GAj0gXr\_Q5^G74.D6hNjckC.e<,C5j/)s.4QJFlVC6`KB//S]gLW[GdS\BYPo7nG7"1Z]-_I#!u]P)I"trn`QNakkABf@4#N:V'<Jo`1V?Vd1Dg$)9$i7X#2HIBmSgQ',^iK\@pHC@&[OnT=D+ptN?&a5I2i%VSBeR_f#rkl_B``F3kUmj'#q+Q@'*GS50hK*T;Q9E+E7Pe]!5n%nobf7.$9;*m64SS\boB.;H*:bY]^[Q=Z!;AuWrY,e)%V*^28I#f,=_X\g_Rj^*>jq(I,lI'.^b!;H@[WVfZ1V=SBf=BsSF2!bE7I'0]#X/bj'Z?`"^XROgG70YgCV*W$23V:KWZ'W1V'apS%B%X^^/JDuJ4'R6ohM^]k:$J!(WJKG]KDE_]geIeIT$oiC7eoaFhZ<O,P^S6RmDa0WSD?sq)NGpPP,\!lcK+RBdjB'hO1BY$jCMpQ"<u.\DC7Z\$I[n(dkpO%i7q+\](Pkd]o!rf=W$*$El+gX?Z/0=h]'HCKoB4Ogj`!F`cFNb\VJ&8.iZ7$g`4P6i~>endstream
endobj
26 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1871
>>
stream
Gb"/'8TWWE'Ya0?]L`WR1(f+l$^g0h`b@>,4fRHkm_ousKI#7s_31&3o[)LP#O[a$=`RgB;TU/HO21Kj]Yt:-ITcUgV>tP3nFA^E`aLn1AeQ,\VsnS=rGE58EmJoZ#YB`rJb[*0TU8q$3%f3!mPBsS,LWp7QAeB!9Am>+o9]c:K%Y4'!V'F[8K3IrN[rpiqZUt"8(KS<>h<??ho[=+GWp+OktoUd&&E2gCR+0^KhWt:49q`4*.@\FiWMI1\LPLNed6`'$5G0C\E)Ftj-.T)=iP$L,'^/+<2/t`A^rT(?GZ9EiAJK]D,)&bD\`:OQ,!O\V6'<jJNXD'E>F'EiI"+aG!IZu"nef>6;S2@Z.%Ws5f6-$]=&*H`?!C^p9_ct*hh)L,8up9.[r-<j^X1sXU2,$L,Vd]AtXI5O6_/uH>LEd28&u1+[c@Cqnut=Q45K]%n(asTg]28&q&nsCcD*'N&2'IKiQ0Xq)5C*q;0%b*P="eD0V"E`[Kf/E<-JfKlgkm12#Ya_AY56?Usm/NUIO0)rF$do.b,an1R`QWWXN2-i1H3Vt-uUE.-FFJiS2)r2mst>04S3#F77Qek";qMb@19ZAPtKj@N1mFHJncFTJ,a*u2=9Ti+0Ki*o@eo"7@NTo@H*-$Ti%Tui2-Rmk?/OiTW/GY<\GCUMKKhPjuB]ljBWg%T.7n_=_'>/0KQf%YuSl)`Vm_%iN'\":&\&s$nT[TghMD0?aU*TYMPA\!=AKc)SrO9IoN+6)SGo'9>2''okNM=7.%I6X9?l4>88:1<]3NhZk)n=-sO8TS0:7RK.*IET.K`V1CAnd!QAm%J.6kr)cK1?^^'"W[I\@m)NKHND_iHjqs.6%G`>Fc>e9n3IbqpJC@.T)?d.';(S@)+G@E+Gk"NHoXF)`U<?+]\thjFE"]nQZ'1t#Gj0]FZ,0@OQe0bPLdI=PXj(Jhb&Tf;A`YeZ<L?j%B:6aJN`Rn,NsdA\"?Dk+@sje)7!>M]:%Ej-+;hlN<,eV@^)']Y>%F7N<-IoKJt2*[;=0)]!.cl=LP#;j$;m624%./H=Mj!eq@("o6f#[;5]06f&9U7NnL'/Q_Id89@-TKC3VGGJLBRC2''Kr^rs(iEmT@IZF9W8D2-f)m=>8Pp&j.#*VdS9N+5*5&]YZ=e\sVrne>Q#bb7U3RsVthX6gI"GYQk0ejH1:q:iSU#iZ09=155YIXo(-2_jI9VC'/SJ8r=T$ET3u`Pl_>N[?W#oGE$4aU9,Ki,uC8X&s!dLY[dBFTeYK)p"@;dK2o7>F+Iq!eN'rU;J+QKt^YJil63Z^;'mGIinTuIB`T%pVCJm0jHZSOK/Gs(-r]gmYhIul!iZ1.bkej1^L7bES^q!5Ltm]Bo(BaZq@K$.jmeIe;n8*lc!Is1;[<R]u0mI7*]c$Y*3*GVIB>,Z)?p'):%jHU0`GC@M3tdcmSCU63f2*n9X.`Nd;"qam8/[@o&M^?<S."An2;Sr7USU0(eM7D!:`6[\`%BRn\uK5*nc@C4[ZHC"m(/]mR8Xe[RI'=,#5USuJAd(@#J1Jc^RM<UK<B31sg<6g"cu2/-Wi1pTf@V()tZ`ON#:=/sQk60E!Xf@NB_bnrF6Qot$KT!ZL_"[?ICqteE?fnp0oSb%?b/cHMbE*^C`F.<=#*e]qk%u_$dK,s\0#)u:<H!=jub3(I[F!Furbo5cQ#?5.2F\u2^*6p1,lbVTbA?j#LSg;Q_&?/58caL=73*aSG;gRAEOOA'!n9SF)AJW,PgpDE&&]7Gb..'VCL89)O'B[mh.!?'Do^OaZGTEUMY>oPD(-7;2VuM]UT<k!7S>p&4r>_JbO/V!MoW*buV\mJ<7\)mb?Y)2\kEX>bU2oP-pR9bQXM4emO]>o~>endstream
endobj
27 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1724
>>
stream
Gatm;>Ar7S'RnZ;35@5`D.A<r.$;YN]<5EAFC!Y3gOOD`A=9lkaHA&Nl*as66cl(`pnBCA*J?2eo:>c4$n-R-o0:n.HoH]$QOF^dfb0Yp!bSXp`Q\5?&KYsA![,4OI`-e*^AU=-no>"&&k4umTVs,.$4r(,V'6'_l(hi&8e&ta*WmTR.[:".=:hM%JDMXskK$hg+:P%BoK2l5'e^ci:-')qrBi8$_0/F2<*p3Z!ZAdi"-YZ.rGi@NMNt?-F\u]=_3?u`)u.LbKP/ocB2t3t7PE\HIAaD\]@"07"LB&74gr!-pd/p4TQpA/YB:#m#@&`W/6gc9nLDdnL^#j-d&%g[aEN5.Weg+BM7trbE6h%l-sqWOKT4"SdQY7Ubi=j_.b%jeX5,3jQjie0-g8i$T%h-fVAQX9DC`SF;#h*\BC-Qa5dulKJDG;/0mq1@Qdc%5a!L7h01CU[S<,gAVoD!n`*7R_9\F@MqTG3`G+.=uM;![(^h&H;Xr-na+g/!Me[S9_r'FM$k54HE5A]g6_@Q0R7'HsWFq=V.e@_gEn16;.O]V&MH7TQCLq5RMW<_hJIX%WM/:*M`[*;Z&d,]o+OTg-&4>1`=k&jO&E1Ir].V$[%j!.+G3k9r5)98_-\2M;1hdAK1Z%X,+8[6VFISB?T'?Ac@A8,r>$-_]l>ED"qNsh"E"6%S[o0=,T^;bG^:'gW"Pdei;pcQjY%kr0C1>3^W'YT&da/s222*$=iMX]2$)c'#X(+cq]GsYA$fPe=#((r*q*:Bcn9]G`Q0Ch]QYnglf]aud2"6dOG,hk[]!k3eYr=>Ae;Q7KJg4ReEQd$f+lTff"GP&R,rSu:!nf_a16'!<iA@Z^(98SN4hNkloJ?2YTY32.B];j%"-%Bj=is!q`"Sh'$"s]0fqJ&5`N_p'NW6\YU7=I+7g.+6`C(CD_lR_q>e=UJ]R5%$g-K6l7;$+tb361BbY2K%KSE><[O>G)`(-'%YHAirB7*nIeO<7DY";p'_r6-T-Os!XSPFH#fN<mKidA5>gW2=fuOTZYV:%imtF^@ei:JfC'gsje(2JE%$&:otA=QD`O.rRq3`JtSQYicEKKt=Q*N8*oMfdK\Hk38ER_.)'\,`RglD=d/WZ3?b!g=4lTGQ%jb%1!>oTjRkm0j@*]_S#$V>Mq\h\u\eFd-*SPef?;t=Dq!iUQL%pA'lC+8/Mp^PiD^CM*M\("M:RbjZ.IPE-`Qn"=3W9\qcEdkg.g'kK/.E8=st2B*jO?Q57o]13&VrFLj#]Irmm!MWArKApQ#IM6[f[#uoY?8,Tu'O4-$NGALIPH0@<Gs)#9\`WfIiY3XMd/l-Xp2d18rd[iC7??ZjsbpD0$,!($dTmerUS/cTGLc_,Lf]dYa"`&naNmf.V$dsY;"tc4db&k&>AK0=91)"p0*p4f=Z#I4V[gO`t(\Z6WGKAgS"C%g#8qm6<X!l9.5]Q<J`S]9mISB2@:$K[^0K5r(0($$URr$Q$=iP)rk$LPirsNr2&(C>e'5u9rLIud8ikcFd-82",)m!=IZMOM'2Q)H[i@g4emCk`tME/TkR,c$60QhMON)a$E>jng+)"XT3_/D4AE..W_5b*%rAos(-gqIOi8]T=W4E`P;e!ic`Y88^+Y(05,D"*a#Cdr;.j)UW'N)+\ns"`Z4!q\&#B!d,[5(9EUrRN]&'lQHCB*L[`<V0KLaO7r/Fh,NXaMN(RM4JJN3s*'E!HZ$NT`~>endstream
endobj
28 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1906
>>
stream
Gb"/'968iG&AIa;CbHeVWF,]/8^O$mN:V#K2GFid8M@eS,UCb]$&+'!8Uc((+@X^3g!2Q@e^Bfr*oF[qliL]_n\a/^ZikV6L&E5#N5K`I9Fq6RGOT50IIRRRGpEG6^8-5%J],;$K6lKV&;2<fH-AP%Lr`,;OGl^J9Am<4MHC"hK)'JK!Js,]8HD5b+I(imof`[B8'unk@2N?WhruMjH8,^fm82($MdSMsF[l&&^*&XD;#o&Xp>5iJq&%uRKkf&4ecgFV[7Zbi:-"Bnn9B!+1jNS1#-ZRf.eL;OQ$ES_1RLA8Z:=W+QNV/^0&s]F%58mnd25#b@6Z#q]Lm_%&^X$il4ti0SYd!a`:mu!_:\!lcUZ;P?\BVVdc1@BRseZ&nVC!kjJ\-_DO)Cr$Mh1ZfZm(%^(2/V11m6VXr-?@!VG#.CZk<sCcbNoE8*!-L8/Q%Ucf:VV5,.QZmbE6Y@P,P2e-BDl7G.h.f3pi^OKe9j4G:`g$rPd1I_\;N;LC%f9_&10pABB&D"Ggl>qV#3rGB7lhH'qJAk(79DC1+Ue6uXY-h-+a`V_B&&O_-4Ks/Cg@&U%UQ[7?gT\'@YZ@I2InWaO8Q'3*8h.n^VJ;qW=DsWS4t7P'1<D$JT=(i(;V;8>;K.a7E2FaN$rjbmjH>ZbX,3s@RQEZV:$Xu>/kgU>Rf[e/?K'`k$Ii"XiV2-?h99b:UYjr0[K\Uf)r?=MpC6Riflk%3l#.SilW1Vdo<H'`m@Frtb7'$!#DM_:XYhrXR(QTZ0_l/]-7RP1bVH-ECUZp&&3bu&r#Q&[-!c:A5mGe-0'ILKQ';X,M(u$5%_tZj5OuDiH^:cqN"h=^Ujin"`V18GC8oW]Dq7rA#5?:fa1@Spegq00E*kN'0ecOINP20gZ"P\3fZbMWDK-3!q%'aB0o=R?OP3hH(l\Z3RVq[3DpK'0;$EgkG]aE2\mEVhIBg[?O5cD^*_pu[T=i`OB`gqX`kh#Bo`PI@bS'.pe*!`,!n[)@G[#1u\9t[Er+8.5#i(u-76nU@'*mK+K^G!mdB-%#nKVFdf]Zm00QQW'<FTBZ75[I3+sZtg\h1)QZr_!Ri&jfn_oXO&b,;RjOS,:3oARV*Me$q_#cUo_Fr)cGNOJ!$\N%`+o]`FrA3J#F,@.3^@KSQHj$Lut]iljbB=k6l6K>f'[[0Rs_C08_Y(mCt^bg#(l4jLTfbe!A-HF;*8'qN9pV(@C3gj#;G5CS)#tERjSis^cIogp4]1W!Rmje>];&WAK?,Fp!Q8Hcl@BQh2A&1@LA2$r,.`#S2L5WrTQPgiB,%rMI;&D`a%#/Cf(e59kT&d"GV'i"9I>-&B'qUN:/f_J!pC&4=8B5_2gs/L)^5r(E@3Q1%e@&/GocZaa*B4^]4/pi)mm3o2LZM,Rj_1"k@!%KAkop?Okb)_#I^!EthFJG)@M_ab'3eoYF2j>p$lL/7fEIHNJE'=3kj2<QMrW'#1]$c$+=mq+?GVgi^su\!"(/2?VN#2idX7XeePDO[`XSTGO+tWZ"n`K"eY[4\,JCO9Zh>%$!`E<i3@:dCh8#igH35_<cjp6lhSKVQD/YAPkUtPk<b6E:j9?r-j9YM^At-HSG,'dH&OK+X:]j,aXKYS:n4d'?eTn;M+B.SmIt\4;"_)sBH&"?144Oca@^TbW3(2[ho!<b>OqLqtF+>=Qe0N6#F&]Qgn_D_&g^Pe9')GJ;Ij;m[F,o,3-rh6HQf8N[i1,P:5iRAt.oYWr%Suj`M1!U\QQuk7QTGE0\H#Fe=)MS./EaO^m-D8AD[h`n1WQ;4A)<.PdiDEQSrVZl^n6=gD:3`'YMi/LP4b964&H-MGGZb3KA6%ji'ZH[5KiPlT&jZq?_OuHG3T=V$/HP(eTUO)+NhX#;!-<J?pRgn2DKN0O/l=lnGWM3oD-i~>endstream
endobj
29 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2014
>>
stream
Gb"/(968iG&AII3ChGdB-&j#KI"@&b`_N#AD8pQ9-1/N))2a(rfHd/@P[%SlgUdS;MN[Y:26K]6SVVk5]DLtJ53sa5/Gu4(h^ARd*Pq4c"j14p%FcO2N'>+^UYb_d@35"\(Pp<f4((;T]D;Ujl/giL`!][4JQk=s^&nkCK_']r_gT6pIF&%u?hGfYhm7^u!9)H#\G'CG9g$GBZpS*E$eG&2rpe.EJ9[&tk[sdOb#![JTN\J+6S.O!/rVRLT7?]`"s6egMJ/_`$=?CWU.GYmN;7F52E3,m=d1+b?c63FC7:!lHjQPe(qlqcFn@aR-]#`Kec/J\5]+<'U/]aPg5@YIGmKXX'`ESK?"ie2j/Hs:@b[30ak.L$SrJlk[c9@hA3K(<qQX=!Hd!k'jm^P6j,K9jW:."ZMS"t,HL2V7:ek`$Wa,s./o[N7D:;[cF^VHcTnA9dZq'Xb])9oi'j7Z(W+R]q8F]F?A@&h#d6SiH-9Y:,ePDb5iOc+79_E^b2tbS?R>9/u(uptk$oE1=aVhKIM'96doD37CJ7f8nKKV0M0]LL\.sJtJKi[^p.]!u\\^5O%b97Jn^2!/IeQ0T^4atfP,Wh:1:)lqAqJTA2(+TU$-JP<T\k^l<Y!\9IR'f'@"6QtG]ZEYi'6p!785L=U`3AshD"?7c]ad7l06;=%?)6(-ESHR[X@CBd\,TUp\W"I=@L+BuE7o[jrD!_$R!lYNT%T&>P$?$-)<SI@)TJK[e!2u!ba*sdBPP/[S/P[?KoSR4a3dBMK,Tg>iMrO6Y`o;bKeAV4;gEG2ol?,ZNJ=nlbKumr+pDMl2q5.iT>ES3j#\-FPKc^]J:+^#]duCl`c-MW7HMK0T:8!fZ7O(-50Q@#K$I/)j&AK%6bbF67)%:G7a\3JJOHOO1F9nSMfn)&]*:=6fPJZBlXD*N)V1sUY6"G!huCd!^!8Jb:;c92SXUe>'4d-/J=e><5Ag!IW+ecX?J-qo\N&@(BJ'3`V5]8^!k"eM4KJ5;A55bF&ZnUc2uJ#TKZ,sXOZaOfppRa03V!J]^@)XMmM>GV/^G]V9B!59VN$8m1@Nm%n["$`>*ep[3RMhka6NL->[r]RIZ>??dc_H:CRq@"l<dEGhs)KDW@l./ZssL/h_!"8WcBX"\/"`AS:CBW2?K:6PY/$\W`k+tiV_<B7c,i4S:3SIqe`01=5DeBoAa7bg3OB4,4[`c<>;;F:6N[r-FpuTi2T"C8r)"09gj(RnfN$!A_-C5/o3[M=K>S6m)/u%Q`:IVa%TsM848p*Gu!X2e3H_/gX1r&(Br%#*$13b0(_X:Io)Hsq[P=3;QIIqhP#bT'H8Z$)8ouJS74:/*088V+lfsgXR]mPs#BB@)R<!]gG&n]5QB@d4kUMDa0D6]=d3`FdBT.VB5-Ul>fC,+)66TS0R/4[f/-M%ftK4h(23%Hd+EorPa7/V4/%d5@Y'57_5[LL^jA.!D2*Y]>bLX(]=@MoYhG/RC*1AP^SbM=?c,,';'@-;B_.[-bRL;cGUClVEeU('j,]Xgo91%#ruZoT3='iDU%g8FiD\guI-]kJ;BD\6,5Y/F4R+#n%;Cebn[tBM^IrTG^pPSc8Tc\S_ij'F9gP"=L9ZVn@5sefc,i8._7c<%!Nt?g'38p(N5ICj//7HD%dG(>>A9';(m$..3$->q?'0)dfXeH0SeqbRfSoBM27D%2<JPGn10(e-S4XEn>pu+^7$J'h:IJ@3c;:@FL/pI"aK-SuC_o;$Lbi%HlnEetBD,&-LZ;EO&14Gb2Qrn=7q=Q-(qD5%\i.$=ZXAkqM`];]8#.X70W&U39sp?]ES!$ifeU$P@##bR@C'YZ5H;PBO2tAi@:ZcSd[GqpL>u6L#tIL-d#.7+I#gg1p0h47qNYXTKI='qbf0k!aG8`,Y;@?Xe46.4=7fAh=KH!Qk0X;B!=gggelL:(9N<6/*/-d8`6$q@-8Sr`hmEeg$pnL<9]uU$AWGBRn\&tWNu*2UrI1,fH1cZ#ae5mgi`=*cR0%^7!pZS^ir~>endstream
endobj
xref
0 30
0000000000 65535 f
0000000061 00000 n
0000000112 00000 n
0000000219 00000 n
0000000331 00000 n
0000000536 00000 n
0000000641 00000 n
0000000846 00000 n
0000001051 00000 n
0000001256 00000 n
0000001461 00000 n
0000001667 00000 n
0000001873 00000 n
0000002079 00000 n
0000002285 00000 n
0000002491 00000 n
0000002697 00000 n
0000002767 00000 n
0000003059 00000 n
0000003189 00000 n
0000005192 00000 n
0000007290 00000 n
0000009467 00000 n
0000010262 00000 n
0000011873 00000 n
0000013917 00000 n
0000015568 00000 n
0000017531 00000 n
0000019347 00000 n
0000021345 00000 n
trailer
<<
/ID
[<1e77dbe9d9f83762252045861df07f97><1e77dbe9d9f83762252045861df07f97>]
% ReportLab generated PDF document -- digest (opensource)
/Info 17 0 R
/Root 16 0 R
/Size 30
>>
startxref
23451
%%EOF

View File

@@ -0,0 +1,436 @@
# Les 13 — Externe APIs + Cursor agents + Vercel deploy
## Slide Overzicht (Klas A — 3 uur fysiek, demo-driven)
**Lesvorm:** Tim demonstreert klassikaal. Studenten kijken. Zelf bouwen = huiswerk.
**Demo-app:** Nieuwe kleine Pokédex (PokéAPI, geen key, visueel)
**Vervolg op:** Les 12 — Tool Calling
**Aansluit op:** Les 14 — Agents
---
## Slide 1: Title
### Les 13 — Externe APIs + Cursor + Vercel
**Visual:**
- Background: CREAM
- "Les 13" in BLUE
- "Externe APIs + Cursor + Vercel" in BLACK
- Subtitle: "Van localhost naar productie — met Cursor agents en preview deploys"
---
## Slide 2: Terugblik
### Waar staan we?
**Vorige lessen:**
- Les 11: AI SDK basics
- Les 12: Tool Calling
- Les 13: Agents
**Tot nu toe:** alles draaide op `localhost:3000`. Geen wereld eromheen.
**Vandaag:**
- Externe APIs aanroepen (geen LLM nodig, gewoon data ophalen)
- App naar productie met Vercel
- Feature branches met Cursor agents (Composer + Background)
- GitHub Actions CI (lint + build per PR)
**Visual:** localhost icoon → cloud icoon met branches eruit.
---
## Slide 3: Planning
### Vandaag — 180 minuten
| Onderwerp | Duur |
|-----------|------|
| Welkom + Terugblik | 10 min |
| Theorie: Externe APIs in Next.js | 15 min |
| Theorie: Cursor agents — Composer vs Background | 15 min |
| Theorie: Vercel preview deploys + GH Actions | 15 min |
| **Live Demo 1** — Pokédex bouwen met Composer | 30 min |
| **Live Demo 2** — Deploy naar Vercel productie | 15 min |
| **Pauze** | 15 min |
| **Live Demo 3** — Feature branch met Background Agent | 25 min |
| **Live Demo 4** — GitHub Actions CI | 15 min |
| Composer vs Background — wanneer welke? | 10 min |
| Lesopdracht + Huiswerk | 10 min |
| Vragen + Afsluiting | 5 min |
**Format:** Demo-driven, Cursor en browser side-by-side.
---
## Slide 4: Wat is een externe API?
### Data van buiten, in jouw app
**Het idee:**
- Andere mensen / bedrijven hebben data of functionaliteit
- Ze stellen die beschikbaar via HTTP endpoints (REST)
- Jij doet een `fetch()` en krijgt JSON terug
**Voorbeelden:**
- PokéAPI — Pokemon data (geen key)
- Open-Meteo — weer (geen key)
- GitHub API — repos, users, issues
- Tavily — web search (key nodig)
- Stripe — betalingen (key + secret)
**API keys** horen in environment variables — nooit in je code committen.
**Visual:** Diagram: Next.js → fetch → externe API → JSON terug.
---
## Slide 5: Drie manieren om een API te fetchen in Next.js
### Static, Server (dynamic), Client — wanneer welke?
| | Wanneer gerenderd | Data versheid | Externe API key veilig? | Wanneer kiezen |
|---|---|---|---|---|
| **Static** (build-time) | Tijdens `next build` | Bij elke build | Ja, server | Data verandert zelden (blog, marketing, lijst) |
| **Server / Dynamic** (request-time) | Bij elke request | Altijd vers | Ja, server | Data per user/request anders, of telkens vers nodig |
| **ISR** (revalidate) | Eerste keer + na X seconden | Periodiek vers | Ja, server | Sweet spot: snel + redelijk vers (Pokédex!) |
| **Client** (`useEffect`) | In de browser na hydration | Live | **Nee** — sleutel staat in browser | User-interactie, live updates, geen geheim |
**Code-verschil = één optie in `fetch`:**
```typescript
// 1. STATIC — gecached forever (default in Next.js!)
const res = await fetch(URL);
// 2. ISR — cache met revalidate
const res = await fetch(URL, { next: { revalidate: 3600 } });
// 3. DYNAMIC — altijd vers
const res = await fetch(URL, { cache: "no-store" });
```
**Vandaag gebruiken we:** Static voor de Pokédex-lijst, ISR voor detail-pages.
---
## Slide 6: Cursor — twee soorten agents
### Composer vs Background
| | Composer | Background Agent |
|---|----------|------------------|
| Waar draait | Lokaal in je editor | Cursor cloud |
| Mode | Synchroon — jij wacht | Async — runt zonder jou |
| Output | Diffs in editor | Branch + PR op GitHub |
| Goed voor | Pair programming, snelle changes | Welomschreven feature, tijdens andere taak |
| Aanroep | `Cmd+I` (Mac) / `Ctrl+I` | `Cmd+Shift+P` → Background Agent |
| Repo nodig | Lokaal | Verbonden met Cursor cloud |
**Beide kanten:**
- Composer: jij stuurt, snel feedback, niet schaalbaar voor lange taken
- Background: schaalbaar (5 agents tegelijk), maar minder controle in flight
**Visual:** Twee tracks naast elkaar — synchroon (jij + composer) vs asynchroon (background agent + jij elders).
---
## Slide 7: Vercel — productie, preview, environment variables
### Wat verandert er per omgeving?
**Drie omgevingen, drie scopes:**
| Scope | Wordt gedeployed bij | URL | Wie ziet het |
|-------|---------------------|-----|--------------|
| **Production** | Push naar `main` | `je-app.vercel.app` | Iedereen |
| **Preview** | Push naar elke andere branch | `je-app-git-{branch}-{user}.vercel.app` | Iedereen met link (delen via PR) |
| **Development** | Lokaal `pnpm dev` | `localhost:3000` | Alleen jij |
**Environment variables per scope:**
In Vercel → Project → Settings → Environment Variables zet je per var **drie keer** een waarde:
| Var | Production | Preview | Development |
|-----|------------|---------|-------------|
| `DATABASE_URL` | prod database | staging database | lokale database |
| `OPENAI_API_KEY` | echte key | test-key (lage limit) | jouw persoonlijke key |
| `STRIPE_KEY` | `sk_live_...` | `sk_test_...` | `sk_test_...` |
| `NEXT_PUBLIC_APP_URL` | je-app.com | preview-url | localhost:3000 |
**Waarom is dit belangrijk?**
- Preview branches mogen NIET stagecoach naar je productie-database schrijven
- Test API-keys voorkomen kosten/quota-problemen bij experiments
- Eén branch = één geïsoleerde wereld om in te testen
**Cursor Background Agent + Vercel preview = killer combo:**
Agent opent PR → preview URL automatisch in PR-comment → reviewer test live in browser, geen lokale setup nodig.
---
## Slide 8: GitHub Actions CI
### Lint + build per PR
**Waarom:**
- Voor merge weet je: code lint clean én build slaagt
- Voorkomt "werkte op mijn laptop"
- Background Agent doet PR → CI draait automatisch → groene check of feedback
**Minimaal `.github/workflows/ci.yml`:**
```yaml
name: CI
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
with: { version: 9 }
- uses: actions/setup-node@v4
with: { node-version: 20, cache: pnpm }
- run: pnpm install
- run: pnpm lint
- run: pnpm build
```
**Tijd per CI-run:** ~1-2 min op een kleine Next.js app.
---
## Slide 9: Wat we vandaag bouwen
### Pokédex — Next.js + PokéAPI
**Doel:** Een mini-Pokédex om de hele pipeline te oefenen.
**Stack:**
- Next.js 16 + TypeScript + Tailwind + Shadcn
- PokéAPI (geen key, gratis, geen rate-limit bij normaal gebruik)
- Cursor (Composer + Background Agent)
- GitHub (repo + Actions)
- Vercel (productie + preview deploys)
**Features die we bouwen vandaag:**
| Feature | Met | Branch |
|---------|-----|--------|
| Pokédex list (eerste 151) | Composer | main |
| Detail page per Pokemon | Composer | main |
| Search met filter | Background Agent | feature/search |
| Type-filter chips | Background Agent | feature/type-filter |
**Visual:** Folder-tree + GitHub branch-diagram + Vercel URLs.
---
## Slide 10: LIVE DEMO 1 — Pokédex met Composer + rendering modes
### ~30 min
**Wat ik laat zien:**
1. `pnpm create next-app pokedex --typescript --tailwind --app`
2. Open in Cursor
3. **Composer (`Cmd+I`)**: "Bouw een lijst-pagina met de eerste 151 Pokémon van PokéAPI als statische pagina. Gebruik Tailwind cards. De fetch moet static zijn — geen revalidate."
4. Composer maakt: `app/page.tsx` met `fetch(URL)` (static) + map + cards
5. `pnpm dev` — werkt
6. **Composer**: "Voeg detail-pagina toe op /pokemon/[name] met sprite + types + stats. Gebruik ISR met revalidate van 3600 seconden."
7. Composer maakt: `app/pokemon/[name]/page.tsx` met `{ next: { revalidate: 3600 } }`
8. **Bewijs in de browser:** open detail-pagina, refresh — direct geladen (statisch). Verander URL → andere Pokémon → ook snel (ISR cache).
9. **Show de Network tab:** geen calls naar PokéAPI vanuit browser, alles server.
10. **Bonus:** maak één pagina met `cache: "no-store"` (dynamic) zodat studenten alle drie de modi gezien hebben.
**Belangrijk om uit te leggen:**
- Composer is een TAB in Cursor — niet hetzelfde als Chat
- Multi-file edits zichtbaar als diffs — je accepteert per file
- Terminal commands vragen toestemming
- De rendering-mode is één optie in `fetch()` — verder verandert je code niet
**Visual:** Cursor screenshot met Composer panel.
---
## Slide 11: LIVE DEMO 2 — Deploy naar Vercel + Environment variables
### ~20 min
**Wat ik laat zien:**
1. **GitHub:** new repo `pokedex` — public
2. **Lokaal:**
```bash
git init
git add . && git commit -m "Initial Pokédex"
git branch -M main
git remote add origin https://github.com/USER/pokedex.git
git push -u origin main
```
3. **Vercel:** Add New → Import Git Repository → kies `pokedex` → Deploy
4. Wachten 60-90s op build → Productie URL — werkt!
5. **Environment Variables demo:** voeg een var toe in `app/page.tsx`:
```tsx
<p>Omgeving: {process.env.NEXT_PUBLIC_APP_ENV}</p>
```
6. **Vercel → Settings → Environment Variables:**
- `NEXT_PUBLIC_APP_ENV` = `production` → vinkje bij Production
- `NEXT_PUBLIC_APP_ENV` = `preview` → vinkje bij Preview
- Lokaal: `.env.local` → `NEXT_PUBLIC_APP_ENV=development`
7. Redeploy → productie URL toont "production"
8. Maak feature branch `chore/banner`, push → preview URL toont "preview"
9. Lokaal `pnpm dev` → toont "development"
**Wat studenten moeten zien:**
- Push to main = productie deploy
- Push to andere branch = preview deploy (eigen URL)
- Per omgeving andere env-waarden
- Voor secrets: vinkje **alleen** bij Production (preview-key gebruiken in preview!)
---
## Slide 12: Pauze
### 15 minuten
---
## Slide 13: LIVE DEMO 3 — Feature branch + Background Agent
### ~25 min
**Wat ik laat zien:**
1. **Cursor:** `Cmd+Shift+P` → "Open Background Agent"
2. **Verbind** repo aan Cursor cloud (eenmalig — accepteren in browser)
3. **Prompt aan Background Agent:**
> "Maak een nieuwe branch `feature/search`. Voeg een zoekbalk toe bovenaan de homepage. Wanneer iemand typt, filter de Pokémon-lijst op naam. Gebruik client-side state. Push de branch + open een PR met titel 'Add search bar'."
4. **Background Agent gaat aan de slag** — Tim laat zien hoe je 'm volgt in het Cursor panel
5. **~3-5 minuten later:** PR verschijnt op GitHub
6. **Vercel** comment in PR: preview URL → klik → live preview!
7. **Demo:** zoeken werkt op preview URL
8. **Code review** in GitHub UI: wat heeft de agent gedaan?
9. **Merge** → main → productie deploy
**Tweede prompt — parallel:**
> "Maak branch `feature/type-filter`. Voeg chips toe bij elk type (fire, water, ...) om te filteren."
Beide branches kunnen tegelijk draaien.
**Visual:** Cursor Background Agent panel + GitHub PR + Vercel preview URL.
---
## Slide 14: LIVE DEMO 4 — GitHub Actions CI
### ~15 min
**Wat ik laat zien:**
1. **Vraag aan Composer:**
> "Voeg een GitHub Actions workflow toe: lint + build moeten slagen op PR's naar main."
2. Composer maakt `.github/workflows/ci.yml`
3. Push naar nieuwe branch `chore/add-ci`
4. Open PR → GitHub Actions tab → workflow draait
5. **Demo failing CI:** introduceer typo (`<dvi>`), push, zie rode X
6. Fix met Composer ("repareer de TypeScript build error in de PR"), push, zie groene check
7. **Branch protection:** main beschermen — PR's mogen pas mergen als CI groen is
**Waarom belangrijk:**
- Background Agents kunnen lint-errors maken — CI vangt dit op
- Voor je merge: weet je dat het op een schone runner ook werkt
- Geen "werkt op mijn laptop"-discussies
---
## Slide 15: Composer vs Background Agent
### Wanneer welke?
| Scenario | Tool |
|----------|------|
| Snelle wijziging tijdens coden | Composer |
| Complex refactor over meerdere files | Composer (kun je begeleiden) |
| Onbekend gebied — wil zien wat agent doet | Composer |
| Welomschreven feature, kan op de achtergrond | Background Agent |
| Parallel werken aan 3 features | 3x Background Agent |
| Tijdens vergadering een PR voorbereiden | Background Agent |
| Repo nog niet in Cursor cloud | Composer |
**Mentale model:**
- **Composer = pair programming** — jij + AI samen aan dezelfde plek
- **Background Agent = delegeren** — je geeft taak, gaat iets anders doen, komt terug
**Risico bij Background Agent:**
- Agent kan vastlopen op rare config — debugging is moeilijker
- Verbruikt credits (Cursor Pro of Business)
- Te vage prompts → matige PR's. Specifiek zijn helpt.
---
## Slide 16: Lesopdracht + Huiswerk
### Pokédex zelf bouwen + deploy
**Lesopdracht (in-class, 30 min):**
- Eigen Pokédex repo opzetten (zelfde setup als demo)
- 1 feature met Composer toevoegen
- Push naar GitHub + connect Vercel
- Productie URL openen — werkt
**Huiswerk (voor Les 16):**
- **A:** Feature branch met Background Agent (1 feature, jouw keuze) → preview URL delen
- **B:** GitHub Actions CI (lint + build) in werking
- **C:** Branch protection — main mag pas merge na groene CI
- **D:** `DEPLOY.md` met:
- Productie URL + 1 preview URL
- Reflectie: Composer vs Background — wanneer welke werkte beter?
- 1 voorbeeld waar Background Agent verbluffend goed of slecht was
**Bonus:**
- Eigen externe API integreren (Open-Meteo, GitHub, jouw keuze)
- Vercel environment vars demo (verschillende waarde in preview vs prod)
---
## Slide 17: Volgende les + Afsluiting
### Vragen?
**Vandaag gezien:**
- Externe API integratie in Server Components
- Static / ISR / Dynamic / Client — vier rendering modes
- Cursor Composer voor synchrone changes
- Cursor Background Agent voor async features
- Vercel productie + preview per branch
- Environment variables per omgeving (production / preview / development)
- GitHub Actions CI (lint + build)
**Volgende les (Les 14): Agents**
- LLM in een loop met tools, autonoom 20-50 stappen
- `ToolLoopAgent`, `stopWhen`, `prepareStep`
- Research-agent from scratch
- Wanneer wel/niet agents gebruiken
**Vragen? Feedback?**
---
## Slide Summary
| # | Title | Type |
|---|-------|------|
| 1 | Title | Opening |
| 2 | Terugblik | Recap |
| 3 | Planning | 180-min |
| 4 | Externe APIs | Theorie |
| 5 | Externe API code | Theorie |
| 6 | Cursor — Composer vs Background | Theorie |
| 7 | Vercel preview deploys | Theorie |
| 8 | GitHub Actions CI | Theorie |
| 9 | Wat we bouwen | Intro demo |
| 10 | **LIVE DEMO 1** — Pokédex + Composer | Demo |
| 11 | **LIVE DEMO 2** — Vercel deploy | Demo |
| 12 | Pauze | Break |
| 13 | **LIVE DEMO 3** — Background Agent + preview | Demo |
| 14 | **LIVE DEMO 4** — GitHub Actions CI | Demo |
| 15 | Wanneer welke Cursor-mode | Reflectie |
| 16 | Lesopdracht + Huiswerk | Praktijk |
| 17 | Afsluiting + Les 13 preview | Closing |
---
## Bronnen
- **PokéAPI:** https://pokeapi.co/
- **Next.js fetching:** https://nextjs.org/docs/app/building-your-application/data-fetching
- **Cursor agents:** https://cursor.com/product
- **Vercel preview deployments:** https://vercel.com/docs/deployments/preview-deployments
- **Vercel environment variables:** https://vercel.com/docs/environment-variables
- **GitHub Actions for Next.js:** https://nextjs.org/docs/app/building-your-application/deploying
- **GitHub branch protection:** https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches

Binary file not shown.

Binary file not shown.

Binary file not shown.