Files
rustycms/README.md

693 lines
29 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# RustyCMS
A file-based headless CMS written in Rust. Content types are defined as JSON5 schemas, content is stored as JSON5 files, and served via a REST API. **Under development.**
## Features
- **Schema-first**: Define content types in `types/*.json5` fields, types, constraints, defaults
- **TypeScript-like type system**: `extends` (including multiple), `pick`, `omit`, `partial`
- **Rich validation**: `minLength`, `maxLength`, `min`, `max`, `pattern`, `enum`, `unique`, `nullable`, `readonly`, `strict`
- **File-based (default)**: No database setup everything is files, versionable with Git
- **Optional SQLite**: Switch store via env same API, different storage layer
- **REST API**: Full CRUD endpoints, auto-generated per content type
- **References & _resolve**: Reference fields as `{ _type, _slug }`; embed with `?_resolve=all`
- **Referrer index**: Reverse index of who references which entry; file-based in `content/_referrers.json`; full reindex when file is missing; `GET /api/content/:type/:slug/referrers`
- **Reusable partials**: `reusable` schemas and `useFields` for shared field groups (e.g. layout)
- **Optional API auth**: Protect write access (POST/PUT/DELETE) with an API key via env
- **Admin UI**: Next.js web interface for browsing collections, creating and editing content; manage types (list, create, edit, delete) with schema files and JSON Schema export updated on save
- **Swagger UI**: Interactive API docs at `/swagger-ui`
- **Asset management**: Upload, serve, and delete image files (jpg, png, webp, avif, gif, svg) stored in `content/assets/` via `GET/POST/DELETE /api/assets`; served with immutable cache headers; combinable with `/api/transform` for on-the-fly resizing
- **Image transformation**: `GET /api/transform` load image from external URL or local asset, optional resize (w, h), aspect-ratio crop (e.g. 1:1), fit (fill/contain/cover), output formats jpeg, png, webp, avif; with response cache
- **JSON5**: Human-friendly format with comments, trailing commas, unquoted keys
- **Editor validation**: Export JSON Schema from your types so VS Code/Cursor validate `content/*.json5` while you edit
- **CORS**: Open by default for frontend development
## Requirements
- [Rust](https://www.rust-lang.org/tools/install) (1.70+)
- [Node.js](https://nodejs.org/) (for Admin UI, optional)
## Quick start
```bash
cd rustycms
cargo run
```
The server starts at `http://127.0.0.1:3000`.
**Start API + Admin UI and open browser:**
```bash
./dev.sh
```
Runs both the API and Admin UI, waits until ready, and opens `http://localhost:2001` in the default browser. Press Ctrl+C to stop both.
### Admin UI (optional)
A Next.js web interface for managing content. Start the API server first, then:
```bash
cd admin-ui
npm install
npm run dev
```
The Admin UI runs at `http://localhost:2001` (different port to avoid conflict with the API).
**Version control:** Only a single demo type (`types/demo.json5`) and one demo content entry (`content/de/demo/demo-welcome.json5`) are tracked. All other files under `types/` and `content/` are ignored via `.gitignore`, so you can add your own types and content without committing them.
**Admin UI:** Dashboard → **Types** lists all content types; you can create a new type, edit (fields, description, category, strict, …) or delete a type. Edits are written to `types/*.json5` and the corresponding `schemas/*.schema.json` is updated. Content is managed under **Collections** (list, new entry, edit, with reference fields and Markdown editor). Schema and data preview are available on the relevant pages.
**Environment variables** (in `admin-ui/.env.local` or as env vars):
| Variable | Default | Description |
|----------|---------|-------------|
| `NEXT_PUBLIC_RUSTYCMS_API_URL` | `http://127.0.0.1:3000` | RustyCMS API base URL |
| `NEXT_PUBLIC_RUSTYCMS_API_KEY` | | API key for write operations (same as `RUSTYCMS_API_KEY`). Optional if you use the Admin UI login. |
**API key setup (backend + Admin UI):**
1. **Backend** In the project root (next to `Cargo.toml`), create or edit `.env`:
```bash
RUSTYCMS_API_KEY=dein-geheimer-schluessel
```
Use any secret string (e.g. `openssl rand -hex 32`). Start the API with `cargo run`; the log should show that API key auth is enabled.
2. **Admin UI** Either:
- **Option A:** In `admin-ui/.env.local` set the same key so the UI can write without logging in:
```bash
NEXT_PUBLIC_RUSTYCMS_API_URL=http://127.0.0.1:3000
NEXT_PUBLIC_RUSTYCMS_API_KEY=dein-geheimer-schluessel
```
- **Option B:** Leave `NEXT_PUBLIC_RUSTYCMS_API_KEY` unset. Open the Admin UI, click **Login** in the sidebar, enter the same API key, and submit. The key is stored in the browser (sessionStorage) until you log out or close the tab.
3. **Check:** Without a key, `GET /api/collections` returns 200; `POST`/`PUT`/`DELETE` return 401. With header `X-API-Key: dein-geheimer-schluessel` (or `Authorization: Bearer …`), write requests succeed.
### CLI options
| Option | Default | Description |
|-----------------|-----------------|---------------------------------------|
| `--types-dir` | `./types` | Directory with type definitions |
| `--content-dir` | `./content` | Directory with content files |
| `-p, --port` | `3000` | Port |
| `--host` | `127.0.0.1` | Host address |
```bash
cargo run -- --types-dir ./my-schemas --content-dir ./my-data -p 8080
```
### Configuration (environment variables)
A `.env` in the project directory is loaded at startup. See `.env.example`.
| Variable | Default | Description |
|--------------------------|---------------|--------------|
| `RUSTYCMS_STORE` | `file` | Store backend: `file` or `sqlite` |
| `RUSTYCMS_DATABASE_URL` | `sqlite:content.db` | When using `sqlite`: SQLite URL (fallback: `DATABASE_URL`) |
| `RUSTYCMS_API_KEY` | | Optional. Single key = full write access (Bearer or X-API-Key). GET stays public. |
| `RUSTYCMS_API_KEYS` | | Optional. Multiple keys with roles: `key1:read_write,key2:read`. Roles: `read`, `read_write`, `admin`. Overrides `RUSTYCMS_API_KEY`. |
| `RUSTYCMS_ENVIRONMENTS` | | Optional. Comma-separated (e.g. `production,staging`). File store only. Content per env; API uses `?_environment=staging`. |
| `RUSTYCMS_WEBHOOKS` | | Optional. Comma-separated URLs. POST with JSON `{ event, collection?, slug?, ... }` on content/asset/schema create/update/delete. |
| `RUSTYCMS_CORS_ORIGIN` | all | Optional. One allowed CORS origin (e.g. `https://my-frontend.com`). Empty or `*` = all allowed. |
| `RUSTYCMS_CACHE_TTL_SECS` | `60` | Optional. Response cache for GET /api/content in seconds. `0` = cache off. |
**Examples:**
```bash
# File store (default), no auth
cargo run
# SQLite backend
RUSTYCMS_STORE=sqlite cargo run
# Write access only with API key
RUSTYCMS_API_KEY=your-secret-token cargo run
```
### Referrer index
When not using `RUSTYCMS_ENVIRONMENTS`, the API maintains a **reverse index** of references: for each entry it knows which other entries reference it. This is stored as a single JSON file in the content directory: **`content/_referrers.json`**. On startup, if that file is missing, a full reindex is run over all collections and locales; on every create/update/delete the index is updated incrementally. Use **`GET /api/content/:type/:slug/referrers`** to list referrers (response: array of `{ collection, slug, field, locale? }`). Documented in Swagger UI and OpenAPI spec.
## Project structure
```
rustycms/
├── admin-ui/ # Next.js Admin UI (optional)
│ ├── src/
│ │ ├── app/ # Dashboard, content list/edit pages
│ │ └── components/ # ContentForm, MarkdownEditor, ReferenceArrayField, Sidebar
│ └── package.json
├── Cargo.toml
├── src/
│ ├── main.rs # Entry point, CLI, server
│ ├── schema/
│ │ ├── types.rs # SchemaDefinition, FieldDefinition
│ │ ├── loader.rs # Load JSON5, resolve extends/pick/omit/partial
│ │ ├── json_schema.rs # Export JSON Schema for editor validation
│ │ ├── validator.rs # Validation (constraints, unique, readonly, …)
│ │ └── mod.rs # SchemaRegistry
│ ├── bin/
│ │ └── export_json_schema.rs # CLI: export schemas + optional .vscode/settings.json
│ ├── store/
│ │ ├── store.rs # ContentStore trait (abstract storage layer)
│ │ ├── filesystem.rs # FileStore CRUD on JSON5 files
│ │ ├── sqlite.rs # SqliteStore optional SQLite backend
│ │ ├── query.rs # Filter, sort, paginate
│ │ ├── slug.rs # Slug normalisation & validation
│ │ └── mod.rs
│ └── api/
│ ├── handlers.rs # REST handlers (CRUD + validation)
│ ├── routes.rs # Axum router
│ ├── openapi.rs # OpenAPI spec & Swagger UI
│ ├── auth.rs # Optional API key check (POST/PUT/DELETE)
│ ├── error.rs # API error types
│ ├── response.rs # Reference formatting, _resolve
│ ├── transform.rs # Image transformation (external URL → resize/crop/format)
│ └── mod.rs
├── types/ # Type definitions (layer 1)
│ ├── blog_post.json5
│ ├── page.json5 # extends blog_post
│ └── product.json5 # strict mode, constraints, unique, pattern
├── scripts/
│ └── contentful-to-rustycms.mjs # Migration: Contentful-Export → content/de (JSON5)
├── schemas/ # Generated JSON Schema (optional, for editor)
└── content/ # Content files (layer 2)
├── blog_post/
├── page/
└── product/
```
### Editor validation (JSON Schema)
You can export JSON Schema files from your type definitions so that VS Code / Cursor validate `content/*.json5` (and `*.json`) while you edit. Invalid fields, wrong types, or constraint violations show as squiggles in the editor.
```bash
# Export one .schema.json per content type into ./schemas
cargo run --bin export-json-schema
# Also write .vscode/settings.json so content/<type>/*.json5 uses schemas/<type>.schema.json
cargo run --bin export-json-schema -- --vscode
```
Options: `--types-dir` (default `./types`), `--out-dir` (default `./schemas`). After running with `--vscode`, open any file under `content/blog_post/`, `content/page/`, etc. the editor will use the matching schema. Re-run the command after changing type definitions to refresh schemas and (with `--vscode`) the settings.
## Defining types
Add a `.json5` file under `types/`:
```json5
// types/blog_post.json5
{
name: "blog_post",
description: "Simple blog post with title, body, tags and publish status",
tags: ["content", "blog"],
category: "content",
fields: {
title: {
type: "string",
required: true,
minLength: 1,
maxLength: 200,
description: "Title of the blog post",
},
body: {
type: "richtext",
required: true,
description: "Main content (Markdown)",
},
author: { type: "string" },
tags: { type: "array", items: { type: "string" } },
published: { type: "boolean", default: false },
created_at: { type: "datetime", auto: true, readonly: true },
}
}
```
### Schema options
| Option | Type | Description |
|--------------|-----------------|----------------------------------------------------------|
| `name` | string | **Required.** Content type name |
| `description`| string | Human-readable description (Admin UI, Swagger) |
| `tags` | string[] | Tags for grouping/filtering (e.g. `["content","blog"]`) |
| `category` | string | Category for Admin UI (e.g. `"content"`, `"components"`) |
| `extends` | string / array | Parent type(s) to inherit from |
| `strict` | boolean | Reject unknown fields |
| `pick` | string[] | Only inherit these fields from parent (`Pick<T>`) |
| `omit` | string[] | Exclude these fields from parent (`Omit<T>`) |
| `partial` | boolean | Make all inherited fields optional (`Partial<T>`) |
| `reusable` | boolean | Schema only as partial (useFields), no collection/API |
### Field options
| Option | Type | Description |
|-------------|----------|-----------------------------------------------------|
| `type` | string | **Required.** Field type (see below) |
| `required` | boolean | Field must be present when creating |
| `unique` | boolean | Value must be unique in the collection |
| `default` | any | Default value if not provided |
| `auto` | boolean | Auto-generated (e.g. timestamp) |
| `readonly` | boolean | Field cannot be changed after creation |
| `nullable` | boolean | Field allows explicit `null` |
| `enum` | array | Allowed values |
| `description` | string | Field description (Swagger UI, Admin UI) |
| `minLength` | number | Minimum string length |
| `maxLength` | number | Maximum string length |
| `pattern` | string | Regex pattern for strings |
| `min` | number | Minimum numeric value |
| `max` | number | Maximum numeric value |
| `minItems` | number | Minimum array length |
| `maxItems` | number | Maximum array length |
| `items` | object | Item definition for array fields |
| `fields` | object | Nested fields for object type |
| `collection`| string | Target collection for reference type |
| `useFields` | string | For `type: "object"`: use fields from this schema (partial) |
| `widget` | string | For `string`: `"textarea"` = multi-line; `"code"` = code editor with syntax highlighting (requires `codeLanguage`) |
| `codeLanguage` | string | When `widget: "code"`: `"css"`, `"javascript"`, `"json"`, or `"html"` used for highlighting and copy in admin UI |
### Field types
| Type | JSON type | Description |
|--------------|-----------|--------------------------------|
| `string` | string | Plain text |
| `richtext` | string | Markdown / rich text |
| `markdown` | string | Markdown content |
| `html` | string | Raw HTML (safely embedded) |
| `number` | number | Integer or float |
| `integer` | number | Integer only |
| `boolean` | boolean | true / false |
| `datetime` | string | ISO 8601 date/time |
| `array` | array | List (optional `items`) |
| `object` | object | Nested object |
| `reference` | string | Reference to another type |
### Inheritance & type composition
**Simple inheritance** inherit all fields from parent:
```json5
{
name: "page",
description: "Page with layout, SEO and content rows",
tags: ["content", "page"],
category: "content",
extends: "blog_post",
fields: {
nav_order: { type: "number", min: 0 },
template: { type: "string", enum: ["default", "landing", "docs"] },
}
}
```
**Multiple inheritance** inherit from several types:
```json5
{
name: "featured_article",
description: "Featured article with expiration",
tags: ["content", "blog"],
category: "content",
extends: ["blog_post", "seo_meta"],
fields: {
featured_until: { type: "datetime" },
}
}
```
**Pick** only certain fields from parent:
```json5
{
name: "blog_post_preview",
extends: "blog_post",
pick: ["title", "excerpt", "author", "created_at"],
}
```
**Omit** exclude certain fields:
```json5
{
name: "blog_post_light",
extends: "blog_post",
omit: ["body"],
}
```
**Partial** make all inherited fields optional:
```json5
{
name: "blog_post_patch",
extends: "blog_post",
partial: true,
}
```
### Reusable partials (useFields)
Define shared field groups once and reuse in multiple types:
1. **Reusable schema** e.g. `types/component_layout.json5` with `reusable: true` and only the shared fields (e.g. `mobile`, `tablet`, `desktop`, `spaceBottom`).
2. **Reference in type** on an object field: `type: "object"`, `useFields: "component_layout"`. On load, the partials fields are inlined.
This keeps layout in one place and reuses it in Markdown, Headline, etc.
### Strict mode
In strict mode, unknown fields are rejected:
```json5
{
name: "product",
description: "Product with SKU, price and optional fields",
tags: ["content", "ecommerce"],
category: "content",
strict: true,
fields: {
title: { type: "string", required: true },
price: { type: "number", required: true, min: 0 },
sku: { type: "string", required: true, unique: true, pattern: "^[A-Z]{2,4}-\\d{3,6}$" },
}
}
```
## Creating content
### Contentful-Migration
Ein Contentful-Export (z.B. `contentful-export.json`) kann nach `content/de/` überführt werden:
```bash
node scripts/contentful-to-rustycms.mjs [Pfad-zum-Export]
# Default: ../www.windwiderstand.de/contentful-export.json
```
Es werden u.a. Pages, Posts, Markdown, Link-Listen, Fullwidth-Banner, Tags, Navigation, Footer, Page-Config und Campaigns geschrieben. Nur unterstützte Row-Komponenten (markdown, link_list, fullwidth_banner, post_overview, searchable_text) landen in `row1Content`.
### Via files
Add a `.json5` file under `content/<type>/` or, for localized content, `content/<locale>/<type>/` (e.g. `content/de/page/`). The filename (without extension) becomes the slug.
```json5
// content/product/laptop-pro.json5
{
title: "Laptop Pro 16",
sku: "EL-1001",
price: 1499.99,
category: "electronics",
images: ["laptop-front.jpg", "laptop-side.jpg"],
}
```
### Via API
```bash
curl -X POST http://localhost:3000/api/content/product \
-H "Content-Type: application/json" \
-d '{
"_slug": "keyboard-mx",
"title": "Keyboard MX",
"sku": "EL-2000",
"price": 79.99,
"category": "electronics",
"images": ["keyboard.jpg"]
}'
```
`_slug` sets the filename. Fields with `default` or `auto` are set automatically.
**Note:** If `RUSTYCMS_API_KEY` is set, POST/PUT/DELETE (including schema and content) must send the key see section “Optional API auth”.
## REST API
### Overview
| Method | Endpoint | Description |
|----------|-----------------------------|--------------------------------|
| `GET` | `/api/collections` | List all content types |
| `GET` | `/api/collections/:type` | Get schema for a type |
| `POST` | `/api/schemas` | Create new type (Admin UI: New type) |
| `PUT` | `/api/schemas/:type` | Update type definition (Admin UI: Edit type) |
| `DELETE` | `/api/schemas/:type` | Delete type definition (Admin UI: Delete type) |
| `GET` | `/api/content/:type` | List all entries |
| `GET` | `/api/content/:type/:slug` | Get single entry |
| `GET` | `/api/content/:type/:slug/referrers` | List entries that reference this entry |
| `POST` | `/api/content/:type` | Create entry |
| `PUT` | `/api/content/:type/:slug` | Update entry |
| `DELETE` | `/api/content/:type/:slug` | Delete entry |
| `GET` | `/api` or `/api/` | API index (living docs: links to Swagger UI + endpoint overview) |
| `GET` | `/swagger-ui` | Swagger UI |
| `GET` | `/api-docs/openapi.json` | OpenAPI 3.0 spec |
| `GET` | `/api/assets` | List all uploaded image assets |
| `POST` | `/api/assets` | Upload an image asset (multipart/form-data) |
| `GET` | `/api/assets/:filename` | Serve an image asset |
| `DELETE` | `/api/assets/:filename` | Delete an image asset |
| `GET` | `/api/transform` | Transform image from external URL (resize, crop, format) |
| `GET` | `/health` | Health check (e.g. for K8s/Docker), always 200 + `{"status":"ok"}` |
### Asset management
Images and other media files can be stored in `content/assets/` and served directly via the API. This is useful for logos, hero images, icons, or any static media that belongs to your content.
**Allowed file types:** `jpg`, `jpeg`, `png`, `webp`, `avif`, `gif`, `svg`
Files are stored at `{content-dir}/assets/` (default: `./content/assets/`). The directory is created automatically on first use.
#### List assets
```bash
GET /api/assets
```
```json
{
"assets": [
{ "filename": "hero.jpg", "url": "/api/assets/hero.jpg", "mime_type": "image/jpeg", "size": 102400 }
],
"total": 1
}
```
#### Upload an asset
```bash
curl -X POST http://localhost:3000/api/assets \
-H "X-API-Key: your-key" \
-F "file=@/path/to/hero.jpg"
```
Response (201):
```json
{ "filename": "hero.jpg", "url": "/api/assets/hero.jpg", "mime_type": "image/jpeg", "size": 102400 }
```
- Auth required (POST/DELETE) when `RUSTYCMS_API_KEY` is set
- Filenames are lowercased and sanitized (only letters, digits, `-`, `_`, `.`)
- Uploading a file that already exists returns `409 Conflict`
#### Serve an asset
```bash
GET /api/assets/hero.jpg
```
Returns the raw image with correct `Content-Type`. Response header:
```
Cache-Control: public, max-age=31536000, immutable
```
Browsers and CDNs cache the file for 1 year. Combine with `/api/transform` for on-the-fly resizing of uploaded assets:
```bash
GET /api/transform?url=http://localhost:3000/api/assets/hero.jpg&w=400&h=300&format=webp
```
#### Delete an asset
```bash
curl -X DELETE http://localhost:3000/api/assets/hero.jpg \
-H "X-API-Key: your-key"
```
Returns `204 No Content`.
### Image transformation (external URLs)
`GET /api/transform` loads an image from an external URL and returns it transformed. Parameters:
| Parameter | Alias | Description |
|---------------|-------|-------------|
| `url` | | **Required.** External image URL (e.g. HTTPS). |
| `width` | `w` | Target width in pixels. |
| `height` | `h` | Target height in pixels. |
| `aspect_ratio`| `ar` | Aspect ratio before resize, e.g. `1:1` or `16:9` (center crop). |
| `fit` | | `fill` (exact w×h), `contain` (keep aspect ratio), `cover` (fill with crop). When **w and h** are set: default `fill`; otherwise default `contain`. |
| `format` | | Output: `jpeg`, `png`, `webp` or `avif`. Default: `jpeg`. WebP is lossy when `quality` is used. |
| `quality` | | Quality 1100 for JPEG and WebP (lossy). Default: 85. |
Example: 50×50 square from any image:
`GET /api/transform?url=https://example.com/image.jpg&w=50&h=50&ar=1:1`
**Caching:** Identical requests (same URL + same params) are served from memory when response cache is enabled (`RUSTYCMS_CACHE_TTL_SECS` > 0) until TTL expires (no re-download/resize).
### ETags (conditional GET)
`GET /api/content/:type/:slug` sets an **ETag** header (content hash). If the client sends **If-None-Match** with that value and the content is unchanged, the API responds with **304 Not Modified** (no body) saves bandwidth and load.
### Resolving references (_resolve)
Reference fields return only `{ "_type": "collection", "_slug": "slug" }` by default. With the `_resolve` query parameter you can **embed** referenced entries:
```bash
# Resolve all references (e.g. page with Markdown blocks, banner)
GET /api/content/page/contact?_resolve=all
# Resolve only a specific field
GET /api/content/page/home?_resolve=row1Content
```
Response then includes the full object in `row1Content` instead of just `{ _type, _slug }`.
### Filtering
Exact match: `?field=value`. Suffixes are also supported:
- `?title_prefix=Hello` title starts with “Hello”
- `?body_contains=rust` body contains “rust”
- `?price_min=10` number/string >= 10
- `?price_max=100` number/string <= 100
```bash
GET /api/content/blog_post?published=true
GET /api/content/blog_post?title_prefix=Intro
GET /api/content/product?category=electronics&price_min=50
```
### Sorting
```bash
GET /api/content/blog_post?_sort=created_at&_order=desc
```
### Pagination
```bash
GET /api/content/blog_post?_page=2&_per_page=10
```
Response:
```json
{
"items": [ ... ],
"total": 42,
"page": 2,
"per_page": 10,
"total_pages": 5
}
```
### Validation
The API validates automatically against the schema:
```
POST with title: 123 → "Expected type 'string', got 'number'"
POST without required field → "Field is required"
POST with sku: "bad" → "Value does not match pattern '^[A-Z]{2,4}-\\d{3,6}$'"
POST with price: -5 → "Value -5 is below minimum 0"
POST with duplicate SKU → "Value must be unique"
POST with unknown field → "Unknown field (strict mode is enabled)"
PUT with changed readonly → "Field is readonly and cannot be changed"
POST with title: null → "Field does not allow null"
POST with rating: null → OK (nullable: true)
POST with images: [] → "Array too short (min 1 items)"
POST with excerpt (501 chars)→ "String too long (max 500 characters)"
```
## Store: file vs SQLite
By default the **file store** is used: each collection is a subdirectory under `content/`, each entry a `.json5` file.
You can switch to **SQLite** same REST API, same schemas, different backing store (one table with `collection`, `slug`, JSON `data`):
```bash
RUSTYCMS_STORE=sqlite RUSTYCMS_DATABASE_URL=sqlite:content.db cargo run
```
The database file is created when needed (`create_if_missing`). Useful for larger setups, concurrent writers, or when you want content in a database.
## Optional API auth
If you set `RUSTYCMS_API_KEY` (e.g. in `.env`), **POST, PUT and DELETE** require a valid key. **GET** stays public (read without auth).
**Send the key** one of:
- Header: `Authorization: Bearer <your-api-key>`
- Header: `X-API-Key: <your-api-key>`
**Example with curl:**
```bash
# Without key → 401 Unauthorized
curl -X POST http://localhost:3000/api/content/tag -H "Content-Type: application/json" -d '{"_slug":"x","name":"y"}'
# With key → 201 Created (or validation error)
curl -X POST http://localhost:3000/api/content/tag \
-H "Content-Type: application/json" \
-H "X-API-Key: your-secret-token" \
-d '{"_slug":"x","name":"y"}'
```
If `RUSTYCMS_API_KEY` is **not** set, all endpoints behave as before without auth.
## Swagger UI
Auto-generated interactive API documentation:
```
http://localhost:3000/swagger-ui
```
The OpenAPI spec is generated at startup from the schemas including all constraints (minLength, pattern, min/max, nullable, readOnly, …). The spec can also be fetched directly; it is served with cache headers so clients can cache it:
```
http://localhost:3000/api-docs/openapi.json
```
**Schema hot-reload:** Changes in `types/*.json5` are watched after saving, schemas and the OpenAPI spec are reloaded automatically without restarting the server.
## Troubleshooting
| Problem | Possible cause |
|--------|-----------------|
| `Collection 'xyz' not found` | Type does not exist or path typo. Check `GET /api/collections`. |
| `Field '_slug' is required` | POST must include `_slug` in the JSON body. |
| `Missing or invalid API key` | `RUSTYCMS_API_KEY` is set; POST/PUT/DELETE need header `Authorization: Bearer <key>` or `X-API-Key: <key>`. |
| `Entry 'slug' not found` | Slug misspelled or entry not present in the collection. |
| Validation errors (type, pattern, unique, …) | Check request body: types, required fields, patterns and uniqueness per schema. |
## Operation
- **Graceful shutdown:** On SIGINT (Ctrl+C) or SIGTERM (e.g. K8s), in-flight requests are completed, then the server exits cleanly.
## Logging
```bash
# Default (info)
cargo run
# Debug
RUST_LOG=rustycms=debug,tower_http=debug cargo run
# Warnings only
RUST_LOG=warn cargo run
```
## License
MIT