developer blog framework

dx5
file-based cms
built for devs

No database. No ORM. No fuss.
Write content as YAML files, render with Tera templates,
deploy a fast Rocket binary.

Rust + Rocket
Tera templates
YAML content
gray_matter frontmatter
zero database
00 — the philosophy
Behind the name: Dx5

Dx5 stands for Developer's Dev Diary at Dawn of Dusk.

This framework was born during the creation of a personal blog—a fictionalized chronicle designed to track the impact of AI on the development landscape. It explores the "twilight" of the traditional, handcrafted nature of software engineering in an era of total automation.

Note: That same blog, which narrates the end of craftsmanship, is effectively powered and rendered by dx5.
// 01
File-based content
Every post is a plain YAML file on disk. Git-friendly, editor-friendly, no lock-in. Move content anywhere without a migration script.
// 02
Typed body fields
Posts are composed of structured blocks — text, code, blockquotes, images. Each field has a schema and a dedicated Tera template.
// 03
Extensible field types
Add any new field type in two steps: a TOML definition and a Tera template. Zero Rust code changes required.
// 04
Multi-language
Routes are language-prefixed out of the box. Content lives under contents/{type}/{lang}/. A t() function handles UI string translation.
// 05
Admin UI
Optional web-based admin panel, protected by a Bearer token. List, preview and manage all content types with a single self-contained HTML file.
// 06
Audio player built-in
Drop sequential MP3s into assets/soundtracks/. ID3 tags are read automatically and exposed as JSON to every template.
01 — setup
Up and running
in 60 seconds
Clone, configure, run. The only dependency is a working Rust toolchain.
Step 01
Scaffold or Clone
The fastest way to start is using the scaffold command, or you can clone the repository manually.
bash terminal
# Option A: Fast scaffolding (cargo-generate) $ cargo generate --git https://github.com/reinchek/dx5.git --name my-blog # Option B: Manual clone $ git clone https://github.com/reinchek/dx5.git my-blog $ cd my-blog && cp config/dx5.toml.dist config/dx5.toml
Step 02
Edit dx5.toml
Set your blog title, author, base URL and preferred language. Enable the optional admin panel with a secret token.
toml dx5.toml
# ── blog metadata [blog] title = "My Dev Blog" author = "Your Name" base_url = "https://my-blog.dev" language = "en" spa_enabled = false # true → all page transitions via async fetch (no full reload) # ── languages (define here your favorites...) [languages] en = "English" it = "Italiano" # ── optional admin panel [admin] enabled = true token = "your-secret-token"
Step 03
Run the server
Rocket auto-detects your port from Rocket.toml. The DX5_CONFIG env var overrides the config path for production deployments.
bash terminal
$ cargo run // or for production: $ DX5_CONFIG=/etc/dx5/prod.toml cargo run --release // server starts at: http://127.0.0.1:8000 http://127.0.0.1:8000/admin (if enabled)
01b — docker setup
Run with Docker
Prefer containers? dx5 ships with a Dockerfile and docker-compose.yml. SSL-ready with Caddy.
docker
Docker Compose
Clone and run with a single command. On Linux, pass your UID/GID so mounted volumes have correct ownership. macOS and Windows handle this transparently.
bash terminal
$ git clone https://github.com/reinchek/dx5.git my-blog $ cd my-blog && sed -i 's/{{ project-name }}/dx5/g' Dockerfile $ docker compose up -d # Linux — pass host UID/GID: $ UID=$(id -u) GID=$(id -g) docker compose up
caddy
HTTPS with Caddy (automatic Let's Encrypt)
The project includes a Caddyfile and a Caddy service in docker-compose.yml. Replace your-blog.com in Caddyfile with your domain, point DNS, and run. Caddy will proxy requests to dx5 internally on port 8000 and handle SSL termination automatically. Certificates are persisted in Docker named volumes.
bash terminal
$ UID=$(id -u) GID=$(id -g) docker compose up -d https://your-blog.com
For local development, Caddy serves https://localhost with a self-signed certificate (Caddy's internal CA). Your browser will show a warning — this is expected. For production, uncomment the domain block in Caddyfile for automatic Let's Encrypt certificates.
02 — content types
Everything is a content type
Define as many content types as you need — posts, code snippets, notes, docs. Each type gets its own directory, route and template.
Config file: config/dx5.content_types.toml — each [types.<name>] section registers a new content type automatically.
POST
posts default
→ /{lang}/posts/{id}
dir./contents/posts
menu_labelPosts
menu_order1
CODE
codes
→ /{lang}/codes/{id}
dir./contents/codes
menu_labelCode
menu_order2
toml config/dx5.content_types.toml
# Add as many types as you need [types.post] dir = "./contents/posts" route = "/posts" menu_icon = "fa-regular fa-keyboard" menu_label = "Posts" menu_order = 1 is_default = true # shown at /{lang} (root route) [types.code] dir = "./contents/codes" route = "/codes" menu_icon = "fa-solid fa-code" menu_label = "Code" menu_order = 2
02a — writing content
Writing your first post
Copy the template file and fill in the front matter. Content structure is flexible through body_fields.
File naming: Files live inside contents/posts/{lang}/. Convention is 0x01.yaml, 0x02.yaml, etc. The filename (without extension) becomes the post id.
yaml contents/posts/en/0x01.yaml
--- id: "0x01" title: "Getting Started with Rust" category: "TUTORIAL" created: "2026-04-22" body_fields: - type: date value: now - type: section_title value: Why Rust - type: text value: Rust is a systems programming language focused on safety, speed, and concurrency. It achieves memory safety without a garbage collector. - type: pre lang: rust value: | fn main() { println!("Hello, world!"); } - type: blockquote author: Rust Documentation reference: "doc.rust-lang.org" value: "Rust is a language empowering everyone to build reliable and efficient software." - type: eol ---
preview /{lang}/posts/0x01
TUTORIAL · 2026-04-22
⏱ 2026-04-22 09:41:00
Why Rust
Rust is a systems programming language focused on safety, speed, and concurrency. It achieves memory safety without a garbage collector.
rust
fn main() {
    println!("Hello, world!");
}

"Rust is a language empowering everyone to build reliable and efficient software."

— Rust Documentation · doc.rust-lang.org
eol
03 — body fields
Built-in field types
Every body_fields entry maps to a Tera template via dx5.fields.toml. Rendered with the render_field() function.
tera templates/ct/posts/single.tera
{# iterate and render every body field #} {% for field in item.body_fields %} {{ render_field(field=field, type=field.type) | safe }} {% endfor %}
Type Properties Notes
text value str, safe bool safe: true enables inline HTML without escaping
text_glitch value str, value_glitch str Text with CSS glitch animation effect
hero_title value str, value_glitch str, env bool, env_id int, font_small bool Monumental heading with glitch effect and optional env background
section_title value str H2-level section divider inside the post body
cite value str Short highlighted inline quotation
blockquote value str, author str, reference str Extended pull quote with attribution
blockquote_figure value str, author str, reference str, figure str Blockquote + author avatar image path
image value str, alt str, caption str value is the image file path
video src str, caption str, autoplay bool YouTube, Vimeo, or direct .mp4/.webm embed
pre lang str, value str, path str? Syntax-highlighted code block. path shows the filename label
terminal value str, prompt str, title str Shell session: commands and output
callout value str, variant str, title str, safe bool Note, warning, tip, or danger callout box
date value str Use "now" for server start timestamp or "2026-04-22"
link href str, value str, alt str External or internal hyperlink
divider value str Visual horizontal divider with optional label
col value str, safe bool Column block with configurable Tailwind width
group value str Wrapper to group fields (open/close pair)
spotify_embed track_id str Embedded Spotify player from a track ID
spreaker episode_id str, theme str, title str Spreaker embedded podcast iframe
eol — no props — Line break / separator between sections
04 — extensibility
Custom field types
in 3 steps
No Rust code changes required. The entire extension lives in TOML + Tera. Restart and the new type is available everywhere.
// step 01
Define in TOML
Add a [fields.<type>] section to dx5.fields.toml with a description and schema.
// step 02
Create a Tera template
Write templates/fields/<type>.tera. All field data is available under field.<prop>.
// step 03
Use it in YAML
Reference the new type in any body_fields array. cargo restart picks it up automatically.
Example: Spotify embed field
1. Register in dx5.fields.toml
toml config/dx5.fields.toml
[fields.spotify_embed] description = "Embedded Spotify player." [fields.spotify_embed.schema] track_id = "string"
2. Create the Tera template
tera templates/fields/spotify_embed.tera
<div class="spotify-embed"> <iframe src="https://open.spotify.com/embed/track/{{ field.track_id }}" width="100%" height="152"loading="lazy"> </iframe> </div>
3. Use in any post YAML
yaml contents/posts/en/0x02.yaml
body_fields: - type: section_title value: "Listening while coding" - type: spotify_embed track_id: "4uLU6hMCjMI75M1A2tKUQC"
04b — i18n
Multi-language UI strings
Place JSON translation files in config/locales/. Use the t() Tera function anywhere in templates.
json config/locales/en.json
{ "nav": { "prev": "← prev", "next": "next →" }, "text__explore": "explore" }
tera any template
<h1> {{ t(key="nav.prev", lang=lang) }} </h1> <a> {{ t(key="text__explore", lang=lang) }} </a>
04c — spa mode
Client-side navigation
Enable SPA mode in dx5.toml for silky page transitions without full reloads.
How it works
Set spa_enabled = true in [blog] section
Links with data-spa attribute are intercepted
The router fetches HTML via fetch and swaps #page-content div
No full page reload — instant transitions
toml dx5.toml
[blog] spa_enabled = true # enable client-side SPA routing
See static/js/spa-router.js for the full implementation.
05 — admin panel
Web UI included
Enable the optional admin panel in dx5.toml. Accessible at /admin, protected by a Bearer token stored in session storage.
http://127.0.0.1:8000/admin
POST + NUOVO
0x01
Getting Started with Rust
TUTORIAL · 2026-04-22
0x02
Understanding Ownership
GUIDE · 2026-04-18
0x03
Rust Modules Explained
GUIDE · 2026-04-10
0x04
Error Handling in Rust
TUTORIAL · 2026-04-02
// editing 0x01
ELIMINA SALVA
0x01
2026-04-22
TUTORIAL
Getting Started with Rust
Hello World
3
Security note: Never commit real tokens to git. In production, override the admin token at runtime with the DX5_ADMIN_TOKEN environment variable.
05b — audio
Integrated audio player
Drop MP3s into assets/soundtracks/ and rename them sequentially. dx5 reads ID3 tags automatically.
toml dx5.toml
[audio] enabled = true soundtracks_dir = "./assets/soundtracks" # files must be sequential: # 1.mp3, 2.mp3, 3.mp3 ... # ID3 tags: title + artist auto-read # fallback: Log_Track_N / Unknown_Source
tera template (playlist JSON)
{# playlist_data is a JSON string #} <script> const playlist = {{ playlist_data | safe }}; // [{ file, title, author, src }] </script>
06 — routing
Route reference
Method Path Description
GET / Redirects to /en (default language)
GET /{lang} Home page (renders default content type list)
GET /{lang}/{type}?page=N Paginated listing of a content type
GET /{lang}/{type}/{id} Single content item with prev/next navigation
GET /admin Admin UI (requires enabled = true + Bearer token)
—— JSON API (public, no auth) ——
GET /api/{lang}/home Homepage content as JSON
GET /api/{lang}/{type}?page=N Paginated content listing as JSON
GET /api/{lang}/{type}/{id} Single content item as JSON
—— Admin API (auth required) ——
GET /admin/api/init Languages, content types, and blog metadata
GET /admin/api/config Blog title and author JSON
GET /admin/api/fields_definitions Field type definitions with schemas
GET /admin/api/contents/{lang}/{type} List all items for a content type
GET /admin/api/contents/{lang}/{type}/{id} Get single content item
POST /admin/api/contents/{lang}/{type} Create a new content item
PUT /admin/api/contents/{lang}/{type}/{id} Update an existing content item
DEL /admin/api/contents/{lang}/{type}/{id} Delete a content item
07 — error handling
Graceful error pages
Rich HTML error pages with status code, human-readable description, context chain, and optional backtrace.
404ContentNotFound returns Status::NotFound. All other errors return Status::InternalServerError (500).
Error response flow

Dx5Error enum converts any error into a styled HTML page via Rocket's Responder trait — no separate route or middleware needed.

ContentNotFound — resource label in page
ParseError — malformed file parsing
IoError — filesystem I/O failure
Debug mode

Set RUST_BACKTRACE=1 before startup to include a full backtrace in the error page.

$ RUST_BACKTRACE=1 cargo run
08 — filesystem watcher
Live reload
without the tooling
Edit content YAML files on disk and see the changes reflected immediately — no restart, no rebuild, no hot-reload daemon.
// event
Create / Modify / Delete
Watcher detects .yaml file changes and rebuilds the .pages/ cache automatically. Other file types are skipped.
// scope
Smart targeting
Only rebuilds the affected content type's pagination cache, not the entire site. Minimal I/O, instant response.
// trade-off
No full hot reload
Tera templates and Rust code still require a restart. The watcher only tracks content .yaml changes in watched directories.
09 — environment
Environment variables
Variable Purpose
DX5_CONFIG Override config file path (e.g. /etc/dx5/prod.toml)
DX5_ADMIN_TOKEN Override admin token at runtime (overrides dx5.toml)
RUST_BACKTRACE Set to 1 or full for full backtrace on errors
RUST_LOG Tracing/log level (e.g. info)
Pro tip: Use DX5_ADMIN_TOKEN to set the admin token at runtime without modifying config files, and DX5_CONFIG to switch configs between environments.