- Rust 53.4%
- HTML 33.6%
- CSS 11.9%
- JavaScript 0.6%
- Dockerfile 0.5%
|
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m23s
|
||
|---|---|---|
| .forgejo/workflows | ||
| .idea | ||
| flyer-assets | ||
| messages | ||
| public | ||
| src | ||
| templates | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| docker-compose.yml | ||
| Dockerfile | ||
| flyer.html | ||
| README.md | ||
Dezentrale Erkelenz – Rust/Axum Website
Reimplementation of the website for the c3e3 – Dezentrale Erkelenz hack- and makerspace, built with Rust and Axum. The site is fully server-rendered, bilingual (German/English), and works without a JavaScript framework.
Overview
- Rust + Axum instead of Next.js, shipped as a single binary.
- Language-independent URLs without a locale prefix, e.g.
/about/,/blog/willkommen/. - Left-hand grouped navigation (Information, Association, Legal, Meta).
- Breadcrumbs with a home icon at the top:
⌂ / blog / willkommen. - Automatic dark/light mode via
prefers-color-scheme. - Minimal JavaScript: only the Leaflet map on the location page. The calendar, navigation, language switch and all other pages work without JS.
- Blog, documents, ICS calendar, SpaceAPI, RSS, sitemap,
robots.txt, legal notice, privacy, contact and support. - Content comes from an external Git repository (
CONTENT_REPO_URL) or from a local checkout.
Requirements
- Rust 1.88 or newer (edition 2024). Tested with
cargo 1.99.1. - Optional:
gitfor automatically syncing the content repository. - Optional: Docker and Docker Compose.
Running Locally
cd /home/mppny/Dev/repo/dezentrale-website-rust
cargo run
By default the server runs at http://127.0.0.1:3000.
On first start the application looks for content in this order:
CONTENT_DIR, if set.../dezentrale-content, if present andCONTENT_REPO_URLis not set../content.
For local development the content repository usually lives at ../dezentrale-content. Example content:
../dezentrale-content/
├── blog/
│ └── willkommen/
│ ├── content.de.md
│ ├── content.en.md
│ └── cover.jpg
└── documents/
├── Satzung.pdf
└── Mitgliedsantrag.pdf
Configuration
All variables can be set through the environment or .env / .env.local. Template: .env.example.
| Variable | Default | Description |
|---|---|---|
HOST |
127.0.0.1 |
Bind address. Use 0.0.0.0 inside Docker. |
PORT |
3000 |
HTTP port. |
SITE_URL |
https://dezentrale-erkelenz.de |
Public base URL for canonical links, RSS and sitemap. |
CONTENT_REPO_URL |
– | Git repository containing blog/ and documents/. Empty = local checkout. |
CONTENT_REPO_BRANCH |
HEAD |
Branch/tag for the content sync. |
CONTENT_SYNC_INTERVAL_SECONDS |
60 |
Background sync interval (min. 5 s). |
CONTENT_DIR |
see above | Forces a specific content directory. |
PUBLIC_DIR |
./public |
Directory for static files. |
SPACE_STATUS_FILE |
./content/space/status.json |
File for the SpaceAPI status. |
SPACEAPI_SECRET_TOKEN |
– | Token for write requests to the SpaceAPI. |
NEXTCLOUD_CALENDAR_ICS_URL |
– | Public ICS export URL for the calendar. |
CALENDAR_CACHE_SECONDS |
120 |
In-memory cache for the ICS feed. |
Routes
| Path | Description |
|---|---|
/ |
Home page with space status, page links and the next meetings. |
/location/ |
Directions and OpenStreetMap map. |
/about/ |
About the space. |
/calendar/ |
All events, grouped by month and day. |
/blog/ |
Blog overview. |
/blog/{slug}/ |
Individual blog post. |
/documents/ |
Documents from the content repository. |
/donate/ |
Support us. |
/contact/ |
Contact. |
/imprint/ |
Legal notice. |
/privacy/ |
Privacy policy. |
/rss.xml |
RSS feed (language via cookie/query). |
/de/rss.xml, /en/rss.xml |
Fixed RSS feeds per language (legacy URLs). |
/sitemap.xml |
Sitemap. |
/robots.txt |
Robots file. |
/api/space |
SpaceAPI 0.13 (GET, PUT, OPTIONS). |
/api/documents/{filename} |
Document download. |
/api/blog-posts/{slug}/images/{image} |
Images from blog posts. |
/lang/de/, /lang/en/ |
Set the language and redirect back (?next=/path/). |
Legacy locale-prefixed URLs such as /de/about/ or /en/blog/ permanently redirect to the unprefixed variant and set the language in the process.
Language
The language is determined in this order:
?lang=de/?lang=en(stored as a cookie),langcookie,Accept-Language,- default: German.
Content
Blog
A post is a directory under blog/. Supported layouts:
content.de.md+content.en.md(bilingual),content.md(single language),index.de.md/index.en.md,- a single Markdown file
blog/{slug}.md.
Frontmatter (YAML):
---
title: Welcome to our site!
excerpt: A quick overview.
publishedAt: '2026-11-07T16:00:00.000Z'
readingTime: 1 min read
category: Space
locale: de/en
status: public
coverImage: ./cover.jpg
coverImageAlt: Dezentrale Erkelenz
---
status can be public or draft. Relative image paths such as ./cover.jpg are automatically served through the image endpoint. Markdown is rendered with pulldown-cmark.
Documents
All files under documents/ are listed, except _metadata.json, metadata.json and readme.md. Optionally, _metadata.json controls display names, dates and visibility:
{
"Satzung.pdf": {
"displayName": "Satzung",
"originalName": "Satzung.pdf",
"uploadedAt": "2026-01-01T00:00:00Z",
"status": "public"
}
}
SpaceAPI
Read the status:
curl http://127.0.0.1:3000/api/space
Write the status (token required):
curl -X PUT http://127.0.0.1:3000/api/space \
-H 'Content-Type: application/json' \
-H 'X-Api-Key: your-secret-token' \
-d '{"open":true}'
Alternatively via query parameters:
curl "http://127.0.0.1:3000/api/space?open=true&token=your-secret-token"
The status is stored in SPACE_STATUS_FILE and read by the home page.
Docker
Build and start:
docker compose up --build -d
Or manually:
docker build -t dezentrale-website .
docker run --rm -p 3000:3000 \
-e SITE_URL=http://localhost:3000 \
-e CONTENT_REPO_URL=https://git.bohmer.systems/moony/dezentrale-content.git \
-v dezentrale-content:/app/content \
dezentrale-website
The image uses a multi-stage build: Rust compiles the binary, and the runtime image contains only the binary, public/, plus git and ca-certificates for content syncing.
Project Structure
src/
├── main.rs # Startup, logging, content sync, HTTP server
├── config.rs # Environment configuration
├── i18n.rs # Language detection, translations, language switching
├── content_repo.rs # Git sync for the content repository
├── blog.rs # Blog posts, frontmatter, images
├── documents.rs # Document list and downloads
├── calendar.rs # ICS parser, recurrence rules, grouping
├── space.rs # SpaceAPI status
├── routes.rs # All HTTP routes
├── templates.rs # Askama template structs
├── rss.rs # RSS 2.0
├── sitemap.rs # Sitemap
├── static_files.rs # Static file serving
└── util.rs # Date formatting, MIME types, URL encoding
templates/ # Askama templates (embedded at compile time)
messages/ # de.json / en.json (embedded at compile time)
public/ # Static assets, Leaflet, fonts, logos
Design System
The visual style follows the 40C3 style guide. Fonts are stored locally under public/fonts/ and loaded via @font-face – there are no Google Fonts and no system font stacks:
- Headings: Jersey 10 (uppercase or mixed case),
- Subheadings: Hack Bold, underlined,
- Body text: Hack Regular, plus Italic and Bold Italic.
The palette consists of style guide colors 0–7 plus A and B. The original green (#75b44b, color 0) has been replaced by a blue (#5276a2). Text and accents use a lighter blue in dark mode (#7fa2cc) and a darker blue in light mode (#3c638d). The site primarily uses blue and neutral surfaces; the remaining colors are available as accents. Dark mode uses neutral grays from #0a0a0a, light mode uses #ebebeb.
Colors, spacing and components live in public/assets/style.css.
Each page has 3–4 very subtle decorative words (C3E3, 40C3, C3, <3, :3) in the heading font in the background. Selection and placement are derived server-side from the page path and stay fixed in the viewport.
Development
cargo check # quick type check
cargo test # unit tests
cargo fmt # formatting
cargo clippy # lints
Templates (templates/) and translations (messages/) are embedded at compile time with include_str!. Changes there require a restart or rebuild; static files under public/ are read directly from disk.