Haal pakketten-data op via /api/v1/packages i.p.v. hardcoded content
Continuous Integration / config (pull_request) Successful in 3m1s
Continuous Integration / prepare (pull_request) Successful in 1m13s
Continuous Integration / build-production (pull_request) Skipped
Continuous Integration / build (pull_request) Successful in 2m14s
Continuous Integration / test (pull_request) Successful in 1m52s
Continuous Integration / deploy-production (pull_request) Skipped
Deploy / deploy (pull_request) Successful in 43s
Continuous Integration / deploy-test (pull_request) Successful in 44s

PackagesSection gebruikte tot nu toe statische data via een placeholder
query-hook (FR-5). usePackagesQuery haalt nu echt op bij /api/v1/packages
(relatief pad, zelfde domein op test en productie), met een foutmelding
in de UI als het request faalt.

Voegt ook een hand-off-document toe voor de agent die de CMS/API-kant
gaat bouwen: het endpoint-contract, per veld het type/gebruik, en de
huidige statische content als seed-data.
This commit is contained in:
2026-08-01 02:12:31 +02:00
parent 32a5bc0185
commit 7fc06091a5
7 changed files with 181 additions and 19 deletions
@@ -0,0 +1,112 @@
# Hand-off: Packages API (`GET /api/v1/packages`)
**Audience**: whoever implements the CMS/backend that will serve package content.
**Status**: frontend is already wired up and waiting on this endpoint to exist.
## Context
The "Drie manieren om te starten" section on the marketing site ([PackagesSection.tsx](../../../../../../../src/features/landing/components/PackagesSection.tsx))
used to render hardcoded package cards. It now fetches them from an API instead, via
[usePackagesQuery.ts](../../../../../../../src/features/landing/hooks/usePackagesQuery.ts),
so package content (title, price, features, ...) can be managed in a CMS without a
frontend deploy.
Until this endpoint exists, the section will show the error state ("Pakketten konden
niet worden geladen. Probeer het later opnieuw.") in any environment that doesn't have
it yet.
## What the frontend needs
### Endpoint
```
GET /api/v1/packages
```
- **Same domain as the site, relative path.** The frontend calls `fetch('/api/v1/packages')`
with no protocol/host — it relies on the API being reachable under the *same* origin the
React app is served from, both on `test.slpsoftware.nl` and on production
(`slpsoftware.nl`). Today, both Pis only serve the static React build (see
`aidlc-docs/features/react-frontend/operations/deployment/nginx/*.conf.example`
there is no `/api/` location anywhere yet). **Whoever builds the backend also needs to
add an nginx `location /api/v1/ { proxy_pass ... }` block (reverse-proxy Pi and/or
webserver Pi, wherever the backend process actually runs) for both the test and
production nginx configs** — the frontend change alone does not create that route.
- **Method**: `GET` only. No mutation endpoints are needed by this frontend (CMS
authoring is out of scope for the marketing site itself).
- **Auth**: none — this is public marketing content, same as the rest of the page.
- **CORS**: not needed if served same-origin as above. If the backend ends up on a
different origin instead, CORS must be enabled for `test.slpsoftware.nl` and
`slpsoftware.nl` and the frontend's relative URL would need to become absolute
(not the current plan, but flagging the coupling).
### Response contract
`200 OK`, `Content-Type: application/json`, body is a **JSON array** of package
objects, **in display order** (the array order is rendered left-to-right as-is — no
client-side sorting):
```json
[
{
"id": "pakket_01",
"title": "Landingspagina",
"description": "Eén overtuigende pagina die je product of dienst helder neerzet.",
"price": "€ 300",
"priceNote": "eenmalig, excl. btw",
"features": [
"Eén pagina in HTML & CSS",
"Ontwerp op maat, geen template",
"Responsive op elk scherm",
"Snelle laadtijd & SEO-basis"
],
"ctaLabel": "Kies landingspagina",
"featured": false
}
]
```
Any non-2xx response (or a network error) is treated by the frontend as a failure —
it shows a generic error message and logs nothing card-specific, so there's no need
for a structured error body.
### Field reference
| Field | Type | Required | Content type / description | Used for |
|---|---|---|---|---|
| `id` | `string` | Yes | Stable slug-like identifier, e.g. `pakket_01`. Currently just an ordinal slug, not a business meaning — reuse as the CMS entry's own key/slug. | React list key + `data-testid` on the card, and referenced in tests. Must be unique and stable across edits (don't regenerate on every content change, or it'll be treated as a different card). |
| `title` | `string` | Yes | Short package name (e.g. "Landingspagina"). Plain text, no markup. | Card heading. |
| `description` | `string` | Yes | One-sentence pitch, plain text. | Subheading under the title. |
| `price` | `string` | Yes | Pre-formatted price display string — e.g. `"€ 300"` or `"Op maat"` for the quote-based package. **Not a number**: it's rendered verbatim, so formatting (currency symbol, spacing, "Op maat") is the CMS editor's responsibility, not computed client-side. | Large price figure on the card. |
| `priceNote` | `string` | Yes | Small print under the price, e.g. `"eenmalig, excl. btw"` or `"offerte na intake"`. Plain text. | Caption under the price. |
| `features` | `string[]` | Yes | Ordered bullet list of what's included, plain text per item (no markup). Typically 3-4 items. | Rendered as a `→`-bulleted feature list; array order = display order. |
| `ctaLabel` | `string` | Yes | Button text, e.g. `"Kies landingspagina"` or `"Vraag offerte aan"`. | Call-to-action button label (the button itself always links to the contact section — the API does not control the link target). |
| `featured` | `boolean` | No (defaults to falsy if omitted) | Marks the "most chosen" package. **Exactly one package in the list should have this `true`** — the frontend doesn't enforce or dedupe this, it will just visually highlight and badge every package where it's true. | Adds the "Meest gekozen" badge and a visually distinct card style. |
This mirrors the existing `PackageCard` domain entity documented in
[domain-entities.md](domain-entities.md) — that table is the authoritative field list;
this doc adds the API/content-type framing on top of it.
### Seed / reference content
The three packages currently hardcoded in
[content.ts](../../../../../../../src/features/landing/data/content.ts) (`pakket_01`
Landingspagina, `pakket_02` Website, `pakket_03` Maatwerk) are the real, currently-live
copy (per business rule BR-4, content fidelity matters — see
[business-rules.md](business-rules.md)). Use that file as the seed data / source of
truth when populating the CMS for the first time, so the site's content doesn't change
the moment this endpoint goes live.
### Caching behaviour on the frontend (informational)
The frontend uses TanStack Query with default settings (query key `['packages']`, no
custom `staleTime`) — it refetches on every mount/window-refocus rather than caching
indefinitely. No `Cache-Control`/ETag support is required from the API for correctness,
but adding reasonable HTTP caching is welcome since this content changes rarely.
## Open items for the API implementer
- [ ] Add the `/api/v1/` nginx proxy location for **both** test and production (see
"Same domain" note above) — without this, the relative fetch has nothing to
reach even once the backend exists.
- [ ] Decide/confirm hosting for the backend + CMS (not decided by this hand-off).
- [ ] Seed initial content from `content.ts` (see "Seed / reference content" above).