nextjsprismapostgresqlcmsarchitecture

Schema-First Portfolio CMS with Next.js and Prisma

Most portfolios start as hardcoded TypeScript arrays. This article walks through the migration to a schema-first CMS—translation tables, JSON seed pipelines, cached getters, and admin Server Actions—using the zaki-ns Platform as the reference implementation.

ZakariaPublished June 1, 202614 min read
Schema-First Portfolio CMS with Next.js and Prisma — 1

Why Schema-First?

Developer portfolios often outgrow a single lib/data.ts file. You add labs with sections, projects with slide decks, products with variants—and suddenly admin edits, seed data, and public pages disagree on what the truth is.

A schema-first CMS treats the portfolio like a product: PostgreSQL as source of truth, Prisma as the access layer, admin forms for edits, and getters that always filter published content.

Domain Types Before Prisma

Start with TypeScript interfaces in lib/project-detail-types.ts and lib/data.ts. UI components, admin forms, and DB mappers all consume these shapes. Prisma types never leak into React—that boundary survives schema refactors.

// UI imports domain types, not Prisma
import type { Project } from '@/lib/project-detail-types'

// Mapper converts Prisma row → Project
export function mapProjectRow(row: ProjectWithRelations): Project { ... }

Translation Tables

Instead of duplicating entire project rows per locale, store locale-agnostic fields (slug, year, link, published) on the parent and translatable fields (title, description, longDescription) on project_translation with @unique([projectId, locale]).

Labs and articles reuse content_section for body paragraphs—one table, two parent FKs, sortOrder for TOC ordering.

  • Parent row: identity, URLs, dates, publish flag
  • Translation row: title, description, excerpt, locale
  • Child collections: slides, metrics, sections with sortOrder

JSON Seed Pipeline

Rich demo content is generated from Python (or AI) into generated-portfolio-content.json matching content-generation-schemas.json. lib/data.ts imports the JSON; prisma/seed/content.ts upserts into PostgreSQL.

This beats hand-maintaining thousand-line TypeScript arrays and makes content updates reviewable in git diffs.

Cached Getters and Fallback

Public pages call getPublishedProjects(locale) which tries the DB first. If migration is incomplete or the database is empty, withDbFallback serves lib/data.ts so developers never see a blank portfolio during setup.

After admin saves, Server Actions call revalidateTag('projects') so the next request bypasses stale cache.

Conclusion

The zaki-ns Platform demonstrates this architecture on itself—the project, lab, and article you are reading were designed to be edited in /admin. Schema-first is more upfront work than mock data, but it is the difference between a demo site and a deployable product.

Related Articles

Want to discuss this topic or collaborate on a project?