98 lines
7.5 KiB
Markdown
98 lines
7.5 KiB
Markdown
# 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`.
|