paulserban.eu

Portfolio Edition

Paul Serban

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

← Back to portfolio

Featured

Personal Portfolio (V2)

Content-driven Next.js SSG with multi-env CDN delivery

Role: Architect & lead implementer

  • #aws s3
  • #cloudfront
  • #github actions
  • #mdx
  • #next.js

Problem

A hand-rolled static site could not keep up with typed collections, cross-cutting tags, SEO compliance, or a clean preview-vs-production release path.

Approach

Designed a two-repo, build-time content contract - private MDX/JSON cloned into a ContentRepository domain layer, Next.js 13 static export with atomic UI, and branch-gated dual hosting (GitHub Pages preview, AWS prod edge).

Outcome

Ran as the live personal site for years and established the content-at-build pattern that v3 later evolved into a SQLite pipeline across four surfaces.

years_active
2023-2026
content_types
5
hosting_paths
2
View code Live demo

Problem

A personal engineering site is not a brochure. It has to carry portfolio work (projects, coursework), long-form writing (posts, snippets, book notes), and discoverability (tags, sitemap, search-engine notification) - while staying cheap to host and safe to preview before going live.

The v1 Webpack/Handlebars site could showcase work, but it had no typed content model, no shared tag graph across collections, no multi-environment release path, and no deliberate SEO contract. Content lived next to the app; unpublished drafts risked shipping with the repo; "publish" meant redeploying the whole frontend for a prose change.

I needed a platform seam, not another theme: author once in a private content repo, build a fully static site that search engines can crawl, and promote from preview to production without rewriting the architecture.

Solution

I rebuilt the portfolio as a Next.js 13 static export (output: 'export') that treats content as a first-class build input. Authors work in a private content repo (content--paulserban.eu). At build time a clone script pulls the publish tree; a ContentRepository singleton walks typed MDX directories, parses frontmatter with next-mdx-remote, and exposes filtered, sorted, pinned, and tag-indexed views. Page shells stay as JSON; article bodies stay as MDX.

The same build CLI selects dataset (live / test / prev) and target hosting (test / prod). Preview deploys land on GitHub Pages; production syncs to S3, invalidates CloudFront, and sits behind Route 53 - with sitemap generation and IndexNow notification only on real production builds.

Architecture

content--paulserban.eu (private Git)
  publish/{projects,coursework,posts,booknotes,snippets,pages}/
        │  clone-repo (CONTENT_REPO_TOKEN)
        ▼
 content/dist/   <- MDX collections + page JSON shells
        │
        ▼
 ContentRepository (singleton)
   serialize -> tags -> published -> sorted -> pinned
        │
        ▼
 Next.js 13 Pages Router  (getStaticPaths / getStaticProps)
   portfolio · blog · tags/[tag] · CV · contact
        │  next build -> out/
        │
   ┌────┴────────────────────────┐
   ▼                             ▼
 target-hosting=test          target-hosting=prod
 GitHub Pages preview         next-sitemap -> IndexNow
 (basePath for project site)  -> package -> S3 sync
                              -> CloudFront invalidate
                              -> Route 53 (paulserban.eu)
Layer / surfaceResponsibility
content--paulserban.euPrivate authoring: MDX articles + JSON page shells; publish / in-progress / backlog trees
scripts/utils/clone-repo.jsToken-authenticated clone into the app repo at build time; .git stripped after copy
scripts/build.jsDataset x hosting matrix (live/test/prev x prod/test)
ContentRepositoryDomain data layer: five typed collections, tag index, pin/sort/status gates
PortfolioProjects + coursework hubs, lists, and detail pages
BlogPosts, snippets, and book notes with shared post template
/tags/[tag]Cross-collection discovery without a search backend
SEO post-stepsnext-sitemap, IndexNow URL submit, robots.txt Host-line strip for S3
PreviewGitHub Actions -> GitHub Pages (/prj--personal-portfolio--v2)
ProductionS3 origin + CloudFront edge + Route 53 aliases

Approach

As architect and lead implementer, I owned the seams between authoring, static generation, SEO, and delivery - so content could evolve without inventing a CMS, and preview could fail safely without touching production DNS.

Platform boundaries first

Product surfaces

SEO as a release concern

Multi-environment delivery

Stack

LayerChoices
AuthoringPrivate Git repo, MDX frontmatter, JSON page shells
BuildNode CLIs, content clone, dataset x hosting matrix
AppNext.js 13 Pages Router, React 18, next-mdx-remote, SCSS modules (atomic + ITCSS)
SEOnext-sitemap, IndexNow, Open Graph, semantic landmarks
PreviewGitHub Actions -> GitHub Pages
ProductionS3 + CloudFront + Route 53

Design decisions that mattered

What shipped

Outcome

Publishing became a build-time contract: change content in the private repo, choose dataset and hosting target, emit crawlable static HTML. The architecture treats a personal site like a small product platform - clear package boundaries between authoring and delivery, a domain content layer, SEO as part of the release, and preview/production isolation. That content-at-build idea is what prj--personal-portfolio--v3 later generalized into a SQLite artifact feeding four surfaces.

Predecessor to prj--personal-portfolio--v3

This stack is the direct predecessor of the current monorepo. The clone-content-at-build idea became the content pipeline SSG platform that now feeds four surfaces from one SQLite artifact. Related pieces of the current platform: