Claude Init und Readme Update

Migration auf Docker Umgebung
This commit is contained in:
2026-07-26 09:56:58 +02:00
parent 4fe45e4e53
commit d53fd8742d
6 changed files with 411 additions and 7 deletions
+1 -1
View File
@@ -138,7 +138,7 @@ Rezepte und Site-Daten in normalisierten Postgres-Tabellen, optional lokal per D
- [x] **`DATABASE_URL` & Docker Compose** - [x] **`DATABASE_URL` & Docker Compose**
- [x] `docker-compose.dev.yml` für lokale Postgres-Instanz - [x] `docker-compose.dev.yml` für lokale Postgres-Instanz
- [x] `init_db()` legt Tabellen an; `scripts/db-seed.php` seedet aus `scripts/seed-data.php` - [x] `init_db()` legt Tabellen an; `scripts/db-seed.php` seedet aus `data/recipes.json`
- [x] **PHP-Datenzugriff** - [x] **PHP-Datenzugriff**
- [x] `load_recipes()` / `save_recipe()` ohne JSON-Fallback - [x] `load_recipes()` / `save_recipe()` ohne JSON-Fallback
- [x] `load_recipe_by_slug()` in `recipe.php` - [x] `load_recipe_by_slug()` in `recipe.php`
+3 -3
View File
@@ -4,7 +4,7 @@ This document summarizes architectural knowledge, conventions, and learnings for
## 1. Project Architecture & Stack ## 1. Project Architecture & Stack
- **Backend:** Vanilla PHP. No heavy frameworks. - **Backend:** Vanilla PHP. No heavy frameworks.
- **Database:** PostgreSQL required (`DATABASE_URL` in `.env`). Normalized tables in `scripts/schema.sql`; `load_recipes()` / `save_recipe()` and `load_site_settings()` / `save_site_settings()` in `helpers.php`. No runtime JSON files. One-time import: `php scripts/db-seed.php` from `scripts/seed-data.php`. - **Database:** PostgreSQL required (`DATABASE_URL` in `.env`). Normalized tables in `scripts/schema.sql`; `load_recipes()` / `save_recipe()` and `load_site_settings()` / `save_site_settings()` in `helpers.php`. Recipes read at runtime only from Postgres. One-time import of sample data: `php scripts/db-seed.php` from `data/recipes.json`.
- **Site settings (legal pages):** Stored in Postgres table `site_settings`; no flat-file fallback. - **Site settings (legal pages):** Stored in Postgres table `site_settings`; no flat-file fallback.
- **Admin Panel (`admin.php`):** Lightweight CMS. Textareas use one line per array element (`ingredients`, `steps`, `step_videos`, `step_timers`). - **Admin Panel (`admin.php`):** Lightweight CMS. Textareas use one line per array element (`ingredients`, `steps`, `step_videos`, `step_timers`).
- **Frontend:** Server-rendered PHP (`index.php`, `recipe.php`, …), Vanilla JS/CSS. Profile/favorites via `assets/fc-local.js`. - **Frontend:** Server-rendered PHP (`index.php`, `recipe.php`, …), Vanilla JS/CSS. Profile/favorites via `assets/fc-local.js`.
@@ -42,7 +42,7 @@ PHP still exposes the same nested arrays (`i18n`, `nutrition`, …) via `hydrate
1. `DATABASE_URL` required → `require_database()` or HTTP 503 (`maintenance/db-unavailable.php`). 1. `DATABASE_URL` required → `require_database()` or HTTP 503 (`maintenance/db-unavailable.php`).
2. `load_recipes()` → SQL → PHP arrays for templates. 2. `load_recipes()` → SQL → PHP arrays for templates.
3. Admin: `save_recipe()`, `delete_recipe()`, `clear_featured_recipes()`. 3. Admin: `save_recipe()`, `delete_recipe()`, `clear_featured_recipes()`.
4. One-time import: `php scripts/db-seed.php` from `scripts/seed-data.php`. 4. One-time import: `php scripts/db-seed.php` from `data/recipes.json`.
### `config.php` functions ### `config.php` functions
- `load_env()` — parses `.env`. - `load_env()` — parses `.env`.
@@ -125,7 +125,7 @@ See `.agents/TODO.md` (e.g. PWA & offline support). README `Local Postgres (Dock
|------|---------| |------|---------|
| `config.php` | `.env`, Firebase config, PDO | | `config.php` | `.env`, Firebase config, PDO |
| `helpers.php` | `init_db`, `load_recipes`, `save_recipe`, site settings | | `helpers.php` | `init_db`, `load_recipes`, `save_recipe`, site settings |
| `scripts/seed-data.php` | Seed arrays for recipes and site settings | | `data/recipes.json` | Seed data for recipes (imported by `scripts/db-seed.php`) |
| `docker-compose.dev.yml` | Local Postgres | | `docker-compose.dev.yml` | Local Postgres |
| `scripts/db-check.php` | Connection + seed smoke test | | `scripts/db-check.php` | Connection + seed smoke test |
| `partials/head.php` | Firebase init via `get_firebase_config()` | | `partials/head.php` | Firebase init via `get_firebase_config()` |
+97
View File
@@ -0,0 +1,97 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project overview
FlixCooks is a premium recipe/food-blog website built as **vanilla server-rendered PHP** — no framework, no build step, no JS bundler, no Composer dependencies. PHP renders HTML directly with heavy inline `<script>`/`<style>` blocks per page. All data lives in PostgreSQL (no JSON files, no ORM).
## Commands
There is no build step, package manager, linter, or test suite in this repo (no `composer.json`, no `phpunit`, no `package.json`). Work is verified by running the app directly.
```bash
# Local app server
php -S localhost:8000
# Local Postgres (separate from production)
docker compose -f docker-compose.dev.yml up -d
docker compose -f docker-compose.dev.yml ps # wait for healthy
docker compose -f docker-compose.dev.yml down -v # wipe + re-seed on next run
# .env setup (never commit .env)
cp .env.example .env # then set DATABASE_URL
# Seed / verify DB
php scripts/db-seed.php # one-time import of data/recipes.json into Postgres
php scripts/db-check.php # connect, init schema, print recipe count
# Inspect DB directly
docker exec -it flixcooks-postgres-dev psql -U flixcooks -d flixcooks_dev
# Full stack via Docker (production-like image)
docker compose build && docker compose up -d # http://127.0.0.1:8080
```
Requires PHP 8.x with the `pgsql`/`pdo_pgsql` extension (`sudo apt install php-pgsql` on Debian/Ubuntu). Without a working `DATABASE_URL`, every page returns HTTP 503 and renders `maintenance/db-unavailable.php` — there is no file/JSON fallback.
## Architecture
### Request flow
Every entry-point page (`index.php`, `recipe.php`, `admin.php`, `login.php`) follows the same pattern:
1. `require __DIR__ . '/helpers.php'` (which itself requires `config.php` and starts the session).
2. Call `load_recipes()` / `load_site_settings()` inside a `try/catch (DatabaseUnavailableException $e)` that calls `handle_database_unavailable($e)` on failure.
3. Resolve `$lang` from `?lang=de|en` (default `en`) and build a `$copy`/`$t` array of inline translation strings for that page.
4. Render HTML directly (`partials/head.php``partials/header.php` → page body → `partials/footer.php`), reading from `helpers.php` data structures.
There is no router and no templating engine — each `.php` file is both controller and view.
### Data layer (`config.php` + `helpers.php`)
- `config.php`: parses `.env` (`load_env()`), exposes `get_db_connection()` (PDO, memoized in a `static` var) and `DatabaseUnavailableException`.
- `helpers.php`: everything else — schema bootstrap (`ensure_recipe_schema()` applies `scripts/schema.sql` idempotently on first DB use, no migrations system), and CRUD:
- `load_recipes()` / `load_recipe_by_slug()` — hydrate recipes from normalized tables into the nested PHP array shape templates expect (`hydrate_recipes_from_db()`).
- `save_recipe()` / `delete_recipe()` — used by `admin.php`; `save_recipe()` deletes+reinserts child rows (translations/tags/ingredients/utensils/steps) inside a transaction rather than diffing.
- `load_site_settings()` / `save_site_settings()` — imprint/privacy legal-page copy, stored as EAV rows (`site_settings(section, lang, setting_key, setting_value)`), merged over `default_site_settings()`.
- `localize_recipe()` / `localize_recipes()` — flatten a recipe's `i18n[lang]` block onto the top level for the current request's language.
- `e()` — the only HTML-escaping helper (`htmlspecialchars` wrapper); always use it when echoing user- or DB-sourced strings.
### Recipe data shape
A recipe row + its children hydrate into:
```php
[
'slug', 'hero', 'prep_time', 'cook_time', 'total_time', 'servings',
'featured', 'coming_soon',
'nutrition' => ['calories','protein','carbs','fat'],
'i18n' => [
'en' => ['title','description','category','difficulty','tags','ingredients','utensils','steps','step_videos','step_timers'],
'de' => [ ... same shape ... ],
],
]
```
`step_videos` and `step_timers` are parallel arrays indexed the same as `steps` (one optional video URL / timer-in-minutes per step). In `admin.php` these are edited as one-line-per-array-element `<textarea>` fields.
### Database schema (`scripts/schema.sql`)
Normalized Postgres tables, all applied via `ensure_recipe_schema()` (not a migrations tool — editing the schema means editing this file, which must stay idempotent `CREATE TABLE IF NOT EXISTS`):
`recipes` (1 row per recipe) → `recipe_translations`, `recipe_tags`, `recipe_ingredients`, `recipe_utensils`, `recipe_steps` (all keyed by `recipe_slug` + `lang`, cascade-deleted with the parent recipe) → `site_settings` (imprint/privacy copy, keyed by `section` + `lang` + `setting_key`).
### Auth
`admin.php` guards itself with a single shared secret (`FLIXCOOKS_ADMIN_KEY` env var, compared via `hash_equals`) rather than user accounts; success sets `$_SESSION['fc_admin'] = true`. Admin POST handlers additionally check a CSRF token (`$_SESSION['csrf_token']` vs `$_POST['token']`). If `FLIXCOOKS_ADMIN_KEY` is unset, `admin.php` renders an "unavailable" page instead of a login form.
### Frontend conventions
- No JS build step — GSAP, ScrollTrigger, and Lenis are loaded from CDN in `partials/head.php`; page-specific behavior lives in inline `<script>` blocks at the bottom of each `.php` file.
- `assets/fc-local.js` is the only standalone JS file — client-side `localStorage` for favorites and dietary-goal personalization (no backend user accounts on the public site).
- Scroll-reveal: elements tagged `.reveal-target` are animated in via a shared `IntersectionObserver` pattern repeated per-page (see bottom of `index.php` / `recipe.php`).
- Lenis smooth scroll must be explicitly stopped/started around fullscreen overlays: `window.lenis.stop()` on open, `window.lenis.start()` on close (see Cooking Mode in `recipe.php`).
- Styling is one large `assets/style.css` using CSS custom properties, `clamp()`-based fluid spacing/typography, and a 24-column asymmetric grid (`FloemaLayoutGrid`). Full visual language (palette options, motion specs, named components like `CapitoliumRevealButton`, `LiquidOverlayMenu`, `AuraMarbleBackground`) is documented in `.agents/DESIGN_GUIDE.md` — consult it before styling new UI so new work matches the established aesthetic vocabulary.
- Assets are cache-busted via `filemtime()` query strings (`$assetVersion()` in `partials/head.php`), not filename hashing.
### Deployment
Single production `Dockerfile` (`php:8.3-apache-bookworm`), pushed to Coolify. `docker/entrypoint.sh` waits for the DB (`scripts/db-check.php` polling loop) and applies schema before starting Apache. `health.php` is the container `HEALTHCHECK` target. Details in `docs/COOLIFY.md`.
## Project workflow rules (from `.agents/rules/`)
- **Never commit directly to `main`.** All work happens on feature branches (`feature/...` or `issue-#...`), merged via PR. Pin GitHub Actions to specific version tags, not `@latest`.
- Merge-conflict resolution on a feature branch is the responsibility of that branch's author — merge `main` in, resolve manually, never force-push over others' work.
- The `close_feature` skill (`.agents/skills/close_feature.json`) encodes the "merge feature → main, verify, push, delete branch" flow.
- Track development progress in `.agents/TODO.md`; record new architectural/aesthetic/workflow knowledge in `.agents/brain.md` (both are living documents future agents rely on — update them as you learn things, don't just read them).
- Non-code-affecting markdown docs can be committed straight to `main`.
+3 -3
View File
@@ -19,7 +19,7 @@ The project is architected to remain extremely lightweight and fast, intentional
- `helpers.php`: Core PHP utilities and the PostgreSQL data access layer for recipes and site settings. - `helpers.php`: Core PHP utilities and the PostgreSQL data access layer for recipes and site settings.
- `config.php`: Environment-independent configuration loader which reads runtime secrets from `.env`. - `config.php`: Environment-independent configuration loader which reads runtime secrets from `.env`.
- `assets/fc-local.js`: Browser-side storage for favorites and dietary goals. - `assets/fc-local.js`: Browser-side storage for favorites and dietary goals.
- `scripts/seed-data.php`: One-time seed data for recipes, imprint, and privacy settings. - `scripts/db-seed.php`: One-time import of `data/recipes.json` into Postgres.
- `scripts/schema.sql`: Relational table definitions for recipes and site settings. - `scripts/schema.sql`: Relational table definitions for recipes and site settings.
--- ---
@@ -154,11 +154,11 @@ Schema: `scripts/schema.sql`. PHP baut daraus dieselben Arrays wie früher (`i18
1. `DATABASE_URL` in `.env` → Verbindung über `config.php`. 1. `DATABASE_URL` in `.env` → Verbindung über `config.php`.
2. Beim ersten Request: Tabellen anlegen (`ensure_recipe_schema()`). 2. Beim ersten Request: Tabellen anlegen (`ensure_recipe_schema()`).
3. `php scripts/db-seed.php` einmalig ausführen → Rezepte und Site-Daten werden in Postgres geschrieben. 3. `php scripts/db-seed.php` einmalig ausführen → Rezepte aus `data/recipes.json` werden in Postgres geschrieben.
4. `load_recipes()` und `load_site_settings()` lesen per SQL; ohne DB → HTTP 503 (`maintenance/db-unavailable.php`). 4. `load_recipes()` und `load_site_settings()` lesen per SQL; ohne DB → HTTP 503 (`maintenance/db-unavailable.php`).
5. Admin: `save_recipe()`, `delete_recipe()` und `save_site_settings()` schreiben direkt in die Tabellen. 5. Admin: `save_recipe()`, `delete_recipe()` und `save_site_settings()` schreiben direkt in die Tabellen.
**Einmalig Daten laden:** `php scripts/db-seed.php` (aus `scripts/seed-data.php`, ohne JSON-Dateien). **Einmalig Daten laden:** `php scripts/db-seed.php` (importiert `data/recipes.json`).
Für **Staging/Production** muss eine externe Postgres-Datenbank vorhanden sein. Setze nur `DATABASE_URL` in der Hosting-Umgebung und mische nie Production-Daten in die lokale Dev-DB. Für **Staging/Production** muss eine externe Postgres-Datenbank vorhanden sein. Setze nur `DATABASE_URL` in der Hosting-Umgebung und mische nie Production-Daten in die lokale Dev-DB.
+264
View File
@@ -0,0 +1,264 @@
[
{
"slug": "frische-tagliatelle-mit-cremiger-tomatensauce",
"hero": "/assets/pasta-tomato.jpg",
"prep_time": 50,
"cook_time": 10,
"total_time": 60,
"servings": 2,
"featured": false,
"coming_soon": false,
"nutrition": {
"calories": 0,
"protein": 0,
"carbs": 0,
"fat": 0
},
"i18n": {
"en": {
"title": "Tagliatelle with tomato sauce",
"description": "Simple but tasty, short cooking time but self made. The root dish to start in a cozy evening and satisfy the carbs cravings without any hidden additives. Noodles with Tomato Sauce, more hearty isnt possible.",
"category": "Pasta",
"difficulty": "Easy",
"tags": [
"Pasta",
"Fast",
"Vegetarian"
],
"ingredients": [
"300 g flour",
"3 eggs",
"10 cherry tomatoes",
"1/2 clove garlic",
"1 tbsp olive oil",
"80 ml cream",
"Fresh basil",
"Parmesan",
"Salt and pepper"
],
"utensils": [
"Rolling pin",
"Optional: Pasta machine",
"Optional: Blender"
],
"steps": [
"Pile the flour on your counter, press a well in the center, crack in the eggs. Gradually pull flour into the eggs, then knead 510 minutes until smooth.",
"Roll the dough to 12 mm thickness (rolling pin or pasta machine) and cut into tagliatelle.",
"Bring salted water to a boil. Meanwhile, halve tomatoes and sear in olive oil with a pinch of salt until lightly charred.",
"Toast the garlic briefly, then blend tomatoes with cream (or crush in the pan) and simmer to thicken slightly.",
"Boil tagliatelle for 23 minutes, drain, toss with the sauce, and finish with basil and Parmesan."
]
},
"de": {
"title": "Tagliatelle mit Tomatensauce",
"description": "Einfach, aber richtig lecker; kurze Kochzeit und trotzdem hausgemacht. Das Basisgericht für einen gemütlichen Abend, stillt den Kohlenhydrat-Hunger ohne versteckte Zusätze. Nudeln mit Tomatensauce herzhafter geht es kaum.",
"category": "Pasta",
"difficulty": "Einfach",
"tags": [
"Pasta",
"Schnell",
"Vegetarisch"
],
"ingredients": [
"300 g Mehl",
"3 Eier",
"10 Cherry-Tomaten",
"1/2 Knoblauchzehe",
"1 EL Olivenöl",
"80 ml Sahne",
"Frischer Basilikum",
"Parmesan",
"Salz und Pfeffer"
],
"utensils": [
"Nudelholz",
"Optional: Nudelmaschine",
"Optional: Mixer"
],
"steps": [
"Mehl auf der Arbeitsfläche anhäufen, eine Kuhle drücken, Eier hineingeben. Mehl nach und nach einarbeiten, dann 510 Minuten kneten, bis der Teig glatt ist.",
"Teig auf 12 mm ausrollen (mit Nudelholz oder Nudelmaschine) und in Tagliatelle schneiden.",
"Gesalzenes Wasser aufsetzen; währenddessen Tomaten halbieren und mit Olivenöl und einer Prise Salz in der Pfanne anrösten, bis Röstnoten entstehen.",
"Knoblauch kurz mitrösten, dann Tomaten mit Sahne pürieren (Mixer) oder in der Pfanne zerdrücken und kurz einkochen lassen.",
"Tagliatelle 23 Minuten kochen, abgießen und mit der Sauce vermengen. Mit Basilikum und Parmesan anrichten."
]
}
}
},
{
"slug": "oat-pancakes",
"hero": "/assets/pancakes.jpg",
"prep_time": 0,
"cook_time": 0,
"total_time": 0,
"servings": 2,
"featured": false,
"coming_soon": true,
"nutrition": {
"calories": 0,
"protein": 0,
"carbs": 0,
"fat": 0
},
"i18n": {
"en": {
"title": "Oat-Pancakes",
"description": "",
"category": "",
"difficulty": "Easy",
"tags": [],
"ingredients": [],
"utensils": [],
"steps": []
},
"de": {
"title": "Hafer-Pfannkuchen",
"description": "",
"category": "",
"difficulty": "Einfach",
"tags": [],
"ingredients": [],
"utensils": [],
"steps": []
}
}
},
{
"slug": "steak-with-onion-jam",
"hero": "/assets/steak.JPEG",
"prep_time": 15,
"cook_time": 15,
"total_time": 30,
"servings": 2,
"featured": true,
"coming_soon": false,
"nutrition": {
"calories": 0,
"protein": 0,
"carbs": 0,
"fat": 0
},
"i18n": {
"en": {
"title": "Steak with onion jam",
"description": "A hearty meal after a long day, best enjoyed with a glass of red wine and in good company—because the onion jam still isnt satisfied on its own.",
"category": "",
"difficulty": "Easy",
"tags": [
"meat",
"dinner"
],
"ingredients": [
"2 x 250g rump steak",
"300g potatoes",
"1 large red onion",
"1/2 clove garlic",
"50ml cream",
"50ml red wine",
"4 carrots (yellow and purple)",
"parmesan",
"Sicilian orange salt",
"thyme",
"1 tbsp butter"
],
"utensils": [
"knife",
"cutting board",
"pan",
"pot",
"oven"
],
"steps": [
"Take the steak out 30 minutes before cooking",
"Halve the potatoes",
"Halve the carrots",
"Dice the onion",
"Cook the potatoes and carrots until al dente",
"Sear the steak with butter",
"Once both sides are golden brown, bake for 6 minutes at 160°C (medium); for medium rare, let it rest after searing both sides for 3 minutes",
"Sauté the onion in the steak butter, deglaze with red wine, and add cream once the alcohol has evaporated",
"Plate and serve!"
]
},
"de": {
"title": "Steak mit Zwiebelmarmelade",
"description": "Eine schwere Mahlzeit nach einem langen Tag, am besten zu einem Schluck Rotwein und in Gesellschaft genießen, da die Zwiebelmarmelade noch nicht genug davon hat.",
"category": "",
"difficulty": "Einfach",
"tags": [
"Fleisch",
"Abendessen"
],
"ingredients": [
"2 x 250g Rumpsteak",
"300gr Kartoffeln",
"1 Große Rote Zwiebel",
"1/2 Knoblauch-Zehe",
"50ml Sahne",
"50ml Rotwein",
"4 Karotten (Geld und Lila)",
"Parmesan",
"Sizilianisches Orangensalz",
"Thymian",
"1 EL Butter"
],
"utensils": [
"Messer",
"Brett",
"Pfanne",
"Topf",
"Ofen"
],
"steps": [
"Steak 30 Minuten vor dem anbraten raus legen",
"Kartoffeln halbieren",
"Karotten halbieren",
"Zwiebel würfeln",
"Kartoffeln und Karotten al dente kochen",
"Steak anbraten mit Butter",
"sobald beide seiten goldbraun sind für 6 Minuten bei 160grad backen (Medium) für Medium Rare nur ruhen lassen nachdem es von beiden Seiten für 3 Minuten angebraten wurde",
"Zwiebel in Steak-Butter anschwitzen, mit Rotwein ablöschen und Sahne dazu geben nachdem er Alkohol verdünstet ist",
"Anrichten!"
]
}
}
},
{
"slug": "oat-cake",
"hero": "/assets/haferkuchen.JPEG",
"prep_time": 0,
"cook_time": 0,
"total_time": 0,
"servings": 2,
"featured": false,
"coming_soon": true,
"nutrition": {
"calories": 0,
"protein": 0,
"carbs": 0,
"fat": 0
},
"i18n": {
"en": {
"title": "Oat cake",
"description": "",
"category": "",
"difficulty": "Easy",
"tags": [],
"ingredients": [],
"utensils": [],
"steps": []
},
"de": {
"title": "Haferkuchen",
"description": "",
"category": "",
"difficulty": "Einfach",
"tags": [],
"ingredients": [],
"utensils": [],
"steps": []
}
}
}
]
+43
View File
@@ -0,0 +1,43 @@
<?php
/**
* Einmaliger Import aus data/recipes.json in die relationale DB.
* Die Website liest zur Laufzeit nur noch aus Postgres.
*
* php scripts/db-seed.php
*/
require_once dirname(__DIR__) . '/helpers.php';
$seedPath = dirname(__DIR__) . '/data/recipes.json';
if (!file_exists($seedPath)) {
fwrite(STDERR, "Seed-Datei fehlt: data/recipes.json\n");
exit(1);
}
$raw = file_get_contents($seedPath);
$recipes = json_decode($raw, true);
if (!is_array($recipes)) {
fwrite(STDERR, "Ungültiges JSON in data/recipes.json\n");
exit(1);
}
try {
$pdo = require_database();
} catch (DatabaseUnavailableException $e) {
fwrite(STDERR, $e->getMessage() . "\n");
exit(1);
}
$count = 0;
foreach ($recipes as $recipe) {
if (!is_array($recipe) || empty($recipe['slug'])) {
continue;
}
if (save_recipe($recipe, $pdo)) {
$count++;
echo " + {$recipe['slug']}\n";
} else {
fwrite(STDERR, " ! Fehler bei {$recipe['slug']}\n");
}
}
echo "\nImport abgeschlossen: {$count} Rezepte.\n";