Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1122acd5a5 |
+1
-1
@@ -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
@@ -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 33–43)
|
||||
- `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.
|
||||
@@ -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
|
||||
@@ -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..."
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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; ?>
|
||||
|
||||
@@ -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 5–10 minutes until smooth.",
|
||||
"Roll the dough to 1–2 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 2–3 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 5–10 Minuten kneten, bis der Teig glatt ist.",
|
||||
"Teig auf 1–2 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 2–3 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 isn’t 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
@@ -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
|
||||
|
||||
@@ -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";
|
||||
Reference in New Issue
Block a user