Yes, the name “Content Analyzer” sounds boring. It’s the internal name for a new software product I’m working on, which (like the name entails) helps customers analyze and find areas of improvement in their WordPress blogs.
Content Analyzer will connect to customers’ WordPress website’s to scan their post & pages, determine metrics like word count, image count, content length, age, and other data to provide insights into areas of improvement.
This blog will go over some of the more interesting technical points in how the pieces of the product work and fit together, including everything from UI/UX choices to the DevOps deployment setup.
Project Structure
I setup the project structure as a monorepo, using Bun workspaces to manage the different projects inside the same repo. I’ve found monorepos to work best when there are multiple projects that typically need to have changes deployed together, such as a frontend + backend. In this case, I have the following projects:
@content-analyzer/backend
@content-analyzer/frontend
@content-analyzer/wordpress-plugin
@content-analyzer/ui
Frontend
The frontend is rendered with React through the Vite framework. I’ve found Vite to be extremely performant, especially when coupled with the SWC compiler.
Routing
I’ve picked the Tanstack Router library for routing. I’ve used it on several projects in the past, in my opinion it’s one of the most well-developed libraries released in recent time for the frontend. It boasts type-safe routing, as well as type-safe path & search parameters, among many other features.
One of the motivations behind the creation of the Tanstack Router library was the ability to reliably use the browser query parameters (?key=value
) as a state manager for the purpose a dashboard project similar to this one. Historically, it’s been difficult to keep this URL state in sync with a local state.
UI Components
I’ve used a few different UI libraries in the past, including good ol’ Bootstrap, Material UI, and ShadCN. MUI looks great, but I’ve experienced issues with customization and performance. I like ShadCN, especially with their approach to owning your own code and acting as a template.
I ended up going homebrew with the UI, using design practices that ShadCN and TailwindUI use for theming and headless logic through headlessui
. I used Tailwind as the CSS library for styling.
To allow sharing UI components between the frontend and WordPress plugin UI, I separated it into a different project and imported it via bun workspaces.
Deployment
The frontend is built & deployed via CloudFlare pages. CloudFlare has a very generous free tier, and because the frontend is a static one, I can take full advantage of CloudFlare’s excellent edge caching features.
Backend
The backend is powered via the Bun runtime, using the NestJS framework. I’m tentatively dipping my toes into using Bun for production, a few months ago it had several issues that prevented me from using it, however they have made a lot of progress since then and it appears to be stable.
Database
The database is a PostgresDB database hosted with Neon. Not only does Neon provide quick and easy to use databases that scale, they also have a branching feature, allowing both developers and feature branches/PR to deploy in a dedicated environment branched off of a staging environment. The goal is to have the ability for E2E tests and branch preview deployments run in a dedicated environment, allowing for easier and more reliable testing.
To communicate with the database, I’ve setup Drizzle, a ORM-like library that does a great job of sitting in the gap between an easy to use query builder and a full blown ORM. They boast advanced querying that will always result in a single query for the best performance, and top-notch typed results.
Drizzle also provides an easy to use migration feature, allowing both schema updates and custom SQL migrations to keep developers and deployments in sync without any manual database schema updates made.
Authentication
For authentication, I’ve pulled on the Descope service. I’ve used Descope as an identity & authentication provider for banking applications, so they more than cover my use case here. They provide the ability to quickly customize how users log in, allowing me to launch immediately with social sign in features (such as Google) and biometrics for devices that support it.
Deployment
The backend is deployed using Railway, which provides a good balance between customizability, scaling, and out-of-the-box features for preview deployments, which would otherwise require much more effort to setup compared to something like Google Cloud Run, Kubernetes, or AWS.
Railway’s pricing is also usage based, and I plan to take advantage of their replicas and cron feature to make processing WordPress synchronization imports scaleable.
WordPress
Docker
To facilitate a WordPress development environment, I used WP’s official docker image, with some modifications to allow for the use of XDebug for in-IDE debugging. Combined with a MariaDB container and a docker compose configuration, I can boot up a WordPress installation from scratch on a new development environment without any prerequisites other than Docker itself.
The WordPress docker container shares a volume with the plugin being developed, allowing changes to be made and reflected immediately without a rebuild or restart.
In order to provide actual content to test against, I generated ~750 articles using OpenAI’s API with joke names and contents (10-15 paragraphs each), which is included in the WordPress installation via a startup script that MariaDB runs at startup.
The WordPress Plugin
The goal of the WordPress plugin is to provide a light way for the Content Analyzer synchronization service to retrieve rendered post contents, as well as any post meta that it can be supplemented with (such as structured recipe contents).
The WordPress plugin uses composer for autoloading and dependencies, along with the strauss
library to re-scope Composer dependencies to solve the issue of a dependency & version conflict I’ve experienced in the past (specifically with the Google JWT library).
The admin UI for the WordPress plugin is also built via Vite, which was surprisingly painless to setup. I used the vite-for-wp
package (still being actively maintained!), which handles rendering the contents of both the Vite development server and the built JS files inside of WordPress. This means that I can use modern features of Vite, including hot reloading, all inside WordPress.
I even used the Tanstack Router library with a hash-based router, which is likely overkill for what is basically a configuration plugin, but the effort for setup was pretty minimal.
The WordPress plugin is compiled and built ready for use through the Gulp library, which runs the build commands (composer & Vite), copies the files, and compresses them.
In the spirit of making the plugin as easy to test as possible, I’m making heavy use of dependency injection in class constructors, partitioning each part of the plugin into self-contained classes that rely only on what’s injected to perform logic.