Ga naar inhoud

Bestaand API-oppervlak

Doel & URL('s)

Dit document beschrijft het bestaande, werkende API-oppervlak van de WordPress-installatie zoals dat vandaag door de Waarheidsvriend-app wordt gebruikt en als startpunt dient voor de Next.js-rebuild. Peildatum: 2026-08-06.

Wat URL
REST-basis https://<host>/wp-json/
OpenAPI 3.1-spec (machineleesbaar) GET /wp-json/custom/v1/openapi (publiek)
Swagger UI (interactieve documentatie) /api-docs (publiek; achter login te zetten via filter gb_api_docs_require_login)

Omgevings-eigenaardigheid lokaal

De gb-*-mu-plugins staan in web/app/mu-plugins/ en zijn niet actief op de lokale omgeving (localhost:8080 draait met WP_CONTENT_DIR = wp-content/). Alle gb-app/v1- en custom/v1-gedrag is dus alleen te testen tegen dev.gereformeerdebond.nl (of staging/prod). De thema-logica (401-afscherming, CPT's) draait wél lokaal.

Opbouw: de 17 endpoints

Auth-opties: Bearer-JWT (uit POST /gb-app/v1/login; geldt via determine_current_user óók op wp/v2) of WP Application Password (HTTP Basic). Alle afgeschermde content-endpoints geven zonder authenticatie 401 rest_forbidden.

Auth (gb-app/v1)

# Endpoint Methode Parameters Responses
1 /wp-json/gb-app/v1/login POST body: email (verplicht), wachtwoord (verplicht; alias password) 200 TokenPair · 401 generiek "Onjuiste inloggegevens" · 429 te veel pogingen
2 /wp-json/gb-app/v1/refresh POST body: vernieuwbewijs (verplicht; alias refresh_token) — token roteert, het oude vervalt 200 TokenPair · 401
3 /wp-json/gb-app/v1/logout POST body: vernieuwbewijs (verplicht) altijd 200 {ok:true} (onthult niet of het bewijs bestond)
4 /wp-json/gb-app/v1/me GET Bearer vereist 200 {id, email, display_name, is_subscriber} · 401

Drukversie / PDF (gb-app/v1)

# Endpoint Methode Parameters Responses
5 /wp-json/gb-app/v1/issues/{id}/pdf-link GET path id (integer). Auth: Bearer of app-password; rol wv-subscriber vereist 200 {url, expires_in} (standaard 300 s) · 401 · 403 gb_app_no_subscription · 404 (gb_app_issue_not_found / gb_app_no_pdf)
6 /wp-json/gb-app/v1/issues/{id}/pdf GET path id; query uid (user-id), exp (unix-tijd), sig (HMAC-SHA256). De handtekening ís de autorisatie, geen header nodig 200 application/pdf (inline, ondersteunt Range/206) · 302 (redirect-modus) · 403 (ongeldig/verlopen/geen abonnement) · 404

Gedragsdetails: link-TTL 5 min (filter gb_app_pdf_link_ttl); handtekening over issue_id|user_id|exp met sleutel secret + '|' + wachtwoord-hash → links vervallen bij wachtwoordwijziging; abonnement wordt bij uitlevering opnieuw gecontroleerd; headers o.a. Content-Disposition: inline, CSP frame-ancestors op de CORS-allowlist, X-Robots-Tag: noindex. Range-requests (HTTP 206) worden ondersteund, ook doorgeschakeld naar Azure Blob, zodat PDF.js per pagina laadt.

Content (wp/v2)

Gedeelde lijstparameters: page (default 1), per_page (default 10, max 100), search, orderby (default date), order (asc/desc, default desc), _embed.

# Endpoint Extra parameters Responseschema (kern)
7 GET /wp-json/wp/v2/issues id, date, slug, link, title.rendered, gb_locked, acf.{issue_number, issue_cover, issue_file}issue_file alleen voor abonnees
8 GET /wp-json/wp/v2/issues/{id} idem
9 GET /wp-json/wp/v2/wv-articles article_issue (int ≥ 1; onbekend/concept → lege lijst; niet-numeriek → 400) · article_author (int ≥ 1; zelfde gedrag) id, date, slug, link, title, content (leeg + protected:true voor niet-abonnees), excerpt, gb_locked, article_author_data{id,name,bio,image,link}, acf.{article_type, article_subtitle, article_intro, article_image, article_image_as_cover, article_author[], article_issue, article_category}
10 GET /wp-json/wp/v2/wv-articles/{id} idem
11 GET /wp-json/wp/v2/author id, slug, title, acf.{name, image, description} (description = bio-HTML)
12 GET /wp-json/wp/v2/author/{id} idem
13 GET /wp-json/wp/v2/media/{id} id, source_url, mime_type, media_details (incl. alle image sizes)
14 GET /wp-json/wp/v2/wv_article_categories id, name, slug, parent, count
15 GET /wp-json/wp/v2/faith-questions sorteren op redactievolgorde: ?orderby=menu_order&order=asc id, title.rendered (= de vraag), menu_order, acf.{faith_question_summary, faith_question_term, faith_question_category, faith_question_articles} (elk …|false)
16 GET /wp-json/wp/v2/faith-questions/{id} idem
# Endpoint Parameters Opmerking
17 GET /wp-json/menus/v1/menus/{id} path id = menu-slug of -id plugin wp-rest-api-v2-menus; geen schema in de OpenAPI-spec

Zie § menus/v1 voor de bijbehorende collectie-/locatie-routes en de productiestatus.

Databronnen

Bron Details
CPT's in REST wv-article (wv-articles, 8.728), tr-article (tr-articles, 49), cgc-article (cgc-articles, 35), issue (issues, 205), author (author, 1.005), faith-question (faith-questions), page (pages, 54), attachment (media)
Taxonomieën in REST wv_article_categories, tr_article_categories, cgc_article_categories, categories, tags
ACF-groepen in REST Artikelen, Auteur, Uitgaven, Geloofsvraag (show_in_rest=1); alle overige groepen niet
Artikelbody ACF flexible content components met layouts: text, text_image, frame, quote, gallery, call_to_action, donation, video, downloads, paywall (marker), table. Een headless renderer moet al deze layouts aankunnen
SEO elk REST-object bevat yoast_head (HTML-string) en yoast_head_json (og/twitter/canonical/robots/schema.org), ook op terms en pages

Gedrag & rollen (auth-model)

JWT (gb-app/v1) + Application Passwords

  • Access-token TTL 1 uur (filter gb_app_access_ttl); refresh-token 90 dagen (gb_app_refresh_ttl), max 10 actieve refresh-tokens per gebruiker. Geheim = wp_salt('auth'); het token bevat de wachtwoord-hash zodat álle tokens vervallen bij wachtwoordwijziging.
  • Bearer wordt op alle REST-routes geaccepteerd (determine_current_user, prio 20); een ongeldig token maakt de request anoniem (en loopt dan tegen de 401-afscherming aan).
  • Rate limiting login: max 5 pogingen per 15 minuten, per e-mail én per IP (transients); succesvolle login wist alleen de e-mailteller. Nginx heeft daarnaast limit_req (1 r/s) op login — er is géén rate-limit op de rest van wp-json.

Thema-afscherming: anonieme 401

rest_pre_dispatch (thema, app/filters.php) geeft 401 rest_forbidden ("Authenticatie vereist.") aan iedere niet-ingelogde request op wp/v2-routes van: wv-articles, cgc-articles, tr-articles, issues, author, faith-questions, wv_article_categories, cgc_article_categories, tr_article_categories.

Open zonder auth blijven o.a.: /wp/v2/pages, /wp/v2/media, /wp/v2/search, /wp/v2/categories, /wp/v2/tags, custom/v1/openapi, gb-app/v1/* en menus/v1/* (dev).

/wp/v2/search lekt titels

/wp/v2/search?search=… is publiek en geeft wél titels + URL's van wv-article-posts terug (met subtype), ook anoniem — alleen de detail-endpoints zijn dicht.

Rol-afscherming: gb_locked

Voor geauthenticeerde gebruikers zonder rol wv-subscriber (en voor iedereen met een service-account dat die rol mist):

Resource Veld Gedrag zonder wv-subscriber
wv-article gb_locked boolean op elk artikel; content{rendered:'', protected:true}; acf.article_components én acf.components verwijderd (artikelbody). Chapeau/titel/lead/auteur/beeld blijven staan
issue gb_locked boolean; acf.issue_file (PDF-verwijzing) verwijderd

Dit gb_locked-model is het fundament voor de paywall in de rebuild: teaser-data is er altijd, de body alleen voor abonnees.

Rollenoverzicht per endpoint

Endpoint(s) Anoniem Ingelogd (geen abonnee) wv-subscriber
gb-app/v1/login, refresh, logout
gb-app/v1/me 401 ✓ (is_subscriber:false) ✓ (is_subscriber:true)
wp/v2/wv-articles e.a. afgeschermde types 401 ✓, maar gb_locked-velden gestript ✓ volledig
gb-app/v1/issues/{id}/pdf-link 401 403 gb_app_no_subscription 200
custom/v1/openapi, /api-docs, wp/v2/search, wp/v2/pages, wp/v2/media

CORS-situatie

Functie GB\ApiDocs\allowed_origins() vervangt de WP-default (die elke origin terugkaatst) op rest_api_init prio 15:

  • Altijd toegestaan: https://app.gereformeerdebond.nl, https://app-dev.gereformeerdebond.nl.
  • Alleen buiten productie: http://localhost:8081 (Expo dev), http://localhost:19006 (Expo web), http://localhost:3000 (web-frontend dev).
  • Uitbreidbaar via filter gb_api_allowed_origins.

Headers bij match: Access-Control-Allow-Origin: <origin>, Allow-Methods: GET, POST, OPTIONS, Allow-Headers: Authorization, Content-Type, X-WP-Nonce, X-Requested-With, Range, Expose-Headers: X-WP-Total, X-WP-TotalPages, Link, Allow-Credentials: true, Max-Age: 600, altijd Vary: Origin. Elke OPTIONS-preflight mét Origin slaagt altijd met 204 (rest_pre_dispatch prio 99 — anders blokkeerde de thema-afscherming ook de preflight).

Rebuild-domein staat niet in de allowlist

Het toekomstige Next.js-domein (bijv. www.gereformeerdebond.nl) staat niet in de allowlist. Server-side fetches (SSR/ISR) hebben geen CORS nodig; client-side browser-calls wél → allowlist uitbreiden via gb_api_allowed_origins (zie gap-analyse).

Plugin wp-rest-api-v2-menusactief op dev, maar niet in de active_plugins van de productie-DB-snapshot (op prod dus vermoedelijk niet actief; activeren is een rebuild-voorwaarde, zie gap-analyse).

Publiek getest op dev:

Route Levert
GET /wp-json/menus/v1/menus alle 5 menu's
GET /wp-json/menus/v1/locations locaties: GB_primary_navigation → menu 12, WV_primary_navigation → 19, TR_primary_navigation → 20, CGC_primary_navigation → 18
GET /wp-json/menus/v1/menus/{id} menu incl. items (2 niveaus)

Menu's in de DB: Gereformeerde Bond (12, 26 items), Waarheidsvriend (19, 11), Theologia Reformata (20, 4), CGC (18, 17), Primary (2, legacy/leeg).

Yoast REST-oppervlak

  • yoast_head + yoast_head_json op alle REST-zichtbare objecten (posts, pages, terms): og-/twitter-tags, canonical, robots, schema.org. Let op: canonicals/og:url's wijzen naar de WP-URL en moeten in de rebuild herschreven worden.
  • GET /wp-json/yoast/v1/get_head?url=… — publiek; levert de volledige Yoast-<head>-HTML voor een willekeurige site-URL. Bruikbaar als SEO-bron voor content die (nog) niet via wp/v2 loopt.
  • Sitemaps: /sitemap_index.xml (200 op prod) wordt door WP/Yoast gegenereerd — proxy- of hergeneratiestrategie: zie gap-analyse.

Gravity Forms REST v2 (gf/v2)

  • Namespace actief (lokaal én dev). Web API-instellingen: enabled=1, impersonate user 23.
  • Auth: GF API-keys via HTTP Basic (consumer key/secret); Application Passwords en cookies werken ook. Bestaande keys: 3× "dynamics" (read_write), 2× "gb-mail-overzicht" (read).
  • Publiek zonder auth: POST /wp-json/gf/v2/forms/{id}/submissions — een headless frontend kan hiermee formulieren insturen; bij validatiefouten komt is_valid:false + de volledige formulierdefinitie + veldfouten terug.
  • GET /wp-json/gf/v2/forms → 401 zonder key; formulierdefinities ophalen vereist een read-key.
  • 39 actieve formulieren (contact, lid worden, abonnementen, donaties incl. Mollie/iDEAL, registratie, account, nieuwsbrief). Add-ons Mailchimp, Mollie en User Registration draaien server-side op de submission — betaal-redirects en registratieflows werken dus niet vanzelf headless (zie gap-analyse).

Overige aanwezige namespaces

Lokaal (productie-DB): oembed/1.0, advanced-ads/v1 (alleen admin-routes), redirection/v1 (auth vereist), yoast/v1, wp-all-import/v1, wp/v2, gf/v2. Dev daarbovenop: custom/v1, gb-app/v1, menus/v1, code-snippets/v1, wp-abilities/v1.

WPGraphQL: onbruikbaar én onbedoeld open

/graphql werkt lokaal anoniem (omzeilt de REST-401-afscherming; artikelbody lekt niet omdat ACF niet in het schema zit), maar de drie article-CPT's hebben hetzelfde graphqlSingleName: "article" (registratie defect). Live geven prod en dev HTML terug i.p.v. JSON. Besluit voor de rebuild: verwijderen of volledig opnieuw inrichten — zie gap-analyse.

Acceptatiecriteria

Testbaar met Playwright (API request context) tegen dev/staging; <host> = omgeving met actieve mu-plugins.

  • Als anonieme client op GET <host>/wp-json/custom/v1/openapi dan HTTP 200 met een OpenAPI 3.1-document (titel "Gereformeerde Bond — Headless app-API").
  • Als anonieme client op GET <host>/api-docs dan HTTP 200 met een Swagger UI-pagina.
  • Als anonieme client op POST /wp-json/gb-app/v1/login met geldig email+wachtwoord van een abonnee, dan 200 met access- én refresh-token.
  • Als anonieme client op POST /gb-app/v1/login met onjuist wachtwoord, dan 401 met generieke melding "Onjuiste inloggegevens" (geen indicatie of het account bestaat).
  • Als client die 6× binnen 15 minuten fout inlogt op hetzelfde e-mailadres, dan bij poging 6 een 429.
  • Als client op POST /gb-app/v1/refresh met een geldig vernieuwbewijs, dan 200 met een nieuw tokenpaar én het oude vernieuwbewijs werkt daarna niet meer (rotatie).
  • Als abonnee (Bearer) op GET /gb-app/v1/me, dan 200 met is_subscriber:true; als ingelogde niet-abonnee dan is_subscriber:false.
  • Als anonieme client op GET /wp-json/wp/v2/wv-articles dan 401 rest_forbidden; idem voor issues, author, faith-questions, wv_article_categories, tr_article_categories, cgc_article_categories, tr-articles, cgc-articles.
  • Als anonieme client op GET /wp-json/wp/v2/pages, /wp/v2/media/{id}, /wp/v2/search?search=x dan 200 (géén afscherming).
  • Als ingelogde niet-abonnee (Bearer) op GET /wp/v2/wv-articles/{id} van een afgeschermd artikel, dan 200 met gb_locked:true, content.rendered leeg + content.protected:true, en zonder acf.components/acf.article_components; titel, article_subtitle, article_intro, article_image en article_author_data zijn wél aanwezig.
  • Als wv-subscriber (Bearer) op hetzelfde artikel, dan 200 met gevulde acf.components.
  • Als wv-subscriber op GET /wp/v2/issues/{id}, dan bevat acf.issue_file een waarde; als niet-abonnee dan ontbreekt issue_file.
  • Als client op GET /wp/v2/wv-articles?article_issue={geldig-id}, dan alleen artikelen van die uitgave; met een niet-bestaand id een lege lijst; met article_issue=abc een 400.
  • Als client op GET /wp/v2/wv-articles?article_author={geldig-id}, dan alleen artikelen van die auteur (geen false positives op deel-id's, bijv. 6064 matcht niet 16064).
  • Als wv-subscriber op GET /gb-app/v1/issues/{id}/pdf-link, dan 200 met {url, expires_in:300}; de URL levert binnen de TTL een application/pdf (inline) op; ná de TTL een 403.
  • Als ingelogde niet-abonnee op GET /gb-app/v1/issues/{id}/pdf-link, dan 403 gb_app_no_subscription; anoniem 401.
  • Als client op de getekende PDF-URL met een Range-header, dan HTTP 206 met het gevraagde bytebereik.
  • Als client op GET /wp-json/menus/v1/menus/12 (dev), dan 200 met de 26 GB-menu-items; /menus/v1/locations bevat de vier *_primary_navigation-locaties.
  • Als browser-client met Origin: https://app.gereformeerdebond.nl een OPTIONS-preflight doet op een willekeurige wp-json-route, dan 204 met Access-Control-Allow-Origin gelijk aan die origin.
  • Als browser-client met een onbekende origin, dan geen Access-Control-Allow-Origin-header in de respons.
  • Als anonieme client op POST /wp-json/gf/v2/forms/{id}/submissions met lege verplichte velden, dan 200 met is_valid:false en veldvalidatiefouten; GET /wp-json/gf/v2/forms zonder key geeft 401.
  • Als client op GET /wp-json/yoast/v1/get_head?url=<site-url>, dan 200 met de Yoast-head-HTML voor die URL.
  • Elke wp/v2-respons van artikelen/uitgaven/auteurs bevat yoast_head_json met minimaal title, canonical en og_image (waar van toepassing).

Bekende afwijkingen

Niet nabouwen in Next.js — wel vermelden in het testrapport

  • WPGraphQL staat lokaal anoniem open en omzeilt de REST-afscherming; de drie article-types delen één GraphQL-naam (defecte registratie). Live levert /graphql HTML. → Saneren, niet repliceren.
  • /wp/v2/search geeft anoniem titels/URL's van afgeschermde wv-article-posts terug terwijl de detail-endpoints 401 geven — inconsistentie in de afscherming.
  • menus/v1 is niet actief op productie (wel op dev) — de rebuild vereist activering (zie gap-analyse); het huidige ontbreken is een omgevingsafwijking, geen feature.
  • CORS-allowlist bevat het toekomstige sitedomein niet; dit is een bekende beperking, geen bug.
  • Debug-advertentie #16280 (Advanced Ads, allow_php=1 met var_dump($_SERVER)) staat nog in de database; nergens geplaatst, wel opruimen.