088 —Business
Pre-quote audit in vijf minuten: ls, curl en SQL counts
De vaste-prijs offerte die uitloopt tot drie weken migratie begint meestal met het overslaan van een audit van vijf minuten. Onze checklist voor legacy werk.
Een bureau stuurt om 23:41 een Loom door. De briefing: "kleine WordPress-fix op de checkout van een klant die niet doorgaat, kun je vandaag een offerte sturen?" Drie screenshots, een staging-URL, FTP-credentials in een 1Password-vault. De verleiding is om er een halve dag voor te quoten en door te gaan. Dat doen we niet, omdat we hebben geleerd wat de eerste vijf minuten na binnenkomst van de credentials ons kunnen besparen.
De pre-quote audit op een legacy site is geen discovery sprint. Het is het verschil tussen een bedrag dat je kunt verdedigen en een bedrag waar je spijt van krijgt. Hierna volgt de exacte sequence die we draaien voordat we een eurobedrag in een antwoordmail zetten. Vijf minuten, drie terminalvensters, nog geen app geïnstalleerd.
Eerst de directorylisting
Voordat je de briefing nog eens leest, vraag je de document root op. Niet in een file manager. In een shell, met timestamps.
ssh deploy@client-host.tld
cd /var/www/html
ls -la
ls -la wp-content/
ls -la wp-content/mu-plugins/ 2>/dev/null
ls -la wp-content/plugins/ | wc -l
du -sh wp-content/uploads/
Wat je zoekt staat niet in de briefing. Het is de vorm van de working tree. Een document root met radio.php, m.php, lock360.php of een ander PHP-bestand met een kleinletter-zelfstandig-naamwoord op het topniveau is bijna altijd een signaal van een compromittering. Een wp-config.php.bak naast wp-config.php is de ontslagbrief van de vorige developer. Een advanced-cache.php of object-cache.php drop-in in wp-content/ betekent een cachinglaag (W3 Total Cache, Redis Object Cache, LiteSpeed) waar je rekening mee moet houden iedere keer dat je gedrag aanpast.
De twee getallen die er nu al toe doen: hoeveel directories er in wp-content/plugins/ staan, en hoe groot wp-content/uploads/ is. Een site met 48 plugins en 31 GB aan uploads is een andere offerte dan een site met 9 plugins en 800 MB, ongeacht wat de briefing zegt.
Headers voor SQL
Open een tweede paneel. Sla de live-URL aan met curl, niet met een browser. Browsers liegen door te behulpzaam te zijn.
curl -sI https://client-site.tld/ | sort
curl -sI https://client-site.tld/wp-login.php
curl -s https://client-site.tld/wp-json/ | head -c 400
curl -sI https://client-site.tld/?p=1
Je leest de headers op vier dingen na. De PHP-versie (X-Powered-By: PHP/7.4.33 op een site die je in 2026 gaat quoten betekent dat PHP-end-of-life-werk in scope zit, zie de lijst met ondersteunde versies op php.net). De server (Apache/2.4.6 (CentOS) is EL7, ook EOL, wat het migratiegesprek verandert). De CMS-bevestiging (een Link: <https://client-site.tld/wp-json/>; rel="https://api.w.org/"-header is WordPress; X-Drupal-Cache is Drupal; X-Magento-Tags is Magento). En de afwezigheid van security headers, die het OWASP Secure Headers project beter in kaart brengt dan welke blogpost dan ook.
Wat je liever niet ziet, in willekeurige volgorde: Set-Cookie: PHPSESSID=... zonder HttpOnly; Secure, een ontbrekende Strict-Transport-Security, een Server-header die de patch-versie etaleert, of een 200 OK op /wp-config.php~ (ja, dat komt in 2026 nog steeds voor).
Drie SQL counts die de prijs bepalen
Derde paneel. Open de database. De briefing had het over checkout. De database vertelt je of checkout het echte probleem is of het symptoom.
-- 1. Real publish volume
SELECT post_type, post_status, COUNT(*) AS n
FROM wp_posts
GROUP BY post_type, post_status
ORDER BY n DESC
LIMIT 20;
-- 2. The autoload bloat that slows every page
SELECT
ROUND(SUM(LENGTH(option_value)) / 1024 / 1024, 2) AS autoload_mb,
COUNT(*) AS rows
FROM wp_options
WHERE autoload = 'yes';
-- 3. The join cost on every WP_Query
SELECT COUNT(*) AS postmeta_rows FROM wp_postmeta;
SELECT meta_key, COUNT(*) AS n
FROM wp_postmeta
GROUP BY meta_key
ORDER BY n DESC
LIMIT 10;
De autoload-query is degene die de meeste engineers overslaan, en waar de meeste offerte-spijt mee begint. Iedere waarde boven 1 MB doet schade bij elke uncached request; we hebben 47 MB gezien op een site waarvan de eigenaar niet kon begrijpen waarom de TTFB rond de vier seconden bleef hangen. De fix is meestal een half uur. Het diagnosticeren nadat je de checkout-klus al hebt gequoteerd, kost je het weekend.
De postmeta-count vertelt je of er iemand ACF, WooCommerce en een event-plugin samen heeft gedraaid zonder ooit op te ruimen. Een row count boven de vier miljoen op een site met 1.200 gepubliceerde posts is een verhaal dat je vóór je commitment wilt horen, niet erna.
Het .htaccess-blok bepaalt de moeilijkheid
Laatste zestig seconden van de pre-quote audit. Lees de root .htaccess. Niet scannen, lezen.
cat .htaccess
cat wp-admin/.htaccess 2>/dev/null
find . -name ".htaccess" -not -path "./wp-content/cache/*" 2>/dev/null
Je leest op vier patronen. Een RewriteRule-keten die niet de WordPress-default is (alles buiten het canonieke blok uit de Apache htaccess-docs) betekent dat een vorige developer business-logica in URL rewrites heeft verwerkt, en die erf je. Een AddHandler application/x-httpd-php74 .php-regel betekent dat de host de PHP-versie per directory vastpint, dus een server-brede upgrade verzet deze site niet. SecRule-directives betekenen dat mod_security aanstaat, en je toekomstige WooCommerce-admin AJAX-calls krijgen onvoorspelbaar een 403. Een Require ip-blok op wp-admin betekent dat het kantoor-IP van het vorige bureau gewhitelist staat, dat van jou niet, en dat ontdek je vrijdag om 23:00.
De vijf getallen die de offerte veranderen
Zodra de drie panelen gesproken hebben, is de offerte een functie van vijf getallen, niet van de briefing:
- PHP-versie op de draaiende site. Alles onder 8.1 betekent dat upgrade-werk in scope zit, of de klant er nu om gevraagd heeft of niet.
- Autoload-bytes. Boven de 1 MB: tel een performance-pass erbij op. Boven de 10 MB: tel er twee dagen bij op.
- Aantal actieve plugins. Lees dit uit
wp_optionswaaroption_name = 'active_plugins', niet uit het filesystem. Het verschil tussen filesystem en database is het aantal verlaten plugins, en elk daarvan is een toekomstig supportticket. - Postmeta-rows per gepubliceerde post. Boven de 200 is de traagheid van de site structureel, geen caching-tweak.
- Last-modified date op
wp-content/uploads/<current-year>/. Als die ouder is dan vier maanden, is de site feitelijk verlaten en is de briefing waarschijnlijk een reddingsoperatie, geen fix.
Deze vijf getallen passen in de eerste alinea van je antwoordmail. Het zijn ook de vijf getallen waar de klant niet over kan redetwisten, omdat ze van zijn eigen server komen.
Wat we met de checklist hebben gedaan
Twee jaar lang draaiden we deze pre-quote audit met de hand bij iedere binnenkomende vraag. Drie terminal-tabs, een notitiebestand, een spreadsheet met getallen per klant. Het werkte, en het was vervelend genoeg dat we het SQL-paneel soms oversloegen als de briefing simpel klonk. De briefing klinkt altijd simpel.
Toen we Pier bouwden liepen we hier in ons eigen klantwerk precies tegenaan, dus hebben we de audit direct in het dock-scherm vastgezet: zodra een FTP- en MySQL-verbinding zijn opgeslagen, draaien de directorylisting, de response headers en de drie SQL counts hierboven tegelijk en staan de resultaten naast de chat. De MySQL editor onthoudt de autoload-query zoals een goede editor je laatste grep onthoudt, en de version history zorgt dat het origineel één klik terug is op het moment dat je tijdens een audit aan de .htaccess komt.
Het kleinste wat je vandaag kunt doen: bewaar de drie SQL-queries hierboven als een gist met de naam pre-quote-audit.sql, en voeg een shell-alias quote-prep toe die de ls- en curl -sI-sequence in één keer draait. De volgende keer dat er om 23:41 een Loom binnenkomt, ben je vijf minuten verwijderd van een bedrag dat je kunt verdedigen.
— Vragen —
Hoe lang duurt de pre-quote audit eigenlijk?
Vijf tot tien minuten vanaf het moment dat je de credentials binnen hebt, aangenomen dat je SSH of FTP en een MySQL-prompt hebt. Langer als je achter de toegang aan moet, en die vertraging zelf is een nuttig signaal over de opdracht.
Wat als de klant me alleen SFTP geeft, geen shell?
ls -la over SFTP werkt nog steeds, curl draai je vanaf je eigen machine, en iedere serieuze klant levert phpMyAdmin of een MySQL-bastion zodra je daar schriftelijk om vraagt vóór het uitbrengen van een offerte.
Verschillen deze queries voor WooCommerce of Magento?
De autoload-check is WordPress-specifiek. Op Magento controleer je de row size van core_config_data en de aantallen in de EAV-attribuuttabellen; op Drupal kijk je naar de groottes van de cache_*-tabellen en de row count van watchdog.