paulserban.eu

Portfolio Edition

Paul Serban

AI & full-stack engineer · agentic platforms · distributed systems

← Back to portfolio

Featured

Content Pipeline SSG Platform

One content source, four static surfaces

Role: Architect & lead implementer

  • #astro
  • #cms
  • #drizzle orm
  • #pnpm
  • #sqlite

Problem

Portfolio, blog, news digest, and quiz each needed the same material - content lived in multiple places, drifted apart, and publish was a manual ritual.

Approach

Designed a two-repo, schema-first pipeline - content-sync + dedicated ingest/export CLIs into one SQLite build artifact, then parallel SSG consumers with lock-aware upserts for a future CMS.

Outcome

Publishing collapsed to one pipeline command; new content types are a migration plus an ingest mapping; CI ingests once and builds four surfaces in parallel.

surfaces
4
migrations
9
rss_feeds
84
View code Live demo

Problem

A personal engineering platform is not one site. Portfolio case studies, long-form blog posts, a curated news digest, and a spaced-repetition quiz all need to share the same intellectual material - and stay consistent when that material changes.

Without a deliberate content architecture, every surface becomes its own copy of the truth. Edits fork. Tags diverge. Quiz questions drift from the posts they belong to. "Publish" turns into a manual, error-prone ritual of copying prose between places and hoping the builds still agree.

I needed a platform contract: author once, deliver many times, with zero runtime database and a clear path to a future CMS without rewriting the pipeline.

Solution

The content pipeline is the backbone of prj--personal-portfolio--v3: a TypeScript pnpm monorepo that treats content as a first-class system, not as files dropped into each app.

Authors work in a private content repo (content--paulserban.eu). A sync step pulls published (and staged) trees into the app monorepo. Dedicated ingest CLIs parse MDX and JSON into a single SQLite database via Drizzle. Three Astro sites query that database at build time and emit static HTML; quiz-export turns the same rows into versioned static JSON for a Vite/React PWA. There is exactly one source of truth at build time - content.db - and no content API in production.

Architecture

content--paulserban.eu (private Git)
        │  content-sync
        ▼
 content/live/…/publish/
        │
   ┌────┼──────────────┐
   ▼    ▼              ▼
mdx-ingest  json-ingest  news-ingest
   └────┬──────────────┘
        ▼
 database/output/content.db   <- Drizzle schema (shared/db-schema)
        │
   ┌────┼──────────┬────────────┐
   ▼    ▼          ▼            ▼
portfolio  blog  news-feed   quiz-export -> public/data/*.json
 (Astro)  (Astro) (Astro)         │
                                  ▼
                            quiz-web-app (CSR PWA)
Package / surfaceResponsibility
tools--content-syncClone the private content repo; keep only the authoring trees the pipeline needs
tools--mdx-ingestPosts, book notes, snippets, projects, coursework, co-located questions, cheat sheets, learning plans
tools--json-ingestProfile, skills, pages, experience (sync_source: 'json')
tools--news-sync / news-ingestDaily RSS (84 feeds / 7 categories) -> durable cache -> news_items
tools--quiz-exportDB -> static JSON v2 (compiled HTML stems, tag shards, offline assets)
shared--db-schema / shared--dbSole schema source; WAL + FK connection; migrations; lock-aware upserts
Astro sitesBuild-time Drizzle reads against DATABASE_PATH -> static HTML
Quiz PWARuntime fetch of exported JSON - browsers do not open SQLite

CI ingests once, publishes content-db (and quiz data) as artifacts, then builds four surfaces in parallel before merge/deploy.

Approach

As architect and lead implementer, I owned the seams between authoring, storage, and delivery - so each surface could evolve without inventing its own content story.

Platform boundaries first

Schema that can grow

The schema evolved in reviewable migrations as the product did - normalised tags, question types and options, project case-study fields, companion cheat sheets / learning plans, then news items. Content shape changes are diffs, not tribal knowledge.

Questions stay co-located with posts via a slug convention ({post-slug}--{uid}) and a foreign key to posts.slug, so study material cannot silently detach from the article it teaches.

CMS-ready writes before there is a CMS

Every writable content row carries sync_source (mdx | json | rss) and locked. Upserts go through upsertWithLockCheck: if a row is locked (future CMS-owned), the pipeline skips it instead of clobbering editorial edits. That is architectural foresight - a state machine for conflict resolution without shipping a CMS UI in v0.1.

Operational design

Stack

LayerChoices
AuthoringPrivate Git repo, MDX frontmatter, JSON site data, RSS cache
Pipelinepnpm workspaces, TypeScript CLIs, Drizzle Kit migrations
Arti7d621ff8factSQLite (content.db), WAL, foreign keys on
DeliveryAstro SSG x 3, Vite/React quiz PWA via static JSON
CI/CDIngest-once -> artifact fan-out -> parallel builds -> GitHub Pages / S3+CloudFront

Design decisions that mattered

What shipped

Outcome

Publishing collapsed to a single pipeline invocation. Adding a content type is a schema migration plus an ingest mapping - not bespoke glue per surface. The architecture treats a personal site like a small product platform: clear package boundaries, an evolvable data contract, CI that fans out from one artifact, and write semantics that leave room for a CMS without a rewrite.

Part of prj--personal-portfolio--v3

This pipeline is the content backbone of the monorepo. Related pieces: