Configuration

Configuration#

Lesstruct has two layers of configuration, each for a different concern:

  • config.toml — your site’s content schema: languages, custom post types, user profile fields, thumbnail variants. Edited by hand, version-controlled.
  • Environment variables (in .env or the process env) — your deployment configuration: host, port, database, secrets, SMTP, AI integrations. Treated as deployment state, not committed.

This document covers both.

Table of Contents#

Where the Files Live#

ConcernDefault locationOverride via env
Content schema./config.toml in the working directoryCONFIG_DIR (directory), CONFIG_FILE (filename)
Deployment config.env in the working directoryprocess env wins over .env
Custom themeempty (uses embedded theme)THEME_DIR=themes/<name>
Pluginsplugins/ in the working directorynot configurable (hard-coded)
Databasedata/lesstruct.db (SQLite)DB_DRIVER, DB_PATH, DB_DSN

config.toml is loaded once at startup from CONFIG_DIR/CONFIG_FILE. The runtime does not auto-merge or auto-generate anything; the file is read as-is. If the file is missing, the runtime falls back to defaults (one language: English, default post and page post types, default thumbnail at 370 px). Validation errors at startup are reported with a clear message; the server does not start with an invalid config.toml.

Stale comment in older config.toml files. Some early Lesstruct releases shipped a config.toml whose header says “this file is generated by merging all TOML files in config/”. There is no such auto-merge. Edit config.toml directly.

Environment Variables#

All env vars are loaded by internal/config/config.go:Load() and override the corresponding defaults. The runtime also loads a .env file in the working directory via godotenv (env vars in the actual process env take precedence over .env).

Server#

VariableDefaultDescription
HOST0.0.0.0Bind address for the HTTP server.
PORT8080Bind port. Validated to be in [1, 65535].

Database#

VariableDefaultDescription
DB_DRIVERsqliteOne of sqlite, postgres, mysql.
DB_PATHdata/lesstruct.dbSQLite file path (used when DB_DRIVER=sqlite).
DB_DSNemptyRequired for postgres and mysql. See per-driver requirements below.
DB_POOL_MAX_CONNS20Max connections in the pool (used with postgres and mysql). Must be ≥ 1 when set.

Per-driver requirements (enforced at startup):

  • SQLite — no extra requirements. Just set DB_PATH (or use the default).
  • PostgresDB_DSN is required. Format: postgres://user:password@host:port/db?sslmode=disable. DB_POOL_MAX_CONNS must be ≥ 1 if set.
  • MySQLDB_DSN is required. The DSN must contain parseTime=true and multiStatements=true. Without them, DATE columns scan as []byte and migrations with multiple statements fail. Format: user:password@tcp(host:port)/db?parseTime=true&multiStatements=true&charset=utf8mb4&collation=utf8mb4_general_ci.

Authentication#

VariableDefaultDescription
JWT_SECRETempty (required)HMAC secret for JWTs. Required. Must be at least 32 characters.
API_KEY_PEPPERemptyPepper prepended to API key secrets before hashing. Adding or changing it invalidates all existing API keys.

SMTP#

VariableDefaultDescription
SMTP_HOSTemptySMTP server hostname. When unset, the email-verification and password-reset flows will not work.
SMTP_PORT587SMTP port.
SMTP_USERemptySMTP auth username.
SMTP_PASSWORDemptySMTP auth password.
SMTP_FROMemptyFrom: address for outbound emails.

For local development, Mailtrap or any sandbox SMTP service is a safe choice.

CORS#

VariableDefaultDescription
CORS_ALLOWED_ORIGINShttp://localhost:5173Comma-separated list of allowed origins. Each must be a valid http:// or https:// URL with a non-empty host. The default is suitable for local admin dev.

A typical production value:

1
CORS_ALLOWED_ORIGINS=https://example.com,https://www.example.com,https://admin.example.com

Site#

VariableDefaultDescription
SITE_URLhttp://localhost:8080Base URL of the public site. Used in email verification links, password reset links, and Open Graph tags.
DEV_MODEfalseWhen true, the admin panel is served from the Vite dev server (ADMIN_DEV_URL) instead of the embedded build. Same env var enables plugin hot-reload — see the plugin skill.
ADMIN_DEV_URLhttp://localhost:5173URL of the Vite dev server (only used when DEV_MODE=true).
THEME_DIRemptyPath to a custom theme directory. Empty uses the embedded theme. See the theme skill.

Rate limits#

VariableDefaultDescription
RATE_LIMIT_ENABLEDtrueMaster toggle. Set to false to disable all rate limiting (not recommended in production).
RATE_LIMIT_AUTH_PER_MINUTE5Per-IP cap on auth endpoints (login, register, forgot-password, reset-password).
RATE_LIMIT_API_PER_MINUTE100Per-token cap on authenticated API endpoints.
RATE_LIMIT_PUBLIC_PER_MINUTE60Per-IP cap on public endpoints (search, content listing, etc.).

Logging#

VariableDefaultDescription
LOG_LEVELinfoOne of debug, info, warn, error.

AI image generation (optional)#

When AI_IMAGE_GENERATION_API_KEY is set, the admin panel shows a “Generate with AI” button in the media library and content editor.

VariableDefaultDescription
AI_IMAGE_GENERATION_API_KEYemptyAPI key for the image provider.
AI_IMAGE_GENERATION_MODELimagen-4.0-fast-generate-001Model name. Examples: imagen-4.0-fast-generate-001, imagen-4.0-ultra-generate-001, gpt-image-1, gpt-image-1-mini, gemini-2.5-flash-image.
AI_IMAGE_GENERATION_SIZEemptyPixel size, e.g. 1024x1024.
AI_IMAGE_GENERATION_ASPECT_RATIOemptyAspect ratio, e.g. 16:9, 1:1, 4:3.

AI text generation (optional)#

When AI_TEXT_GENERATION_API_KEY is set, the admin panel enables AI text enhancement and translation. Works with any OpenAI-compatible API.

VariableDefaultDescription
AI_TEXT_GENERATION_API_KEYemptyAPI key for the text provider.
AI_TEXT_GENERATION_BASE_URLemptyOverride the API base URL. Default is OpenAI’s API. For other providers: https://api.deepseek.com, https://openrouter.ai/api/v1, http://localhost:11434/v1 (Ollama), etc.
AI_TEXT_GENERATION_MODELgpt-5-miniChat model name. Examples by provider: OpenAI gpt-5-mini, DeepSeek deepseek-chat, OpenRouter openai/gpt-5-mini, Together meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8, Ollama llama3.

Duplicate-key gotcha#

.env files are parsed by godotenv, which is a last-value-wins parser. If the same variable appears twice, only the second value is used:

1
2
THEME_DIR=themes/dark-warm
THEME_DIR=

The first line is silently overridden by the empty second line — Lesstruct ends up using the embedded theme. This is a common operator mistake. Check your .env for duplicate keys if a setting appears to have no effect.

config.toml Reference#

config.toml is loaded at startup. If the file is missing, the runtime uses defaults: English only, default post and page post types, default thumbnail at 370 px. If the file is present but invalid, the server fails to start with a validation error.

Top-level keys#

KeyTypeDefaultDescription
languages[]string["en"]ISO 639-1 language codes. The first is the primary language. Used by the i18n catalog, the admin language switcher, and the content language switcher.
[user_fields]tableemptyGlobal user profile fields. Applies to all users.
[[post_type]]array of tablesone default post typeCustom post types (see below). Add as many as needed.
[[thumbnail]]array of tables[{max_width=370, suffix="_thumb"}]Image processing variants. See below.

[user_fields]#

KeyTypeDescription
fields[]FieldSchemaUser-editable fields shown in the user profile.
system_fields[]FieldSchemaRead-only fields managed by plugins or operators.

The schema for each field is the same as post-type fields (below).

[[post_type]]#

Each entry defines one custom post type. The default post and page post types are always present; you can extend them by adding custom fields (use the same slug as the default post type) or you can define entirely new post types.

KeyTypeRequiredDescription
namestringyesDisplay name (e.g. "Product"). 1-200 characters.
slugstringyesURL slug (e.g. "product"). 1-200 characters. Kebab-case only: lowercase letters, digits, hyphens, underscores. No leading/trailing hyphens, no consecutive hyphens.
descriptionstringnoHuman-readable description. Shown in the admin panel.
supports[]stringyes (non-empty)Features the post type supports. Each entry must be one of: title, content, tags, featured_image, excerpt. At least one is required.
fields[]FieldSchemanoUser-editable custom fields.
system_fields[]FieldSchemanoRead-only system fields. Often set by plugins via before_save hooks.

Field schema (FieldSchema)#

Used in [user_fields].fields, [user_fields].system_fields, [[post_type]].fields, and [[post_type]].system_fields.

KeyTypeRequiredDescription
namestringyesDisplay name. 1-200 characters.
slugstringyesIdentifier. 1-200 characters, snake_case (regex-enforced). Must be unique within the parent (user fields or a post type).
typestringyesOne of text, textarea, number, date, select, checkbox.
requiredboolnoWhen true, the field must have a value when saving.
options[]stringfor selectThe list of allowed values. Required and non-empty for select.
minfloatfor numberMinimum allowed value.
maxfloatfor numberMaximum allowed value.
max_lengthintfor text/textareaMaximum character count.

[[thumbnail]]#

Each entry defines one image processing variant. When media is uploaded, Lesstruct generates one file per variant.

KeyTypeRequiredDescription
max_widthintyesMaximum width in pixels. Must be > 0.
suffixstringnoFilename suffix. Must be unique. The default thumbnail has suffix _thumb.

If no [[thumbnail]] entries are defined, the runtime uses a single default variant: max_width = 370, suffix = "_thumb".

Validation Rules#

These are enforced at startup by the runtime. Violations cause the server to fail to start with a clear error.

Post type rules#

  • name must be 1-200 characters (internal/domain/posttype/types.go:93-99).
  • slug must be 1-200 characters, contain only lowercase letters, digits, hyphens, and underscores; cannot start or end with a hyphen; cannot contain consecutive hyphens (types.go:102-126).
  • supports must be non-empty and each entry must be one of: title, content, tags, featured_image, excerpt (types.go:26-32, 129-146).
  • Duplicate post-type slugs are rejected (types.go:20).

Field rules#

  • name must be 1-200 characters (internal/domain/customfield/types.go:98-104).
  • slug must be 1-200 characters and match the snake-case regex (types.go:106-115).
  • type must be one of: text, textarea, number, date, select, checkbox (types.go:35-41, 118-123).
  • select fields must have a non-empty options list (types.go:125-128).
  • number fields can have min and max; text and textarea fields can have max_length. Other combinations are rejected (types.go:141-159).
  • Duplicate field slugs within the same parent (user fields or a single post type) are rejected (types.go:90-93).

File rules#

  • CONFIG_FILE must not contain path separators or .. (internal/config/posttypes.go:19-21).
  • The config directory must exist and be readable; the file is optional (defaults apply if missing).

Worked Examples#

Example A — Minimal blog#

A personal blog with one language and the default post types. config.toml only sets the language; everything else falls back to defaults.

1
2
# ── Languages ───────────────────────────────────────────────────────
languages = ["en"]

That’s it. You can omit [[thumbnail]] entirely (the runtime uses the default 370 px _thumb variant). No custom post types, no custom user fields, no custom themes, no plugins.

For a more useful starting point, add a [[thumbnail]] for medium-sized previews:

1
2
3
4
5
6
7
8
9
languages = ["en"]

[[thumbnail]]
max_width = 370
suffix = "_thumb"

[[thumbnail]]
max_width = 800
suffix = "_medium"

Example B — Multilingual site (English + Indonesian)#

A two-language site with a custom user profile (system fields for gamification, regular fields for bio/links).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# ── Languages ───────────────────────────────────────────────────────
# English is primary; Indonesian is supported.
# Both must have translation TOML files in internal/i18n/translations/
# (en.toml, id.toml) — these ship with Lesstruct.
languages = ["en", "id"]

# ── User Profile Fields ─────────────────────────────────────────────
[user_fields]
fields = [
  { name = "Job Title",  slug = "job_title",  type = "text" },
  { name = "Company",    slug = "company",    type = "text" },
  { name = "Website",    slug = "website",    type = "text" },
  { name = "Bio",        slug = "bio",        type = "textarea", max_length = 500 },
]
system_fields = [
  { name = "Points",        slug = "points",        type = "number",   min = 0, max = 100000 },
  { name = "Account Tier",  slug = "account_tier",  type = "select",   options = ["free", "basic", "pro", "enterprise"] },
  { name = "Internal Notes", slug = "internal_notes", type = "textarea", max_length = 1000 },
]

# ── Thumbnail Sizes ─────────────────────────────────────────────────
[[thumbnail]]
max_width = 370
suffix = "_thumb"

[[thumbnail]]
max_width = 800
suffix = "_medium"

[[thumbnail]]
max_width = 1600
suffix = "_large"

The runtime falls back to the default post and page post types (in both languages) for the content schema. Users can write posts and pages; the i18n switcher in the layout lets visitors pick English or Indonesian.

Example C — Shop with custom post types#

A two-post-type content schema: product for a storefront and portfolio for a work showcase. Both have realistic field combinations.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# ── Languages ───────────────────────────────────────────────────────
languages = ["en"]

# ── User Profile Fields ─────────────────────────────────────────────
[user_fields]
fields = [
  { name = "Job Title", slug = "job_title", type = "text" },
  { name = "Company",   slug = "company",   type = "text" },
  { name = "Website",   slug = "website",   type = "text" },
]
system_fields = [
  { name = "Points",        slug = "points",        type = "number", min = 0, max = 100000 },
  { name = "Account Tier",  slug = "account_tier",  type = "select", options = ["free", "basic", "pro", "enterprise"] },
]

# ── Custom Post Types ───────────────────────────────────────────────
# A product catalog with pricing, stock, and category.
[[post_type]]
name = "Product"
slug = "product"
description = "E-commerce product listings"
supports = ["title", "content", "excerpt"]

[[post_type.fields]]
name = "SKU"
slug = "sku"
type = "text"
required = true

[[post_type.fields]]
name = "Price"
slug = "price"
type = "number"
min = 0.01
max = 999999.99
required = true

[[post_type.fields]]
name = "Sale Price"
slug = "sale_price"
type = "number"
min = 0.01
max = 999999.99

[[post_type.fields]]
name = "Stock Quantity"
slug = "stock_quantity"
type = "number"
min = 0
max = 100000

[[post_type.fields]]
name = "Product Details"
slug = "product_details"
type = "textarea"
max_length = 2000

[[post_type.fields]]
name = "Release Date"
slug = "release_date"
type = "date"

[[post_type.fields]]
name = "Category"
slug = "category"
type = "select"
options = ["Electronics", "Clothing", "Home & Garden", "Books", "Toys"]
required = true

[[post_type.fields]]
name = "Size"
slug = "size"
type = "select"
options = ["XS", "S", "M", "L", "XL", "XXL"]

[[post_type.fields]]
name = "On Sale"
slug = "on_sale"
type = "checkbox"

[[post_type.fields]]
name = "Free Shipping"
slug = "free_shipping"
type = "checkbox"

[[post_type.system_fields]]
name = "Fulfillment Status"
slug = "fulfillment_status"
type = "select"
options = ["unfulfilled", "partial", "fulfilled", "returned"]

[[post_type.system_fields]]
name = "Warehouse Code"
slug = "warehouse_code"
type = "text"

# A portfolio / case-study post type with project metadata.
[[post_type]]
name = "Portfolio"
slug = "portfolio"
description = "Portfolio items to showcase work"
supports = ["title", "content", "tags", "featured_image", "excerpt"]

[[post_type.fields]]
name = "Client Name"
slug = "client_name"
type = "text"
required = true

[[post_type.fields]]
name = "Project Description"
slug = "project_description"
type = "textarea"
max_length = 1000

[[post_type.fields]]
name = "Project Date"
slug = "project_date"
type = "date"
required = true

[[post_type.fields]]
name = "Budget"
slug = "budget"
type = "number"
min = 0
max = 1000000

[[post_type.fields]]
name = "Project Type"
slug = "project_type"
type = "select"
options = ["Website", "Mobile App", "Desktop App", "API", "Consulting"]

[[post_type.fields]]
name = "Published"
slug = "published"
type = "checkbox"

[[post_type.system_fields]]
name = "Portfolio Status"
slug = "portfolio_status"
type = "select"
options = ["draft", "in_review", "approved", "archived"]

[[post_type.system_fields]]
name = "Internal Notes"
slug = "internal_notes"
type = "textarea"
max_length = 500

# ── Thumbnail Sizes ─────────────────────────────────────────────────
[[thumbnail]]
max_width = 370
suffix = "_thumb"

[[thumbnail]]
max_width = 800
suffix = "_medium"

[[thumbnail]]
max_width = 1600
suffix = "_large"

Pair this with a .env that enables the AI integrations, e.g.:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# Database (SQLite is fine for a small shop)
DB_DRIVER=sqlite
DB_PATH=data/shop.db

# JWT
JWT_SECRET=replace_me_with_a_32_plus_character_random_string

# SMTP
SMTP_HOST=sandbox.smtp.mailtrap.io
SMTP_PORT=587
SMTP_USER=your_user
SMTP_PASSWORD=your_pass
SMTP_FROM=orders@example.com

# CORS
CORS_ALLOWED_ORIGINS=https://shop.example.com,https://admin.example.com

# Site
SITE_URL=https://shop.example.com

# Theme (optional)
THEME_DIR=themes/custom

# AI image generation (optional)
AI_IMAGE_GENERATION_API_KEY=sk-...
AI_IMAGE_GENERATION_MODEL=gpt-image-1-mini

# AI text generation (optional, e.g. DeepSeek)
AI_TEXT_GENERATION_API_KEY=sk-...
AI_TEXT_GENERATION_BASE_URL=https://api.deepseek.com
AI_TEXT_GENERATION_MODEL=deepseek-chat

What is NOT Configurable from config.toml#

config.toml and the env vars cover deployment and content schema, but several other surfaces are configured elsewhere:

SurfaceHow to configureReference
Public site theme (CSS, JS, HTML templates)THEME_DIR=themes/<name> env var → a themes/<name>/ directoryskills/lesstruct-theme-development/
WASM plugins (custom hooks, external API calls)<name>.wasm and <name>.manifest in plugins/skills/lesstruct-plugin-development/
Admin panel branding (logo, colors, copy)Edit web/admin/ source and rebuildmake build-admin
API response shapesEdit internal/api/handlers/source only
CLI flagslesstruct-cli --helpbuilt-in

Plugins and themes are loaded at startup and hot-reloaded only when DEV_MODE=true is set (and even then, only the plugin watcher is recursive; the theme is not). Admin and API changes always require a rebuild / redeploy.

Upgrading Lesstruct#

When you bump the Lesstruct version (via go install github.com/aristorinjuang/lesstruct@<version> or a new release tarball):

  1. Back up your config.toml and .env. New versions may add fields that your old config doesn’t have; the runtime applies sensible defaults for any field that is missing.
  2. Diff the new config.toml.example and .env.example against your files. Lesstruct’s release notes call out new env vars; copy them into your .env only if you need the feature.
  3. Validate before starting. Start the server with the new binary. If config.toml has a new validation rule (e.g. a new field type), the runtime reports it at startup. Fix and retry.
  4. New field types or supported features are documented here. If you see a new entry in the Validation Rules section, your existing config.toml will keep working; only new post types you add will need to use the new field types.
  5. Theme and plugin skills ship independently of the runtime. After a runtime upgrade, re-run the theme and plugin skills to compare your themes/<name>/ and plugins/<name>.wasm against any new defaults.

Troubleshooting#

JWT_SECRET is required at startup#

You didn’t set JWT_SECRET in .env (or it’s empty). It must be present and at least 32 characters:

1
JWT_SECRET=$(head -c 48 /dev/urandom | base64)

unsupported DB_DRIVER "X"#

DB_DRIVER must be sqlite, postgres, or mysql. The runtime rejects other values at startup.

DB_DSN must contain parseTime=true (MySQL)#

The MySQL DSN is missing the parseTime=true query parameter. Without it, DATE columns scan as []byte. Add it to the DSN:

1
user:pass@tcp(host:port)/db?parseTime=true&multiStatements=true&charset=utf8mb4&collation=utf8mb4_general_ci

DB_DSN must contain multiStatements=true (MySQL)#

Same fix as above — the multiStatements=true parameter is required for golang-migrate to run migrations with multiple SQL statements.

post type slug "X" is invalid#

X violates the slug rules: it must be kebab-case, lowercase letters/digits/hyphens/underscores only, no leading/trailing hyphens, no consecutive hyphens (--). Examples:

  • product, team-member, case_study
  • Product (uppercase), -product (leading hyphen), team--member (consecutive hyphens), team.member (period not allowed)

field "x": duplicate slug#

Two fields in the same parent (user fields, or a single post type) have the same slug. Slugs must be unique within a parent.

field type must be one of: text, textarea, number, date, select, checkbox#

Typo in the type field, or a new field type that this version of Lesstruct doesn’t support. Check the Field schema table for the current list.

select field requires non-empty options#

A select field has no options = [...] list. Add at least one option.

CONFIG_FILE must not contain path separators#

You tried to set CONFIG_FILE to a path like config/shop.toml. The runtime only supports a flat filename in CONFIG_DIR; subdirectories are not allowed.

Theme changes are not taking effect#

Cross-reference the lesstruct-theme-development skill. Common causes: THEME_DIR is empty, points to a missing directory, or the server was not restarted after the last change.

Plugin hooks are not firing#

Cross-reference the lesstruct-plugin-development skill. The currently-invoked hooks are before_save, after_create, and after_publish. on_plugin_loaded and before_delete are defined but not invoked today.

Env var appears to have no effect (.env)#

Check for duplicate keys in .env. The godotenv parser uses last-value-wins, so a later THEME_DIR= line silently overrides an earlier THEME_DIR=themes/dark-warm.

Quick Reference#

All env vars (with defaults)#

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
HOST=0.0.0.0
PORT=8080
DB_DRIVER=sqlite
DB_PATH=data/lesstruct.db
DB_DSN=
DB_POOL_MAX_CONNS=20
JWT_SECRET=                  # required, ≥ 32 chars
API_KEY_PEPPER=
SMTP_HOST=
SMTP_PORT=587
SMTP_USER=
SMTP_PASSWORD=
SMTP_FROM=
CORS_ALLOWED_ORIGINS=http://localhost:5173
SITE_URL=http://localhost:8080
DEV_MODE=false
ADMIN_DEV_URL=http://localhost:5173
THEME_DIR=
LOG_LEVEL=info
RATE_LIMIT_ENABLED=true
RATE_LIMIT_AUTH_PER_MINUTE=5
RATE_LIMIT_API_PER_MINUTE=100
RATE_LIMIT_PUBLIC_PER_MINUTE=60
AI_IMAGE_GENERATION_API_KEY=
AI_IMAGE_GENERATION_MODEL=imagen-4.0-fast-generate-001
AI_IMAGE_GENERATION_SIZE=
AI_IMAGE_GENERATION_ASPECT_RATIO=
AI_TEXT_GENERATION_API_KEY=
AI_TEXT_GENERATION_BASE_URL=
AI_TEXT_GENERATION_MODEL=gpt-5-mini

All config.toml keys#

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
languages = ["en"]

[user_fields]
fields = [{ name, slug, type, required, ... }]
system_fields = [{ name, slug, type, ... }]

[[post_type]]
name = ""                        # required, 1-200 chars
slug = ""                        # required, kebab-case
description = ""
supports = ["title", "content", "tags", "featured_image", "excerpt"]   # ≥ 1
fields = [{ name, slug, type, required, ... }]
system_fields = [{ name, slug, type, ... }]

[[thumbnail]]
max_width = 370                  # > 0
suffix = "_thumb"                # unique

All field types#

TypeRequired sub-keysOptional sub-keys
textname, slug, typerequired, max_length
textareaname, slug, typerequired, max_length
numbername, slug, typerequired, min, max
datename, slug, typerequired
selectname, slug, type, options (non-empty)required
checkboxname, slug, typerequired

All supported supports values#

title, content, tags, featured_image, excerpt.