Remove seed step: migrate all content to Postgres-only backend #13
+7
-7
@@ -30,8 +30,8 @@ Dieses Dokument enthält den aktuellen Entwicklungsstand und detaillierte Aufgab
|
||||
- Mikro-Animationen & Quick-Save Funktion
|
||||
- [x] **Phase 6: PostgreSQL Datenbank**
|
||||
- `DATABASE_URL` in `.env`, Docker Compose für lokale Dev-DB
|
||||
- `init_db()` / `save_recipes()` in `helpers.php`
|
||||
- Fallback auf `data/recipes.json`
|
||||
- `init_db()` / `save_recipe()` / `save_site_settings()` in `helpers.php`
|
||||
- Kein JSON-Fallback; Rezepte und Site-Daten liegen in Postgres
|
||||
|
||||
---
|
||||
|
||||
@@ -82,7 +82,7 @@ Favoriten, Ernährungsziel und Newsletter ohne Cloud-Backend.
|
||||
Jedes Rezept erhält präzise Makronährstoffe (Kalorien, Proteine, Kohlenhydrate, Fette), die vom Admin gepflegt und dem Nutzer edel präsentiert werden.
|
||||
|
||||
- [x] **Datenmodell & Admin-Panel Erweiterung**
|
||||
- [x] `data/recipes.json` Struktur anpassen: Hinzufügen von `nutrition` (`calories`, `protein`, `carbs`, `fat`)
|
||||
- [x] Rezeptstruktur um `nutrition` (`calories`, `protein`, `carbs`, `fat`) erweitern
|
||||
- [x] `admin.php` Formular erweitern:
|
||||
- [x] Eingabefeld für Kalorien (kcal)
|
||||
- [x] Eingabefeld für Eiweiß / Protein (g)
|
||||
@@ -101,7 +101,7 @@ Jedes Rezept erhält präzise Makronährstoffe (Kalorien, Proteine, Kohlenhydrat
|
||||
Ein immersiver Kochmodus, der Anwendern Schritt-für-Schritt durch die Zubereitung führt – inklusive Videoanleitungen und integrierten Timern.
|
||||
|
||||
- [x] **Video-Verknüpfung im Admin-Panel**
|
||||
- [x] Rezeptstruktur in `data/recipes.json` erweitern, damit jeder Arbeitsschritt (`steps`) eine optionale `video_url` (externe MP4-URL) besitzen kann
|
||||
- [x] Rezeptstruktur erweitern, damit jeder Arbeitsschritt (`steps`) eine optionale `video_url` (externe MP4-URL) besitzen kann
|
||||
- [x] `admin.php` erweitern, um Video-URLs pro Einzelschritt einzugeben
|
||||
- [x] **Fullscreen Cooking-Mode Overlay**
|
||||
- [x] Trigger-Button "Kochmodus starten" (`CapitoliumRevealButton` Stil) auf `recipe.php` einbauen
|
||||
@@ -134,13 +134,13 @@ Ein hochgradig interaktives, touch-freundliches Karussell direkt auf der Startse
|
||||
---
|
||||
|
||||
### 🔥 Phase 6: PostgreSQL Datenbank
|
||||
Rezepte in Postgres (JSONB), optional lokal per Docker.
|
||||
Rezepte und Site-Daten in normalisierten Postgres-Tabellen, optional lokal per Docker.
|
||||
|
||||
- [x] **`DATABASE_URL` & Docker Compose**
|
||||
- [x] `docker-compose.dev.yml` für lokale Postgres-Instanz
|
||||
- [x] `init_db()` legt Tabelle `recipes` an und seedet aus `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_recipes()` mit JSON-Fallback
|
||||
- [x] `load_recipes()` / `save_recipe()` ohne JSON-Fallback
|
||||
- [x] `load_recipe_by_slug()` in `recipe.php`
|
||||
|
||||
---
|
||||
|
||||
+7
-7
@@ -4,8 +4,8 @@ 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()` in `helpers.php`. No runtime JSON recipe file. One-time import: `php scripts/db-seed.php` from `data/recipes.json`.
|
||||
- **Site settings (legal pages):** Flat-file `data/site.json` via `load_site_settings()` / `save_site_settings()` — not in Postgres.
|
||||
- **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`.
|
||||
- **Config:** `config.php` loads `.env`, `get_db_connection()`. Never commit `.env` (see `.gitignore`).
|
||||
@@ -27,13 +27,14 @@ This document summarizes architectural knowledge, conventions, and learnings for
|
||||
## 5. PostgreSQL — Schema & Data Flow
|
||||
|
||||
### Relational schema (`scripts/schema.sql`)
|
||||
Created by `ensure_recipe_schema()` on first DB use. Legacy JSONB `recipes.data` is migrated once automatically.
|
||||
Created by `ensure_recipe_schema()` on first DB use. Legacy JSONB/file fallbacks are not supported.
|
||||
|
||||
| Table | Role |
|
||||
|-------|------|
|
||||
| `recipes` | slug, hero, times, servings, nutrition columns, featured, coming_soon |
|
||||
| `recipe_translations` | title, description, category, difficulty (en/de) |
|
||||
| `recipe_tags`, `recipe_ingredients`, `recipe_utensils`, `recipe_steps` | Ordered lists per language |
|
||||
| `site_settings` | imprint/privacy fields per language |
|
||||
|
||||
PHP still exposes the same nested arrays (`i18n`, `nutrition`, …) via `hydrate_recipes_from_db()`.
|
||||
|
||||
@@ -41,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` (not read at runtime).
|
||||
4. One-time import: `php scripts/db-seed.php` from `scripts/seed-data.php`.
|
||||
|
||||
### `config.php` functions
|
||||
- `load_env()` — parses `.env`.
|
||||
@@ -123,9 +124,8 @@ See `.agents/TODO.md` (e.g. PWA & offline support). README `Local Postgres (Dock
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `config.php` | `.env`, Firebase config, PDO |
|
||||
| `helpers.php` | `init_db`, `load_recipes`, `save_recipes`, site settings |
|
||||
| `data/recipes.json` | Seed + fallback + admin backup |
|
||||
| `data/site.json` | Imprint/privacy settings |
|
||||
| `helpers.php` | `init_db`, `load_recipes`, `save_recipe`, site settings |
|
||||
| `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()` |
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"version": "0.0.1",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Dev DB (Postgres only)",
|
||||
"runtimeExecutable": "docker",
|
||||
"runtimeArgs": ["compose", "-f", "docker-compose.dev.yml", "up"],
|
||||
"port": 5432
|
||||
},
|
||||
{
|
||||
"name": "Full stack (PHP app + Postgres)",
|
||||
"runtimeExecutable": "docker",
|
||||
"runtimeArgs": ["compose", "up", "--build"],
|
||||
"port": 8080
|
||||
}
|
||||
]
|
||||
}
|
||||
+1
-1
@@ -8,4 +8,4 @@ DATABASE_URL="postgresql://flixcooks:flixcooks_dev@127.0.0.1:5432/flixcooks_dev"
|
||||
# --- Production / Coolify (im Dashboard setzen, nicht committen) ---
|
||||
# DATABASE_URL="postgresql://user:pass@postgresql-service:5432/flixcooks"
|
||||
# FLIXCOOKS_ADMIN_KEY="langes-zufaelliges-passwort"
|
||||
# RUN_DB_SEED="true" # nur beim allerersten Deploy, danach entfernen
|
||||
# RUN_DB_SEED="true" # nur beim allerersten Deploy: Rezepte + Site-Daten in Postgres seeden
|
||||
|
||||
+1
-3
@@ -24,9 +24,7 @@ WORKDIR /var/www/html
|
||||
COPY --chown=www-data:www-data . /var/www/html
|
||||
|
||||
RUN sed -i 's/\r$//' /var/www/html/docker/entrypoint.sh \
|
||||
&& chmod +x /var/www/html/docker/entrypoint.sh \
|
||||
&& mkdir -p /var/www/html/data \
|
||||
&& chown -R www-data:www-data /var/www/html/data
|
||||
&& chmod +x /var/www/html/docker/entrypoint.sh
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
|
||||
@@ -16,11 +16,11 @@ The project is architected to remain extremely lightweight and fast, intentional
|
||||
- `login.php`: Local profile page for dietary goals and saved favorites (browser storage).
|
||||
- **Support & Layouts**:
|
||||
- `partials/`: Contains modular templates (`head.php`, `header.php`, `footer.php`) to maintain a clean DRY structure.
|
||||
- `helpers.php`: Core PHP utilities and the PostgreSQL data access layer for recipes.
|
||||
- `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.
|
||||
- `data/recipes.json`: Optional seed file only (`php scripts/db-seed.php`), not used at runtime.
|
||||
- `scripts/schema.sql`: Relational table definitions for recipes.
|
||||
- `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.
|
||||
|
||||
---
|
||||
|
||||
@@ -37,7 +37,7 @@ FlixCooks uses a modern, carefully curated vanilla tech-stack focused on lightni
|
||||
|
||||
### ⚙️ Backend & Data
|
||||
- **Engine**: Vanilla PHP.
|
||||
- **Database**: PostgreSQL only. `DATABASE_URL` is required; without a working DB connection the site returns HTTP 503.
|
||||
- **Database**: PostgreSQL only. `DATABASE_URL` is required; without a working external DB connection the site returns HTTP 503.
|
||||
- **Environment**: Custom `.env` variable parser integrated into PHP bootstrap.
|
||||
|
||||
---
|
||||
@@ -58,7 +58,7 @@ Make sure you have the following installed on your local machine:
|
||||
cp .env.example .env
|
||||
```
|
||||
2. Set **`DATABASE_URL`** in `.env` (required). See [Local Postgres (Docker)](#local-postgres-docker) below.
|
||||
3. Seed recipes once: `php scripts/db-seed.php` (imports `data/recipes.json` into SQL tables).
|
||||
3. Seed the external database once: `php scripts/db-seed.php` (imports recipes and legal/site settings into SQL tables).
|
||||
|
||||
### 2. Local Postgres (Docker)
|
||||
|
||||
@@ -97,7 +97,8 @@ Nützliche SQL-Befehle in `psql`:
|
||||
\dt -- alle Tabellen
|
||||
\d recipes -- Spalten der Tabelle recipes
|
||||
SELECT slug, created_at FROM recipes;
|
||||
SELECT slug, data->>'title' AS title FROM recipes, jsonb_to_record(data) AS x(title text); -- optional
|
||||
SELECT recipe_slug, title FROM recipe_translations WHERE lang = 'en';
|
||||
SELECT section, lang, setting_key FROM site_settings ORDER BY section, lang, setting_key;
|
||||
\q -- beenden
|
||||
```
|
||||
|
||||
@@ -135,7 +136,7 @@ If you prefer running a full local stack:
|
||||
|
||||
## 🗄️ Postgres in diesem Projekt (Kurzüberblick)
|
||||
|
||||
Rezepte liegen in **normalisierten SQL-Tabellen** (kein JSONB-Blob, kein Laufzeit-Fallback auf Dateien):
|
||||
Rezepte und Site-Daten liegen in **normalisierten SQL-Tabellen**. Es gibt keinen JSONB-Blob, keinen Datei-Fallback und kein Laden von `data/*.json` zur Laufzeit.
|
||||
|
||||
| Tabelle | Inhalt |
|
||||
|---------|--------|
|
||||
@@ -145,19 +146,21 @@ Rezepte liegen in **normalisierten SQL-Tabellen** (kein JSONB-Blob, kein Laufzei
|
||||
| `recipe_ingredients` | Zutatenzeilen |
|
||||
| `recipe_utensils` | Werkzeugzeilen |
|
||||
| `recipe_steps` | Schritte inkl. Video-URL und Timer |
|
||||
| `site_settings` | Impressum- und Datenschutzfelder pro Sprache |
|
||||
|
||||
Schema: `scripts/schema.sql`. PHP baut daraus dieselben Arrays wie früher (`i18n.en`, `nutrition`, …), damit Templates unverändert bleiben.
|
||||
|
||||
**Ablauf:**
|
||||
|
||||
1. `DATABASE_URL` in `.env` → Verbindung über `config.php`.
|
||||
2. Beim ersten Request: Tabellen anlegen (`ensure_recipe_schema()`). Alte JSONB-Tabelle wird einmalig migriert.
|
||||
3. `load_recipes()` liest per SQL; ohne DB → HTTP 503 (`maintenance/db-unavailable.php`).
|
||||
4. Admin: `save_recipe()` / `delete_recipe()` – direkt in die Tabellen.
|
||||
2. Beim ersten Request: Tabellen anlegen (`ensure_recipe_schema()`).
|
||||
3. `php scripts/db-seed.php` einmalig ausführen → Rezepte und Site-Daten werden in Postgres geschrieben.
|
||||
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` (aus `data/recipes.json`).
|
||||
**Einmalig Daten laden:** `php scripts/db-seed.php` (aus `scripts/seed-data.php`, ohne JSON-Dateien).
|
||||
|
||||
Für **Staging/Production** nur `DATABASE_URL` in der Hosting-Umgebung setzen – nie Production-Daten in der lokalen Dev-DB mischen.
|
||||
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.
|
||||
|
||||
### Docker / Coolify
|
||||
|
||||
|
||||
@@ -78,10 +78,10 @@ if (!$authed) {
|
||||
|
||||
try {
|
||||
$allRecipes = load_recipes();
|
||||
$siteSettings = load_site_settings();
|
||||
} catch (DatabaseUnavailableException $e) {
|
||||
handle_database_unavailable($e);
|
||||
}
|
||||
$siteSettings = load_site_settings();
|
||||
$message = null;
|
||||
$errors = [];
|
||||
$editing = null;
|
||||
@@ -186,9 +186,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'save_
|
||||
$siteSettings['imprint'] = $imprint;
|
||||
$siteSettings['privacy'] = $privacy;
|
||||
if (save_site_settings($siteSettings)) {
|
||||
$siteSettings = load_site_settings(true);
|
||||
$message = 'Settings saved.';
|
||||
} else {
|
||||
$errors[] = 'Could not save settings. Check permissions on data/site.json.';
|
||||
$errors[] = 'Could not save settings. Check the database connection.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,54 +0,0 @@
|
||||
{
|
||||
"imprint": {
|
||||
"de": {
|
||||
"owner_name": "Felix Brockers",
|
||||
"address": "N.A",
|
||||
"email": "contact@flixcooks.at",
|
||||
"phone": "N.A",
|
||||
"legal_form": "Kleines Impressum; kein Firmenbucheintrag/UID hinterlegt.",
|
||||
"business_purpose": "Foodblog & Rezeptmarketing.",
|
||||
"wko_membership": "",
|
||||
"authority": "",
|
||||
"uid": "",
|
||||
"odr": "https://consumer-redress.ec.europa.eu/",
|
||||
"last_updated": "2026-03-23"
|
||||
},
|
||||
"en": {
|
||||
"owner_name": "Felix Brockers",
|
||||
"address": "N.A",
|
||||
"email": "contact@flixcooks.at",
|
||||
"phone": "N.A",
|
||||
"legal_form": "Small website notice; no commercial register/UID provided.",
|
||||
"business_purpose": "Food blog & recipe marketing.",
|
||||
"wko_membership": "",
|
||||
"authority": "",
|
||||
"uid": "",
|
||||
"odr": "https://consumer-redress.ec.europa.eu/",
|
||||
"last_updated": "2026-03-23"
|
||||
}
|
||||
},
|
||||
"privacy": {
|
||||
"de": {
|
||||
"controller": "Felix Brockers",
|
||||
"contact_email": "contact@flixcooks.at",
|
||||
"contact_phone": "N.A",
|
||||
"address": "N.A",
|
||||
"hosting_provider": "Hetzner Online GmbH, Industriestr. 25, 91710 Gunzenhausen, Deutschland",
|
||||
"log_retention_days": 30,
|
||||
"cookie_statement": "Keine Tracking- oder Marketing-Cookies; nur technisch notwendige.",
|
||||
"purposes": "Betrieb und Bereitstellung der Website, Beantwortung von Kontaktanfragen.",
|
||||
"last_updated": "2026-03-23"
|
||||
},
|
||||
"en": {
|
||||
"controller": "Felix Brockers",
|
||||
"contact_email": "contact@flixcooks.at",
|
||||
"contact_phone": "N.A",
|
||||
"address": "N.A",
|
||||
"hosting_provider": "Hetzner Online GmbH, Industriestr. 25, 91710 Gunzenhausen, Germany",
|
||||
"log_retention_days": 30,
|
||||
"cookie_statement": "No tracking or marketing cookies; only technically necessary cookies.",
|
||||
"purposes": "Operating and providing the website, responding to contact requests.",
|
||||
"last_updated": "2026-03-23"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,11 @@
|
||||
require __DIR__ . '/helpers.php';
|
||||
|
||||
$lang = strtolower((string)(filter_input(INPUT_GET, 'lang', FILTER_SANITIZE_FULL_SPECIAL_CHARS) ?: 'en')) === 'de' ? 'de' : 'en';
|
||||
try {
|
||||
$settings = load_site_settings();
|
||||
} catch (DatabaseUnavailableException $e) {
|
||||
handle_database_unavailable($e);
|
||||
}
|
||||
$privacy = $settings['privacy'][$lang] ?? ($settings['privacy']['en'] ?? []);
|
||||
|
||||
$langSwitchLabel = $lang === 'de' ? 'DE' : 'EN';
|
||||
|
||||
+2
-8
@@ -3,28 +3,23 @@
|
||||
# docker compose build
|
||||
# docker compose up -d
|
||||
# curl http://127.0.0.1:8080/health.php
|
||||
#
|
||||
# Erstes Deployment mit Seed:
|
||||
# RUN_DB_SEED=true docker compose up -d
|
||||
|
||||
services:
|
||||
web:
|
||||
build: .
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8080:80"
|
||||
environment:
|
||||
DATABASE_URL: postgresql://flixcooks:flixcooks_prod@postgres:5432/flixcooks
|
||||
FLIXCOOKS_ADMIN_KEY: ${FLIXCOOKS_ADMIN_KEY:-change-me-in-production}
|
||||
RUN_DB_SEED: ${RUN_DB_SEED:-false}
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
# Impressum/Datenschutz-Einstellungen persistent halten
|
||||
- site_data:/var/www/html/data
|
||||
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: flixcooks
|
||||
POSTGRES_PASSWORD: flixcooks_prod
|
||||
@@ -39,4 +34,3 @@ services:
|
||||
|
||||
volumes:
|
||||
pg_data:
|
||||
site_data:
|
||||
|
||||
@@ -19,10 +19,5 @@ done
|
||||
echo "[flixcooks] Applying schema..."
|
||||
php -r "require 'helpers.php'; require_database(); echo \"schema ok\n\";"
|
||||
|
||||
if [ "${RUN_DB_SEED:-false}" = "true" ]; then
|
||||
echo "[flixcooks] Seeding recipes from data/recipes.json..."
|
||||
php scripts/db-seed.php
|
||||
fi
|
||||
|
||||
echo "[flixcooks] Starting Apache..."
|
||||
exec "$@"
|
||||
|
||||
+11
-33
@@ -12,13 +12,12 @@ Zwei getrennte Ressourcen: **PostgreSQL** und **PHP-Web-App** (dieses Repo als D
|
||||
│ │ (Service B) │ :5432 │ Apache + PHP 8.3 │ │
|
||||
│ └──────────────┘ │ Port 80 → Traefik/HTTPS │ │
|
||||
│ ▲ └─────────────────────────┘ │
|
||||
│ │ ▲ │
|
||||
│ Volume (Daten) Volume optional: │
|
||||
│ data/ (site.json) │
|
||||
│ │ │
|
||||
│ Volume (Daten) │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Die App startet **nicht**, wenn `DATABASE_URL` fehlt oder Postgres nicht erreichbar ist.
|
||||
Die App startet **nicht**, wenn `DATABASE_URL` fehlt oder Postgres nicht erreichbar ist. Rezepte, Impressum und Datenschutz liegen komplett in Postgres.
|
||||
|
||||
---
|
||||
|
||||
@@ -66,24 +65,12 @@ postgresql://flixcooks:geheim@postgresql-flixcooks:5432/flixcooks
|
||||
|
||||
|
||||
| Variable | Default | Beschreibung |
|
||||
| ------------------- | ------- | ------------------------------------------------------------------ |
|
||||
| `RUN_DB_SEED` | `false` | Einmalig `true` setzen → importiert `data/recipes.json` beim Start |
|
||||
| ------------------- | ------- | ---------------------------------------------------------------------------- |
|
||||
| `DB_WAIT_MAX_TRIES` | `30` | Warteversuche bis Postgres da ist (à 2 s) |
|
||||
|
||||
### Persistent Storage
|
||||
|
||||
Nach dem ersten erfolgreichen Deploy: `RUN_DB_SEED` wieder auf `false` oder entfernen.
|
||||
|
||||
### Persistent Storage (empfohlen)
|
||||
|
||||
Mount für Impressum/Datenschutz (`data/site.json`):
|
||||
|
||||
|
||||
| Mount Path (Container) | Inhalt |
|
||||
| ---------------------- | ----------------------------------------- |
|
||||
| `/var/www/html/data` | `site.json` bleibt nach Redeploy erhalten |
|
||||
|
||||
|
||||
Rezepte liegen in Postgres – **kein** Volume für Rezepte nötig.
|
||||
Nur PostgreSQL benötigt persistenten Speicher. Für die Web-App selbst ist kein `/var/www/html/data`-Volume mehr nötig, weil Rezepte und Site-Settings in Postgres liegen.
|
||||
|
||||
---
|
||||
|
||||
@@ -91,28 +78,21 @@ Rezepte liegen in Postgres – **kein** Volume für Rezepte nötig.
|
||||
|
||||
1. Postgres-Service läuft (healthy).
|
||||
2. App mit `DATABASE_URL` + `FLIXCOOKS_ADMIN_KEY` deployen.
|
||||
3. Einmalig `RUN_DB_SEED=true` → Redeploy → Rezepte prüfen auf der Startseite.
|
||||
4. `RUN_DB_SEED` deaktivieren.
|
||||
5. `https://deine-domain/admin.php` testen.
|
||||
6. `https://deine-domain/health.php` → `{"status":"ok"}`.
|
||||
3. `https://deine-domain/health.php` → `{"status":"ok"}`.
|
||||
4. `https://deine-domain/admin.php` → Rezepte anlegen, Impressum/Datenschutz unter „Site settings" befüllen.
|
||||
|
||||
### Schema ohne Seed
|
||||
|
||||
Tabellen legt der Container beim Start automatisch an (`scripts/schema.sql` via `require_database()`). Ohne Seed ist die DB leer → Seite lädt, aber keine Rezepte, bis du im Admin anlegst oder seedest.
|
||||
Tabellen legt der Container beim Start automatisch an (`scripts/schema.sql` via `require_database()`). Eine leere DB ist kein Fehler — die Site startet mit Platzhalter-Impressum/Datenschutz und zeigt eine leere Rezeptliste. Inhalte werden ausschließlich über `/admin.php` gepflegt.
|
||||
|
||||
---
|
||||
|
||||
## 4. Lokaler Test vor Coolify
|
||||
|
||||
```bash
|
||||
# Starkes Admin-Passwort setzen
|
||||
export FLIXCOOKS_ADMIN_KEY="dein-geheimes-passwort"
|
||||
|
||||
# Mit Seed
|
||||
export RUN_DB_SEED=true
|
||||
docker compose build
|
||||
docker compose up -d
|
||||
|
||||
curl http://127.0.0.1:8080/health.php
|
||||
```
|
||||
|
||||
@@ -122,7 +102,6 @@ curl http://127.0.0.1:8080/health.php
|
||||
|
||||
- Neues Image bauen lassen (Git push → Coolify rebuild).
|
||||
- Postgres-Volume bleibt → Daten bleiben.
|
||||
- `data/`-Volume bleibt → Site-Settings bleiben.
|
||||
- Kein manuelles `db-seed` bei Updates, außer du leerst die DB bewusst.
|
||||
|
||||
---
|
||||
@@ -139,11 +118,10 @@ curl http://127.0.0.1:8080/health.php
|
||||
|
||||
|
||||
| Problem | Lösung |
|
||||
| -------------------------------- | --------------------------------------------------------------- |
|
||||
| ------------------------------- | --------------------------------------------------------------- |
|
||||
| Container startet nicht | Logs: DB nicht erreichbar → `DATABASE_URL` Host/Passwort prüfen |
|
||||
| 503 „Datenbank nicht verfügbar“ | Gleiches Netzwerk in Coolify? Internal URL? |
|
||||
| Leere Seite, Health OK | `RUN_DB_SEED=true` einmalig oder Admin-Rezepte anlegen |
|
||||
| Leere Seite, Health OK | Rezepte über `/admin.php` anlegen, Site-Settings befüllen |
|
||||
| Admin geht nicht | `FLIXCOOKS_ADMIN_KEY` gesetzt? |
|
||||
| `site.json` verloren nach Deploy | Volume auf `/var/www/html/data` mounten |
|
||||
|
||||
|
||||
|
||||
+60
-65
@@ -69,61 +69,12 @@ function recipe_base_from_row(array $row): array {
|
||||
];
|
||||
}
|
||||
|
||||
function decode_legacy_jsonb_value($value): ?array {
|
||||
if (is_array($value)) {
|
||||
return $value;
|
||||
}
|
||||
if (is_string($value)) {
|
||||
$decoded = json_decode($value, true);
|
||||
return is_array($decoded) ? $decoded : null;
|
||||
}
|
||||
if ($value !== null) {
|
||||
$decoded = json_decode(json_encode($value), true);
|
||||
return is_array($decoded) ? $decoded : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function migrate_legacy_jsonb_storage(PDO $pdo): void {
|
||||
if (!db_table_has_column($pdo, 'recipes', 'data')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$legacy = [];
|
||||
$stmt = $pdo->query('SELECT slug, data FROM recipes');
|
||||
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
$recipe = decode_legacy_jsonb_value($row['data']);
|
||||
if (is_array($recipe) && !empty($recipe['slug'])) {
|
||||
$legacy[] = $recipe;
|
||||
}
|
||||
}
|
||||
|
||||
$pdo->exec('DROP TABLE IF EXISTS recipe_steps CASCADE');
|
||||
$pdo->exec('DROP TABLE IF EXISTS recipe_utensils CASCADE');
|
||||
$pdo->exec('DROP TABLE IF EXISTS recipe_ingredients CASCADE');
|
||||
$pdo->exec('DROP TABLE IF EXISTS recipe_tags CASCADE');
|
||||
$pdo->exec('DROP TABLE IF EXISTS recipe_translations CASCADE');
|
||||
$pdo->exec('DROP TABLE IF EXISTS recipes CASCADE');
|
||||
|
||||
apply_recipe_schema($pdo);
|
||||
|
||||
foreach ($legacy as $recipe) {
|
||||
save_recipe($recipe, $pdo);
|
||||
}
|
||||
}
|
||||
|
||||
function ensure_recipe_schema(PDO $pdo): void {
|
||||
static $ready = false;
|
||||
if ($ready) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (db_table_has_column($pdo, 'recipes', 'data') && !db_table_has_column($pdo, 'recipes', 'calories')) {
|
||||
migrate_legacy_jsonb_storage($pdo);
|
||||
$ready = true;
|
||||
return;
|
||||
}
|
||||
|
||||
apply_recipe_schema($pdo);
|
||||
$ready = true;
|
||||
}
|
||||
@@ -446,29 +397,73 @@ function default_site_settings(): array {
|
||||
}
|
||||
|
||||
/**
|
||||
* Load site-wide settings for legal pages, merging with defaults.
|
||||
* Load site-wide settings for legal pages from Postgres.
|
||||
*/
|
||||
function load_site_settings(): array {
|
||||
$defaults = default_site_settings();
|
||||
$path = __DIR__ . '/data/site.json';
|
||||
if (!file_exists($path)) {
|
||||
return $defaults;
|
||||
function load_site_settings(bool $refresh = false): array {
|
||||
static $cache = null;
|
||||
if ($cache !== null && !$refresh) {
|
||||
return $cache;
|
||||
}
|
||||
$raw = file_get_contents($path);
|
||||
$data = json_decode($raw, true);
|
||||
if (!is_array($data)) {
|
||||
return $defaults;
|
||||
|
||||
$settings = default_site_settings();
|
||||
$pdo = require_database();
|
||||
$stmt = $pdo->query(
|
||||
'SELECT section, lang, setting_key, setting_value FROM site_settings ORDER BY section, lang, setting_key'
|
||||
);
|
||||
|
||||
$hasRows = false;
|
||||
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
$section = $row['section'];
|
||||
$lang = $row['lang'];
|
||||
$key = $row['setting_key'];
|
||||
if (!isset($settings[$section][$lang])) {
|
||||
continue;
|
||||
}
|
||||
return array_replace_recursive($defaults, $data);
|
||||
$settings[$section][$lang][$key] = $key === 'log_retention_days'
|
||||
? (int) $row['setting_value']
|
||||
: $row['setting_value'];
|
||||
$hasRows = true;
|
||||
}
|
||||
|
||||
$cache = $settings;
|
||||
return $cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist site-wide settings for legal pages.
|
||||
* Persist site-wide settings for legal pages in Postgres.
|
||||
*/
|
||||
function save_site_settings(array $settings): bool {
|
||||
$path = __DIR__ . '/data/site.json';
|
||||
$json = json_encode($settings, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
return (bool) file_put_contents($path, $json);
|
||||
function save_site_settings(array $settings, ?PDO $pdo = null): bool {
|
||||
$pdo = $pdo ?? require_database();
|
||||
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
$pdo->exec("DELETE FROM site_settings WHERE section IN ('imprint', 'privacy')");
|
||||
|
||||
$stmt = $pdo->prepare(
|
||||
'INSERT INTO site_settings (section, lang, setting_key, setting_value, updated_at)
|
||||
VALUES (?, ?, ?, ?, NOW())
|
||||
ON CONFLICT (section, lang, setting_key) DO UPDATE SET
|
||||
setting_value = EXCLUDED.setting_value,
|
||||
updated_at = NOW()'
|
||||
);
|
||||
|
||||
foreach (['imprint', 'privacy'] as $section) {
|
||||
foreach (['de', 'en'] as $lang) {
|
||||
foreach (($settings[$section][$lang] ?? []) as $key => $value) {
|
||||
$stmt->execute([$section, $lang, $key, (string) $value]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
error_log('save_site_settings failed: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function slugify(string $text): string {
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
require __DIR__ . '/helpers.php';
|
||||
|
||||
$lang = strtolower((string)(filter_input(INPUT_GET, 'lang', FILTER_SANITIZE_FULL_SPECIAL_CHARS) ?: 'en')) === 'de' ? 'de' : 'en';
|
||||
try {
|
||||
$settings = load_site_settings();
|
||||
} catch (DatabaseUnavailableException $e) {
|
||||
handle_database_unavailable($e);
|
||||
}
|
||||
$imprint = $settings['imprint'][$lang] ?? ($settings['imprint']['en'] ?? []);
|
||||
|
||||
$langSwitchLabel = $lang === 'de' ? 'DE' : 'EN';
|
||||
|
||||
@@ -3,6 +3,7 @@ require __DIR__ . '/helpers.php';
|
||||
|
||||
try {
|
||||
$allRecipes = load_recipes();
|
||||
load_site_settings();
|
||||
} catch (DatabaseUnavailableException $e) {
|
||||
handle_database_unavailable($e);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ require __DIR__ . '/helpers.php';
|
||||
|
||||
try {
|
||||
$allRecipes = load_recipes();
|
||||
load_site_settings();
|
||||
} catch (DatabaseUnavailableException $e) {
|
||||
handle_database_unavailable($e);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ require __DIR__ . '/helpers.php';
|
||||
|
||||
try {
|
||||
$allRecipes = load_recipes();
|
||||
load_site_settings();
|
||||
} catch (DatabaseUnavailableException $e) {
|
||||
handle_database_unavailable($e);
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
@@ -68,3 +68,14 @@ CREATE TABLE IF NOT EXISTS recipe_steps (
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_recipe_steps_slug_lang ON recipe_steps (recipe_slug, lang);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS site_settings (
|
||||
section VARCHAR(32) NOT NULL CHECK (section IN ('imprint', 'privacy')),
|
||||
lang CHAR(2) NOT NULL CHECK (lang IN ('en', 'de')),
|
||||
setting_key VARCHAR(64) NOT NULL,
|
||||
setting_value TEXT NOT NULL DEFAULT '',
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
PRIMARY KEY (section, lang, setting_key)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_site_settings_section_lang ON site_settings (section, lang);
|
||||
|
||||
Reference in New Issue
Block a user