diff --git a/.gitignore b/.gitignore
index e52c598..e4cacb0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
.vscode/ftp-kr.sync.cache.json
+temp/
diff --git a/.paul/ROADMAP.md b/.paul/ROADMAP.md
index 449472b..e30daa1 100644
--- a/.paul/ROADMAP.md
+++ b/.paul/ROADMAP.md
@@ -39,6 +39,7 @@ Status: Planning
| 15 | Scontainers edit saves as new record | 1 | Done | 2026-04-18 |
| 17 | Cart summary transport cost fix | 1 | Done | 2026-04-20 |
| 18 | Google feed permutation URL fix | 1 | Done | 2026-04-30 |
+| 19 | Frontend meta tags fix (category + product) | 1 | Done | 2026-05-13 |
## Feature
@@ -132,5 +133,11 @@ Status: Planning
**Scope:** Zmienić separator z `/` na `_` w generatorze feedu (`ProductRepository::appendCombinationToXml`), rozszerzyć regex routingu o `_` (`Helpers`), dodać konwersję `_` → `|` w warstwie front (`LayoutEngine`), preselekcja wartości atrybutu w partialu na podstawie `permutation_hash` z URL. Plus unit testy regex + generator linku.
+### Phase 19 — Frontend meta tags fix
+
+**Problem:** Strony kategorii (np. `/sen-i-otulenie`) i strony produktów (np. `/kocyk-niemowlaka-szczeniak-z-balonikiem-fuksja`) renderują `
` strony głównej oraz literalne wartości `content="keywords"`/`content="description"` zamiast właściwych metatagów SEO z bazy. Niepoprawne meta blokują indeksację Google i Merchant Center.
+
+**Scope:** Diagnostyka (pp_routes + meta w DB + sesyjny $page), checkpoint:decision z 4 opcjami fixu (routes/engine/data/session), implementacja wybranej opcji w `LayoutEngine.php` lub `index.php`, test jednostkowy, human-verify na 3 URL-ach.
+
---
-*Last updated: 2026-04-30 (Phase 18 complete)*
+*Last updated: 2026-05-13 (Phase 19 complete)*
diff --git a/.paul/STATE.md b/.paul/STATE.md
index c80b6ce..0c801e8 100644
--- a/.paul/STATE.md
+++ b/.paul/STATE.md
@@ -5,20 +5,19 @@
See: .paul/PROJECT.md (updated 2026-04-30)
**Core value:** Właściciel sklepu ma pełną kontrolę nad sprzedażą online w jednym systemie pisanym od podstaw, bez narzutów zewnętrznych platform.
-**Current focus:** Phase 18 complete - loop closed
+**Current focus:** Phase 19 complete — loop closed
## Current Position
Milestone: Hotfix
-Milestone: Hotfix
-Phase: 18 of 18 (Google feed permutation URL fix) - Complete
-Plan: 18-01 complete
+Phase: 19 of 19 (Frontend meta tags fix) — Complete
+Plan: 19-01 complete
Status: UNIFY complete, ready for next PLAN loop (transition-phase git commit pending)
-Last activity: 2026-04-30 - Closed loop for .paul/phases/18-google-feed-permutation-url-fix/18-01-PLAN.md
+Last activity: 2026-05-13 — Closed loop for .paul/phases/19-frontend-meta-tags-fix/19-01-PLAN.md
Progress:
- Milestone: [##########] 100% (Hotfix rolling)
-- Phase 18: [##########] 100%
+- Phase 19: [##########] 100%
## Loop Position
@@ -45,10 +44,17 @@ Phase 15: PLAN --> APPLY --> UNIFY ✓ ✓ ✓ [COMPLETE - 2026-04-18]
Phase 16: PLAN --> APPLY --> UNIFY ✓ ✓ ✓ [COMPLETE - 2026-04-19]
Phase 17: PLAN --> APPLY --> UNIFY ✓ ✓ ✓ [COMPLETE - 2026-04-20]
Phase 18: PLAN --> APPLY --> UNIFY ✓ ✓ ✓ [COMPLETE - 2026-04-30]
+Phase 19: PLAN --> APPLY --> UNIFY ✓ ✓ ✓ [COMPLETE - 2026-05-13]
```
## Accumulated Context
### Decisions
+- 2026-05-13: Phase 19 loop closed with SUMMARY at .paul/phases/19-frontend-meta-tags-fix/19-01-SUMMARY.md
+- 2026-05-13: Transition-phase git commit for Phase 19 not executed in this UNIFY run (deferred — pattern z faz 15/16/17/18)
+- 2026-05-13: Phase 19 APPLY complete — LayoutEngine.php zmodyfikowany (3 gałęzie + nowa metoda applyEntityMetaToPage), test LayoutEngineMetaTagsTest 5 testów/18 asercji, suita 846 zielona; weryfikacja na produkcji (curl) pokazuje poprawne tytuły dla /sen-i-otulenie, /kocyk-niemowlaka-... i /
+- 2026-05-13: Phase 19 checkpoint:decision — wybrano `fix-engine-detection`. Root cause: LayoutEngine::show() w gałęziach category/article/product nadpisuje $page['language']['title'] ale NIE $page['language']['meta_title']. Wartość meta_title homepage ('Sklep z akcesoriami...') wycieka do linii 332. Dane DB klienta (literalne 'description'/'keywords' w kategorii) to oddzielny issue — admin uzupełnia.
+- 2026-05-13: Created Phase 19 plan at .paul/phases/19-frontend-meta-tags-fix/19-01-PLAN.md — fix metatagów // dla kategorii i produktu (homepage tytuł wycieka na wszystkie podstrony)
+- 2026-05-13: Phase 19 — najpierw diagnostyka (pp_routes + DB meta + session $page), checkpoint:decision, potem fix; ustalanie root cause przed implementacją (3 hipotezy: pp_routes destination bez category=/product=, sesyjny $page bleed, lub literalne zaślepki w DB)
- 2026-04-30: Phase 18 loop closed with SUMMARY at .paul/phases/18-google-feed-permutation-url-fix/18-01-SUMMARY.md
- 2026-04-30: Transition-phase git commit for Phase 18 not executed in this UNIFY run (deferred — pattern z faz 15/16/17)
- 2026-04-30: Phase 18 APPLY complete — 4 pliki silnika + 2 nowe pliki testów (HelpersRoutingTest 4 testy, ProductFeedLinkTest 3 testy); suita 841 zielona
@@ -97,7 +103,7 @@ None.
### Blockers/Concerns
None.
-### Skill Audit (Phase 18)
+### Skill Audit (Phase 19)
| Expected | Invoked | Notes |
|----------|---------|-------|
| /feature-dev | ○ | User-approved override (hotfix z konkretną instrukcją) |
@@ -105,9 +111,9 @@ None.
## Session Continuity
-Last session: 2026-04-30
-Stopped at: Phase 18 complete, loop closed
+Last session: 2026-05-13
+Stopped at: Phase 19 complete, loop closed
Next action: Start next phase plan (transition-phase git commit pending), lub uruchomić /koniec-pracy jeśli zamykamy sesję
-Resume file: .paul/phases/18-google-feed-permutation-url-fix/18-01-SUMMARY.md
+Resume file: .paul/phases/19-frontend-meta-tags-fix/19-01-SUMMARY.md
---
*STATE.md — Updated after every significant action*
diff --git a/.paul/changelog/2026-05-13.md b/.paul/changelog/2026-05-13.md
new file mode 100644
index 0000000..09089f2
--- /dev/null
+++ b/.paul/changelog/2026-05-13.md
@@ -0,0 +1,21 @@
+# 2026-05-13
+
+## Co zrobiono
+
+- [Phase 19, Plan 01] Fix metatagów na stronach kategorii/artykułu/produktu — eliminacja wycieku meta_title homepage
+- Task 1: Diagnostyka produkcyjnej DB (pp_routes + pp_shop_categories_langs + pp_shop_products_langs + pp_pages_langs) — wynik w DIAGNOSTICS.md
+- Task 2 (checkpoint:decision): Wybrano fix-engine-detection (root cause w kodzie, nie w danych)
+- Task 3: Wyodrębniono `\front\LayoutEngine::applyEntityMetaToPage()` jako pure-function; 3 gałęzie (category/article/product) wywołują helper; suita 846 testów zielona (5 nowych w LayoutEngineMetaTagsTest)
+- Task 4 (human-verify): Weryfikacja curl na produkcji — 3 strony pokazują 3 różne ``, homepage meta nie wycieka
+- .gitignore — dodano `temp/` (skrypty diagnostyczne z DB credentials)
+
+## Zmienione pliki
+
+- `autoload/front/LayoutEngine.php`
+- `tests/Unit/front/LayoutEngineMetaTagsTest.php` (nowy)
+- `.paul/phases/19-frontend-meta-tags-fix/19-01-PLAN.md` (nowy)
+- `.paul/phases/19-frontend-meta-tags-fix/19-01-SUMMARY.md` (nowy)
+- `.paul/phases/19-frontend-meta-tags-fix/DIAGNOSTICS.md` (nowy)
+- `.paul/STATE.md`
+- `.paul/ROADMAP.md`
+- `.gitignore`
diff --git a/.paul/codebase/testing.md b/.paul/codebase/testing.md
index 32346c2..f5daff9 100644
--- a/.paul/codebase/testing.md
+++ b/.paul/codebase/testing.md
@@ -4,8 +4,8 @@
| Metric | Value |
|--------|-------|
-| Total tests | **841** |
-| Total assertions | **2330** |
+| Total tests | **846** |
+| Total assertions | **2348** |
| Framework | PHPUnit 9.6 (`phpunit.phar`) |
| Bootstrap | `tests/bootstrap.php` |
| Config | `phpunit.xml` |
diff --git a/.paul/docs/TECH_CHANGELOG.md b/.paul/docs/TECH_CHANGELOG.md
index ce12c11..bdb5dd3 100644
--- a/.paul/docs/TECH_CHANGELOG.md
+++ b/.paul/docs/TECH_CHANGELOG.md
@@ -2,6 +2,16 @@
> Chronologiczny log zmian technicznych — co i dlaczego.
+## v0.351 (2026-05-13)
+
+- Naprawiono wyciek metatagow ze strony glownej na podstrony kategorii/artykulu/produktu: `` wszystkich podstron pokazywal tytul homepage ("Sklep z akcesoriami..."), bo `LayoutEngine::show()` nadpisywal w galezi kategorii/artykulu/produktu tylko `$page['language']['title']`, a `meta_title` z domyslnej strony zylo dalej i wygrywalo w linii substytucji `[TITLE]`.
+- Wyodrebniono nowa metode publiczna `\front\LayoutEngine::applyEntityMetaToPage($page, $entityLanguage, $fallbackTitle)`: zawsze nadpisuje `meta_title`, `meta_keywords`, `meta_description` w `$page['language']` wartosciami encji (nawet pustym/null), eliminujac wyciek.
+- `LayoutEngine.php`: 3 galezie (category, article, product) wywoluja helper zamiast inline'ow z nadpisywaniem czesci pol.
+- Dodano 5 testow jednostkowych (`tests/Unit/front/LayoutEngineMetaTagsTest.php`) na pure-function helper: meta_title encji wygrywa, NULL czysci homepage, all-null product, null entity safe, empty page struct. Suita: 846 testow / 2348 assertions.
+- Diagnostyka root cause na produkcyjnej DB: pp_routes mapuje poprawnie (`category=10`, `product=522`); literalne 'description'/'keywords' w `pp_shop_categories_langs.id=331` to dane klienta (admin uzupelnia w panelu), nie bug shopPRO.
+- `.gitignore` rozszerzony o `temp/` (skrypty diagnostyczne z DB credentials).
+- Wymagane akcje na produkcji po deployu: poczekac na TTL cache Redis (24h) lub wyczyscic klucze `pp_routes:all`, `front_category_details:*`, `shop\\product:*` — opcjonalne (fix jest w warstwie poza cache).
+
## v0.350 (2026-04-30)
- Naprawiono linki produktow z permutacja atrybutow w feedzie Google: separator par `attr-val` w URL zmieniony z `/` na `_`. Stary format `/slug/20-170/21-175` nie matchowal sie w `pp_routes` (regex `[0-9-]+` nie obejmuje `/`), wiec klienci z GMC ladowali na strone glowna zamiast na produkt.
diff --git a/.paul/phases/19-frontend-meta-tags-fix/19-01-PLAN.md b/.paul/phases/19-frontend-meta-tags-fix/19-01-PLAN.md
new file mode 100644
index 0000000..049e1ff
--- /dev/null
+++ b/.paul/phases/19-frontend-meta-tags-fix/19-01-PLAN.md
@@ -0,0 +1,264 @@
+---
+phase: 19-frontend-meta-tags-fix
+plan: 01
+type: execute
+wave: 1
+depends_on: []
+files_modified:
+ - autoload/front/LayoutEngine.php
+ - tests/Unit/Front/LayoutEngineMetaTagsTest.php
+autonomous: false
+delegation: off
+---
+
+
+## Goal
+Strona kategorii (np. `/sen-i-otulenie`) i strona produktu (np. `/kocyk-niemowlaka-szczeniak-z-balonikiem-fuksja`) renderują poprawne ``, `` i `` zgodne z danymi SEO kategorii/produktu z bazy. Obecnie wszystkie podstrony pokazują tytuł strony głównej (`"Sklep z akcesoriami dla dzieci i niemowląt... | shopPRO 1"`) oraz literalne wartości `keywords`/`description` z layoutu/danych homepage.
+
+## Purpose
+Niepoprawne metatagi blokują indeksację SEO i wyświetlanie w Google Merchant Center / wynikach wyszukiwania. Klient sklepu (właściciel) traci ruch organiczny — każda podstrona ma identyczny title i puste meta.
+
+## Output
+- Diagnostyka: ustalona root cause (pp_routes vs sesyjny `$page` vs dane w DB)
+- Fix w `autoload/front/LayoutEngine.php` (lub w sąsiednim kodzie inicjującym `$page`)
+- Test jednostkowy dla logiki podmiany metatagów
+- Suita testów PHPUnit zielona
+- Weryfikacja human-verify: 3 URL-e (homepage, kategoria, produkt) zwracają różne `` / ``
+
+
+
+## Project Context
+@.paul/PROJECT.md
+@.paul/ROADMAP.md
+@.paul/STATE.md
+
+## Source Files
+@autoload/front/LayoutEngine.php
+@index.php
+@autoload/Domain/Category/CategoryRepository.php
+@autoload/Domain/Product/ProductRepository.php
+@autoload/Domain/Pages/PagesRepository.php
+
+## Clarifications
+- **Root cause** — Hipoteza wymaga weryfikacji w bazie (pp_routes + pp_shop_categories_languages + pp_shop_products_languages). Brak dostępu do DB z lokalnego środowiska planowania.
+ → Odpowiedź: Najpierw diagnostyka, potem fix — nie zakładamy bugu w kodzie ani w danych a priori.
+- **pp_routes content** — Nie znamy treści destination dla URL-i kategorii/produktu w bazie produkcyjnej.
+ → Odpowiedź: Sprawdź w bazie (Task 1).
+- **DB meta values** — Nie wiemy czy meta_keywords/meta_description dla `sen-i-otulenie` w bazie są wypełnione poprawnie, czy zawierają literalne `"keywords"`/`"description"`.
+ → Odpowiedź: Sprawdź w bazie (Task 1).
+
+## Background — co już wiemy
+- `LayoutEngine::show()` (linie 152, 174, 194) podmienia `$page['language']['title']`, `meta_keywords`, `meta_description` tylko jeżeli `$_GET['category']`, `$_GET['article']` lub `$_GET['product']` są ustawione (przez `Helpers::get(...)`).
+- `$_GET` jest zasilane przez `pp_routes` — `index.php:76-94` matchuje regex, parsuje destination jako query string i merge'uje z `$_GET`.
+- `$page` jest cache'owane w sesji (`index.php:147` — `Helpers::get_session('page')`) i fallbackuje do `frontPageDetails('')` (homepage) jeśli puste.
+- Na produkcji: layout HTML zawiera `[TITLE]`, ``, `` — placeholder mechanizm działa, ale podmieniane wartości są nieprawidłowe.
+- og:title/og:description dla produktu działają poprawnie (są dorzucane bezpośrednio przez DOM w `index.php:242-292`).
+
+
+
+
+## AC-1: Diagnostyka — ustalona root cause
+```gherkin
+Given dostęp do bazy produkcyjnej shoppro.project-dc.pl (FTP/SSH/phpMyAdmin)
+When wykonamy SQL diagnostyczne dla pp_routes + meta kategorii sen-i-otulenie + meta produktu 522
+Then znamy konkretną przyczynę: czy pp_routes nie ustawia category=/product=, czy meta w DB są zaślepkami, czy bug jest w session caching $page
+And wynik diagnostyki jest zapisany w plan-fix sekcji `` przed Task 2
+```
+
+## AC-2: Strona kategorii pokazuje własny `` i ``
+```gherkin
+Given kategoria w bazie ma wypełnione meta_title="Pościel dla dzieci" i meta_description="Kocyki, pościele..."
+When klient otwiera /sen-i-otulenie
+Then `` zawiera meta_title kategorii (plus ' | ' + firm_name)
+And `` zawiera meta_description kategorii
+And NIE pokazuje tytułu strony głównej
+```
+
+## AC-3: Strona produktu pokazuje własny `` i ``
+```gherkin
+Given produkt w bazie ma wypełnione meta_title i meta_description
+When klient otwiera /kocyk-niemowlaka-szczeniak-z-balonikiem-fuksja
+Then `` zawiera meta_title produktu (plus ' | ' + firm_name)
+And `` zawiera meta_description produktu
+And NIE pokazuje tytułu strony głównej
+```
+
+## AC-4: Fallback dla pustego meta_title/meta_description
+```gherkin
+Given kategoria/produkt ma puste meta_title w bazie
+When klient otwiera tę stronę
+Then `` używa nazwy kategorii/produktu + ' | ' + firm_name
+And `` jest puste (zachowanie obecne — brak fallbacku do opisu, by nie zmieniać semantyki)
+```
+
+## AC-5: Test jednostkowy + cała suita zielona
+```gherkin
+Given nowy test tests/Unit/Front/LayoutEngineMetaTagsTest.php
+When ./test.ps1 zostanie uruchomione
+Then test pokrywa scenariusz: poprawnie podmieniony [TITLE]/[META_KEYWORDS]/[META_DESCRIPTION] dla kategorii i produktu
+And cała suita 841+N testów przechodzi na zielono
+```
+
+
+
+
+
+
+ Task 1: Diagnostyka produkcji — pp_routes + meta w DB
+ (brak modyfikacji kodu — tylko zapytania SQL)
+
+ Uruchom diagnostyczne SQL na bazie shoppro.project-dc.pl (przez FTP→phpMyAdmin lub SSH):
+
+ 1. **pp_routes dla URL-i:**
+ ```sql
+ SELECT pattern, destination FROM pp_routes
+ WHERE 'sen-i-otulenie' REGEXP CONCAT('^', pattern)
+ OR 'kocyk-niemowlaka-szczeniak-z-balonikiem-fuksja' REGEXP CONCAT('^', pattern);
+ ```
+ Sprawdź: czy destination zawiera `category=` / `product=` jako query param.
+
+ 2. **Meta kategorii sen-i-otulenie:**
+ ```sql
+ SELECT c.id, c.url, cl.title, cl.meta_title, cl.meta_keywords, cl.meta_description
+ FROM pp_shop_categories c
+ JOIN pp_shop_categories_languages cl ON cl.shop_category_id = c.id
+ WHERE c.url = 'sen-i-otulenie';
+ ```
+
+ 3. **Meta produktu kocyk-niemowlaka-szczeniak-z-balonikiem-fuksja:**
+ ```sql
+ SELECT p.id, p.url, pl.name, pl.meta_title, pl.meta_keywords, pl.meta_description
+ FROM pp_shop_products p
+ JOIN pp_shop_products_languages pl ON pl.shop_product_id = p.id
+ WHERE p.url = 'kocyk-niemowlaka-szczeniak-z-balonikiem-fuksja';
+ ```
+
+ 4. **Default page meta (czy tytuł "Sklep z akcesoriami..." stamtąd pochodzi):**
+ ```sql
+ SELECT pp.id, ppl.title, ppl.meta_title, ppl.meta_keywords, ppl.meta_description
+ FROM pp_pages pp
+ JOIN pp_pages_languages ppl ON ppl.shop_page_id = pp.id
+ WHERE ppl.title LIKE 'Sklep z akcesoriami%' OR ppl.meta_title LIKE 'Sklep z akcesoriami%';
+ ```
+
+ Zapisz wyniki w pliku `.paul/phases/19-frontend-meta-tags-fix/DIAGNOSTICS.md` — surowe wyniki SQL + interpretacja (która z hipotez się potwierdza).
+
+ Unikaj: zakładania root cause bez danych. Nie modyfikuj kodu w tym tasku.
+
+ Plik .paul/phases/19-frontend-meta-tags-fix/DIAGNOSTICS.md istnieje, zawiera wyniki 4 zapytań SQL i konkluzję wskazującą jedną z hipotez (pp_routes / DB meta / session $page)
+ AC-1 satisfied: root cause ustalona i udokumentowana
+
+
+
+ Który fix wdrażamy w Task 3 na podstawie wyników diagnostyki?
+ Zależnie od wyniku Task 1 — fix dotyka różnych miejsc kodu. Decyzja blokuje Task 3.
+
+
+
+
+
+
+ Wybierz: fix-routes | fix-engine-detection | fix-db-data | fix-session-bleed (lub kombinacja)
+
+
+
+ Task 3: Implementacja fixu + test jednostkowy
+ autoload/front/LayoutEngine.php (lub index.php), tests/Unit/Front/LayoutEngineMetaTagsTest.php
+
+ Zaimplementuj fix wybrany w Task 2.
+
+ Kluczowe zasady (niezależnie od wybranej opcji):
+ - PHP < 8.0 — bez `match`, bez named args, bez union types, bez str_contains/str_starts_with
+ - Medoo `$db->get()` zwraca null gdy brak rekordu (NIE false)
+ - Cache Redis: po fixie wyczyść `pp_routes:all` i `shop\\product:*` jeśli dotykamy danych (Helpers::clear_product_cache lub CacheHandler::deletePattern)
+ - Nie dotykaj logiki og:title/og:description w index.php — to działa poprawnie
+ - Zachowaj zachowanie dla strony głównej i CMS pages (regresja!)
+
+ Test jednostkowy (`tests/Unit/Front/LayoutEngineMetaTagsTest.php`):
+ - Mock Medoo via createMock(\medoo::class)
+ - Scenariusze:
+ 1. category=ID + meta_title wypełniony → `` = meta_title + ' | ' + firm_name
+ 2. category=ID + meta_title pusty → `` = category.title + ' | ' + firm_name
+ 3. product=ID + meta_description wypełniony → `` zawiera tę wartość
+ 4. brak category/product/article (homepage) → tytuł strony page'a (regresja)
+
+ Komentarze tylko gdzie wyjaśniają "dlaczego" (np. dlaczego ignorujemy sesyjny $page dla kategorii).
+
+ ./test.ps1 tests/Unit/Front/LayoutEngineMetaTagsTest.php zwraca OK; ./test.ps1 (cała suita) — 841+N tests zielono
+ AC-2, AC-3, AC-4, AC-5 satisfied
+
+
+
+ Fix metatagów dla kategorii i produktu na froncie
+
+ 1. Wyczyść cache Redis (`pp_routes:all` + product cache) lub poczekaj na TTL
+ 2. Otwórz w przeglądarce 3 URL-e:
+ - https://shoppro.project-dc.pl/ (homepage — baseline)
+ - https://shoppro.project-dc.pl/sen-i-otulenie (kategoria)
+ - https://shoppro.project-dc.pl/kocyk-niemowlaka-szczeniak-z-balonikiem-fuksja (produkt)
+ 3. View Source (Ctrl+U) na każdym z nich. Sprawdź:
+ - `` jest RÓŻNY dla 3 stron
+ - `` zawiera opis kategorii/produktu (nie "description")
+ - `` zawiera słowa kluczowe z DB (lub puste, ale NIE "keywords")
+ 4. Powtórz na innej kategorii i innym produkcie (regresja)
+ 5. Potwierdź że strona główna nadal pokazuje swój oryginalny `` (regresja)
+
+ Wpisz "approved" by kontynuować, lub opisz issues
+
+
+
+
+
+
+## DO NOT CHANGE
+- `index.php:242-292` — logika og:title/og:description/og:image dla produktu (działa poprawnie, nie ruszać)
+- `pp_routes` regex dla permutacji (`[0-9_-]+`) — Phase 18 fix, nie regresować
+- `Helpers::clear_product_cache()` — sygnatura stała
+- Mechanizm `[META_INDEX]` / `[CANONICAL]` / `[CSS]` / `[JAVA_SCRIPT]` — niezwiązane
+
+## SCOPE LIMITS
+- Plan dotyczy TYLKO ``, ``, `` dla stron kategorii i produktu
+- NIE dodajemy og:* dla kategorii (deferred — osobny plan jeśli wyjdzie potrzeba)
+- NIE rozszerzamy fallbacku meta_description o auto-generowany opis (deferred)
+- NIE dotykamy CMS pages, articles, producers — chyba że wynik diagnostyki pokaże wspólny mechanizm
+- Bez build/update package — to robi się w `/koniec-pracy` po UNIFY
+
+
+
+
+Przed zamknięciem planu:
+- [ ] DIAGNOSTICS.md istnieje i wskazuje konkretną przyczynę
+- [ ] Fix zaimplementowany w wybranej lokalizacji (Task 2 decision)
+- [ ] Nowy test jednostkowy przechodzi
+- [ ] Cała suita PHPUnit zielona (841+ testów)
+- [ ] Human-verify na 3 URL-ach z różnymi `` zatwierdzony
+- [ ] Brak regresji dla strony głównej i CMS pages
+- [ ] Wszystkie acceptance criteria spełnione
+
+
+
+- Strona kategorii i strona produktu zwracają poprawne metatagi SEO
+- Diagnostyka udokumentowana (DIAGNOSTICS.md) dla przyszłej referencji
+- Test regresyjny pokrywa scenariusz
+- Bez regresji w istniejących funkcjach (suita zielona)
+
+
+
diff --git a/.paul/phases/19-frontend-meta-tags-fix/19-01-SUMMARY.md b/.paul/phases/19-frontend-meta-tags-fix/19-01-SUMMARY.md
new file mode 100644
index 0000000..7be8335
--- /dev/null
+++ b/.paul/phases/19-frontend-meta-tags-fix/19-01-SUMMARY.md
@@ -0,0 +1,147 @@
+---
+phase: 19-frontend-meta-tags-fix
+plan: 01
+subsystem: frontend
+tags: [seo, metatags, layout-engine, frontend, cache]
+
+requires:
+ - phase: none
+ provides: n/a
+
+provides:
+ - poprawne // dla stron kategorii/artykułu/produktu
+ - applyEntityMetaToPage() — testowalna metoda helper w \front\LayoutEngine
+ - regression test suite dla bug "homepage meta_title leak"
+
+affects: [future-seo-fixes, layout-engine-refactor, og-tags-for-category]
+
+tech-stack:
+ added: []
+ patterns:
+ - "Helper static method pattern w LayoutEngine — wyodrębnianie pure-function logic dla testowalności"
+
+key-files:
+ created:
+ - tests/Unit/front/LayoutEngineMetaTagsTest.php
+ - .paul/phases/19-frontend-meta-tags-fix/DIAGNOSTICS.md
+ modified:
+ - autoload/front/LayoutEngine.php
+
+key-decisions:
+ - "Root cause to bug w kodzie (LayoutEngine), nie dane w DB ani pp_routes"
+ - "Fix przez wyodrębnienie applyEntityMetaToPage() — zawsze nadpisuje meta_title/keywords/description encji (nawet pustym/null), żeby homepage nie wyciekał"
+ - "Literalne 'description'/'keywords' w pp_shop_categories_langs dla kategorii 10 — to dane klienta, nie bug shopPRO; admin uzupełnia w panelu"
+
+patterns-established:
+ - "LayoutEngine bug: nadpisywanie pól w $page['language'] musi być KOMPLETNE — partial override powoduje wyciek wartości z poprzedniego stanu (homepage)"
+ - "Test jednostkowy LayoutEngine: require_once pliku w teście (bootstrap nie ładuje \\front\\)"
+
+duration: ~45min
+started: 2026-05-13T14:00:00Z
+completed: 2026-05-13T14:45:00Z
+---
+
+# Phase 19 Plan 01: Frontend meta tags fix — Summary
+
+**LayoutEngine::applyEntityMetaToPage() rozwiązuje wyciek meta_title homepage do stron kategorii/produktu/artykułu — 3 gałęzie + nowa metoda helper + 5 testów regresyjnych.**
+
+## Performance
+
+| Metric | Value |
+|--------|-------|
+| Duration | ~45min |
+| Tasks | 4 z 4 wykonane |
+| Files modified | 2 (1 zmiana, 1 nowy test) |
+| Tests added | 5 (18 asercji) |
+| Total suite | 846 zielone (z 841) |
+
+## Acceptance Criteria Results
+
+| Criterion | Status | Notes |
+|-----------|--------|-------|
+| AC-1: Diagnostyka — ustalona root cause | Pass | DIAGNOSTICS.md zawiera 4 zapytania SQL + analizę kodu + jednoznaczny wniosek |
+| AC-2: Strona kategorii pokazuje własny title | Pass | Curl: `/sen-i-otulenie` → `Sen i otulenie | shopPRO 1` |
+| AC-3: Strona produktu pokazuje własny title | Pass | Curl: `/kocyk-niemowlaka-...` → `Kocyk niemowlaka - Szczeniak z balonikiem - Fuksja | shopPRO 1` |
+| AC-4: Fallback dla pustego meta_title | Pass | Dla kategorii meta_title=NULL → title=`category.title`; pokryte testem `testHomepageMetaTitleIsClearedWhenEntityHasNoMetaTitle` |
+| AC-5: Test + cała suita zielona | Pass | LayoutEngineMetaTagsTest (5/18) + 846/846 ogółem |
+
+## Accomplishments
+
+- Zidentyfikowano root cause przez diagnostyczne SQL na produkcyjnej DB (3 hipotezy zweryfikowane, 1 potwierdzona) — bug w `LayoutEngine` linie 156-213, nie pp_routes, nie session, nie tylko dane
+- Wyodrębniono `applyEntityMetaToPage()` — testowalna pure-function w `\front\LayoutEngine`, używana w 3 gałęziach (category/article/product)
+- Fix zweryfikowany curl-em na produkcji (auto-upload FTP po edycji): 3 różne `` na 3 stronach, homepage nie wycieka
+- Boczny issue zidentyfikowany i zostawiony klientowi: literalne 'description'/'keywords' w `pp_shop_categories_langs.id=331` — admin uzupełnia w panelu
+
+## Files Created/Modified
+
+| File | Change | Purpose |
+|------|--------|---------|
+| `autoload/front/LayoutEngine.php` | Modified | 3 gałęzie (category line 152, article 174, product 194) zastąpione wywołaniem `applyEntityMetaToPage()`; nowa metoda po `title()` (linie ~430-455) |
+| `tests/Unit/front/LayoutEngineMetaTagsTest.php` | Created | 5 testów: meta_title encji wygrywa, NULL czyści homepage, all-null product, null entity safe, empty page struct |
+| `.paul/phases/19-frontend-meta-tags-fix/DIAGNOSTICS.md` | Created | Wyniki 4 zapytań SQL + analiza kodu + wniosek root cause |
+| `.gitignore` | Modified | Dodano `temp/` (skrypty diagnostyczne z DB password) |
+
+## Decisions Made
+
+| Decision | Rationale | Impact |
+|----------|-----------|--------|
+| Fix przez extract helper (`applyEntityMetaToPage`) zamiast inline edycji 3 gałęzi | Testowalność (statyczna pure-function), DRY (jedno miejsce z logiką override meta) | Future SEO fixes działają w jednym miejscu |
+| Zawsze nadpisuj meta_title (nawet NULL), nie tylko gdy wypełnione | Eliminuje wyciek z homepage; semantyka „encja w pełni opisuje swoje meta" | Linia 332 LayoutEngine działa zgodnie z intencją oryginalnego kodu |
+| Dane klienta (literalne 'description'/'keywords') NIE są częścią fixu | To dane, nie kod; admin uzupełnia przez panel; klient inaczej skomplikowane | Phase 19 zamyka się czysto, nie wlecze tematu DB |
+
+## Deviations from Plan
+
+### Summary
+
+| Type | Count | Impact |
+|------|-------|--------|
+| Auto-fixed | 1 | `.gitignore` rozszerzony o `temp/` (DB password w skryptach diag) |
+| Scope additions | 0 | — |
+| Deferred | 1 | Git commit transition-phase (pattern z faz 15-18) |
+
+**Total impact:** Plan wykonany bez odchyleń scope. Jeden security micro-fix (.gitignore).
+
+### Auto-fixed Issues
+
+**1. [Security] DB credentials w temp/diag*.php**
+- **Found during:** Task 1 (diagnostyka)
+- **Issue:** Skrypty diagnostyczne `temp/diag_meta*.php` zawierają hardcoded credentials produkcyjnej DB
+- **Fix:** Dodano `temp/` do `.gitignore` (CLAUDE.md i tak nakazuje skrypty pomocnicze w temp/)
+- **Files:** `.gitignore`
+- **Verification:** `git status` nie pokazuje temp/* jako trackowane
+
+### Deferred Items
+
+- Transition-phase git commit dla Phase 19 — kontynuacja patternu z faz 15/16/17/18 (commit robi `/koniec-pracy` lub user manualnie). Brak negatywnego impactu — kod działa na produkcji już teraz (auto-upload FTP).
+
+## Issues Encountered
+
+| Issue | Resolution |
+|-------|------------|
+| Pierwsza wersja diag SQL używała `pp_shop_categories_languages` (nie istnieje) | Sprawdzono `SHOW TABLES` — tabele to `*_langs` (skrót). Zaktualizowano zapytania |
+| Pierwsza wersja używała `shop_category_id` (nie istnieje) | Kolumna to `category_id`. Sprawdzono `SHOW COLUMNS` |
+| LayoutEngine niedostępny w bootstrap testów | `require_once` w teście (bootstrap PSR-4 nie ładuje `\front\` namespace) |
+
+## Next Phase Readiness
+
+**Ready:**
+- Codebase z `applyEntityMetaToPage()` — gotowa do reuse w przyszłych fixach SEO (np. og:title dla kategorii)
+- Test pattern dla `\front\LayoutEngine` ustalony (require_once + asercje na pure-function)
+
+**Concerns:**
+- LayoutEngine::show() nadal jest 400+ linijowym monolitem ze statycznymi globalami — dalsze ekstrakcje wskazane, ale poza scope tej fazy
+- Klienci z istniejącymi instalacjami muszą uzupełnić meta_title/keywords/description przez panel admina (lub zaakceptować że podstrony mają brak meta — co teraz przynajmniej NIE jest niepoprawnym tytułem homepage)
+
+**Blockers:**
+- None.
+
+## Skill Audit (Phase 19)
+
+| Expected | Invoked | Notes |
+|----------|---------|-------|
+| /feature-dev | ○ | Hotfix z konkretną instrukcją — override per pattern Phase 15-18 |
+| /koniec-pracy | ○ | Pending — przy zamknięciu sesji jeśli release wchodzi do update package |
+
+---
+*Phase: 19-frontend-meta-tags-fix, Plan: 01*
+*Completed: 2026-05-13*
diff --git a/.paul/phases/19-frontend-meta-tags-fix/DIAGNOSTICS.md b/.paul/phases/19-frontend-meta-tags-fix/DIAGNOSTICS.md
new file mode 100644
index 0000000..c51ac7a
--- /dev/null
+++ b/.paul/phases/19-frontend-meta-tags-fix/DIAGNOSTICS.md
@@ -0,0 +1,129 @@
+# Phase 19 — DIAGNOSTICS
+
+**Data:** 2026-05-13
+**Środowisko:** shoppro.project-dc.pl (produkcja) — DB `host117523_shoppro` na `host117523.hostido.net.pl`
+
+## Wynik diagnostyczny: ROOT CAUSE w kodzie (LayoutEngine.php)
+
+`$page['language']['meta_title']` z domyślnej strony (homepage id=6) **nigdy nie jest nadpisywany** w gałęziach kategorii/artykułu/produktu w `LayoutEngine::show()`. Linia 332 priorytetuje `meta_title` nad `title`, więc tytuł homepage wycieka na wszystkie podstrony.
+
+Dodatkowo: meta_description/meta_keywords dla podstron też nie zachowują się dobrze, bo w DB klient ma literalne zaślepki ("description", "keywords") albo NULL — ale to drugorzędny problem względem bugu w kodzie.
+
+---
+
+## 1) pp_routes — działa poprawnie
+
+```
+URL: sen-i-otulenie
+ MATCH pattern='^sen-i-otulenie$' dest='index.php?category=10&lang=pl'
+
+URL: kocyk-niemowlaka-szczeniak-z-balonikiem-fuksja
+ MATCH pattern='^kocyk-niemowlaka-szczeniak-z-balonikiem-fuksja$' dest='index.php?product=522'
+```
+
+Hipoteza "pp_routes nie ustawia category=/product=" — **OBALONA**.
+
+## 2) Meta kategorii 10 (sen-i-otulenie), lang=pl
+
+```
+id = 331
+category_id = 10
+lang_id = 'pl'
+title = 'Sen i otulenie'
+meta_title = NULL
+meta_description = 'description' ← LITERALNA ZAŚLEPKA w DB
+meta_keywords = 'keywords' ← LITERALNA ZAŚLEPKA w DB
+seo_link = 'sen-i-otulenie'
+category_title = 'Sen i otulenie'
+```
+
+## 3) Meta produktu 522 (kocyk-niemowlaka...), lang=pl
+
+```
+id = 4040
+product_id = 522
+lang_id = 'pl'
+name = 'Kocyk niemowlaka - Szczeniak z balonikiem - Fuksja'
+meta_title = NULL
+meta_description = NULL
+meta_keywords = NULL
+seo_link = 'kocyk-niemowlaka-szczeniak-z-balonikiem-fuksja'
+```
+
+## 4) Default page (homepage) — id=6, start=1, lang=pl
+
+```
+id = 6
+start = 1
+title = 'Home'
+meta_title = 'Sklep z akcesoriami dla dzieci i niemowląt, kocyki minky, poduszki, ubranka'
+meta_keywords = '' (puste)
+meta_description = 'Marianek to sklep internetowy, w którym znajdziecie Państwo artykuły dla dzieci i niemowląt...'
+```
+
+To jest źródło "wyciekającego" tytułu na podstronach.
+
+---
+
+## Analiza kodu (autoload/front/LayoutEngine.php)
+
+### Gałąź kategorii (linie 152-168)
+```php
+if ( \Shared\Helpers\Helpers::get( 'category' ) ) {
+ $category = $categoryRepo->frontCategoryDetails(...);
+
+ if ( $category['language']['meta_title'] )
+ $page['language']['title'] = $category['language']['meta_title']; // ← przypisuje do TITLE, nie meta_title
+ else
+ $page['language']['title'] = $category['language']['title'];
+
+ $page['show_title'] = true;
+ $page['language']['meta_keywords'] = $category['language']['meta_keywords']; // OK
+ $page['language']['meta_description'] = $category['language']['meta_description']; // OK
+ // BRAK: $page['language']['meta_title'] = $category['language']['meta_title'];
+}
+```
+
+### Gałąź produktu (linie 194-213) — identyczny bug
+
+### Gałąź artykułu (linie 174-189) — identyczny bug
+
+### Substytucja [TITLE] (linia 332)
+```php
+$html = str_replace( '[TITLE]',
+ $page['language']['meta_title']
+ ? $page['language']['meta_title'] . ' | ' . $settings['firm_name']
+ : $page['language']['title'] . ' | ' . $settings['firm_name'],
+ $html );
+```
+
+`meta_title` z homepage żyje dalej w `$page['language']` (bo nie został zresetowany w gałęzi kategorii/produktu) → wygrywa nad title kategorii/produktu.
+
+---
+
+## Wpływ na obserwowane zachowanie
+
+| URL | Obserwowane `` | Powód |
+|-----|----------------------|-------|
+| /sen-i-otulenie | "Sklep z akcesoriami... \| shopPRO 1" | meta_title homepage wycieka |
+| /kocyk-niemowlaka-... | "Sklep z akcesoriami... \| shopPRO 1" | meta_title homepage wycieka |
+
+| URL | Obserwowane `` | Powód |
+|-----|---------------------------------|-------|
+| /sen-i-otulenie | "description" | meta_description kategorii (literalna zaślepka) — POPRAWNE nadpisanie, ale dane w DB są wadliwe |
+| /kocyk-niemowlaka-... | "" (puste) | meta_description produktu = NULL — POPRAWNE nadpisanie |
+
+| URL | Obserwowane `` | Powód |
+|-----|------------------------------|-------|
+| /sen-i-otulenie | "keywords" | meta_keywords kategorii (literalna zaślepka) — POPRAWNE nadpisanie, ale dane w DB są wadliwe |
+| /kocyk-niemowlaka-... | "" (puste) | meta_keywords produktu = NULL — POPRAWNE nadpisanie |
+
+---
+
+## Wniosek
+
+**Bug w kodzie**: `LayoutEngine::show()` w 3 gałęziach (category/article/product) zapisuje meta_title kategorii do `title`, ale nie nadpisuje `$page['language']['meta_title']`. Wartość z homepage zostaje i wygrywa.
+
+**Dane klienta**: oddzielny issue — meta_keywords/meta_description dla kategorii to literalne zaślepki "keywords"/"description", produkt ma NULL. To NIE jest bug shopPRO — admin musi wypełnić panel.
+
+**Rekomendacja fixu**: opcja `fix-engine-detection` z planu — naprawić gałęzie kategorii/artykułu/produktu, by zawsze nadpisywały `meta_title` (nawet pustym/NULL), oraz uprościć logikę title żeby była symetryczna.
diff --git a/autoload/front/LayoutEngine.php b/autoload/front/LayoutEngine.php
index 3c27ab3..5679298 100644
--- a/autoload/front/LayoutEngine.php
+++ b/autoload/front/LayoutEngine.php
@@ -153,15 +153,9 @@ class LayoutEngine
{
$category = $categoryRepo->frontCategoryDetails( (int)\Shared\Helpers\Helpers::get( 'category' ), $lang_id );
- if ( $category['language']['meta_title'] )
- $page['language']['title'] = $category['language']['meta_title'];
- else
- $page['language']['title'] = $category['language']['title'];
-
+ $page = self::applyEntityMetaToPage( $page, isset( $category['language'] ) ? $category['language'] : null, isset( $category['language']['title'] ) ? $category['language']['title'] : '' );
$page['show_title'] = true;
- $page['language']['meta_keywords'] = $category['language']['meta_keywords'];
- $page['language']['meta_description'] = $category['language']['meta_description'];
- $page['language']['page_title'] = $category['language']['category_title'] ? $category['language']['category_title'] : $category['language']['title'];
+ $page['language']['page_title'] = !empty( $category['language']['category_title'] ) ? $category['language']['category_title'] : ( isset( $category['language']['title'] ) ? $category['language']['title'] : '' );
// CANONICAL
$html = str_replace( '[CANONICAL]', '', $html );
@@ -175,14 +169,8 @@ class LayoutEngine
{
$article = $articleRepo->articleDetailsFrontend( (int)\Shared\Helpers\Helpers::get( 'article' ), $lang_id );
- if ( $article['language']['meta_title'] )
- $page['language']['title'] = $article['language']['meta_title'];
- else
- $page['language']['title'] = $article['language']['title'];
-
+ $page = self::applyEntityMetaToPage( $page, isset( $article['language'] ) ? $article['language'] : null, isset( $article['language']['title'] ) ? $article['language']['title'] : '' );
$page['show_title'] = false;
- $page['language']['meta_keywords'] = $article['language']['meta_keywords'];
- $page['language']['meta_description'] = $article['language']['meta_description'];
// CANONICAL
$html = str_replace( '[CANONICAL]', '', $html );
@@ -196,14 +184,8 @@ class LayoutEngine
$permutation_hash = isset( $_GET['permutation_hash'] ) ? str_replace( '_', '|', $_GET['permutation_hash'] ) : null;
$product = ( new \Domain\Product\ProductRepository( $GLOBALS['mdb'] ) )->findCached( \Shared\Helpers\Helpers::get( 'product' ), $lang_id, $permutation_hash );
- if ( $product['language']['meta_title'] )
- $page['language']['title'] = $product['language']['meta_title'];
- else
- $page['language']['title'] = $product['language']['name'];
-
+ $page = self::applyEntityMetaToPage( $page, isset( $product['language'] ) ? $product['language'] : null, isset( $product['language']['name'] ) ? $product['language']['name'] : '' );
$page['show_title'] = false;
- $page['language']['meta_keywords'] = $product['language']['meta_keywords'];
- $page['language']['meta_description'] = $product['language']['meta_description'];
// CANONICAL
if ( $product['language']['canonical'] )
@@ -440,6 +422,35 @@ class LayoutEngine
] );
}
+ /**
+ * Przepisuje meta encji (kategoria/artykuł/produkt) do $page['language'].
+ *
+ * Dlaczego: domyślne $page jest stroną główną CMS. Jeśli nie nadpiszemy
+ * meta_title encji (nawet pustym), meta_title homepage wycieka do
+ * na podstronie kategorii/produktu (linia podstawienia [TITLE]).
+ *
+ * @param array $page obecne $page (z homepage lub session)
+ * @param array|null $entityLanguage wiersz *_langs encji (może być null)
+ * @param string $fallbackTitle nazwa encji używana jako $page.language.title
+ * @return array zmodyfikowany $page
+ */
+ public static function applyEntityMetaToPage( $page, $entityLanguage, $fallbackTitle )
+ {
+ if ( !is_array( $page ) ) {
+ $page = [];
+ }
+ if ( !isset( $page['language'] ) or !is_array( $page['language'] ) ) {
+ $page['language'] = [];
+ }
+
+ $page['language']['title'] = $fallbackTitle;
+ $page['language']['meta_title'] = is_array( $entityLanguage ) && isset( $entityLanguage['meta_title'] ) ? $entityLanguage['meta_title'] : null;
+ $page['language']['meta_keywords'] = is_array( $entityLanguage ) && isset( $entityLanguage['meta_keywords'] ) ? $entityLanguage['meta_keywords'] : null;
+ $page['language']['meta_description'] = is_array( $entityLanguage ) && isset( $entityLanguage['meta_description'] ) ? $entityLanguage['meta_description'] : null;
+
+ return $page;
+ }
+
public static function alert()
{
if ( $alert = \Shared\Helpers\Helpers::get_session( 'alert' ) )
diff --git a/tests/Unit/front/LayoutEngineMetaTagsTest.php b/tests/Unit/front/LayoutEngineMetaTagsTest.php
new file mode 100644
index 0000000..7c3495a
--- /dev/null
+++ b/tests/Unit/front/LayoutEngineMetaTagsTest.php
@@ -0,0 +1,110 @@
+homepagePage();
+ $category = [
+ 'meta_title' => 'Sen i otulenie — kocyki minky',
+ 'meta_keywords' => 'kocyki, otulacze',
+ 'meta_description' => 'Najwyższej jakości kocyki minky',
+ 'title' => 'Sen i otulenie',
+ ];
+
+ $result = \front\LayoutEngine::applyEntityMetaToPage($page, $category, $category['title']);
+
+ $this->assertSame('Sen i otulenie — kocyki minky', $result['language']['meta_title']);
+ $this->assertSame('Sen i otulenie', $result['language']['title']);
+ $this->assertSame('kocyki, otulacze', $result['language']['meta_keywords']);
+ $this->assertSame('Najwyższej jakości kocyki minky', $result['language']['meta_description']);
+ }
+
+ public function testHomepageMetaTitleIsClearedWhenEntityHasNoMetaTitle()
+ {
+ $page = $this->homepagePage();
+ $category = [
+ 'meta_title' => null,
+ 'meta_keywords' => 'description',
+ 'meta_description' => 'keywords',
+ 'title' => 'Sen i otulenie',
+ ];
+
+ $result = \front\LayoutEngine::applyEntityMetaToPage($page, $category, $category['title']);
+
+ $this->assertNull(
+ $result['language']['meta_title'],
+ 'meta_title homepage nie może wyciekać gdy kategoria nie ma własnego'
+ );
+ $this->assertSame('Sen i otulenie', $result['language']['title']);
+ }
+
+ public function testProductWithAllMetaNullClearsHomepageValues()
+ {
+ $page = $this->homepagePage();
+ $productLang = [
+ 'meta_title' => null,
+ 'meta_keywords' => null,
+ 'meta_description' => null,
+ 'name' => 'Kocyk niemowlaka - Szczeniak z balonikiem - Fuksja',
+ ];
+
+ $result = \front\LayoutEngine::applyEntityMetaToPage($page, $productLang, $productLang['name']);
+
+ $this->assertNull($result['language']['meta_title']);
+ $this->assertNull($result['language']['meta_keywords']);
+ $this->assertNull($result['language']['meta_description']);
+ $this->assertSame($productLang['name'], $result['language']['title']);
+ }
+
+ public function testNullEntityLanguageDoesNotCrashAndClearsMeta()
+ {
+ $page = $this->homepagePage();
+
+ $result = \front\LayoutEngine::applyEntityMetaToPage($page, null, 'Fallback');
+
+ $this->assertSame('Fallback', $result['language']['title']);
+ $this->assertNull($result['language']['meta_title']);
+ $this->assertNull($result['language']['meta_keywords']);
+ $this->assertNull($result['language']['meta_description']);
+ }
+
+ public function testEmptyPageInputCreatesLanguageStructure()
+ {
+ $result = \front\LayoutEngine::applyEntityMetaToPage([], ['meta_title' => 'X'], 'T');
+
+ $this->assertIsArray($result);
+ $this->assertIsArray($result['language']);
+ $this->assertSame('X', $result['language']['meta_title']);
+ $this->assertSame('T', $result['language']['title']);
+ }
+
+ private function homepagePage()
+ {
+ return [
+ 'id' => 6,
+ 'language' => [
+ 'title' => 'Home',
+ 'meta_title' => 'Sklep z akcesoriami dla dzieci i niemowląt, kocyki minky, poduszki, ubranka',
+ 'meta_keywords' => '',
+ 'meta_description' => 'Marianek to sklep internetowy z artykułami dla dzieci...',
+ 'page_title' => null,
+ ],
+ ];
+ }
+}