Creating Content Types
How to add a new content type to the dynamic sections system.
Overview
The registry pattern maps content types (what data looks like) to display types (how it renders). Adding a new content type means:
- Define the data model
- Create list and detail components
- Register them in the content registry
Step 1: Data Model
Add your type to shared/types/api.ts:
export interface Recipe {
id: string;
title: string;
slug: string;
content: string;
ingredients: string[];
is_published: boolean;
createdDate: string;
updatedDate: string;
}Step 2: Create Module
Create your module under modules/your-type/:
modules/recipes/
├── index.ts
└── components/
├── index.ts
├── RecipeCard.tsx
└── RecipeDetail.tsxList Component (RecipeCard)
import React from 'react';
import Link from 'next/link';
import { Recipe } from '@/shared/types/api';
interface RecipeCardProps {
recipe: Recipe;
}
export const RecipeCard: React.FC<RecipeCardProps> = ({ recipe }) => {
return (
<Link href={`/recipes/${recipe.slug}`} className="card card--hoverable card--link">
<h3 className="section-title mb-sm">{recipe.title}</h3>
<div className="tech-tags">
{recipe.ingredients.slice(0, 5).map((ing) => (
<span key={ing} className="tech-tag">{ing}</span>
))}
</div>
</Link>
);
};Detail Component (RecipeDetail)
import React from 'react';
import { Recipe } from '@/shared/types/api';
interface RecipeDetailProps {
recipe: Recipe;
}
export const RecipeDetail: React.FC<RecipeDetailProps> = ({ recipe }) => {
return (
<>
<h1 className="page-title">{recipe.title}</h1>
<ul>
{recipe.ingredients.map((ing) => (
<li key={ing}>{ing}</li>
))}
</ul>
<div className="card">
<div className="prose prose--card">
<p>{recipe.content}</p>
</div>
</div>
</>
);
};Barrel Exports
// components/index.ts
export { RecipeCard } from './RecipeCard';
export { RecipeDetail } from './RecipeDetail';
// index.ts
export * from './components';Step 3: Register
Add the content type to modules/registry/types.ts:
export type ContentType = 'story' | 'project' | 'page' | 'recipe';Add the components to modules/registry/contentRegistry.ts:
import { RecipeCard, RecipeDetail } from '@/modules/recipes';
export const contentRegistry: Record<ContentType, ContentEntry> = {
// ... existing entries
recipe: {
listItem: RecipeCard,
detail: RecipeDetail,
},
};Step 4: Backend
Add the corresponding backend model, handler, and API endpoints following existing patterns in backend/models/ and backend/handlers/.
Add 'recipe' to the ContentType literal in backend/models/section.py.
Display Types
Content types render inside display types. The available display types are:
| Display Type | Component | Use Case |
|---|---|---|
feed | FeedDisplay | Infinite scroll list (stories) |
card-grid | CardGridDisplay | Responsive grid (projects) |
static-page | StaticPageDisplay | Single content block (about, contact) |
When creating a section that uses your content type, choose the display type that fits. A recipe section might use card-grid for browsing or feed for a chronological list.
Step 5: Routing (Automatic)
As of Phase 4, the [...slugPath] catch-all route handles all section URLs automatically. Once you:
- Create a section in the database with your content type
- Register the components in the content registry
- Add a fetcher for your content type in
modules/registry/hooks/useFetchContent.ts - Add API endpoints and proxy routes for the new content type
The catch-all route will:
- Resolve the section by slug
- Fetch content using the
useFetchContenthook - Render list views via
displayRegistry[section.display_type]with yourlistItemcomponent - Render detail views via your
detailcomponent - Handle SSR, OG meta tags, and infinite scroll
No new page files needed.
Step 5: Editor Form
Create an editor form component at modules/editor/components/RecipeEditorForm.tsx:
import { useRouter } from 'next/router';
import { Section } from '@/shared/types/api';
interface RecipeEditorFormProps {
section: Section;
}
export function RecipeEditorForm({ section }: RecipeEditorFormProps) {
// Use a custom hook for form state and API calls
// Follow useStoryEditor or useProjectEditor patterns
// Include section.id as sectionId in create/update payloads
// Navigate to /{section.slug} after save or cancel
}Register the form in pages/editor.tsx:
{section.content_type === 'recipe' && <RecipeEditorForm section={section} />}The editor page routes to the correct form based on section.content_type. When a user navigates to /editor?section_id={id}, the section is fetched and its content_type determines which form renders.
Existing Content Types
| Type | List Component | Detail Component | Editor Form | Typical Display |
|---|---|---|---|---|
story | StoryCard | StoryDetail | StoryEditorForm | feed |
project | ProjectCard | ProjectDetail | ProjectEditorForm | card-grid |
page | none | none | PageEditorForm | static-page |