The Problem
Every content type on this portfolio — projects and experience — had its own slug file, thumbnail file, MDX import index, content ordering array, reading time map, and structured data file. Adding a single project meant touching six files with overlapping data. Adding a third content type (blog) would triple the duplication.
The Solution: A Content Registry
Instead of maintaining parallel data pipelines, I created a single content registry that serves as the source of truth for all content queries:
getContentByType('project')— ordered array for listing pagesgetContentBySlug('blog', 'my-post')— single entry for detail pagesgetContentSlugs('experience')— forgenerateStaticParamsgetContentPagination('blog', slug)— circular prev/next navigation
The registry is populated at module load time from the existing per-type data files, so the migration was zero-risk — existing pages continued to work identically while the new abstraction layer was built on top.
Type-Safe Content Configs
Each content type has a config entry that maps it to its URL base path, display label, and content directory:
const contentTypeConfigs = {
project: { basePath: '/projects', label: 'Project', contentDir: 'projects' },
experience: {
basePath: '/experience',
label: 'Experience',
contentDir: 'experience',
},
blog: { basePath: '/blog', label: 'Blog', contentDir: 'blog' },
};This replaced hard-coded if/else branches in the metadata builder with a
simple config lookup, making the system extensible without code changes.
Results
- Adding a new blog post: 3 files instead of 6+
- Zero runtime changes to existing pages
- 18 new registry tests covering all query paths
- Type-safe throughout —
ContentTypeunion prevents typos