# 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 ```sh 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): ```sh 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: ```sh cd backend ADMIN_TOKEN=devtoken COOKIE_SECURE=false DATA_DIR=../data cargo run ``` Frontend (separate terminal, Node ≥ 22.12): ```sh 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/.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. ```markdown --- 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. ```sh 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