PaperDuck
The CMS behind this site: one schema-driven content engine, a dense admin, and a replaceable theme for each site.
- Role
- Solo, design + code
- Stack
- PHP · Laravel · React · Inertia
- Year
- 2024–2026
- Status
- Active
Why
I kept building small sites for people, and each one came with the same choice: a big CMS with plugins for everything, or a custom backend that would only ever serve that one site. I wanted a third option: one well-tested backend that I can install for each site, with the design as the only part that changes.
How it works
All content goes through one schema-driven engine. A content type (projects, photos, notes, a client's videos) declares what it supports and which custom fields it has. Validation, revisions, SEO, routing and the editor all come from that definition, so a new type needs no new code.
Pages are separate from content. A page is a URL with a theme view, and the view has named slots. Each slot shows hand-picked items or a query: these types, this category, newest first, twelve per page. The theme decides how a slot looks, and the editor decides what goes in it.
Each theme is a self-contained folder with its own templates, assets, PHP and tests, and the core never refers to one by name. This site is built with PaperDuck as one of those themes; a photographer and filmmaker's portfolio is another, and it is already live as a separate install on the same code and the same server.
What is in it
- A dense React admin: content per type, pages and slots, menus, media with focal points and per-theme image sizes, categories and tags.
- Revisions and optimistic locking, so two people editing the same item never overwrite each other silently.
- Draft previews, scheduled publishing, redirects with hit counts, a sitemap, feeds and structured data.
- Roles for super admins, admins and editors, checked on the server for every read and write.
- Location data stripped from every uploaded photo before it is stored.
- Traffic numbers from Oatbird, my privacy-first analytics service, so the CMS itself stores no visit data.
- One-file backups of the database and media, which also serve as the way to move a site to a new server.
Running it
Every site is its own install with its own database, storage and Redis keys, all on one small VPS. A release is one command: the image is built on my machine, sent over SSH, migrated, switched over and health-checked, and it switches back by itself if the new version is unhealthy.
Built with
PHP 8.4 and Laravel, React and Inertia for the admin, MariaDB, Redis for queues and caching, and Caddy in front, all in Docker. Every change runs against a PHP test suite with coverage, static analysis and Playwright browser tests.
Screens