Gap-analyse datalaag (rebuild)¶
Doel & context¶
Dit document benoemt wat er ontbreekt om de volledige site headless na te bouwen in Next.js, en per gap: de gekozen oplossing en waar die gebouwd wordt (WP mu-plugin of Next.js). Basis: het bestaande API-oppervlak dekt de Waarheidsvriend-app (artikelen, uitgaven, auteurs, geloofsvragen, auth, PDF), maar de site toont veel meer content en gedrag.
Architectuurkeuze (uitgangspunt voor alle gaps):
Gekozen model: Next.js fetcht server-side met een service-account
De REST-API geeft anoniem 401 op alle artikel-/uitgave-endpoints (thema-afscherming). We versoepelen die afscherming niet. In plaats daarvan doet Next.js alle content-fetches server-side (SSR/ISR/route handlers) met een service-account (WP Application Password, rol met leesrechten maar zónder wv-subscriber). Daardoor krijgt Next.js van afgeschermde artikelen precies de teaser-variant (gb_locked:true, body gestript) en bouwt het de paywall zelf. Voor ingelogde abonnees wisselt Next.js naar het JWT van de gebruiker (gb-app/v1/login) zodat de volledige body meekomt. Browser-clients praten nooit rechtstreeks met afgeschermde wp/v2-routes.
Consequenties:
- Geen CORS nodig voor content (server-side); wél allowlist-uitbreiding voor de client-side calls die overblijven (login, GF-submissions) — zie G15.
- De service-account-credential is een server-secret in Next.js (env var), nooit in de browser.
- De WP-kant blijft de enige bron van waarheid voor gb_locked/rolcontrole; Next.js beslist alleen over weergave.
Overzicht gaps¶
| # | Gap | Oplossing | Waar |
|---|---|---|---|
| G1 | Anonieme 401-blokkade op artikel-endpoints | Server-side fetches met service-account (zie kader hierboven) | Next.js |
| G2 | Nieuws (gb-post/wv-post/cgc-post, 99 posts) niet in REST |
show_in_rest aanzetten + REST-veld voor ACF-zichtbaarheidsveld Websites |
WP mu-plugin |
| G3 | Agenda (gb-event/wv-event/cgc-event, 100 events) niet in REST |
show_in_rest aanzetten + ACF-groep "Evenementen" in REST |
WP mu-plugin |
| G4 | Subsite-pagina's (wv-page/tr-page/cgc-page, 114) + gb-theme-page niet in REST |
show_in_rest aanzetten + ACF-groep "Content" (flexibele opbouw) in REST |
WP mu-plugin |
| G5 | Legacy core post (125 items, verborgen maar bereikbaar) |
Keep/kill-besluit; bij behoud: in REST ontsluiten of redirecten | Besluit + WP mu-plugin óf Next.js-redirects |
| G6 | Producten/boekenshop (product, 147 + product_category) niet in REST |
show_in_rest aanzetten voor CPT + taxonomie + ACF-groepen Producten |
WP mu-plugin |
| G7 | ACF-paginacomponenten (Content, CTA, Galerij, Downloads, Video, Donatie, Nieuws, …) niet in REST | per veldgroep show_in_rest=1 (ACF-JSON) |
WP (acf-json in thema/mu-plugin) |
| G8 | ACF-options / thema-instellingen (vm_get_option) niet ontsloten |
Eigen endpoint custom/v1/options/{subsite} |
WP mu-plugin |
| G9 | Menu-API niet actief op prod | Plugin wp-rest-api-v2-menus activeren op prod |
WP (pluginactivatie) |
| G10 | Subsite-context (GB/WV/TR/CGC) heeft geen API | Routing-model repliceren op URL-prefix | Next.js |
| G11 | Zoekfunctie dekt headless niet hetzelfde als de site-zoek | Eigen zoek-endpoint met subsite-filter | WP mu-plugin + Next.js-UI |
| G12 | Sitemap/SEO: canonicals wijzen naar WP; sitemaps op WP-domein | SEO-proxy + URL-herschrijving | Next.js (+ Yoast-config) |
| G13 | Previews voor de redactie ontbreken headless | Next.js draft mode + preview_post_link-filter |
Next.js + WP mu-plugin |
| G14 | Gebruikersflows (registratie, account, wachtwoord, betalingen) alleen via GF | GF REST v2-submissions + per formulier toets | Next.js (formulier-UI) + WP (bestaand) |
| G15 | CORS: nieuw sitedomein niet in allowlist | Allowlist uitbreiden via filter gb_api_allowed_origins |
WP mu-plugin |
| G16 | Real3D Flipbook (4 stuks) heeft geen API | Vervangen door PDF.js-route van de app, of saneren | Besluit + Next.js |
| G17 | Advanced Ads: geen serve-API (1 shortcode-ad live) | Geen ad-server-integratie; inline HTML-blok renderen | Next.js |
| G18 | Redirects (mu-plugin domain-redirects + Redirection-DB-regels) | Migreren/synchroniseren naar middleware | Next.js middleware |
| G19 | Nieuwsbrief-inschrijving (GF → Listmonk-hook) | GF-submission via gf/v2 (hook vuurt server-side) |
Next.js (formulier) — WP ongewijzigd |
| G20 | Cookie-consent + GA4 | Opnieuw bouwen, GA4 aan consent koppelen | Next.js |
| G21 | Feeds/oEmbed (/feed/mailchimp e.a.) |
Mailchimp-feed behouden op WP; overige keep/kill | WP (bestaand) + besluit |
| G22 | WPGraphQL defect én onbedoeld open | Verwijderen (alles via REST) | WP (deactiveren) |
Uitwerking per gap¶
G1 — Anonieme 401-blokkade (fundamenteel)¶
De publieke site toont artikel-teasers, archieven en categorielijsten aan iedereen; de REST-API geeft anoniem 401 op wv-articles, tr-articles, cgc-articles, issues, author, faith-questions en de drie *_article_categories.
Gekozen oplossing (zie kader bovenaan): Next.js fetcht server-side met een service-account. Het alternatief — de afscherming versoepelen naar "anoniem = gb_locked-gedrag" — is functioneel haalbaar (de lock stript alleen de body) maar vergroot het publieke aanvalsoppervlak en is niet nodig zolang alle fetches server-side lopen. Waar: Next.js (datalaag/fetch-helper met token-switching: service-account voor anoniem, gebruikers-JWT voor ingelogde bezoekers).
- Er bestaat een WP-gebruiker "nextjs-service" met Application Password, leesrol zonder
wv-subscriber, en dat account kan alle afgeschermde collecties lezen (teaser-variant). - Geen enkele browser-request van de Next.js-site gaat rechtstreeks naar
wp/v2/wv-articlesc.s.
G2/G3/G4/G6 — CPT's zonder REST¶
Nieuws, agenda, subsite-pagina's en producten zijn publiek zichtbaar op de site maar hebben show_in_rest=false. Oplossing: één mu-plugin (werktitel gb-headless-rest.php) die via register_post_type_args voor deze types show_in_rest=true + rest_base zet, en de bijbehorende taxonomie product_category ontsluit. Aandachtspunten:
- Nieuws: het ACF-veld
Websites(zichtbaarheid per subsite; het archief filtert hierop metmeta LIKE '<KEY>') moet als REST-veld of ACF-in-REST mee, plus een collectieparameter (bijv.?website=WV) die hetzelfde filter toepast alsOverview::getNewsQueryArgs. - Agenda: ACF-groep "Evenementen" (datum/locatie) in REST; sortering op evenementdatum als collectieparameter.
- Subsite-pagina's zijn hiërarchisch:
parent-veld en volledige pad-slug nodig voor routing. - Producten: ACF-groepen "Producten"/"Product categorieën" in REST;
product_categoryals rest-taxonomie.
Waar: WP mu-plugin. De afscherming van §3.4 moet deze nieuwe rest_bases niet blokkeren (publieke content) — of wél, in lijn met G1 (service-account leest alles); vast te leggen bij implementatie: consistent kiezen voor afgeschermd + service-account.
G5 — Legacy core post¶
125 oude berichten zijn via /⟨slug⟩/ nog bereikbaar hoewel het type verborgen is. Oplossing: redactioneel keep/kill-besluit; standaardvoorstel: 301-redirect naar het nieuwsarchief voor de bekende slugs, geen REST-ontsluiting. Waar: Next.js middleware (redirect-tabel) — tenzij besloten wordt ze te behouden, dan als G2 behandelen.
G7 — ACF-paginacomponenten in REST¶
Voor pages komt een acf-sleutel mee, maar de inhoudsgroepen (Content/flexibele opbouw, CTA, Galerij, Downloads, Video, Donatie, Nieuws, …) hebben geen REST-vlag. Oplossing: per veldgroep show_in_rest: 1 in de ACF-JSON zetten. De headless renderer moet dezelfde flexible-content-layouts aankunnen als de artikelbody (text, text_image, frame, quote, gallery, call_to_action, donation, video, downloads, paywall, table). Waar: WP (acf-json), rendering in Next.js (één gedeelde componentrenderer).
G8 — ACF-options / thema-instellingen¶
Volledig niet ontsloten, wel overal nodig: per subsite (prefix GB_/WV_/TR_/CGC_) footer (kolommen, links, contact, social), contactgegevens, CTA/sidebar-banner, nieuwsbrief (titel/tekst/form_id), paywall-teksten (paywall_title, paywall_text, paywall_title_subscriber, paywall_text_subscriber, paywall_form_id), archiefverwijzingen (news_archive, events_archive, products_archive), fallback_image, sidebar_banner, homepage-toggles (display_categories_on_frontpage, display_digibron_search_on_frontpage, display_newsletter_form), topbar-links.
Oplossing: eigen endpoint GET /wp-json/custom/v1/options/{subsite} (subsite ∈ GB/WV/TR/CGC) dat de vm_get_option-waarden als één JSON-object levert, plus de menu-toewijzing per locatie. Cachebaar (geen persoonsdata). Waar: WP mu-plugin.
-
GET /custom/v1/options/WVlevert o.a. de vier paywall-teksten, footerdata en homepage-toggles van WV.
G9 — Menu-plugin op prod activeren¶
menus/v1 werkt op dev maar de plugin staat niet in de active_plugins van de prod-DB-snapshot. Oplossing: wp-rest-api-v2-menus op prod activeren (of de menu's opnemen in het options-endpoint van G8 — één van beide, voorkeur: pluginactivatie, want al getest op dev). Waar: WP (pluginactivatie, deploy-checklist).
-
GET https://gereformeerdebond.nl/wp-json/menus/v1/locationsgeeft 200 met de vier*_primary_navigation-locaties.
G10 — Subsite-context¶
Er is geen API die per URL vertelt welke subsite actief is; de site leidt dit af uit de URL-prefix (/de-waarheidsvriend/, /theologia-reformata/, /cornelis-graafland-centrum/, anders GB) met ?t=KEY-override (die tevens noindex zet). Oplossing: dit routing-model 1-op-1 repliceren in de Next.js-router (route-segmenten per subsite; thema-klasse per segment). Geen WP-werk nodig. Waar: Next.js.
G11 — Zoeken¶
Site-zoek is server-rendered (?s=…&t=KEY), beperkt tot de post types van de actieve subsite en zoekt via plugin acf-better-search óók in ACF-tekstvelden (de artikelbody). /wp/v2/search kent geen subsite-filter, dekt geen nieuws/agenda/producten (zolang G2–G6 niet gebouwd zijn) en zoekt niet in ACF. Oplossing: eigen endpoint GET /custom/v1/search?s=…&site={KEY}&page=… dat dezelfde pre_get_posts-logica en acf-better-search-dekking gebruikt als de huidige site, met X-WP-Total-headers. Waar: WP mu-plugin; UI en paginering in Next.js.
- Een term die alleen in de artikelbody (ACF
components) voorkomt, levert via het zoek-endpoint dezelfde treffer als de huidige site-zoek. -
site=WVlevert alleen WV-types;site=GBlevert gb-event, gb-post, wv-article én product (huidig gedrag).
G12 — Sitemap & SEO-proxy¶
yoast_head_json dekt REST-zichtbare content; voor al het overige bestaat publiek GET /wp-json/yoast/v1/get_head?url=…. Canonicals/og:url's wijzen naar de WP-URL. Oplossing:
- Next.js herschrijft bij het renderen alle URL's uit
yoast_head_jsonvan WP-host naar sitehost (één helper). /sitemap_index.xmlen onderliggende sitemaps: Next.js proxyt de Yoast-sitemaps en herschrijft de hosts (route handler), óf genereert eigen sitemaps uit de REST-collecties. Voorkeur: proxy + herschrijven (Yoast blijft bron).robots.txtin Next.js met verwijzing naar de eigen sitemap.
Waar: Next.js (route handlers); eventueel Yoast-instelling voor het canonicale domein in WP.
-
view-sourcevan een artikelpagina op het nieuwe domein bevat canonical + og:url op het nieuwe domein (niet de WP-host). -
GET <nieuw-domein>/sitemap_index.xmlgeeft 200 en alle URL's daarin wijzen naar het nieuwe domein.
G13 — Previews¶
Geen preview-tokens of draft-endpoints. Oplossing: Next.js draft mode: WP-filter preview_post_link laat "Voorbeeld" in wp-admin naar https://<site>/api/preview?secret=…&id=… wijzen; de Next.js-route zet draft mode aan en fetcht het concept met het service-account (dat edit_posts-context mag lezen) of een editor-token. Waar: Next.js (route) + WP mu-plugin (filter).
G14 — Gebruikersflows en formulieren¶
Login op de site kan op gb-app/v1 (JWT) draaien; registratie, accountbeheer, wachtwoord-reset en alle betaalflows lopen via Gravity Forms (User Registration-, Mollie- en Mailchimp-add-ons, server-side). POST /gf/v2/forms/{id}/submissions is publiek en triggert die add-ons. Oplossing: per formulier toetsen of de submissions-API volstaat (validatie-JSON komt terug); de Mollie-betaalredirect (confirmation-URL naar de checkout) moet uit de submission-respons worden gevolgd. Formulierdefinities (velden, conditionele logica) haalt Next.js op met een GF read-API-key (server-side, want GET /gf/v2/forms vereist auth). Waar: Next.js (formulier-rendering + submit-proxy via route handler zodat de API-key servergeheim blijft); WP ongewijzigd.
- Een submit van het contactformulier via de Next.js-site levert een entry + notificatie op precies zoals een submit op de huidige site.
- Een donatie-submit levert een redirect naar de Mollie-checkout op; na betaling landt de bezoeker op de bedankpagina van de nieuwe site.
G15 — CORS en rate limiting¶
Client-side calls die overblijven (login/refresh vanuit de browser, evt. GF-submits als die niet geproxyt worden): het nieuwe sitedomein toevoegen via filter gb_api_allowed_origins. Waar: WP mu-plugin (config-regel). N.B. nginx rate-limit bestaat alleen op login; overweeg een limiet op gf/v2/…/submissions (spam).
G16 — Real3D Flipbook¶
4 flipbooks op /?r3d=<slug> (jQuery-plugin, geen API); één ervan serveert een openbare Waarheidsvriend-PDF buiten de abonnee-afscherming (nginx beschermt alleen uploads/issues/). Oplossing: besluit met redactie: saneren of vervangen door de PDF.js-viewer + het beveiligde gb-app/v1/issues/{id}/pdf-mechanisme. Waar: besluit; implementatie Next.js. De open PDF is een bekende afwijking (niet repliceren).
G17 — Advanced Ads¶
Geen placements, geen groepen; één "plain" HTML-advertentie via shortcode in één artikel. Oplossing: geen ad-server-integratie; de shortcode-output komt als HTML mee in de artikelbody en wordt door de componentrenderer getoond. Debug-ad #16280 opruimen. Waar: Next.js (niets extra's), opruimen in WP.
G18 — Redirects¶
mu-plugin domain-redirects.php (oude domeinen → paden) + Redirection-plugin (DB-regels, REST alleen met auth). Oplossing: regels exporteren en opnemen in Next.js middleware/edge-redirects; de oude-domeinen-redirects verhuizen mee naar de DNS/edge-laag van de nieuwe site. Eventueel periodieke sync via redirection/v1 met service-account. Waar: Next.js middleware.
G19 — Nieuwsbrief¶
Inschrijving loopt via GF-formulier → server-side Listmonk-hook (gform_after_submission). Werkt headless zodra de submission via gf/v2 binnenkomt. Nieuwsbrief-edities (newsletter-edition) blijven een WP-interne e-mailproductie — buiten scope van de site-rebuild (afbakening vastleggen). Waar: Next.js (formulier), WP ongewijzigd.
G20 — Cookie-consent en GA4¶
Consent-banner (plugin) en GA4 (mu-plugin, gtag onvoorwaardelijk — niet consent-gated) zijn frontend-gedrag. Oplossing: opnieuw bouwen in Next.js; GA4 daarbij wél aan consent koppelen (AVG-verbetering; huidige niet-gekoppelde gedrag is een bekende afwijking, niet repliceren). Waar: Next.js.
G21 — Feeds en oEmbed¶
/?feed=mailchimp (custom feed t.b.v. Mailchimp-RSS-campagnes) heeft een aantoonbare afnemer en blijft op WP draaien (geen rebuild nodig zolang het WP-domein bereikbaar blijft voor Mailchimp). Overige feeds (/feed/, archief-feeds): keep/kill-besluit; standaard niet nabouwen. Waar: WP (bestaand) + besluit.
G22 — WPGraphQL¶
Defect (naamconflict drie article-types) én lokaal anoniem open; live levert het endpoint HTML. Oplossing: plugin verwijderen/gedeactiveerd houden; de rebuild gebruikt uitsluitend REST. Waar: WP (deactiveren + uit image).
Acceptatiecriteria (datalaag als geheel)¶
- Als Next.js-server met service-account kan élk publiek zichtbaar content-item van de site (pagina, artikel, nieuws, event, product, uitgave-teaser, auteur, categorie, menu, thema-instellingen) via een REST-endpoint worden opgehaald.
- Als anonieme browser-bezoeker van de nieuwe site is geen enkel WP-credential (service-account, GF-key) zichtbaar in netwerkverkeer of bundel.
- Als
wv-subscriberingelogd op de nieuwe site is de volledige artikelbody zichtbaar; als anonieme bezoeker de teaser + paywall-teksten uit de ACF-options van de betreffende subsite. - Als redacteur die in wp-admin op "Voorbeeld" klikt bij een concept-artikel, opent de Next.js-site het concept in draft mode.
- Als crawler op het nieuwe domein bevatten alle pagina's canonicals/og-tags op het nieuwe domein en is er een werkende sitemap.
Bekende afwijkingen¶
Niet nabouwen in Next.js — wel vermelden in het testrapport
- Open flipbook-PDF:
/?r3d=de-waarheidsvriendserveert een Waarheidsvriend-PDF zonder abonnee-check (G16). - GA4 zonder consent-koppeling: gtag laadt onvoorwaardelijk terwijl de cookiebanner analytische cookies belooft te gaten (G20).
- WPGraphQL anoniem open + defecte registratie (G22).
/wp/v2/search-titel-lek van afgeschermde artikelen (zie bestaand API-oppervlak).- Debug-advertentie #16280 met
var_dump($_SERVER)in de database (G17).