— Artikel — № 070

070 —AI

AI scopen op een WordPress uit 2014: nuchtere methode

Een klant vraagt om 'gewoon een kleine AI-functie' op een WordPress-site uit 2014. Dit is de scoping-methode die voorkomt dat je blind offreert op PHP 5.6.

Bovenaanzicht van papieren AI-scopeblad, PHP 5.6-auditvel, manilamap wp-content, messing plaat, rode lakzegel.
Hero · gestileerd stilleven№ 070

De Loom kwam binnen om 23:41 op een dinsdag. Een oprichter van een Nederlands bureau van vijf man, pratend over een gedeeld scherm van de wp-admin van zijn klant, met de woorden die elke freelancer dit jaar heeft gehoord: ze willen gewoon een kleine AI-functie, niks geks, misschien een chatbot op de productpagina's en iets dat de beschrijvingen herschrijft. De site is gebouwd in 2014. De themamap had functions.php op 2.400 regels. Advanced Custom Fields stond op v4. De header zei Generator: WordPress 4.9.8.

Een AI-functie scopen op zo'n verouderde site is geen technisch probleem in de eerste plaats. Het is een triageprobleem. De klant denkt dat hij om één ticket vraagt. Jij kijkt naar zes maanden aan opgestapelde keuzes die iemand anders heeft gemaakt, waarvan de helft tegen alles vecht wat je erop bouwt. Deze post is de scoping-methode die we gebruiken voordat we ook maar één uur offreren.

Het gesprek dat de klant denkt te voeren

De klant heeft ChatGPT productteksten zien schrijven. Hij heeft een concurrent een supportwidget zien toevoegen. Hij heeft gelezen dat de API van OpenAI fracties van een cent per call kost. Vanuit zijn stoel is de vraag redelijk: voer de productcatalogus aan een model, krijg betere beschrijvingen, misschien een chat-dingetje ernaast. Hij begroot twee tot vier duizend euro en twee weken.

Het gesprek dat jij zo gaat voeren gaat over PHP-versiebeperkingen, een database die twee keer gemigreerd en één keer hernoemd is, een options-tabel met 14.000 autoloaded rijen, een caching plugin die REST-routes onderschept, en de vraag of de host überhaupt uitgaande HTTPS naar een third-party endpoint toelaat zonder whitelist-aanvraag. Niets hiervan staat in de briefing.

De scoping-methode bestaat zodat je dat tweede gesprek kunt voeren zonder de klant het project uit te jagen. Hij loopt in vier rondes.

Ronde één: de read-only audit

Voor elke offerte, voor elke architectuurschets, besteed je twee uur read-only aan de live site. Niet staging. De live site is de waarheid; staging is wat iemand in 2019 heeft gekopieerd. Je beantwoordt vijf vragen, in deze volgorde.

De daadwerkelijke runtime

Plaats een phpinfo.php op een niet-publiek pad, open hem, verwijder hem. Je wilt de PHP-versie, de geladen extensies, memory_limit, max_execution_time, en of allow_url_fopen aanstaat. Een site uit 2014 draait vaak nog op PHP 7.4 of, erger, 5.6 met een hostingpaneel dat upgrades stilletjes weigert omdat een plugin in mu-plugins boven 7.2 stuk gaat.

Als je iets onder PHP 8.1 draait, installeren de OpenAI PHP-clients waar je als eerste naar zou grijpen niet. De officiële openai-php/client vereist 8.1+. Je kunt je eigen versie bouwen met wp_remote_post, maar dat is nu een regel op de offerte, geen aanname.

De vorm van de database

Maak verbinding met MySQL. Voer drie queries uit en schrijf de antwoorden op.

SELECT COUNT(*) FROM wp_posts WHERE post_status = 'publish';
SELECT COUNT(*) FROM wp_options WHERE autoload = 'yes';
SELECT table_name, ROUND(data_length/1024/1024,1) AS mb
  FROM information_schema.tables
  WHERE table_schema = DATABASE()
  ORDER BY data_length DESC LIMIT 10;

Als de autoload-telling van wp_options boven de 1.000 zit, sleept elke page load nu al. Is wp_postmeta met grote afstand de grootste tabel, dan slaat ACF waarschijnlijk een geserialiseerde blob per post op en kun je er niet op queryen zonder LIKE '%...%'. Beide feiten veranderen hoe je de datalaag van de AI-functie ontwerpt.

functions.php en mu-plugins

Open wp-content/themes/<active>/functions.php en wp-content/mu-plugins/. Je zoekt naar filters op the_content, hooks op save_post, custom REST-routes, en alles dat curl_exec of file_get_contents tegen een externe host noemt. Hier wonen de verrassingen van de vorige developer. Een save_post-hook die vier andere hooks afvuurt is de reden dat je 'beschrijving regenereren'-knop een time-out krijgt.

Egress-policy van de host

Sommige shared hosts blokkeren uitgaande HTTPS naar alles behalve hun eigen update-servers. Test het vanaf de machine zelf, niet vanaf je laptop.

curl -sS -o /dev/null -w "%{http_code}\n" https://api.openai.com/v1/models \
  -H "Authorization: Bearer $KEY"

Een 401 is prima. Een timeout betekent dat je een whitelist-ticket nodig hebt, wat bij sommige Nederlandse hosts twee weken duurt.

De .htaccess-werkelijkheid

Lees de root .htaccess. Als er een caching-laag is die requests herschrijft voordat ze PHP raken, wordt je nieuwe REST-endpoint de eerste keer als HTML gecachet, en alle volgende calls krijgen stale JSON terug. De fix is één blok, maar alleen als je weet dat hij erbij hoort.

<IfModule mod_rewrite.c>
  RewriteRule ^wp-json/myplugin/ - [L]
</IfModule>

Ronde twee: de functie, vertaald

Vertaal nu de vraag van de klant naar het kleinste concrete ding dat live kan. 'Een chatbot op de productpagina's' is geen werkpakket. 'Een floating widget op single-product templates die POST'ed naar /wp-json/agency/v1/chat, die proxiet naar een model met een system prompt van 1.200 tokens, beperkt tot de titel, beschrijving en drie custom fields van dat product' is een werkpakket.

Doe dit op papier, in hun taal, voordat je over uren praat. Twee kolommen: wat de klant zei, wat dat betekent in de codebase. De vertaling is wat ze daadwerkelijk kopen. Tekenen ze niet op de vertaling, dan ben je niet klaar om te offreren.

Ronde drie: de failure modes waar je voor offreert

De reden dat kleine AI-functies op oude sites over budget gaan is niet het AI-stuk. Het is de afhandeling van fouten. Hiervoor offreer je expliciet, bij naam, als regels op de offerte.

  • Het model is traag of down. Je endpoint heeft een timeout nodig onder de max_execution_time van de host, plus een nette fallback. Op een host-limiet van 30 seconden zet je de HTTP-client op 20 seconden en geef je bij timeout een gecachete of vooraf bedachte respons terug.
  • De cache-plugin cachet de AI-respons. WP Rocket, W3 Total Cache en LiteSpeed cachen allemaal vrolijk een REST-respons alsof hij statisch is. Je sluit de route uit, of je stuurt Cache-Control: no-store vanuit PHP.
  • De content-filter herschrijft de output. Als de site iets draait dat op the_content haakt (Yoast, een vertaalplugin, een eigen shortcode-parser), gaat je AI-gegenereerde tekst er doorheen. Test de round-trip voor je WYSIWYG belooft.
  • De databaseschrijfactie blaast een bestaande hook op. Een AI-gegenereerde beschrijving opslaan via wp_update_post triggert save_post, wat op een site uit 2014 vier andere dingen kan triggeren, waarvan één een e-mail verstuurt. Gebruik wp_update_post( $args, true, false ) met het derde argument op skip-hooks, of schrijf rechtstreeks naar wp_posts met een $wpdb->update-call en een duidelijke comment over waarom.
  • De API-key lekt. Als de developer van de klant de productie wp-config.php naar een staging-box kopieert, gaat de key mee. Sla hem alleen op via wp-config-constanten, nooit in de options-tabel, en roteer bij overdracht.

Elk van die punten is een alinea in het voorstel. De klant hoeft ze niet te begrijpen, maar hij moet zien dat jij ze begrijpt. Dit is wat een offerte die houdt onderscheidt van een offerte die in week drie verdubbelt.

Ronde vier: de version-history-vraag

De laatste ronde is degene die de meeste freelancers overslaan. Voordat je een AI-functie live zet die naar de database schrijft, beslis je hoe je het ongedaan maakt. Het model gaat op een gegeven moment een productbeschrijving genereren waar de klant een hekel aan heeft, of erger, die fout is. Als je enige rollback 'herstel de back-up van gisteravond' is, ben je zojuist degene geworden die een dag aan bestellingen kwijtraakte om een typfout te fixen.

Bij greenfield-werk is dit triviaal. Op een site uit 2014 is het een ontwerpkeuze. De opties: schrijven naar een custom tabel en pas spiegelen naar wp_posts bij goedkeuring, een snapshot van de originele post_content in postmeta voor elke schrijfactie, of bovenop een tool zitten die je een version history geeft voor elke bestands- en rijwijziging. Kies er één, prijs het, en zet het in het voorstel.

Toen we Pier bouwden liepen we hier precies tegenaan bij de Magento 1-shop van een klant: een AI-rewrite-job die 800 productbeschrijvingen aanraakte en zes uur later deels teruggedraaid moest worden. De manier waarop we het uiteindelijk hebben aangepakt was elke wijziging, bestand of rij, undo-baar maken vanuit één history-view, zodat de rollback een klik werd in plaats van een restore. De MySQL editor wordt nu standaard met die functie ingebouwd geleverd vanwege dat incident.

Wat je vandaag kunt doen

Heb je nu een AI-functievraag in je inbox staan, doe dan ronde één voordat je antwoordt. Twee uur, read-only, de vijf vragen hierboven. Je stuurt of een offerte terug die je vertrouwt, of een mailtje van één alinea waarin staat dat de site eerst een PHP-upgrade nodig heeft. Beide is beter dan de offerte die je vanavond zou hebben verstuurd.

— Vragen —

Waarom niet gewoon een vaste prijs offreren voor een kleine AI-functie?

Omdat op een codebase uit 2014 de AI-integratie zelden het dure stuk is. Het dure zit in PHP-versiebeperkingen, egress-regels van de host, en interferentie van cache-plugins, en geen daarvan is zichtbaar vanuit een briefing.

Is PHP 7.4 genoeg om moderne AI-clients te draaien?

Voor de officiële openai-php/client niet. Die vereist PHP 8.1+. Je kunt terugvallen op wp_remote_post rechtstreeks tegen de REST API, maar offreer dat als bewuste keuze, niet als omissie.

Mag AI-gegenereerde content direct naar wp_posts schrijven?

Alleen als je een rollback-pad hebt. Maak voor de schrijfactie een snapshot van de originele post_content naar postmeta, of stage de wijzigingen in een custom tabel en promoot pas bij goedkeuring.