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 full of overlapping data, and adding a third content type (blog) would have tripled the duplication and pushed every future content addition to 18+ file touches.
The solution: a content registry
Rather than keep maintaining parallel data pipelines, I built a single content registry that acts as the source of truth for every content query:
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 gets populated at module load time from the existing per-type data files, so the migration carried no risk; the existing pages kept working exactly as before while I built the new abstraction layer 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 swapped the hard-coded if/else branches in the metadata builder for a
plain config lookup, so the system extends without any 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 worth reading. If it takes six files, the architecture is paying tax on every addition, and each one quietly raises the price of the next. 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. So design for the access pattern, not for the first type that happened to ship.
