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 and make every future content addition cost 18+ file touches.
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
The Lesson
The cost of adding a new content type is a signal. If it takes six files, the architecture is paying taxes on every addition, and every addition compounds the next one's cost. A single registry with a typed config flips the math: adding a content type is three files, and the existing query API already works on day one. Design for the access pattern, not for the first type that shipped.
