470 lines
11 KiB
Markdown
470 lines
11 KiB
Markdown
# 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
|
|
|
|
```json
|
|
{
|
|
"$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}`).
|
|
|
|
```json
|
|
{
|
|
"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
|
|
|
|
```json
|
|
{
|
|
"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:**
|
|
|
|
```json
|
|
"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:**
|
|
|
|
```json
|
|
"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):**
|
|
|
|
```json
|
|
"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.
|
|
|
|
```json
|
|
{
|
|
"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.
|
|
|
|
```json
|
|
{
|
|
"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.
|
|
|
|
```json
|
|
{
|
|
"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.
|
|
|
|
```json
|
|
{
|
|
"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.
|
|
|
|
```json
|
|
{
|
|
"id": "byline-editorial",
|
|
"slug": "emdash-editorial",
|
|
"displayName": "EmDash Editorial"
|
|
}
|
|
```
|
|
|
|
Guest bylines:
|
|
|
|
```json
|
|
{
|
|
"id": "byline-guest",
|
|
"slug": "guest-contributor",
|
|
"displayName": "Guest Contributor",
|
|
"isGuest": true
|
|
}
|
|
```
|
|
|
|
## Settings
|
|
|
|
Site-wide settings:
|
|
|
|
```json
|
|
"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:
|
|
|
|
```json
|
|
"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:
|
|
|
|
```json
|
|
"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:
|
|
|
|
```json
|
|
"featured_image": "https://images.unsplash.com/photo-xxx?w=1200"
|
|
```
|
|
|
|
### Reference fields in seed content
|
|
|
|
Use `$ref:id` format to reference other entries:
|
|
|
|
```json
|
|
"author": "$ref:byline-editorial"
|
|
```
|
|
|
|
### Portable Text in seed content
|
|
|
|
Content fields of type `portableText` are arrays of blocks:
|
|
|
|
```json
|
|
[
|
|
{
|
|
"_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):
|
|
|
|
```json
|
|
{
|
|
"_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:
|
|
|
|
```json
|
|
{
|
|
"id": "post-draft",
|
|
"slug": "work-in-progress",
|
|
"status": "draft",
|
|
"data": { ... }
|
|
}
|
|
```
|
|
|
|
## Validation
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
npx emdash seed seed/seed.json # Apply with content
|
|
npx emdash seed seed/seed.json --no-content # Schema only (no sample content)
|
|
```
|
|
|
|
## Exporting Seeds
|
|
|
|
```bash
|
|
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
|
|
```
|