Documentation
Guides

Runtime-defined content types

Smeldr lets an AI agent or operator declare a new content type at runtime — its name, fields, and public URL — without touching Go code or redeploying. The framework provisions storage, public HTTP routes, admin routes, MCP tools, and sitemap entries automatically. The type participates in the same draft/publish/archive lifecycle as compiled types.


Defining a type via MCP

Call the define_content_type tool with a type_name, optional label, optional url_prefix, and a fields array:

define_content_type
  type_name:  "recipe"
  label:      "Recipe"
  url_prefix: "/recipes"
  fields:
    - name: "Title",       type: "string",  required: true, role: "title"
    - name: "Ingredients", type: "string",  format: "markdown"
    - name: "Steps",       type: "string",  format: "markdown"

Verified field types: string, integer, boolean, array, object.

Verified role values: title, description, body, summary, og_image. The field with role "title" is used to derive the item slug automatically.

url_prefix

url_prefix is optional. When set, Smeldr registers two public GET routes immediately after the call succeeds — no restart required:

GET /recipes        — published items (JSON list)
GET /recipes/{slug} — single published item (JSON)

url_prefix must start with "/". Leave it empty and the type gets no public HTTP surface at all. It is still fully accessible via admin routes, MCP tools, and CLI.


CRUD via MCP tools

Six generic tools are available whenever app.ServeDynamicContent() is called. All tools target the type by type_name.

ToolRoleWhat it does
define_content_typeAdminDeclare a new content type and register its routes
create_contentEditorCreate a new draft item
get_contentAuthorGet a single item by slug
list_contentAuthorList items with status filter, pagination, and sort
update_contentEditorUpdate fields on an existing item
set_content_statusEditorTransition status: draft -> published / archived

Slug is auto-derived from the field with role "title". Collision suffixes (-2, -3, ...) are added automatically. Status transitions are the same as compiled types: Draft -> Published/Scheduled -> Archived.


Admin HTTP routes

The admin namespace uses type_name as the path segment, not the URL prefix. The admin surface is stable regardless of where the type is hosted publicly.

MethodRouteRoleOperation
POST/_content/typesAdminDefine type
GET/_content/{type}EditorList items at any status
GET/_content/{type}/{id}EditorGet item by ID
POST/_content/{type}EditorCreate draft
PATCH/_content/{type}/{id}EditorUpdate fields
POST/_content/{type}/{id}/statusEditorSet status

{type} is the type_name: recipe, not /recipes.


ContentList block integration

The content_list block type renders a live feed of any registered content type — compiled or runtime-defined — inside a block-composed page. Set the ContentType field to the type_name:

{
  "Type": "content_list",
  "ContentType": "recipe",
  "Limit": 10
}

Use "recipe", not "/recipes". The block resolves the type by name, so it stays valid even if the public URL changes. Existing content_list blocks from before A154 that stored a URL prefix need their ContentType field updated to the type_name.


Wiring in Go

One call in main.go enables the feature:

app.ServeDynamicContent()

This registers the /_content/ admin namespace, runs idempotent startup migrations, and re-registers public routes for all previously defined types from the database. Call it before app.Handler().


Migration note

Deployments that ran core v1.41.0 (A153) need the url_prefix column added to smeldr_content_type_schemas. MigrateURLPrefixColumn handles this idempotently at startup, called by ServeDynamicContent() automatically. No manual step is required.