yourveggiefairy.com website
  • Rust 52.3%
  • Svelte 28.6%
  • TypeScript 17.6%
  • CSS 1.1%
  • Dockerfile 0.2%
  • Other 0.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Zoe 354d7f8e49
Some checks are pending
CI / frontend (push) Waiting to run
CI / backend (push) Waiting to run
Cleanup: remove the newsletter, matching image previews, photo downscaling, Forgejo CI
- 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>
2026-10-17 18:53:06 +02:00
.claude Add initial site: Svelte 5 frontend, axum backend, deploy config 2026-10-06 18:43:48 +02:00
.forgejo/workflows Cleanup: remove the newsletter, matching image previews, photo downscaling, Forgejo CI 2026-10-17 18:53:06 +02:00
backend Cleanup: remove the newsletter, matching image previews, photo downscaling, Forgejo CI 2026-10-17 18:53:06 +02:00
frontend Cleanup: remove the newsletter, matching image previews, photo downscaling, Forgejo CI 2026-10-17 18:53:06 +02:00
.dockerignore Docker: compose file for running the site locally, dockerignore, scripts, docs 2026-10-17 18:29:31 +02:00
.env.example Single compose.yml, remove Caddy and the deploy folder 2026-10-17 18:31:39 +02:00
.gitignore Rebuild the frontend on SvelteKit; preload hero, dark header, consistent spacing 2026-10-06 19:06:52 +02:00
Cargo.lock Admin security: two-factor codes, recovery codes, passkeys-only mode, session timeouts and re-authentication 2026-10-17 18:45:37 +02:00
Cargo.toml Add initial site: Svelte 5 frontend, axum backend, deploy config 2026-10-06 18:43:48 +02:00
compose.yml Single compose.yml, remove Caddy and the deploy folder 2026-10-17 18:31:39 +02:00
Dockerfile Add initial site: Svelte 5 frontend, axum backend, deploy config 2026-10-06 18:43:48 +02:00
package.json Docker: compose file for running the site locally, dockerignore, scripts, docs 2026-10-17 18:29:31 +02:00
README.md Cleanup: remove the newsletter, matching image previews, photo downscaling, Forgejo CI 2026-10-17 18:53:06 +02:00

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 (or localhost).
  • 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.json in DATA_DIR and 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) and SITE_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.svelte in frontend/src/routes/ (e.g. routes/about/+page.svelte is /about); mirror public routes in backend/src/seo.rs. The build is a single-page app (ssr = false): axum serves dist/index.html for 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 @theme in frontend/src/app.css.
  • New API endpoint: a handler in backend/src/routes/ and a test in backend/tests/api.rs.