Ga naar inhoud

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-articles c.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 met meta LIKE '<KEY>') moet als REST-veld of ACF-in-REST mee, plus een collectieparameter (bijv. ?website=WV) die hetzelfde filter toepast als Overview::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_category als 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/WV levert 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/locations geeft 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=WV levert alleen WV-types; site=GB levert 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:

  1. Next.js herschrijft bij het renderen alle URL's uit yoast_head_json van WP-host naar sitehost (één helper).
  2. /sitemap_index.xml en 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).
  3. robots.txt in Next.js met verwijzing naar de eigen sitemap.

Waar: Next.js (route handlers); eventueel Yoast-instelling voor het canonicale domein in WP.

  • view-source van een artikelpagina op het nieuwe domein bevat canonical + og:url op het nieuwe domein (niet de WP-host).
  • GET <nieuw-domein>/sitemap_index.xml geeft 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.

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-subscriber ingelogd 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-waarheidsvriend serveert 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).