genuinebasil.dev/Projects/Blog
DepthArchitectureDataShippedStackDecisions
LIVE · SHIPPED
Rust/Next.js 16/Postgres

A CMS I
actually write in.

A portfolio and technical blog built as a database-backed CMS. Authoring happens entirely in a browser admin — no git ingest, no markdown build step, no static generator, with Postgres as the single source of truth. The only one of the three that is finished, deployed and in daily use, which is why it is the front door.

Go to the blog What shipped
Backend  Rust 2024 · axum · sqlx Frontend  Next 16 · React 19 Store  Postgres + pgvector Deploy  VPS · Compose · Caddy CI  Actions → GHCR
CONTENT PIPELINERENDERED ON SAVE
tiptap admin — browser authoring markdown + ::: directiveshero · stack · cards · accordion · matrix comrak + syntectrendered once, on save documents.body_htmlreaders never pay for parsing search_tsv generated always · GIN indexed
ENGINEERING DEPTH shipped & running

A blog is a modest brief, so the interest is in what was deliberately over-built — a directive layer, a theme engine, a document graph, and every social feature written first-party rather than embedded.

DIRECTIVE RENDERER :::hero:::stack <header><div>

A ::: block layer over Markdown rendering timelines, callouts, service grids and phase accordions — which is what makes a project page authorable as a document.

THEME FAMILIES
nordgruvboxcatppuccinroséayuone

A dozen palettes with light twins and per-topic accents, stored in Postgres and inlined before first paint — so there is no theme flash on load.

DOCUMENT GRAPH

document_links feeds a navigable graph clustered by topic, with backlinks and multi-chapter wiki books over the top.

REVISIONS & SCHEDULING rev 1rev 2rev 3 publish

Full revision history and scheduled publishing — the two things a static generator turns into build-pipeline problems and Postgres turns into columns.

FIRST-PARTY ANALYTICS

Comments, reactions, newsletter and analytics all written first-party. No third-party script runs on a reader's page — and so there is no consent banner to design around.

HEXAGONAL LAYERS domain app infra api = the web edge

Overkill for a blog on its own — the point was to practise the shape on something real and shipped before applying it to cairn and marginal.

GENERATED SEARCH COLUMN title summary search_tsv GIN indexed

Full-text search as a generated always tsvector column with a GIN index — the database keeps it correct, so no application code can ever forget to reindex.

SYNTAX HIGHLIGHTING 123 syntect

Server-side highlighting with line numbers, filenames and diff markers — done once at save time, so a code-heavy post costs a reader nothing extra.

WIKI BOOKS idx ch1ch2ch3ch4

Long-form work organised into multi-chapter books with their own index and reading order, backed by wiki_index.

SYSTEM ARCHITECTURE single VPS · docker compose
docker compose — one host, four containers browser — reader + admininstallable PWA caddyautomatic TLS · reverse proxy next.js 16app router · react 19SCSS theming · tiptap admin axum APIdomain → app → infracomrak + syntect · JWT postgres 1714 migrations · pgvector · tsvector
DATA MODEL — 14 MIGRATIONS
documents
slug text unique
kind 'post' | 'project' | 'page'
title, summary text
body_markdown → body_html
status 'draft' | 'published'
metadata jsonb
search_tsv generated, GIN-indexed

One table for posts, projects and pages — they differ by kind, not by schema. This page you are reading is a kind='project' row.

document_revisions document_links wiki_index tags comments reactions subscribers notifications analytics media site_settings ai_chat · pgvector

Comments, reactions, the newsletter and analytics are all first-party tables. Nothing on the site calls a third-party tracker or an embedded widget.

WHAT IS ACTUALLY RUNNING 16 shipped · 2 partial

Everything below is deployed and in use today, not planned. The two amber rows are honest about being half-built — the migration is in, the surface is not.

Posts, projects, pages
one documents table, three kinds
Browser admin
TipTap, serialises to Markdown
Revision history
every save kept and diffable
Scheduled publishing
status + published_at, no build step
Media library
uploads, references, cleanup
Block directives
hero, stack, cards, accordion, matrix
Code highlighting
syntect, line numbers, filenames, diffs
Theme families
a dozen palettes, light twins, topic accents
Wiki books
multi-chapter series with their own index
Newsletter
double opt-in, notify on publish
Self-hosted comments
first-party, no embed
Reactions
per-document, first-party
Analytics dashboard
first-party, no third-party tracker
RSS / JSON feeds
plus sitemap
Installable PWA
offline shell, add to home screen
Hexagonal refactor
domain / app / infra — closed 2026-07-19
pgvector
migration in, semantic search still ahead
AI chat over the corpus
tables exist, surface stubbed
TECH STACK
axum
JSON API, the web edge
sqlx
Postgres, compile-time checked queries
comrak
markdown → HTML, rendered on save
syntect
code highlighting with diffs and filenames
Next.js 16
App Router, React 19, the reading surface
TipTap
the browser admin editor
SCSS + custom properties
the theme-family engine
Postgres 17 + pgvector
14 migrations, tsvector, the only store
Caddy
automatic TLS, reverse proxy
Docker Compose
postgres + backend + frontend + caddy
GitHub Actions → GHCR
build and push images on merge to master
Nerd Fonts
unicode-range scoped so ~1MB loads only when needed
DECISIONS WORTH DISCUSSING
content

A database-backed CMS rather than git-and-markdown. Static generators turn scheduled publishing, revision history and a media library into build-pipeline problems; in Postgres they are columns. The cost is a server to run — which this project wanted anyway.

rendering

Markdown is rendered once, on save, and both forms are stored. Readers never pay for parsing or syntax highlighting, and a directive that changes shape is re-rendered across every document by replaying the pipeline rather than by migrating content.

privacy

Comments, reactions, newsletter and analytics are all first-party. No third-party script runs on a reader's page — both a privacy position and the reason the site has no consent banner to design around.

structure

A hexagonal-leaning layout — domain → app → infra, with api as the web edge. Overkill for a blog on its own; the point was to practise the shape on something real before applying it to cairn and marginal.

deploy

One VPS, one Compose file, Caddy for automatic TLS, GitHub Actions pushing images to GHCR on merge. Deliberately boring — the interesting infrastructure work belongs to cairn, where it is the actual subject.

theming

Theme families live in Postgres and are inlined into the document before first paint, rather than applied by a script after hydration. It costs a little server-rendered CSS on every page; it buys the absence of a theme flash, which is the one thing readers actually notice.

live →Shipped© 2026 genuinebasil.dev│
← cairnmarginal →