- Rust 52.3%
- Svelte 28.6%
- TypeScript 17.6%
- CSS 1.1%
- Dockerfile 0.2%
- Other 0.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
- newsletter removed (signups were stored but nothing could send them): form, endpoint, admin list, privacy template - admin image previews use the shape the site shows (hero 16:9, portrait 4:5, recipe 4:3) - JPEG/WebP photos over 2560 px are scaled down in the browser before upload - unused texts and a German fallback page removed; CI runs on Forgejo runners Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
| .claude | ||
| .forgejo/workflows | ||
| backend | ||
| frontend | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| compose.yml | ||
| Dockerfile | ||
| package.json | ||
| README.md | ||
yourveggiefairy
SvelteKit (Svelte 5) + Tailwind CSS v4 + TypeScript frontend, built with nub, served by a Rust/axum backend.
backend/ axum server (library + thin main)
src/content.rs content model + validation src/store.rs content + edit history
src/inbox.rs contact messages, comments src/analytics.rs cookie-less page views
src/media.rs upload validation src/seo.rs per-page meta, JSON-LD, sitemap
src/routes/ public · auth · admin · pages tests/api.rs end-to-end API tests
data/content.json seed content (copied to DATA_DIR on first edit)
frontend/ SvelteKit (Svelte 5, adapter-static SPA; axum serves the build)
src/app.css design tokens (@theme) + motion
src/routes/ SvelteKit pages (file-based routing, SPA mode) src/lib/recipes.ts search/filter/pagination (tested)
src/lib/components/ ui · layout · sections · recipe · admin
src/routes/admin/ admin pages
compose.yml run the site in Docker
Develop
First run: nub run install:web. Then two terminals:
nub run dev:api # axum on :3000
nub run dev:web # vite on :5173, proxies /api and /media to :3000
To use the admin area locally, start the API with a password:
$env:ADMIN_PASSWORD = "something-long"; nub run dev:api
Then open /admin and log in as admin.
Test, build, run
nub run check:web && nub run test:web # type-check + unit tests
nub run test:api # unit + end-to-end API tests
nub run build && nub run start # http://localhost:3000
Configuration (environment variables)
| Variable | Default | Meaning |
|---|---|---|
HOST / PORT |
0.0.0.0 / 3000 |
listen address |
ADMIN_PASSWORD |
– | required for the admin area; unset = admin disabled |
ADMIN_USER |
admin |
admin username |
SITE_URL |
http://localhost:<port> |
public URL, for canonical links, sitemap, link previews |
DATA_DIR |
data |
content, history, inbox, statistics, uploads (media/) – back this up |
STATIC_DIR |
frontend/dist |
built frontend |
RP_ID |
host of SITE_URL |
domain passkeys are bound to |
ALLOWED_ORIGINS |
– | extra page origins allowed to use passkeys (comma separated); SITE_URL is always allowed, and for localhost the Vite dev server (:5173) |
TRUST_PROXY |
false |
set true behind a reverse proxy (client IP from X-Forwarded-For) |
COOKIE_SECURE |
true if SITE_URL is https |
Secure flag on the session cookie |
RUST_LOG |
info |
log level |
Docker
The Dockerfile builds the frontend (nub), the backend (cargo) and a small runtime image with both. To run the site locally:
ADMIN_PASSWORD=something-long docker compose up --build # http://localhost:3000
Or only build the image: nub run docker:build, then docker run -p 3000:3000 -v yvf-data:/data -e ADMIN_PASSWORD=… yourveggiefairy. Everything you edit (content, uploads, inbox) lives in the /data volume, so keep it.
Image from Forgejo (one click)
In Forgejo open Actions › Build image › Run workflow (.forgejo/workflows/build-image.yml). It builds the Docker image and pushes it to your Forgejo container registry as <server>/<owner>/<repo>:latest (plus a tag with the commit; pushing a tag like v1.2.0 runs it too). Needed once: a runner with Docker, and a repository secret REGISTRY_TOKEN (a personal access token with the write:package permission). On the server, copy compose.yml and .env.example (as .env, with IMAGE, SITE_URL and ADMIN_PASSWORD filled in), then docker compose up -d --pull always fetches the new :latest.
The container serves plain HTTP on port 3000. For HTTPS put your own reverse proxy in front and set SITE_URL (https) and TRUST_PROXY=true; uploads of up to 64 MB must be allowed.
What lives where (admin area, /admin)
Overview (recipes: order, draft, edit, delete) · Home page · New recipe · Social channels (Instagram, TikTok, YouTube, any other) · Legal (imprint, privacy policy, contact text) · Inbox (contact messages, comment moderation) · Statistics · Security (passkeys, two-factor, sessions) · History (undo).
URLs are English: /recipes, /recipes/<slug>, /social, /contact, /imprint, /privacy, /admin/home, /admin/recipes/new, … The old German URLs (/rezepte, /kontakt, ?seite=2, …) are redirected permanently by the server (legacy_redirect in backend/src/seo.rs).
Recipes: ingredients are amount + unit + name. Visitors can change the servings (amounts are recalculated) and switch between metric and US measures (frontend/src/lib/ingredients.ts: 600 g ↔ 1¼ lb, 250 ml ↔ 1 cup; tablespoons, teaspoons and counts are the same in both). Temperatures inside the instruction text are not converted. Preparation and cooking time are separate, their sum is shown as a tag. The card colour is a hue slider (saturation and lightness come from the theme, see frontend/src/lib/color.ts).
Languages: English is the default; the public site has an EN/DE toggle (the choice is remembered, the browser language is ignored). UI texts live in frontend/src/lib/i18n/ (the compiler checks that both languages have the same keys). Content is written in English with an optional "German version" in the admin forms (de in content.json); empty German fields fall back to English. The admin area is English only.
Loading screen and speed: on the first page of a visit a loading screen waits for the content, the fonts and the hero image/video (1–6 s). The server embeds the public content in the HTML and adds a preload link for the hero image, so both start loading immediately. Photos are prepared in the browser before upload: HEIC becomes JPEG, and JPEG/WebP photos larger than 2560 px are scaled down (frontend/src/lib/images.ts). Photos uploaded before that keep their size: upload them again.
Admin sign-in security (Security page, backend/src/security.rs):
- Passkeys: "Create passkey" asks the browser, and a password manager such as Vaultwarden/Bitwarden offers to store it. Passkeys are tied to the domain (
RP_ID) and need HTTPS (orlocalhost). - Two-factor: after the password the sign-in asks for a 6-digit code from an authenticator app (QR code on the Security page), or one of 10 one-time recovery codes.
- Passkeys only: once a passkey exists, password sign-in can be switched off completely.
- Sessions end after 30 idle minutes (the admin pages keep them alive while you work) and after 8 hours. Security changes need a sign-in within the last 15 minutes and sign out all other browsers. Wrong passwords/codes are limited per IP (5 per 15 min) and overall (20 per hour); passkey sign-in is not affected by the overall limit.
- Locked out? Delete
security.jsoninDATA_DIRand restart: the password alone works again (passkeys stay).
Old data is upgraded automatically on load, using schema in content.json: German base texts become the German version and English becomes the base (untouched German placeholder texts are translated); text ingredients are split into amount/unit/name; the old cooking time becomes the cook time; the old colour name becomes a hue; the per-channel button style is dropped (all channels look the same).
Every change is saved to DATA_DIR/content.json; the previous state is kept in DATA_DIR/history/ (last 50) and can be restored.
Before going live
- Replace the
[PLACEHOLDERS]in Legal with your real details and have the imprint (Impressum) and privacy policy (Datenschutz) checked. The built-in texts are templates, not legal advice. - Upload the hero video/photo, your portrait, and recipe photos in the admin area.
- Set a long random
ADMIN_PASSWORD(12+ characters) andSITE_URL, then add a passkey or two-factor authentication in Security. - Contact messages are stored (Inbox), not emailed. There is no newsletter: it would need a mail service with double opt-in, which is not included.
Extending
- New page: a folder with a
+page.svelteinfrontend/src/routes/(e.g.routes/about/+page.svelteis/about); mirror public routes inbackend/src/seo.rs. The build is a single-page app (ssr = false): axum servesdist/index.htmlfor every URL, adds the page's SEO tags and allows SvelteKit's start-up script in the CSP by hash. - New colour / size: add a token to
@themeinfrontend/src/app.css. - New API endpoint: a handler in
backend/src/routes/and a test inbackend/tests/api.rs.