Content Type Expansion — From Fairy Tales to a General Creative Platform

Content Type Expansion — From Fairy Tales to a General Creative Platform

The Problem: “Fairy Tale” Hardcoded in 30 Places

The service launched as a children’s storybook creator. “fairy tale,” “children,” and “children’s book” were embedded in 30+ locations across the codebase. An adult essay generation request still received “use simple words children can understand.” The output was unusable.

Hardcoded Location Content
Scheduler context.ts “Write a children’s fairy tale in JSON format…”
Scheduler image-regen.ts “children’s book illustration”
Backend seed.ts System prompts, evaluation criteria
Backend generation.service.ts Fallback title “Generated Fairy Tale”
Backend prompt.routes.ts “fairy tale sample,” “children’s content suitability”

Every prompt, every fallback, every validation assumed a single content type.

Design Principles

Five rules governed the expansion.

Principle Application
100% backward compatibility content_type defaults to fairytale. Old code without changes produces identical behavior.
Follow existing patterns Feature-based structure, Drizzle ORM, Express, Zod — no new paradigms.
Single source of truth Prompts come from DB. Hardcoded fallbacks branch by type.
Gradual expansion content_type is a text field. Adding essay, poem, novel needs no migration.
Minimal changes Add columns to existing tables. Only one new table: writings.

Content Type Definitions

Value Label Audience Vocabulary
fairytale Fairy Tale Children Simple, educational
general General Adults Unrestricted, free style

Default is fairytale. Existing users notice zero change.

Core Architecture: PromptResolver

The Problem with Branching

Thirty locations, each with if (contentType === 'fairytale') ... else ..., doubles the code. Maintenance cost scales linearly with content types.

Solution: Central Prompt Interpreter

                    +--------------------+
 context.ts ------->|  PromptResolver    |
 image-regen.ts --->|                    |
 generation.svc --->|  resolve(key,      |--> app_config DB
 prompt.routes ---->|    contentType)    |--> fallback constants map
                    +--------------------+

PromptResolver unifies all prompt lookups through one entry point. It returns the appropriate prompt for the content type. DB first, fallback second.

// PromptResolver.resolve(baseKey, contentType):
// 1. Derive the settings key: base content type uses the base key,
//    other types append a type suffix (e.g., "key_general")
// 2. Look up the prompt in the app_config DB table
// 3. If found, return the DB value
// 4. Otherwise, fall back to a hardcoded prompt constant for that key + type

Fallback Prompts Map

The fallback map is a two-level lookup: prompt key, then content type. It covers writing system prompts (children’s vocabulary vs. unrestricted style), image generation prompts (children’s illustration vs. general illustration), and content evaluation prompts (child-safety criteria vs. general content filters).

When DB settings are missing, fallbacks keep the service running. A new content type can launch without seeding the database first.

DB Schema Changes

Existing Table Extensions

Three existing tables received new text columns: a content type field on both the contents and prompt templates tables, and an image type classification field on the characters table. All default to their original-behavior values (fairytale and person respectively). Rolling back requires reverting code only. The DB remains compatible.

New Table: writings

The writings table stores user-authored text drafts. Each record belongs to a user and captures a title, the text body, content type, and a status (draft or used). A reference to the resulting book tracks conversion when a writing becomes the basis for book generation. A JSONB metadata field provides flexibility for future extensions.

This enables a two-step workflow: write first, create a book later. The status flips from draft to used on conversion.

Feature Scope: F1 through F10

# Feature Description
F1 Content type selection “Fairy Tale” / “General” picker in creation UI
F2 Type-based prompt branching PromptResolver auto-switches by content type
F3 DB schema extension contents.content_type column
F4 Hardcoding removal 30 locations converted to DB settings or type branching
F5 Writing feature Standalone text drafting and saving
F6 Writing-to-book conversion Saved writing becomes “situation” input for book generation
F7 Auto character profiling Extract visual profiles from generated text
F8 Character image type classification Auto-classify as person/landscape/object
F9 Writing prompt type linking Prompts categorized by content_type
F10 Image mode Spread (16:9) vs illustration layout selection

F7: Auto Character Profiling

The multi-character system (previous post) requires admin-registered characters. F7 removes that dependency.

Text generation completes
    |
    v
Claude extracts character visual profiles from the text
    |
    v
{
  "Toto": "6-year-old girl, curly brown hair,
           large brown eyes, pink dress",
  "Mimi": "white rabbit, long ears, blue ribbon"
}
    |
    v
Profiles injected into image generation prompts

No registered characters needed. Claude reads the generated story, identifies characters, and produces visual descriptions. These descriptions feed into Gemini’s image prompts for consistency.

This works alongside the registered character system. Registered characters take precedence with their Claude Vision analysis. Auto-profiling fills the gap when users skip character selection.

Change Map

packages/db        -> schema extensions (columns, writings table)
        |
src/modules        -> generation validation, writings CRUD,
                      prompt type branching
        |
scheduler/src      -> PromptResolver, character-profiler,
                      image size branching
        |
web/src            -> content_type selection UI, writing feature
admin/src          -> prompts content_type field,
                      characters image_type badge

Every layer touches the content type. The PromptResolver centralizes the branching logic. Individual modules pass contentType as a parameter rather than making their own decisions.

Extensibility: The Core Design Goal

content_type is a text field. Adding a new type requires:

// Steps to add a new content type (e.g., "essay"):
// 1. Add fallback prompts for the new type
// 2. Optionally seed the config table with optimized prompts
// 3. Add the option in the content type picker UI
// -> Zero DB schema migration needed

The PromptResolver’s resolve(baseKey, contentType) pattern makes this work. Missing DB prompts fall through to hardcoded defaults. Teams can launch a new type with fallbacks, then iteratively optimize prompts in the database.

No enum migration. No new tables. No code deployment for prompt changes (DB-driven). The architecture absorbs new content types without structural modification.

Conclusion

Expanding from “fairy tale only” to “general creative platform” is not about adding if-else branches. It requires an abstraction layer — the PromptResolver — that routes prompts by type. It requires backward compatibility so existing users see no change. It requires a text-type field so future types need no migration.

Nothing changes for existing users. New possibilities open.