2020 in review: why I stopped taking WordPress work
The year everything moved into containers and a lot of things moved out of my working week. The ddev setup I trialled last December went onto every project by summer. Tailwind showed up in May and quietly won. And I stopped accepting WordPress work.
That last one is the decision worth writing up, because it was not a technical judgement.
The maintenance is the product
A WordPress site with a page builder, a form plugin, an SEO plugin, a cache plugin and a slider is six independent release cycles bolted to one codebase. Each of them can break the others, and each of them is a way in if it goes unpatched. I have spent whole afternoons finding out which plugin update broke a layout that had nothing to do with it.
Clients hear "website" and budget for a one-off. What they actually bought is a subscription to somebody keeping five plugins current forever. That mismatch has been the source of nearly every uncomfortable invoice conversation I have had.
The break-ins are boring and constant
Nothing exotic: an abandoned plugin with a file upload hole, a stale admin account, a theme
functions.php with a base64 blob appended to the end. Always the same shape, always on the site
where the client cancelled the maintenance contract eighteen months ago.
The tell I check first, before anything else:
# files changed in the last week, ignoring the ones that legitimately change
find . -type f -mtime -7 -not -path "./wp-content/cache/*" \
-not -path "./wp-content/uploads/*" -ls
# PHP that showed up somewhere PHP has no business being
find ./wp-content/uploads -type f -name "*.php"
# the usual obfuscation
grep -rIl --include="*.php" -E "eval\(|base64_decode\(|gzinflate\(" .
If the second command returns anything at all, you are not patching. You are restoring from backup and rotating every credential.
A footnote: the only time I ever used Grunt
Going through this properly, I noticed that Grunt appears exactly once in twenty-odd years of my work, and it is dated 29 October 2020: a WordPress plugin scaffold, three commits, one day.
"devDependencies": {
"grunt": "~0.4.5",
"grunt-wp-i18n": "^1.0.3",
"grunt-wp-readme-to-markdown": "~1.0.0"
}
I did not choose it. It arrived inside the plugin boilerplate, and it existed to extract translation strings and turn a readme into markdown. Gulp had already won my actual builds six years earlier and webpack had replaced it four years after that, so Grunt, the tool that started the whole JavaScript task-runner era, only ever touched my work as somebody else's scaffolding.
Three weeks after that commit I wrote the section above.
What I do instead
For anything that is mostly content: a flat-file CMS or a static build. No database to inject into, no plugin surface, and a deploy is a file copy. For anything with real logic: a PHP framework, where I control what is installed and an audit command tells me the truth about it.
Neither is a moral position. It is that I would rather write code than triage someone else's.
If you are staying
Nothing wrong with that. WordPress runs a large part of the web for good reasons. Three things that cost nothing and prevent most of what I have cleaned up:
- Stop PHP from executing in uploads. One nginx block, and the most common shell drop stops
working:
location ~* /wp-content/uploads/.*\.php$ { deny all; } - Turn off the file editor.
define('DISALLOW_FILE_EDIT', true);inwp-config.php. An admin session should not be a code editor. - Delete deactivated plugins and themes. Deactivated code still sits on disk and is still reachable by URL. "Deactivated" is not "removed".
The rest of the year
Containers everywhere, which mostly meant deleting setup documentation.
And a first look at Tailwind, which I expected to hate. What actually happened is that I stopped inventing class names for things that have no name, and the stylesheet stopped growing every time a page was added. I am not fully converted, but the argument that it is "inline styles again" did not survive contact with a real project.
In short
Stopped taking WordPress work, put ddev on every project, and used Tailwind for the first time.