RustyCMS: file-based headless CMS — API, Admin UI (content, types, assets), Docker/Caddy, image transform; only demo type and demo content in version control
Made-with: Cursor
This commit is contained in:
179
README.md
179
README.md
@@ -1,6 +1,6 @@
|
||||
# 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.
|
||||
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
|
||||
|
||||
@@ -13,8 +13,10 @@ A file-based headless CMS written in Rust. Content types are defined as JSON5 sc
|
||||
- **References & _resolve**: Reference fields as `{ _type, _slug }`; embed with `?_resolve=all`
|
||||
- **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`
|
||||
- **Image transformation**: `GET /api/transform` – load image from external URL, optional resize (w, h), aspect-ratio crop (e.g. 1:1), fit (fill/contain/cover), output formats jpeg, png, webp, avif; with response cache
|
||||
- **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
|
||||
@@ -22,6 +24,7 @@ A file-based headless CMS written in Rust. Content types are defined as JSON5 sc
|
||||
## Requirements
|
||||
|
||||
- [Rust](https://www.rust-lang.org/tools/install) (1.70+)
|
||||
- [Node.js](https://nodejs.org/) (for Admin UI, optional)
|
||||
|
||||
## Quick start
|
||||
|
||||
@@ -32,6 +35,39 @@ 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`) |
|
||||
|
||||
If the API requires auth, set `NEXT_PUBLIC_RUSTYCMS_API_KEY` so the Admin UI can create, update and delete entries.
|
||||
|
||||
### CLI options
|
||||
|
||||
| Option | Default | Description |
|
||||
@@ -74,6 +110,11 @@ RUSTYCMS_API_KEY=your-secret-token cargo run
|
||||
|
||||
```
|
||||
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
|
||||
@@ -105,6 +146,8 @@ rustycms/
|
||||
│ ├── 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/
|
||||
@@ -134,6 +177,9 @@ Add a `.json5` file under `types/`:
|
||||
// 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",
|
||||
@@ -157,15 +203,18 @@ Add a `.json5` file under `types/`:
|
||||
|
||||
### Schema options
|
||||
|
||||
| Option | Type | Description |
|
||||
|-----------|-----------------|----------------------------------------------------------|
|
||||
| `name` | string | **Required.** Content type name |
|
||||
| `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 |
|
||||
| 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
|
||||
|
||||
@@ -179,7 +228,7 @@ Add a `.json5` file under `types/`:
|
||||
| `readonly` | boolean | Field cannot be changed after creation |
|
||||
| `nullable` | boolean | Field allows explicit `null` |
|
||||
| `enum` | array | Allowed values |
|
||||
| `description` | string | Description (shown in Swagger UI) |
|
||||
| `description` | string | Field description (Swagger UI, Admin UI) |
|
||||
| `minLength` | number | Minimum string length |
|
||||
| `maxLength` | number | Maximum string length |
|
||||
| `pattern` | string | Regex pattern for strings |
|
||||
@@ -198,9 +247,12 @@ Add a `.json5` file under `types/`:
|
||||
|--------------|-----------|--------------------------------|
|
||||
| `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 |
|
||||
| `datetime` | string | ISO 8601 date/time |
|
||||
| `array` | array | List (optional `items`) |
|
||||
| `object` | object | Nested object |
|
||||
| `reference` | string | Reference to another type |
|
||||
@@ -212,6 +264,9 @@ Add a `.json5` file under `types/`:
|
||||
```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 },
|
||||
@@ -225,6 +280,9 @@ Add a `.json5` file under `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" },
|
||||
@@ -278,6 +336,9 @@ 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 },
|
||||
@@ -289,9 +350,20 @@ In strict mode, unknown fields are rejected:
|
||||
|
||||
## 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>/`. The filename becomes the slug:
|
||||
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
|
||||
@@ -321,7 +393,7 @@ curl -X POST http://localhost:3000/api/content/product \
|
||||
|
||||
`_slug` sets the filename. Fields with `default` or `auto` are set automatically.
|
||||
|
||||
**Note:** If `RUSTYCMS_API_KEY` is set, POST/PUT/DELETE must send the key – see section “Optional API auth”.
|
||||
**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
|
||||
|
||||
@@ -331,6 +403,9 @@ curl -X POST http://localhost:3000/api/content/product \
|
||||
|----------|-----------------------------|--------------------------------|
|
||||
| `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 |
|
||||
| `POST` | `/api/content/:type` | Create entry |
|
||||
@@ -339,9 +414,79 @@ curl -X POST http://localhost:3000/api/content/product \
|
||||
| `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:
|
||||
@@ -353,8 +498,8 @@ curl -X POST http://localhost:3000/api/content/product \
|
||||
| `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 = lossless; AVIF at default quality.) |
|
||||
| `quality` | – | JPEG quality 1–100. Default: 85. |
|
||||
| `format` | – | Output: `jpeg`, `png`, `webp` or `avif`. Default: `jpeg`. WebP is lossy when `quality` is used. |
|
||||
| `quality` | – | Quality 1–100 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`
|
||||
|
||||
Reference in New Issue
Block a user