Stack: Next.js 16 App Router · React 19 · TypeScript strict · Drizzle ORM · Neon Postgres · Vercel Blob · Tailwind CSS v4 · shadcn/ui · Anthropic Claude API
Engagement: Build — new marketing site + custom CMS from scratch
Status: Launching July 2026

Built a full editorial CMS on top of a modern Next.js site for a Maryland AV integrator with 35+ years of work to show off — 231 manufacturer brands, 17 active case studies, and federal agencies in the client roster, on a WordPress site that couldn’t keep up.
At a glance
| Project | RTZ Audio Visual website + custom CMS |
| Architecture | Server-rendered marketing site + admin SPA + Postgres-backed CMS |
| Content types managed | 9 — projects, testimonials, team, news, FAQs, clients, brands, brand categories, client categories |
| Brands managed | 231 manufacturer partners across a 4-main / 10-sub taxonomy |
| Clients managed | 22 featured organizations across a 5-main / 7-sub taxonomy |
| Knowledge base | 4,117 source documents → 33,672 indexed chunks |
The brief
RTZ Audio Visual Associates has been designing and installing AV systems out of Elkridge, Maryland since 1988. Their client roster reads like an institutional who’s-who — federal research agencies, military academies, major universities, the National Aquarium, multi-campus religious organizations. They carry 200+ manufacturer partnerships and install everything from huddle-room conferencing to multi-million-dollar conference centers.
The brief: rebuild the marketing site to match the level of the work; bring the entire content surface under editorial control so RTZ staff can keep it current without a developer in the loop; and build it on infrastructure that will still be the right choice in five years.
Three engineering shifts since the original WordPress build made the new approach not just better, but cheaper to maintain over time:
- Serverless Postgres (Neon) makes a typed schema, real migrations, and a proper admin surface as cheap to run as a static site.
- Server Components + App Router (Next.js 16) render database-driven pages with the SEO and performance of static HTML.
- Vercel Blob + AI alt-text (Anthropic Claude) let non-technical editors upload images and get accessible markup for free.
What it does
A marketing site that ranks like a brochure but updates like a CMS
The public site is a textbook React-on-the-edge build — Server Components per page, structured-data JSON-LD on every route, automatic Open Graph meta, lazy-loaded next/image throughout. Lighthouse SEO is 100, performance in the 90s.
The notable bit is what’s underneath. Every public surface that displays content — homepage hero, manufacturer brand list, client credibility wall, projects portfolio, news feed, FAQs, testimonials, team page — reads from a typed Drizzle schema against Postgres. There are no markdown files in the repo. There is no headless CMS bill. RTZ owns the data and the editing surface end-to-end.
A custom CMS — built without a CMS
We didn’t reach for Sanity, Strapi, or Payload. The content model was specific enough that bolting onto an off-the-shelf CMS would have meant fighting it. Instead, the admin at /admin is a small SPA built from the same primitives as the public site:
- A sidebar nav lists every editable entity (9 types).
- Each entity has a list view with sortable columns and a featured-item toggle.
- Each entity has a form view with React Hook Form + Zod validation, Vercel Blob–backed image uploads, and Anthropic-powered alt-text suggestions.
- Drag-and-drop reordering via @dnd-kit on the lists that benefit from it.
- Every mutation writes an audit row to a
content_revisionstable — so if a content editor breaks something, we can replay exactly what they did.
The whole admin is auth-gated through iron-session, sits behind a /admin/(authed) route group, and renders force-dynamic so it never gets statically baked into the build.
A two-level category system visitors can actually browse
The brief asked us to surface RTZ’s 231 manufacturer brands cleanly on the public /brands page. We could have shipped 231 logos in a flat grid. Instead we designed a two-level taxonomy that does meaningful work for both the editor and the visitor:
- Two levels by design, enforced by a Postgres trigger that rejects any third level. Intentional YAGNI — the deepest reasonable taxonomy for “we sell AV gear” is “Video → Displays,” not five levels deep.
- Multi-select, so a brand or client can sit in multiple categories. A manufacturer that makes both PTZ cameras and audio interfaces belongs in both.
- Mixed tagging, where each item can be tagged at the main level, the sub level, or both. A deterministic “lowest display-order wins” rule renders each brand exactly once.
- Editor-managed, with a two-column drag-and-drop interface — mains on the left, subs of the selected main on the right.
The same system runs for clients (Government → Federal, Education → Higher Education, Houses of Worship → Local Church). The visitor sees a main heading on /clients, with sub-headings appearing as soon as any client is tagged that specifically — content-driven progressive disclosure rather than a static information architecture.
A homepage hero that feels like the inside of an AV install
The hero is a custom React carousel of full-bleed install photography — university lecture halls, federal command centers, broadcast spaces, houses of worship. Over the photos we ship a custom HTML Canvas component, CircuitPulse, that draws PCB traces and animated signal pulses traveling across the foreground: subtle enough to read as ambient texture, on-brand enough that the metaphor lands instantly.
The carousel is custom because Swiper EffectFade — the obvious off-the-shelf choice — does not coexist with Turbopack’s module graph. We wrote a small React state machine instead: Ken Burns zoom, 5-second slide / 800ms cross-fade, pause-on-hover, dots pagination, full prefers-reduced-motion support.
A searchable AV knowledge base
RTZ maintains a 4,117-document internal knowledge base on SharePoint — install guides, manufacturer datasheets, project specs, training material. We scraped it, chunked it into 33,672 searchable units, and embedded the corpus with MiniLM-L6-v2. The vector index ships to Vercel Blob and is served by a Web Worker, giving RTZ staff single-pane search across years of accumulated tribal knowledge. The route is currently hidden from public nav — it surfaces as part of a customer-portal initiative in a future phase.
A digital site-survey form that replaced an Excel template
We rebuilt RTZ’s field survey as a multi-step web form at /site-survey. Reps fill it in on a tablet during a walkthrough; results email back to the office on submit. Same data, no paper, no version drift.
Built for content scale
Every editorial decision had to assume the people maintaining this site might be the same people who scheduled the installs that week:
- Saves are idempotent. Each form save round-trips through a typed Zod schema, validates, writes to Postgres, fires
revalidatePathfor affected pages, and writes the audit row — in a single server action. No async indexing step to misfire. - Reorders are drag-and-drop everywhere. Editors drag to bump a brand up the list rather than typing into a “display order” field.
- Image uploads handle themselves. Drop a file, get a Vercel Blob upload, an AI-generated alt-text suggestion, and a
next/imageeverywhere it renders. The editor never touches a filename. - Two parallel taxonomies, one admin pattern. Brand and client categories work identically — the cognitive cost of learning category management is paid once.
Tech stack highlights
| Layer | Choice | Why |
|---|---|---|
| Framework | Next.js 16 App Router + Turbopack | Server Components keep pages SEO-clean while running real DB queries at render time |
| Language | TypeScript strict | Every DB column, API route, and React prop is type-checked end-to-end |
| Database | Neon serverless Postgres | Per-millisecond billing fits the read-heavy admin; branchable DBs make previews cheap |
| ORM | Drizzle | Migrations live as TypeScript; zero runtime overhead; clean SQL escape hatches |
| Schema enforcement | Postgres triggers | Two-level category depth is enforced at the storage layer, not just app code |
| Auth | iron-session | Cookie sessions are the right tool for an admin used by ~5 staff |
| Image storage | Vercel Blob | Direct browser upload, CDN-served, near-zero ops |
| AI alt-text | Anthropic Claude API | Runs on save; the editor sees a suggestion and accepts or edits |
| Styling | Tailwind CSS v4 + shadcn/ui | Inline-theme config, no runtime CSS-in-JS, no component-library tax |
Why this matters
Three patterns from this build show up in almost every content-heavy project:
The CMS is the product. Companies with 35 years of work to show want to keep showing it — and how fast they can publish new work, partnerships, and wins is a function of how good the admin feels. A great public site with a bad CMS goes stale within a year; a great CMS with a decent public site ages well.
The right level of structure is “exactly enough.” A two-level taxonomy with a hard cap, a multi-select join, and a deterministic ordering rule covers 95% of real-world cases without paying for arbitrary-depth tree complexity. Saying no to deeper structure is part of the design.
The database is part of the design system. Schema decisions shape what content editors can express. We modeled the category system, the audit log, and the image-upload flow as first-class design surfaces, not engineering afterthoughts — and the editorial experience is better for it.
If your business runs on relationships, work product, and credibility — and your website is supposed to represent all three — this is the shape of the build.