I have been developing for the web for about 20 years, and WordPress has always been part of my toolkit. Themes, plugins, client sites, you name it. But most of my plugin work has been utilitarian: quick solutions for specific client needs, internal tools, or small automations. Nothing I ever considered publishing.
This time I wanted to do it differently. I wanted to build something properly, following WordPress guidelines, with a codebase I would be proud to share. The result is Completionist, a reading progress tracker that I am now preparing to submit to WordPress.org.
The Problem I Wanted to Solve
I am someone who gets obsessed with completion. When I discover a blog from an author I like, I start wondering: how many of these articles have I read? What am I missing? It is the same feeling I get with books; I want to read everything.
The problem is that most blogs have no memory. You visit, you read, you leave. Next time you return, the site has no idea you were ever there.
Some sites have user systems, sure. WordPress has its full user management, and you can add membership plugins like WooCommerce or others. But most content sites do not require registration just to read articles. And I did not want that to be a restriction.
So I thought: what if we track reading progress in localStorage? No registration needed. The browser remembers what you read. And for sites that do have logged-in users, we can store that data server-side too.
Two Tracking Modes, One Codebase
The plugin works in two modes.
localStorage mode works for any visitor. No login required. The data stays in the browser, completely private. If you clear your browser storage, it is gone, but for most use cases that is fine.
Server-side mode works for logged-in users. Reading history persists across devices. This is useful for membership sites, courses, or any situation where users have accounts.
I wanted both modes from the start. The challenge was avoiding code duplication. I ended up with a single JavaScript codebase that pulls data from either localStorage or inline data injected by the server. The rendering logic is identical regardless of the source.
TDD: Because I Actually Enjoy It
I always work with TDD. Not just because it is a good practice, but because it makes development fun. The opposite, having no tests and refreshing the browser constantly, is stressful and slow.
But here is the problem with WordPress: running tests against a full WordPress installation is painful. You need a database, you need WordPress loaded, and your tests run slow. I cannot afford that.
So I did what I have done before with Drupal: I abstracted everything. Any WordPress function I need to call gets wrapped in an interface. In production, the real implementation calls WordPress. In tests, I use mocks that return predictable data.
interface Database
{
public function query(string $sql, ...$args): bool;
public function getResults(string $query, ...$args): array;
public function delete(string $table, array $where): bool;
public function getPrefix(): string;
}
The real implementation uses $wpdb. The mock just stores calls in arrays so I can assert against them. My tests run in milliseconds without touching WordPress at all.
I did the same for hooks, options, script loading, JSON responses… anything that touches the framework. The result: 154 PHPUnit tests that run in under 100ms. Plus Jest tests for all the JavaScript logic.
Most things worked on the first try. When something failed in the browser, I would go back, write a failing test that exposed the bug, fix it, see the test pass, and then check the browser. That workflow is satisfying in a way that print-debugging never is.
Fighting with WordPress Guidelines
I have built many plugins before, but never one intended for WordPress.org submission. This time I installed the Plugin Check tool and ran it against my code.
Some issues were straightforward: replacing short PHP tags, adding proper input sanitization, using dbDelta() for table creation instead of raw queries.
Others required understanding why the rules exist. The linter complained about my database abstraction because it could not see that I was calling $wpdb->prepare() before $wpdb->query(). The code was safe, but the static analysis could not follow the data flow through my abstraction layer.
I ended up with some phpcs:ignore comments, but only after understanding each warning and confirming they were false positives. The process taught me things about WordPress internals I had never needed to know before.
What It Does Now
The current version covers the core use case well: a Gutenberg block and shortcode that display a widget with three sections (Continue Reading, Completed, and Suggestions), configurable scroll threshold, support for any post type, suggestion modes, category filters, an optional consent banner, and customizable labels for multilingual sites. The full feature list is on GitHub.
What Comes Next
This first version is the sidebar widget. I have plans for a full-page panel where users can see their complete reading history with filters and search. Also per-instance settings so each widget can override the global configuration.
The plugin is already on GitHub and I have a release pipeline that generates the ZIP automatically. I am testing it on this site before submitting for review, because I want to be confident it holds up in real production conditions before it goes through the WordPress.org process.
Try It
The code is public: github.com/raulprdev/completionist
If you want to test it, grab the ZIP from the releases page. I would appreciate any feedback, especially if you find bugs or have ideas for features.
And if you are a recruiter reading this: yes, I write tests, I follow coding standards, and I can build WordPress plugins that are not embarrassing to share publicly.