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.
