7.5 KiB
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.
# 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:
require __DIR__ . '/helpers.php'(which itself requiresconfig.phpand starts the session).- Call
load_recipes()/load_site_settings()inside atry/catch (DatabaseUnavailableException $e)that callshandle_database_unavailable($e)on failure. - Resolve
$langfrom?lang=de|en(defaulten) and build a$copy/$tarray of inline translation strings for that page. - Render HTML directly (
partials/head.php→partials/header.php→ page body →partials/footer.php), reading fromhelpers.phpdata 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()), exposesget_db_connection()(PDO, memoized in astaticvar) andDatabaseUnavailableException.helpers.php: everything else — schema bootstrap (ensure_recipe_schema()appliesscripts/schema.sqlidempotently 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 byadmin.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 overdefault_site_settings().localize_recipe()/localize_recipes()— flatten a recipe'si18n[lang]block onto the top level for the current request's language.e()— the only HTML-escaping helper (htmlspecialcharswrapper); always use it when echoing user- or DB-sourced strings.
Recipe data shape
A recipe row + its children hydrate into:
[
'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.phpfile. assets/fc-local.jsis the only standalone JS file — client-sidelocalStoragefor favorites and dietary-goal personalization (no backend user accounts on the public site).- Scroll-reveal: elements tagged
.reveal-targetare animated in via a sharedIntersectionObserverpattern repeated per-page (see bottom ofindex.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 inrecipe.php). - Styling is one large
assets/style.cssusing CSS custom properties,clamp()-based fluid spacing/typography, and a 24-column asymmetric grid (FloemaLayoutGrid). Full visual language (palette options, motion specs, named components likeCapitoliumRevealButton,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()inpartials/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/...orissue-#...), 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
mainin, resolve manually, never force-push over others' work. - The
close_featureskill (.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.