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
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:
+112
@@ -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).
|
||||
Reference in New Issue
Block a user