2026-05-14 08:27:02 +02:00
2026-05-14 08:24:41 +02:00
2026-05-14 08:24:41 +02:00
2026-05-14 08:24:41 +02:00
2026-05-14 08:27:02 +02:00
2026-05-14 08:24:41 +02:00

Ela's Atelier

A single-curator art portfolio. Rust/Axum API backed by markdown files on disk; Astro/React frontend with a parchment "Salon Hang" aesthetic. Each entry is a markdown post that must contain at least one image; the first image becomes the cover plate in the catalogue.

backend/   Rust + Axum API (filesystem-backed)
frontend/  Astro + React, SSRs pages and proxies /api/*
data/      Runtime data — posts, uploads, config.json (host volume)

Quick start

cp .env.example .env
# Generate a strong admin token:
echo "ADMIN_TOKEN=$(openssl rand -hex 32)" >> .env

docker compose up --build
# → http://localhost:4321
# → log in at /admin/login with the token from .env

Make sure the data/ host directory is owned by UID 1000 (the backend container's app user):

sudo chown -R 1000:1000 data/

Environment

Variable Required Default Notes
ADMIN_TOKEN yes Long random string. Stored as an HttpOnly cookie after login.
PORT no 3000 Backend port.
DATA_DIR no /app/data Where posts/uploads/config live.
COOKIE_SECURE no true Set false only for local HTTP development.
FRONTEND_ORIGIN no empty Set to your frontend's URL only if you expose the backend directly.
RUST_LOG no info tracing-subscriber filter.
PUBLIC_API_URL no http://backend:3000 Backend URL the Astro proxy hits server-to-server.

Local development

Backend:

cd backend
ADMIN_TOKEN=devtoken COOKIE_SECURE=false DATA_DIR=../data cargo run

Frontend (separate terminal, Node ≥ 22.12):

cd frontend
npm install
npm run dev
# → http://localhost:4321 (proxies to PUBLIC_API_URL, default http://backend:3000)

For a fully local stack, set PUBLIC_API_URL=http://localhost:3000 in frontend/.env.

Authoring a work

Each work is a markdown file at data/posts/<slug>.md with YAML frontmatter. Every work must contain at least one markdown image — the first image becomes the cover plate on the gallery index. Image alt text becomes the figure caption on the work page.

---
date: 2026-05-09
summary: Optional short caption shown beneath the plate on the index.
tags:
  - oil
  - 2026
draft: false
---
# Untitled (charcoal on paper)

![A view of the cliff at dawn](/uploads/cliff-dawn.jpg "Plate I — graphite, A3")

Notes on the piece: materials, references, what worked, what didn't.

![Detail of the foreground](/uploads/cliff-detail.jpg "Plate II — detail")
  • draft: true hides a work from the public catalogue and 404s for non-curators.
  • Works are sorted by date descending on the index.
  • The web editor at /admin/editor writes the same format and updates atomically.

Uploads

Allowlisted extensions: jpg, jpeg, png, webp, gif, avif, pdf, txt, md, mp3, wav, ogg, mp4, webm, mov. Magic bytes are checked against the extension. SVG and HTML are intentionally rejected — /uploads/* is served as-is, so any active content there would be XSS.

Max upload size: 50 MB.

Theme

The default theme is Salon — aged parchment, oxblood ink, Fraunces/EB Garamond/Caveat typography. A Salon Noir variant (black gallery wall) is available via the theme switcher in the header.

Influences: Friedrich, Goya, Kahlo, Tillmans, Basquiat, Sherman, Matisse, Dix, Abramović.

Backups

The deployed data/ directory is the entire gallery. Back it up with whatever you trust — rsync, restic, borg, a sidecar container; nothing fancy is built in.

rsync -av data/ backup-host:/path/to/gallery-data/

Stack

  • Backend: Rust 2024 edition (requires 1.85+), axum 0.8, serde_yaml frontmatter, subtle constant-time auth, infer for upload sniffing
  • Frontend: Astro 6, React 19, Tailwind 4, marked + marked-katex-extension + marked-highlight + DOMPurify
  • Editor: CodeMirror 6 with vim mode and asset autocomplete
S
Description
No description provided
Readme 2.8 MiB
Languages
TypeScript 40.3%
CSS 32.1%
Rust 16.5%
Astro 10.4%
Dockerfile 0.3%
Other 0.4%