Files
emdash-patch-imageupload/templates/starter-cloudflare/.claude/skills/building-emdash-site/references/schema-and-seed.md
kunthawat 2d1be52177 Emdash source with visual editor image upload fix
Fixes:
1. media.ts: wrap placeholder generation in try-catch
2. toolbar.ts: check r.ok, display error message in popover
2026-05-03 10:44:54 +07:00

11 KiB

Schema and Seed Files

The seed file (seed/seed.json) defines the site's entire schema and optional demo content. It's applied on first run or via npx emdash seed seed/seed.json.

Seed File Structure

{
	"$schema": "https://emdashcms.com/seed.schema.json",
	"version": "1",
	"meta": {
		"name": "My Site",
		"description": "A description of this site",
		"author": "Author Name"
	},
	"settings": { ... },
	"collections": [ ... ],
	"taxonomies": [ ... ],
	"menus": [ ... ],
	"widgetAreas": [ ... ],
	"sections": [ ... ],
	"bylines": [ ... ],
	"content": { ... }
}

Collections

Collections define content types. Each collection becomes a database table (ec_{slug}).

{
	"slug": "posts",
	"label": "Posts",
	"labelSingular": "Post",
	"supports": ["drafts", "revisions", "search", "seo"],
	"commentsEnabled": true,
	"fields": [ ... ]
}

Collection Supports

Support Description
drafts Draft/published workflow
revisions Revision history
search Full-text search indexing
seo SEO meta fields in admin

Slug Rules

  • Lowercase alphanumeric + underscores: /^[a-z][a-z0-9_]*$/
  • Max 63 characters
  • Cannot conflict with reserved slugs

Field Types

Type Column type Runtime shape Notes
string TEXT string Single line text
text TEXT string Multi-line text (textarea)
number REAL number Floating point
integer INTEGER number Whole numbers
boolean INTEGER boolean Stored as 0/1
datetime TEXT Date ISO 8601 string in DB
image TEXT { id, src?, alt?, width?, height? } Object, not a string
reference TEXT string (ID) Reference to another entry
portableText JSON PortableTextBlock[] Rich text as structured JSON
json JSON any Arbitrary JSON data

Field Definition

{
	"slug": "title",
	"label": "Title",
	"type": "string",
	"required": true,
	"searchable": true
}

Fields can have:

  • slug (required) -- field identifier
  • label (required) -- display label in admin
  • type (required) -- one of the types above
  • required -- validation
  • searchable -- include in full-text search index

Common Field Patterns

Blog post:

"fields": [
	{ "slug": "title", "label": "Title", "type": "string", "required": true, "searchable": true },
	{ "slug": "featured_image", "label": "Featured Image", "type": "image" },
	{ "slug": "content", "label": "Content", "type": "portableText", "searchable": true },
	{ "slug": "excerpt", "label": "Excerpt", "type": "text" }
]

Portfolio project:

"fields": [
	{ "slug": "title", "label": "Title", "type": "string", "required": true, "searchable": true },
	{ "slug": "featured_image", "label": "Featured Image", "type": "image", "required": true },
	{ "slug": "client", "label": "Client", "type": "string" },
	{ "slug": "year", "label": "Year", "type": "string" },
	{ "slug": "summary", "label": "Summary", "type": "text", "searchable": true },
	{ "slug": "content", "label": "Content", "type": "portableText", "searchable": true },
	{ "slug": "gallery", "label": "Gallery", "type": "json" },
	{ "slug": "url", "label": "Project URL", "type": "string" }
]

Page (minimal):

"fields": [
	{ "slug": "title", "label": "Title", "type": "string", "required": true, "searchable": true },
	{ "slug": "content", "label": "Content", "type": "portableText", "searchable": true }
]

Taxonomies

Taxonomies are tag/category systems attached to collections.

{
	"name": "category",
	"label": "Categories",
	"labelSingular": "Category",
	"hierarchical": true,
	"collections": ["posts"],
	"terms": [
		{ "slug": "development", "label": "Development" },
		{ "slug": "design", "label": "Design" }
	]
}
  • hierarchical: true -- tree structure (like WordPress categories)
  • hierarchical: false -- flat list (like WordPress tags)
  • collections -- which collections this taxonomy applies to
  • terms -- pre-defined terms to create

Menus

Navigation menus, managed from the admin UI.

{
	"name": "primary",
	"label": "Primary Navigation",
	"items": [
		{ "type": "custom", "label": "Home", "url": "/" },
		{ "type": "custom", "label": "About", "url": "/pages/about" },
		{ "type": "custom", "label": "Posts", "url": "/posts" }
	]
}

Menu item types:

  • custom -- arbitrary URL
  • Content references are resolved at render time

Widget Areas

Named regions where editors can add configurable widgets.

{
	"name": "sidebar",
	"label": "Sidebar",
	"description": "Widget area displayed on single post pages",
	"widgets": [
		{
			"type": "component",
			"componentId": "core:search",
			"title": "Search"
		},
		{
			"type": "component",
			"componentId": "core:categories",
			"title": "Categories"
		},
		{
			"type": "component",
			"componentId": "core:tags",
			"title": "Tags"
		},
		{
			"type": "component",
			"componentId": "core:recent-posts",
			"title": "Recent Posts",
			"settings": { "count": 5, "showDate": true }
		},
		{
			"type": "component",
			"componentId": "core:archives",
			"title": "Archives",
			"settings": { "type": "monthly", "limit": 6 }
		},
		{
			"type": "content",
			"title": "About",
			"content": [
				{
					"_type": "block",
					"style": "normal",
					"children": [{ "_type": "span", "text": "Some rich text content." }]
				}
			]
		}
	]
}

Widget types

Type Description Key fields
content Rich text (Portable Text) content
menu Navigation menu menuName
component Core or custom component componentId, settings

Core widget components

  • core:search -- search form
  • core:categories -- category list with counts
  • core:tags -- tag cloud
  • core:recent-posts -- latest posts list
  • core:archives -- monthly archive links

Sections (Reusable Blocks)

Reusable content blocks that editors can insert via /section slash command in the editor.

{
	"slug": "newsletter-signup",
	"title": "Newsletter Signup",
	"description": "A call-to-action block for newsletter subscriptions",
	"keywords": ["newsletter", "subscribe", "email", "cta"],
	"source": "theme",
	"content": [
		{
			"_type": "block",
			"style": "h3",
			"children": [{ "_type": "span", "text": "Stay in the loop" }]
		},
		{
			"_type": "block",
			"style": "normal",
			"children": [{ "_type": "span", "text": "Get notified when new posts are published." }]
		}
	]
}

Bylines

Named author profiles, independent of user accounts.

{
	"id": "byline-editorial",
	"slug": "emdash-editorial",
	"displayName": "EmDash Editorial"
}

Guest bylines:

{
	"id": "byline-guest",
	"slug": "guest-contributor",
	"displayName": "Guest Contributor",
	"isGuest": true
}

Settings

Site-wide settings:

"settings": {
	"title": "My Blog",
	"tagline": "Thoughts on building for the web"
}

Available keys: title, tagline, logo, favicon, social, timezone, dateFormat.

Content

Sample content organized by collection slug:

"content": {
	"posts": [
		{
			"id": "post-1",
			"slug": "hello-world",
			"status": "published",
			"data": {
				"title": "Hello World",
				"excerpt": "My first post.",
				"featured_image": {
					"$media": {
						"url": "https://images.unsplash.com/photo-xxx?w=1200&h=800&fit=crop",
						"alt": "Description of image",
						"filename": "hello-world.jpg"
					}
				},
				"content": [
					{
						"_type": "block",
						"style": "normal",
						"children": [{ "_type": "span", "text": "This is the body text." }]
					}
				]
			},
			"bylines": [
				{ "byline": "byline-editorial" }
			],
			"taxonomies": {
				"category": ["development"],
				"tag": ["webdev", "opinion"]
			}
		}
	],
	"pages": [
		{
			"id": "about",
			"slug": "about",
			"status": "published",
			"data": {
				"title": "About",
				"content": [
					{
						"_type": "block",
						"style": "normal",
						"children": [{ "_type": "span", "text": "About this site." }]
					}
				]
			}
		}
	]
}

Media references in seed content

Use $media for image fields -- EmDash downloads and stores the image:

"featured_image": {
	"$media": {
		"url": "https://images.unsplash.com/photo-xxx?w=1200&h=800&fit=crop",
		"alt": "Description",
		"filename": "my-image.jpg"
	}
}

For external images without downloading:

"featured_image": "https://images.unsplash.com/photo-xxx?w=1200"

Reference fields in seed content

Use $ref:id format to reference other entries:

"author": "$ref:byline-editorial"

Portable Text in seed content

Content fields of type portableText are arrays of blocks:

[
	{
		"_type": "block",
		"style": "normal",
		"children": [{ "_type": "span", "text": "A paragraph." }]
	},
	{
		"_type": "block",
		"style": "h2",
		"children": [{ "_type": "span", "text": "A heading" }]
	},
	{
		"_type": "block",
		"style": "blockquote",
		"children": [{ "_type": "span", "text": "A quote." }]
	}
]

Inline marks (bold, italic, links):

{
	"_type": "block",
	"style": "normal",
	"children": [
		{ "_type": "span", "text": "This is " },
		{ "_type": "span", "text": "bold", "marks": ["strong"] },
		{ "_type": "span", "text": " and " },
		{ "_type": "span", "text": "italic", "marks": ["em"] }
	]
}

Block styles: normal, h1-h6, blockquote.

Draft content

Set "status": "draft" to create unpublished content:

{
	"id": "post-draft",
	"slug": "work-in-progress",
	"status": "draft",
	"data": { ... }
}

Validation

npx emdash seed seed/seed.json --validate

Catches:

  • Image fields with raw URLs (should use $media)
  • Reference fields with raw IDs (should use $ref:id)
  • PortableText not an array or missing _type
  • Type mismatches (string vs number, etc.)

Applying Seeds

npx emdash seed seed/seed.json              # Apply with content
npx emdash seed seed/seed.json --no-content  # Schema only (no sample content)

Exporting Seeds

npx emdash export-seed                      # Schema only
npx emdash export-seed --with-content       # Schema + all content
npx emdash export-seed --with-content=posts,pages  # Specific collections