Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

rudis-cms

rudis-cms is a headless CMS that compiles Markdown/YAML documents into Cloudflare D1 (SQLite), R2 (object storage), and KV.

Features

  • Schema-driven: Define your content structure in YAML, and rudis-cms generates SQL tables automatically
  • Markdown with embedded images: Images in Markdown are automatically extracted, optimized, and uploaded to R2
  • Multiple storage backends: Support for R2 (objects), KV (key-value), and inline storage
  • Incremental updates: Only upload changed content, skip unchanged files
  • Local development: Dump mode for testing with local SQLite and file storage

Architecture

┌─────────────────┐     ┌──────────────┐     ┌─────────────────┐
│  Markdown/YAML  │────▶│  rudis-cms   │────▶│  Cloudflare D1  │
│     Documents   │     │              │     │  (SQLite)       │
└─────────────────┘     │              │     └─────────────────┘
                        │              │     ┌─────────────────┐
                        │              │────▶│  Cloudflare R2  │
                        │              │     │  (Objects)      │
                        └──────────────┘     └─────────────────┘
                                             ┌─────────────────┐
                                        ────▶│  Cloudflare KV  │
                                             │  (Key-Value)    │
                                             └─────────────────┘

Use Cases

  • Static site generators with dynamic content
  • Blog platforms with structured metadata
  • Documentation sites with search functionality
  • Any application needing structured content on Cloudflare’s edge

Installation

From Source

rudis-cms is written in Rust. You can build it from source:

git clone https://github.com/namachan10777/rudis-cms
cd rudis-cms
cargo install --path .

Requirements

  • Rust 2024 edition (1.85+)
  • Cloudflare account with:
    • D1 database
    • R2 bucket (optional, for object storage)
    • KV namespace (optional, for key-value storage)

Environment Variables

rudis-cms requires the following environment variables for Cloudflare deployment:

VariableDescription
CF_ACCOUNT_IDYour Cloudflare account ID
CF_API_TOKENAPI token with D1, R2, and KV permissions
R2_ACCESS_KEY_IDR2 access key ID
R2_SECRET_ACCESS_KEYR2 secret access key

Creating a Cloudflare API Token

  1. Go to Cloudflare Dashboard > My Profile > API Tokens
  2. Create a custom token with the following permissions:
    • Account > D1 > Edit
    • Account > Workers KV Storage > Edit
    • Account > R2 > Edit

Getting R2 Credentials

  1. Go to R2 > Manage R2 API Tokens
  2. Create a new API token with read/write access

Quick Start

This guide will walk you through setting up a simple blog with rudis-cms.

1. Create Configuration

Create a config.yaml file:

glob: "posts/**/*.md"
name: posts
table: posts
database_id: your-d1-database-id
syntax:
  type: markdown
  column: body
schema:
  id:
    type: id
  title:
    type: string
    required: true
  date:
    type: date
    index: true
    required: true
  body:
    type: markdown
    required: true
    storage:
      type: kv
      namespace: your-kv-namespace-id
    image:
      table: post_images
      inherit_ids: [post_id]
      storage:
        type: r2
        bucket: your-bucket-name
        prefix: images

2. Create Content

Create a Markdown file at posts/hello-world.md:

---
id: hello-world
title: Hello World
date: 2024-01-01
---

This is my first post!

![Sample image](./image.png)

3. Deploy

Set your environment variables and run:

export CF_ACCOUNT_ID=your-account-id
export CF_API_TOKEN=your-api-token
export R2_ACCESS_KEY_ID=your-r2-key
export R2_SECRET_ACCESS_KEY=your-r2-secret

rudis-cms --config config.yaml batch

4. View Progress

rudis-cms shows a progress display during deployment:

📋 Loading configuration...
🔧 Compiling schema...
📄 Processing documents...
⬆️ Uploading to storage...
✅ Completed!

📊 Results:
├── ✅ posts/hello-world.md
│   ├── ⬆️ kv://namespace/hello-world
│   └── ⬆️ r2://bucket/images/image.png

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   📄 Entries:    1 total
   ✅ Successful: 1
   ⬆️ Uploads:    2
   ⏱️ Duration:   1.23s
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Local Development

For local testing without Cloudflare, use dump mode:

rudis-cms --config config.yaml dump --storage ./storage --db ./db

This creates local SQLite databases and stores files on disk.

Configuration

rudis-cms uses a YAML configuration file to define your content structure.

Basic Structure

glob: "posts/**/*.md"      # File pattern to match
name: posts                 # Collection name
table: posts               # Main table name
database_id: xxx-xxx       # Cloudflare D1 database ID
syntax:
  type: markdown           # or "yaml"
  column: body             # Column for markdown content (markdown only)
schema:
  # Field definitions...

Top-Level Options

OptionRequiredDescription
globYesGlob pattern for content files
nameYesCollection name
tableYesMain database table name
database_idYesCloudflare D1 database ID
preview_database_idNoSeparate D1 database for preview
syntaxYesContent format configuration
schemaYesField definitions

Syntax Options

Markdown

syntax:
  type: markdown
  column: body    # Field name for the markdown content

With markdown syntax, frontmatter fields are mapped to schema fields, and the body content is stored in the specified column.

YAML

syntax:
  type: yaml

With YAML syntax, the entire file is parsed as YAML and mapped to schema fields.

Preview Database

You can specify a separate database for preview/draft content:

database_id: production-db-id
preview_database_id: preview-db-id

Use the --preview flag to deploy to the preview database instead.

Schema Definition

The schema defines how your content is structured and stored in the database.

Field Definition

Each field in the schema has a name and configuration:

schema:
  field_name:
    type: string
    required: true
    index: true

Common Options

OptionTypeDefaultDescription
typestring-Field type (required)
requiredboolfalseWhether the field is required
indexboolfalseCreate a database index

Nested Records

You can define nested tables using the records type:

schema:
  id:
    type: id
  comments:
    type: records
    table: post_comments
    inherit_ids: [post_id]
    schema:
      comment_id:
        type: id
      text:
        type: string

This creates a separate post_comments table with a foreign key relationship.

inherit_ids

The inherit_ids option specifies which parent IDs to include in the child table:

comments:
  type: records
  inherit_ids: [post_id]  # Include post_id as foreign key

For deeply nested records, you can inherit multiple IDs:

replies:
  type: records
  inherit_ids: [post_id, comment_id]

Example

schema:
  id:
    type: id
  title:
    type: string
    required: true
  published:
    type: datetime
    index: true
  draft:
    type: boolean
    index: true
  body:
    type: markdown
    storage:
      type: kv
      namespace: content
  tags:
    type: records
    table: post_tags
    inherit_ids: [post_id]
    schema:
      tag:
        type: id

Field Types

rudis-cms supports various field types for different data needs.

Basic Types

id

Primary identifier for the record.

id:
  type: id
  • Stored as TEXT in SQLite
  • Automatically indexed
  • Must be unique within the table

string

Text content.

title:
  type: string
  required: true
  • Stored as TEXT in SQLite

boolean

True/false values.

published:
  type: boolean
  index: true
  • Stored as INTEGER (0/1) in SQLite

date

Date without time.

published_date:
  type: date
  index: true
  • Stored as TEXT in ISO 8601 format (YYYY-MM-DD)
  • Indexed using date() function

datetime

Date with time.

created_at:
  type: datetime
  index: true
  • Stored as TEXT in ISO 8601 format
  • Indexed using datetime() function

hash

Content hash for change detection.

hash:
  type: hash
  • Automatically computed from file content
  • Useful for cache invalidation

Content Types

markdown

Markdown content with optional image extraction.

body:
  type: markdown
  required: true
  storage:
    type: kv
    namespace: content-namespace
  image:
    table: post_images
    inherit_ids: [post_id]
    embed_svg_threshold: 8192
    storage:
      type: r2
      bucket: my-bucket
      prefix: images
  config: {}

Options:

  • storage: Where to store the compiled markdown
  • image: Configuration for extracted images
  • image.embed_svg_threshold: SVG files smaller than this (bytes) are embedded inline
  • config: Additional markdown processing options

image

Single image field.

og_image:
  type: image
  storage:
    type: r2
    bucket: my-bucket
    prefix: og-images

file

Generic file attachment.

attachment:
  type: file
  storage:
    type: r2
    bucket: my-bucket
    prefix: attachments

Relational Types

records

Nested table with multiple records.

tags:
  type: records
  table: post_tags
  inherit_ids: [post_id]
  schema:
    tag:
      type: id

Creates a separate table with foreign key relationship.

Storage Options

rudis-cms supports multiple storage backends for different content types.

R2 (Object Storage)

Cloudflare R2 for binary files like images.

storage:
  type: r2
  bucket: my-bucket
  prefix: images/posts
OptionRequiredDescription
bucketYesR2 bucket name
prefixNoKey prefix for objects

Objects are stored with content-addressed keys based on their hash, ensuring deduplication.

KV (Key-Value)

Cloudflare KV for text content like compiled markdown.

storage:
  type: kv
  namespace: content-namespace-id
OptionRequiredDescription
namespaceYesKV namespace ID

Inline

Store content directly in the database.

storage:
  type: inline

Best for small content that doesn’t need separate storage.

Asset

For static assets served directly.

storage:
  type: asset
  prefix: static

Storage Pointer Format

In the database, storage references are stored as JSON with pointer information:

{
  "hash": "abc123...",
  "pointer": "r2://bucket/prefix/key",
  "content_type": "image/png",
  "size": 12345
}

The pointer format indicates the storage type:

  • r2://bucket/key - R2 object
  • kv://namespace/key - KV entry
  • asset://path - Asset file

Deduplication

rudis-cms uses content hashing (BLAKE3) to deduplicate uploads:

  1. Before upload, the file hash is computed
  2. If an object with the same hash exists, upload is skipped
  3. Progress shows ⬆️ for new uploads and ⏭️ for skipped

Use the -f / --force flag to force re-upload of all objects.

CLI Commands

Global Options

rudis-cms --config <CONFIG> <COMMAND>
OptionShortDescription
--config-cPath to configuration file (required)

Commands

batch

Deploy content to Cloudflare.

rudis-cms -c config.yaml batch [OPTIONS]
OptionShortDescription
--force-fForce re-upload all objects
--preview-pDeploy to preview database

Example:

# Normal deployment
rudis-cms -c config.yaml batch

# Force re-upload everything
rudis-cms -c config.yaml batch -f

# Deploy to preview database
rudis-cms -c config.yaml batch --preview

dump

Export to local files for development/testing.

rudis-cms -c config.yaml dump --storage <PATH> --db <PATH>
OptionDescription
--storageDirectory for storage files
--dbDirectory for SQLite database

Example:

rudis-cms -c config.yaml dump --storage ./local-storage --db ./local-db

show-schema

Display generated schemas.

rudis-cms -c config.yaml show-schema <SUBCOMMAND>

show-schema sql

Show SQL DDL statements.

rudis-cms -c config.yaml show-schema sql [OPTIONS]
OptionDescription
--fetch-objectsInclude fetch objects query

show-schema typescript

Generate TypeScript type definitions.

rudis-cms -c config.yaml show-schema typescript [OPTIONS]
OptionDescription
--save <DIR>Save to directory
--valibotGenerate Valibot schemas

Example:

# Print to stdout
rudis-cms -c config.yaml show-schema typescript

# Save with Valibot validation
rudis-cms -c config.yaml show-schema typescript --save ./generated --valibot

Exit Codes

CodeDescription
0Success
1Error

Environment Variables

See Installation for required environment variables.

SQL Schema

rudis-cms automatically generates SQL tables based on your schema definition.

Generated Tables

For a schema like:

table: posts
schema:
  id:
    type: id
  title:
    type: string
    required: true
  date:
    type: date
    index: true
  body:
    type: markdown
    storage:
      type: kv
      namespace: content
  tags:
    type: records
    table: post_tags
    inherit_ids: [post_id]
    schema:
      tag:
        type: id

The following SQL is generated:

CREATE TABLE IF NOT EXISTS posts (
  id TEXT NOT NULL,
  title TEXT NOT NULL,
  date TEXT NOT NULL,
  body TEXT NOT NULL,
  PRIMARY KEY (id)
);
CREATE INDEX IF NOT EXISTS index_posts_id ON posts(id);
CREATE INDEX IF NOT EXISTS index_posts_date ON posts(date(date));

CREATE TABLE IF NOT EXISTS post_tags (
  post_id TEXT NOT NULL,
  tag TEXT NOT NULL,
  FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
  PRIMARY KEY (post_id, tag)
);
CREATE INDEX IF NOT EXISTS index_post_tags_tag ON post_tags(tag);

Type Mappings

rudis-cms TypeSQLite TypeNotes
idTEXT NOT NULLPrimary key
stringTEXTNOT NULL if required
booleanINTEGER0 or 1
dateTEXTISO 8601 format
datetimeTEXTISO 8601 format
hashTEXTBLAKE3 hash
markdownTEXTJSON with storage pointer
imageTEXTJSON with storage pointer
fileTEXTJSON with storage pointer

Storage Pointers

Content fields (markdown, image, file) are stored as JSON:

{
  "hash": "abc123def456...",
  "size": 12345,
  "content_type": "text/markdown",
  "pointer": "kv://namespace-id/key"
}

Foreign Keys

Child tables created by records type include:

  • Foreign key constraint with ON DELETE CASCADE
  • Composite primary key including parent IDs

Viewing Generated SQL

Use the CLI to view generated SQL:

rudis-cms -c config.yaml show-schema sql