td204

Web development, one post per year since 2002

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:

  1. Stop PHP from executing in uploads. One nginx block, and the most common shell drop stops working:
    location ~* /wp-content/uploads/.*\.php$ { deny all; }
  2. Turn off the file editor. define('DISALLOW_FILE_EDIT', true); in wp-config.php. An admin session should not be a code editor.
  3. 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.

The year in commits

Commits by month in 2020: 1389 in total, peaking at 184 in Apr.
Jan 2020: 54 commits J Feb 2020: 109 commits F Mar 2020: 133 commits M Apr 2020: 184 commits 184 A May 2020: 175 commits M Jun 2020: 88 commits J Jul 2020: 103 commits J Aug 2020: 105 commits A Sep 2020: 83 commits S Oct 2020: 96 commits O Nov 2020: 101 commits N Dec 2020: 158 commits D