GuidesCreating Content Types

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:

  1. Define the data model
  2. Create list and detail components
  3. 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.tsx

List 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 TypeComponentUse Case
feedFeedDisplayInfinite scroll list (stories)
card-gridCardGridDisplayResponsive grid (projects)
static-pageStaticPageDisplaySingle 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:

  1. Create a section in the database with your content type
  2. Register the components in the content registry
  3. Add a fetcher for your content type in modules/registry/hooks/useFetchContent.ts
  4. 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 useFetchContent hook
  • Render list views via displayRegistry[section.display_type] with your listItem component
  • Render detail views via your detail component
  • 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

TypeList ComponentDetail ComponentEditor FormTypical Display
storyStoryCardStoryDetailStoryEditorFormfeed
projectProjectCardProjectDetailProjectEditorFormcard-grid
pagenonenonePageEditorFormstatic-page