Compare commits

1 Commits
11 changed files with 27 additions and 773 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] `docker-compose.dev.yml` für lokale Postgres-Instanz
- [x] `init_db()` legt Tabellen an; `scripts/db-seed.php` seedet aus `data/recipes.json`
- [x] `init_db()` legt Tabellen an; `scripts/db-seed.php` seedet aus `scripts/seed-data.php`
- [x] **PHP-Datenzugriff**
- [x] `load_recipes()` / `save_recipe()` ohne JSON-Fallback
- [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
- **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`. Recipes read at runtime only from Postgres. One-time import of sample data: `php scripts/db-seed.php` from `data/recipes.json`.
- **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`.
- **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`).
- **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`).
2. `load_recipes()` → SQL → PHP arrays for templates.
3. Admin: `save_recipe()`, `delete_recipe()`, `clear_featured_recipes()`.
4. One-time import: `php scripts/db-seed.php` from `data/recipes.json`.
4. One-time import: `php scripts/db-seed.php` from `scripts/seed-data.php`.
### `config.php` functions
- `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 |
| `helpers.php` | `init_db`, `load_recipes`, `save_recipe`, site settings |
| `data/recipes.json` | Seed data for recipes (imported by `scripts/db-seed.php`) |
| `scripts/seed-data.php` | Seed arrays for recipes and site settings |
| `docker-compose.dev.yml` | Local Postgres |
| `scripts/db-check.php` | Connection + seed smoke test |
| `partials/head.php` | Firebase init via `get_firebase_config()` |
@@ -1,324 +0,0 @@
---
name: Coolify React CMS Stack
overview: "Empfohlener Standard-Stack: Next.js (React) + Payload CMS (self-hosted) + PostgreSQL, alles als Docker-Images über GitHub → Coolify. Cursor/MCP für die Entwicklung, GitHub Actions für Qualitätssicherung vor dem Deploy."
todos:
- id: scaffold-monorepo
content: "Greenfield-Monorepo anlegen: apps/web (Next standalone), apps/cms (Payload), packages/shared-types"
status: pending
- id: docker-coolify
content: "Dockerfiles, docker-compose.yml, docs/COOLIFY.md (3 Services: Postgres, CMS, Web) nach FlixCooks-Muster"
status: pending
- id: cms-content-model
content: Payload Collections + Webhooks für On-Demand Revalidation; Env-Beispiele in .env.example
status: pending
- id: github-ci
content: "GitHub Actions: lint, typecheck, build, docker build smoke; branch protection auf main"
status: pending
- id: mcp-agents
content: .cursor/mcp.json + .agents/brain.md mit Stack-, Env- und Deploy-Regeln für KI
status: pending
- id: animation-baseline
content: "GSAP/Lenis/Framer-Baseline-Komponenten und Regel: Overlays stoppen Lenis"
status: pending
isProject: false
---
# Tech-Stack & Workflow: React-Sites, CMS, Docker, Coolify
## Empfehlung (Default)
Du bist unsicher bei CMS und Frontend hier ist ein **bewährtes Default**, das zu deinen Vorgaben passt (React, Animationen, Docker, Coolify, KI/MCP, CI):
| Schicht | Technologie | Warum |
|---------|-------------|--------|
| **Frontend** | [Next.js 15](https://nextjs.org) (App Router, TypeScript) | SEO/SSR/ISR, React-Ökosystem, `output: 'standalone'` für ein schlankes Production-Docker-Image |
| **Animationen** | GSAP (+ ScrollTrigger), Framer Motion, Lenis | GSAP für Scroll/Timeline-Premium-Feel; Framer für UI-Micro-Interactions; Lenis kennst du bereits aus FlixCooks |
| **CMS** | [Payload CMS 3](https://payloadcms.com) (eigener Container) | TypeScript, Postgres-native, Admin-UI out of the box, Docker-freundlich, passt zu Coolify wie dein aktuelles Postgres-Setup |
| **Datenbank** | PostgreSQL 16 (Coolify Database Service) | Eine Instanz, getrennte DBs/User für CMS vs. App optional |
| **Runtime / Deploy** | Docker + [Coolify](https://coolify.io) | Git-Webhook → Build → Traefik/HTTPS; du hast das Muster schon in [docs/COOLIFY.md](docs/COOLIFY.md) |
| **Lokale Dev** | `docker compose` (Web + CMS + Postgres) | Parität zu Production, wie [docker-compose.yml](docker-compose.yml) bei FlixCooks |
| **KI-Entwicklung** | Cursor + MCP-Server | Repo-Kontext, GitHub, Docs, optional DB |
| **CI** | GitHub Actions | Lint, Types, Build, Docker-Smoke, optional E2E; PR-Review wie [.github/workflows/gemini-pr-review.yml](.github/workflows/gemini-pr-review.yml) |
**Alternative Frontend:** Vite + React SPA + nginx-Image maximal frei für reine Animation-Landingpages, aber schlechteres SEO und kein ISR ohne Extra-Aufwand. **Alternative CMS (weniger Ops):** Sanity/Contentful (Cloud) nur Frontend-Container auf Coolify. **Alternative CMS (kein Backend):** Tina/Decap + Markdown im Repo gut für Blogs, schwächer für Redakteur:innen ohne Git.
---
## Zielarchitektur auf Coolify
```mermaid
flowchart TB
subgraph dev [Entwicklung]
Cursor[Cursor + MCP]
LocalCompose[docker compose]
Cursor --> LocalCompose
end
subgraph github [GitHub]
Repo[Monorepo]
GHA[GitHub Actions CI]
Repo --> GHA
end
subgraph coolify [Coolify Server]
PG[(PostgreSQL)]
CMS[Payload CMS Container]
WEB[Next.js Container]
Traefik[Traefik HTTPS]
PG --> CMS
CMS -->|REST/GraphQL| WEB
Traefik --> WEB
Traefik --> CMS
end
dev -->|push main| Repo
GHA -->|grüner Build| Repo
Repo -->|Webhook Deploy| coolify
```
**Drei Coolify-Ressourcen** (analog zu deinem FlixCooks-Setup: Postgres + App):
1. **PostgreSQL** internal URL, nicht öffentlich
2. **CMS-App** Dockerfile aus `apps/cms`, Port z. B. 3001, Env: `DATABASE_URL`, `PAYLOAD_SECRET`
3. **Web-App** Dockerfile aus `apps/web`, Port 3000, Env: `CMS_URL` (internal), `REVALIDATE_SECRET` für On-Demand-ISR
Persistenz: Postgres-Volume (Inhalte), optional Volume für CMS-Uploads (`/app/media`).
---
## Repository-Struktur (Greenfield-Vorlage)
Ein Repo pro „Site-Familie“ oder Monorepo für mehrere Marken:
```
my-site/
├── apps/
│ ├── web/ # Next.js
│ │ ├── Dockerfile
│ │ ├── src/
│ │ └── next.config.ts # output: 'standalone'
│ └── cms/ # Payload
│ ├── Dockerfile
│ └── payload.config.ts
├── packages/
│ └── shared-types/ # optional: gemeinsame TS-Typen CMS ↔ Web
├── docker-compose.yml # lokaler Prod-Parität-Stack
├── docker-compose.dev.yml # nur Postgres (wie bei FlixCooks)
├── .github/workflows/
│ ├── ci.yml
│ └── gemini-pr-review.yml # optional, aus FlixCooks übernehmen
├── .cursor/
│ └── mcp.json # MCP-Server für das Team
├── .agents/
│ ├── brain.md # Architektur für KI (Pattern aus FlixCooks)
│ └── rules/AGENT.md
└── docs/
└── COOLIFY.md
```
---
## Frontend-Stack (React + Animationen)
**Kern:**
- **Next.js App Router** Seiten in `app/`, Server Components für CMS-Daten, Client Components nur für Animation/Interaktion
- **TypeScript** strikt; Typen aus Payload generieren (`payload generate:types`)
- **Styling** CSS Modules oder Tailwind (nur wenn du es willst; FlixCooks bleibt bei Vanilla CSS für neue React-Sites ist Tailwind optional, nicht Pflicht)
**Animation-Toolkit:**
| Tool | Einsatz |
|------|---------|
| **GSAP + ScrollTrigger** | Hero-Sequences, pinned Sections, komplexe Timelines |
| **Framer Motion** | Hover, Page-Transitions, modale UI |
| **Lenis** | Smooth Scroll (wie FlixCooks: bei Overlays `lenis.stop()`) |
| **(optional) @react-three/fiber** | 3D-Hero nur wenn nötig |
**CMS-Anbindung im Web:**
- **Build-Zeit (SSG):** `generateStaticParams` + Fetch von Payload REST für Marketing-Seiten
- **On-Demand Revalidation:** Payload-Webhook → `POST /api/revalidate?secret=...` in Next.js (Inhalt ändert sich ohne Full-Redeploy)
- **Preview:** Draft-Modus mit Payload Preview-URL + Next `draftMode()`
Env im Web-Container (Coolify):
- `CMS_URL=http://payload-service:3001` (internal hostname)
- `REVALIDATE_SECRET`, `NEXT_PUBLIC_SITE_URL`
---
## CMS-Stack (Payload auf Coolify)
**Warum Payload als Default:** Self-hosted, eine Postgres-URL, Admin unter `/admin`, Collections/Blocks für Seitenmodule, Media-Uploads, Webhooks alles containerisierbar.
**Coolify Env (CMS):**
- `DATABASE_URL` internal Postgres URL (gleiches Muster wie [docs/COOLIFY.md](docs/COOLIFY.md) Zeilen 3343)
- `PAYLOAD_SECRET` langer Zufallswert (nur Secrets, nie ins Repo)
- `NEXT_PUBLIC_SERVER_URL` öffentliche CMS-URL (für Admin-Assets)
**Erstes Deployment:** analog FlixCooks `RUN_DB_SEED` einmalig Migration/Seed, danach Flag entfernen.
**Sicherheit:** CMS-Admin nur über HTTPS; CORS auf Web-Domain beschränken; API-Keys für Preview/Revalidate nur als Secrets.
---
## Docker-Images
### Web (`apps/web/Dockerfile`) Next standalone
Mehrstufig: `node:22-alpine``npm ci``npm run build` → Runtime nur `.next/standalone` + `static` + `public`.
- `EXPOSE 3000`
- `HEALTHCHECK` auf `/api/health` (kleine Route: `{ "status": "ok" }`)
- Coolify: Port **3000**, Health Path `/api/health`
### CMS (`apps/cms/Dockerfile`)
Payload-Official-Pattern oder Node-Image mit `npm run build && npm run start`.
- `HEALTHCHECK` auf CMS-Health-Endpoint
- Volume für `/app/media` (Uploads überleben Redeploy)
### Lokales Parität-Compose
Orientierung an deinem bestehenden [docker-compose.yml](docker-compose.yml):
- `postgres` mit `healthcheck`
- `cms` `depends_on: postgres: service_healthy`
- `web` `depends_on: cms` + `DATABASE_URL` nur wenn Web eigene DB braucht (meist nicht nur CMS nutzt DB)
Entrypoint-Pattern von [docker/entrypoint.sh](docker/entrypoint.sh) übernehmen: **DB warten → Migration → dann Prozess starten**.
---
## Coolify-Workflow (End-to-End)
```mermaid
sequenceDiagram
participant Dev as Developer
participant GH as GitHub
participant GHA as GitHub Actions
participant CF as Coolify
participant Web as Next Container
participant CMS as Payload Container
Dev->>GH: push feature branch
Dev->>GH: open PR
GHA->>GHA: lint typecheck build docker
GHA-->>Dev: PR checks green
Dev->>GH: merge to main
GH->>CF: webhook deploy
CF->>CF: build CMS image
CF->>CF: build Web image
CF->>Web: rolling update
CMS->>Web: optional revalidate webhook
```
**Coolify-Konfiguration pro App:**
| Setting | Web | CMS |
|---------|-----|-----|
| Build Pack | Dockerfile | Dockerfile |
| Branch | `main` | `main` |
| Port | 3000 | 3001 |
| Health | `/api/health` | `/api/health` oder Payload-Default |
| Secrets | `REVALIDATE_SECRET`, `CMS_URL` | `DATABASE_URL`, `PAYLOAD_SECRET` |
**Checkliste Erstdeploy** (aus [docs/COOLIFY.md](docs/COOLIFY.md) übertragbar):
1. Postgres healthy, internal URL notieren
2. CMS deployen, Admin anlegen, Collections seeden
3. Web deployen mit internal `CMS_URL`
4. Domain + HTTPS (Traefik)
5. Webhook Payload → Next Revalidate testen
6. `RUN_DB_SEED` / Migration-Flags wieder aus
---
## GitHub Actions (CI vor Coolify)
**Workflow `ci.yml`** (bei jedem PR + push auf `main`):
1. **checkout**
2. **Node 22** + Cache (`apps/web`, `apps/cms`)
3. **Parallel jobs oder Matrix:**
- `npm run lint` (ESLint)
- `npm run typecheck` (`tsc --noEmit`)
- `npm run build` (Web + CMS)
4. **Docker build test** (ohne Push):
- `docker build -f apps/web/Dockerfile apps/web`
- `docker build -f apps/cms/Dockerfile apps/cms`
5. **(optional) Playwright** gegen `docker compose up` Smoke: Startseite, eine CMS-Seite, Health endpoints
6. **(optional) PR Review** bestehendes Gemini-Workflow aus FlixCooks wiederverwenden
**Branch-Schutz:** `main` nur mit grünen Required Checks mergebar.
Coolify deployt **nach** Merge CI blockiert kaputte Images, Coolify baut das echte Production-Image (oder du pushst zu GHCR für den Start reicht Coolify-eigener Build).
---
## KI-gestützte Entwicklung mit MCP
**Cursor `mcp.json` (Team-Standard):**
| MCP | Zweck |
|-----|--------|
| **GitHub** | Issues, PRs, Actions-Logs aus dem Chat |
| **Context7** (oder Fetch) | Aktuelle Next.js / Payload / GSAP-Docs |
| **Postgres** (optional, nur Dev) | Content/Debugging nie Production-Credentials im Repo |
| **Filesystem** | Standard in Cursor |
**Projekt-Wissen für Agenten** (aus FlixCooks übernehmen):
- [.agents/brain.md](.agents/brain.md) Architektur, Env-Regeln, Coolify-Hosts
- [.agents/rules/AGENT.md](.agents/rules/AGENT.md) Commit/PR/TODO-Konventionen
- `docs/COOLIFY.md` Deploy-Runbook pro Projekt
**Typischer KI-Workflow:**
1. Ticket/Issue in GitHub (MCP)
2. Feature-Branch; Agent ändert `apps/web` + Payload-Collection
3. `docker compose up` lokal; Agent nutzt Health-URLs
4. PR → CI grün → Gemini-Review optional
5. Merge → Coolify
---
## Entwickler-Alltag (Kurzablauf)
1. `cp .env.example .env` lokale URLs
2. `docker compose -f docker-compose.dev.yml up -d` (nur Postgres) **oder** volles `docker compose up`
3. `npm run dev` in `apps/web` und `apps/cms` (schneller Hot Reload) **oder** alles in Containern
4. In Payload Inhalte pflegen → Webhook triggert Revalidate
5. `git push` → PR → CI → merge → Coolify rebuild
---
## Bezug zu FlixCooks (dieses Repo)
FlixCooks ist heute **Vanilla PHP + Postgres + eingebautes `admin.php`-CMS** kein React. Das ist ein **paralleler Stack**, kein Widerspruch:
| Aspekt | FlixCooks (aktuell) | Neuer React-Stack |
|--------|---------------------|-------------------|
| Frontend | PHP-Templates | Next.js + React |
| CMS | `admin.php` | Payload (Container) |
| DB | Postgres | Postgres |
| Deploy | [Dockerfile](Dockerfile) + [COOLIFY.md](docs/COOLIFY.md) | gleiches Muster, zwei App-Services |
| Animationen | Lenis, Vanilla CSS | Lenis + GSAP + Framer |
Du kannst FlixCooks auf Coolify weiterbetreiben und **neue Projekte** im Monorepo-Template starten. Eine spätere Migration FlixCooks → Next wäre ein separates Projekt (Content-Export aus Postgres/JSON → Payload-Collections).
---
## Nächste konkrete Schritte (nach Plan-Freigabe)
1. **Greenfield-Repo** aus der Struktur oben scaffolden (oder `create-payload-app` + Next in Monorepo).
2. **Minimale Collections** in Payload: `pages`, `recipes` (oder `projects`), `siteSettings`, Media.
3. **Dockerfiles + compose** + `docs/COOLIFY.md` vom FlixCooks-Muster kopieren/adaptieren.
4. **GitHub Actions `ci.yml`** anlegen.
5. **Coolify:** Postgres → CMS → Web → Webhook Revalidate testen.
6. **`.cursor/mcp.json` + `.agents/brain.md`** für das neue Repo.
Wenn du willst, kann im nächsten Schritt ein **konkretes Starter-Repo** (Dateien + minimale Hero-Animation + eine Payload-Collection) direkt in einem neuen Ordner oder Branch angelegt werden.
-1
View File
@@ -9,4 +9,3 @@ DATABASE_URL="postgresql://flixcooks:flixcooks_dev@127.0.0.1:5432/flixcooks_dev"
# DATABASE_URL="postgresql://user:pass@postgresql-service:5432/flixcooks"
# FLIXCOOKS_ADMIN_KEY="langes-zufaelliges-passwort"
# RUN_DB_SEED="true" # nur beim allerersten Deploy: Rezepte + Site-Daten in Postgres seeden
# APP_PORT=8080
-27
View File
@@ -1,27 +0,0 @@
name: "Gemini AI PR Review"
on:
pull_request:
types: [opened, synchronize, reopened]
permissions:
pull-requests: write
contents: read
jobs:
review:
name: "Code Review"
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Gemini AI Review
uses: petarzarkov/gemini-code-review-action@v1.0.4 # Pinned version for stability
env:
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
model: gemini-2.5-flash-lite # Cheapest Gemini model - $0.10/1M input, $0.40/1M output
# You can add custom prompt instructions here if needed:
# prompt: "You are a senior software engineer. Review this PR..."
-97
View File
@@ -1,97 +0,0 @@
# 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.
- `config.php`: Environment-independent configuration loader which reads runtime secrets from `.env`.
- `assets/fc-local.js`: Browser-side storage for favorites and dietary goals.
- `scripts/db-seed.php`: One-time import of `data/recipes.json` into Postgres.
- `scripts/seed-data.php`: One-time seed data for recipes, imprint, and privacy 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`.
2. Beim ersten Request: Tabellen anlegen (`ensure_recipe_schema()`).
3. `php scripts/db-seed.php` einmalig ausführen → Rezepte aus `data/recipes.json` werden in Postgres geschrieben.
3. `php scripts/db-seed.php` einmalig ausführen → Rezepte und Site-Daten werden in Postgres geschrieben.
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.
**Einmalig Daten laden:** `php scripts/db-seed.php` (importiert `data/recipes.json`).
**Einmalig Daten laden:** `php scripts/db-seed.php` (aus `scripts/seed-data.php`, ohne JSON-Dateien).
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.
+10
View File
@@ -196,6 +196,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'save_
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delete') {
if (!hash_equals($_SESSION['csrf_token'] ?? '', $_POST['token'] ?? '')) {
$errors[] = 'Invalid form token, please retry.';
} else {
$slugDel = trim($_POST['slug'] ?? '');
if ($slugDel !== '' && delete_recipe($slugDel)) {
$message = 'Recipe deleted.';
@@ -205,7 +208,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delet
}
$editing = null;
$editIndex = null;
}
} elseif ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'save') {
if (!hash_equals($_SESSION['csrf_token'] ?? '', $_POST['token'] ?? '')) {
$errors[] = 'Invalid form token, please retry.';
} else {
// helpers
$parse_csv = fn($text) => array_values(array_filter(array_map('trim', explode(',', $text ?? '')), 'strlen'));
$parse_lines = fn($text) => array_values(array_filter(array_map('trim', preg_split('/\\r?\\n/', $text ?? '')), 'strlen'));
@@ -344,6 +351,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delet
$message = 'Could not save to database.';
}
}
} // end CSRF else
}
?>
<!doctype html>
@@ -527,6 +535,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delet
<a class="button" href="/admin.php?edit=<?php echo urlencode($r['slug']); ?>" style="padding:8px 12px;">Edit</a>
<form method="post" style="margin:0;" onsubmit="return confirm('Delete this recipe?');">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="token" value="<?php echo e($_SESSION['csrf_token']); ?>">
<input type="hidden" name="slug" value="<?php echo e($r['slug']); ?>">
<button type="submit" class="button" style="padding:8px 12px;">Delete</button>
</form>
@@ -538,6 +547,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delet
<form class="admin-form" method="post">
<input type="hidden" name="action" value="save">
<input type="hidden" name="token" value="<?php echo e($_SESSION['csrf_token']); ?>">
<?php if ($editing): ?>
<input type="hidden" name="slug_original" value="<?php echo e($editing['slug']); ?>">
<?php endif; ?>
-264
View File
@@ -1,264 +0,0 @@
[
{
"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": []
}
}
}
]
+3 -3
View File
@@ -2,17 +2,17 @@
#
# docker compose build
# docker compose up -d
# curl http://127.0.0.1:${APP_Port}/health.php
# curl http://127.0.0.1:8080/health.php
services:
web:
build: .
restart: unless-stopped
ports:
- "${APP_Port:-8080}:80"
- "8080:80"
environment:
DATABASE_URL: postgresql://flixcooks:flixcooks_prod@postgres:5432/flixcooks
FLIXCOOKS_ADMIN_KEY: ${FLIXCOOKS_ADMIN_KEY:-change-me-in-production}
FLIXCOOKS_ADMIN_KEY: ${FLIXCOOKS_ADMIN_KEY:-}
depends_on:
postgres:
condition: service_healthy
-43
View File
@@ -1,43 +0,0 @@
<?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";