— —Blog
Notes from the dock.
Practical writing on legacy WordPress, Drupal, Joomla, Magento and custom-PHP sites — and the tooling around them. Tuesdays and Thursdays.
— Latest —
54
-
Quoting legacy work: a four-tier scope letter that holds
The four-tier scope letter we send before touching anyone's legacy site. Quote the surface, ringfence the swamp, and keep the client honest about what they're actually asking for. -
Finding the rogue cron filling /tmp: a 12 GB session leak
The alert came in at 3:14 on a Tuesday. /tmp on a small VPS had crossed 12 GB of orphaned PHP session files. Here is how we found the cron behind it. -
Reading phpinfo() output: a topographic map of the host
Drop a phpinfo.php in the docroot and the wall of beige tables you get back is a topographic map of the host. Here is how to read it section by section. -
Magento CSV import without downtime: a 9,000-row swap
A 23:41 Loom, a 9,043-row supplier CSV, and a storefront that could not go down. The post-mortem on how a Dutch agency staged the import without losing a minute of uptime. -
The two-monitor habit: keeping FTP and MySQL in one glance
On a legacy PHP site, the bug almost always sits in the seam between the filesystem and the database. Here is why we never let one out of sight of the other. -
Eleven .htaccess directives worth keeping, three to drop
A cheatsheet for the .htaccess file you inherited on that legacy WordPress or Magento site: eleven directives worth keeping, and three that quietly hurt. -
Latin1 to utf8mb4 on legacy Joomla: a no-mojibake migration
A Dutch agency we work with inherited a 2011 Joomla site. The articles looked fine until somebody searched for an accented name. Here is the routine we used to fix it without mojibake. -
MySQL collation mismatches: a field guide to empty results
Your search returns nothing. The row is right there in phpMyAdmin. Welcome to MySQL collation mismatches, where two strings disagree about what equal means. -
14 PHP includes per page: a dependency-mapping playbook
Fourteen require statements at the top of every page, a sidebar that pulls in three more, and nobody on the team remembers why. Map the graph before you touch it. -
Drupal 7 pharma hack: tracing an old uploads injection
The customer's Google snippet read 'Buy Cialis 20mg' but the page itself looked clean. Tracing the Drupal 7 pharma hack took us through htaccess cloaking and a forgotten CV upload. -
Diffing two SFTP trees: when 'nothing changed' isn't true
A Saturday-morning ticket. The client swears nothing changed since Friday. Three shell commands that find the one file that actually did, and the diff to paste back. -
wp-login.php brute force: the .htaccess block that works
The bots have been hammering wp-login.php every two seconds for three days. Here is the .htaccess block that stops them and the reason it does not break the dashboard. -
AI-assisted SQL: when to trust the suggested JOIN clause
AI will hand you a six-table JOIN in two seconds. Half of them are right. Here is how to tell which half before you press run on production. -
9GB site restore: a partial dump and three FTP shards
An afternoon spent stitching a 9GB WordPress site back together from one truncated mysqldump and three half-broken FTP backups. Here is the restore log. -
Legacy maintenance pricing: surviving the 'tiny fix' trap
The third 'just one tiny fix' on a 2017 WordPress site is where freelancers quietly go broke. A retainer model that bills risk, not keystrokes. -
Database as source of truth: refactor the data, not the code
You inherit a 14-year-old WordPress site. You pull the repo, grep for the broken behaviour, find nothing. The real logic is a 4 KB row in wp_options. -
Fake plugin updates in wp-content: seven signatures to audit
A practical audit for spotting fake plugin updates and supply-chain malware inside wp-content. Seven signatures, with the grep and SQL to actually run. -
Magento 2 indexer stuck: recovery without losing orders
A stuck Magento 2 indexer turns checkout into a minefield: wrong prices, missing products, ghost stock. Here is the recovery sequence that protects the orders already in flight. -
Reading PHP error logs like bloodwork: a triage field guide
A 16:47 Friday message: site is throwing 500s, sometimes. You SSH in, tail the log, see eight hundred lines of red. This is how to triage them in order. -
40GB wp_options autoload: the night we hit 800ms TTFB
A Loom link at 23:41, a homepage TTFB over eight seconds, and a wp_options table that had quietly grown to 40GB. The fix was three SQL statements and a long night. -
Recovering a corrupted MyISAM table: incident walkthrough
A WooCommerce wp_options table crashed on a shared host at 23:41. Ninety minutes, no SSH, only phpMyAdmin and FTP. Here's the incident in order. -
WooCommerce checkout audit: hunting the 2.3s plugin
A Loom from a Dutch agency: WooCommerce checkout hanging three seconds on every postcode change. Here is the twenty-minute audit that finds the plugin. -
Self-hosting client work: a small studio's case for owning the dock
A three-person studio runs eleven legacy sites. The argument for self-hosting the tooling around that work, instead of renting it, gets stronger every quarter. -
Reading inherited .htaccess files: a fundamentals guide
Inherited a site whose .htaccess starts with three commented blocks of dead RewriteRules in Dutch? Read it block by block. This is the order we use. -
PHP 5.6 to PHP 8.2 in one weekend: the realistic version
The Loom landed at 23:41 on a Friday. Forty-eight hours to take a 14-year-old PHP codebase from 5.6 to 8.2. Here is the actual order it broke in. -
Retiring jQuery from a legacy theme: a working playbook
Most legacy themes still ship jQuery for two lines of code: a hamburger toggle and a smooth scroll. Here is how we retire it without spending a Saturday on rollbacks. -
AI-suggested edits: a four-step check before you apply
The chat returns a one-line SQL diff you didn't write and only half recognize. Before you apply it on a live site, run it through four steps. -
Custom PHP intranet migration: off Windows 2008 in a weekend
An undocumented custom PHP intranet, a Windows 2008 box being pulled Monday, and one weekend. Here is the inventory, the breakages, and the cutover. -
Joomla 3 to Joomla 5: template overrides that survive
Most Joomla 3 overrides are sixty percent salvageable. The other forty percent will silently break your site. Here is the rescue order we use on real client sites. -
CMS identification by filesystem: a short field guide
You inherit an FTP login, no documentation, and 20 minutes before the client call. The five places to look that name a CMS before you open a single PHP file. -
Cron jobs over FTP: auditing a server you just inherited
A site lands in your lap with an FTP credential and a database dump. Somewhere in there, jobs are firing on a schedule. You need to know which ones, before one breaks at 03:00 on Sunday. -
Legacy site manifests: a 20-minute audit blueprint
You inherit a 9-year-old WordPress site at 4pm Friday. By 4:20 you should have a manifest that tells the next person, or you in six months, what's actually running. -
Reviving a dead WordPress site: a 90-minute playbook
A Dutch agency owner sent a Loom at 23:41: white screen, no logs, no SSH. Ninety minutes later the cart was live. Here is the order we work in. -
Drupal session token leaks: three patterns and the fixes
An admin getting silently logged out is rarely a flaky cookie. On legacy Drupal sites it usually means the session token is being copied out of a referrer log or a cached page. -
phpMyAdmin alternatives in 2026: when a native client wins
phpMyAdmin still ships with every cPanel on earth. That doesn't make it the right place to run an UPDATE against 180,000 wp_postmeta rows at five past eleven on a Friday. -
Spotting a hacked WordPress install: a 5-minute audit
You have FTP, the database password, and maybe five minutes before the client wakes up. Here is the audit that gets you to a verdict, in roughly that order. -
WordPress without the dashboard: why some agencies live in the FTP
A look at why working WordPress agencies bypass wp-admin entirely and run their day from a file tree, a SQL prompt and a tail on the error log. -
Reading raw .sql dumps without importing: a CLI playbook
A 3 GB backup lands in Slack and the client wants to know if one row is in there. The CLI workflow for reading raw .sql dumps without importing them. -
Backups that aren't really backups: a seven-point audit
An agency lead sent a Loom at 23:41: the restore file was 11 MB, the database is 4.7 GB. Here's how that happens, and how to make sure it doesn't to you. -
Inline CSS and embedded fonts: 2010s habits that aged well
Eleven years on, the parts of a 2014 WordPress theme still doing useful work are the ones we used to mock: inline CSS, self-hosted fonts, plain JavaScript. -
Shared cPanel clients: the four kinds and what they need
Most agency portfolios still have a shared cPanel tail. Four kinds of clients live on it, and spotting which one is in front of you shortens the rest of the conversation. -
Legacy PHP site snapshot: a one-afternoon playbook
You just got handed SFTP creds, a vague Slack thread, and a site nobody has touched in four years. Here is how to know what you are looking at by 5pm. -
Composer and the legacy PHP site you just inherited
You inherited a WordPress install with three plugins, three vendored Guzzles, and zero composer.json. Composer init is the wrong first move. Here is the audit that comes first. -
Magento 1 in 2026: editing live without nuking the catalog cache
A Dutch agency, a frozen Magento 1 shop, a Friday at 23:41, and the catalog_product_flat tables that nearly took the storefront down at checkout time. -
Public web root: seven file types you should never expose
A WordPress install live since 2014 will tell you everything about itself if you know which seven files to ask for. None of them should be there. -
AI-assisted PHP review: what it catches, what it misses
An honest look at what AI code review actually changes when you're refactoring 14-year-old PHP: the bugs it finds in seconds, the legacy decisions it can't read. -
Front-controller routing: why /contact is not contact.php
The contact page on a 12-year-old WordPress site shows a 500 error. The first instinct is to open contact.php in FTP. That file does not exist, and it never did. -
Drupal 7 end-of-life: a checklist for clients still on it
Drupal 7 went unsupported in January 2025. The sites still humming along on it in mid-2026 are not unsupported by accident, they are unsupported by neglect. -
WordPress MySQL queries: a legacy maintainer's cheatsheet
The half-dozen SQL statements that fix legacy WordPress sites at 17:40 on a Tuesday: URL pair, autoload audit, revisions purge, password reset, transient sweep. -
FTP is not dead: when SFTP is overkill and FTPS is just right
A Friday hotfix on a Magento 1 site, no SSH, no port 22 open, and the question gets suddenly real. SFTP is not always the answer, and FTPS is not the compromise it gets framed as. -
wp-config.php on production: a safe edit procedure
wp-config.php is one line away from a white screen. Here is the procedure we use to change it on a live WordPress site without ever dropping a request. -
Joomla migration without SEO loss: a 12-year-old case study
A 12-year-old Joomla 2.5 site, three SEF plugins deep, moving to PHP 8.2 hosting. Here's how the redirects, the database, and the rankings survived the week. -
Legacy PHP reinfection: the .htaccess wall that holds
A client's WordPress kept getting reinfected every few days. Cleaning files wasn't enough. Here's the .htaccess pattern that finally stopped it. -
Legacy WordPress .htaccess: the rules that fix 80%
The .htaccess rules that quietly resolve most of the redirect loops, mixed content and broken pretty-permalinks on inherited WordPress sites.