fix: wyciek meta_title homepage na podstrony kategorii/artykulu/produktu (v0.351)

LayoutEngine::show() w 3 galeziach (category/article/product) nadpisywal
tylko $page['language']['title'], a meta_title homepage zylo dalej i wygrywalo
w substytucji [TITLE]. Wyodrebniono applyEntityMetaToPage() jako pure-function
ktora zawsze nadpisuje meta_title/meta_keywords/meta_description encji
(nawet pustym/null), eliminujac wyciek.

5 nowych testow w LayoutEngineMetaTagsTest. Suita 846 testow zielona.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-13 22:38:02 +02:00
parent 255456dcc5
commit 6c924ae542
11 changed files with 741 additions and 35 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
.vscode/ftp-kr.sync.cache.json
temp/

View File

@@ -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ą `<title>` 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)*

View File

@@ -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 <title>/<meta description>/<meta keywords> 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*

View File

@@ -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 `<title>`, 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`

View File

@@ -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` |

View File

@@ -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: `<title>` 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.

View File

@@ -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
---
<objective>
## Goal
Strona kategorii (np. `/sen-i-otulenie`) i strona produktu (np. `/kocyk-niemowlaka-szczeniak-z-balonikiem-fuksja`) renderują poprawne `<title>`, `<meta name="description">` i `<meta name="keywords">` 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 `<title>` / `<meta description>`
</objective>
<context>
## 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>[TITLE]</title>`, `<meta name="keywords" content="[META_KEYWORDS]">`, `<meta name="description" content="[META_DESCRIPTION]">` — 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`).
</context>
<acceptance_criteria>
## 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 `<context>` przed Task 2
```
## AC-2: Strona kategorii pokazuje własny `<title>` i `<meta description>`
```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 `<title>` zawiera meta_title kategorii (plus ' | ' + firm_name)
And `<meta name="description" content="...">` zawiera meta_description kategorii
And NIE pokazuje tytułu strony głównej
```
## AC-3: Strona produktu pokazuje własny `<title>` i `<meta description>`
```gherkin
Given produkt w bazie ma wypełnione meta_title i meta_description
When klient otwiera /kocyk-niemowlaka-szczeniak-z-balonikiem-fuksja
Then `<title>` zawiera meta_title produktu (plus ' | ' + firm_name)
And `<meta name="description" content="...">` 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 `<title>` używa nazwy kategorii/produktu + ' | ' + firm_name
And `<meta description>` 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
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: Diagnostyka produkcji — pp_routes + meta w DB</name>
<files>(brak modyfikacji kodu — tylko zapytania SQL)</files>
<action>
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.
</action>
<verify>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)</verify>
<done>AC-1 satisfied: root cause ustalona i udokumentowana</done>
</task>
<task type="checkpoint:decision" gate="blocking">
<decision>Który fix wdrażamy w Task 3 na podstawie wyników diagnostyki?</decision>
<context>Zależnie od wyniku Task 1 — fix dotyka różnych miejsc kodu. Decyzja blokuje Task 3.</context>
<options>
<option id="fix-routes">
<name>Fix pp_routes (jeśli destination nie zawiera category=/product=)</name>
<pros>Naprawia źródło — wszystkie podstrony zaczynają działać; nie dotyka silnika</pros>
<cons>Wymaga update SQL w paczce instalacyjnej; ryzyko nadpisania custom routes klienta</cons>
</option>
<option id="fix-engine-detection">
<name>Fix LayoutEngine — niezależna detekcja kategorii/produktu (jeśli pp_routes OK, ale sesyjny $page wycieka)</name>
<pros>Defensywny — engine sam wykrywa kontekst i nadpisuje meta, niezależnie od $page session</pros>
<cons>Dodaje logikę; może zmienić zachowanie dla custom stron</cons>
</option>
<option id="fix-db-data">
<name>Fix danych w DB (jeśli meta_keywords/meta_description = literalne "keywords"/"description")</name>
<pros>Najprostsze — admin uzupełnia poprawne SEO przez panel</pros>
<cons>To nie jest bug w shopPRO tylko w danych klienta; nie wchodzi do update package</cons>
</option>
<option id="fix-session-bleed">
<name>Fix session $page bleed — index.php nie używa session $page gdy URL wskazuje na category/product/article</name>
<pros>Naprawia konkretną klasę bugów; zmiana ograniczona do index.php</pros>
<cons>Wymaga uważnej regresji — $page jest używane też do menu/breadcrumbs</cons>
</option>
</options>
<resume-signal>Wybierz: fix-routes | fix-engine-detection | fix-db-data | fix-session-bleed (lub kombinacja)</resume-signal>
</task>
<task type="auto">
<name>Task 3: Implementacja fixu + test jednostkowy</name>
<files>autoload/front/LayoutEngine.php (lub index.php), tests/Unit/Front/LayoutEngineMetaTagsTest.php</files>
<action>
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 → `<title>` = meta_title + ' | ' + firm_name
2. category=ID + meta_title pusty → `<title>` = category.title + ' | ' + firm_name
3. product=ID + meta_description wypełniony → `<meta description>` 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).
</action>
<verify>./test.ps1 tests/Unit/Front/LayoutEngineMetaTagsTest.php zwraca OK; ./test.ps1 (cała suita) — 841+N tests zielono</verify>
<done>AC-2, AC-3, AC-4, AC-5 satisfied</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<what-built>Fix metatagów dla kategorii i produktu na froncie</what-built>
<how-to-verify>
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ź:
- `<title>` jest RÓŻNY dla 3 stron
- `<meta name="description" content="...">` zawiera opis kategorii/produktu (nie "description")
- `<meta name="keywords" content="...">` 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 `<title>` (regresja)
</how-to-verify>
<resume-signal>Wpisz "approved" by kontynuować, lub opisz issues</resume-signal>
</task>
</tasks>
<boundaries>
## 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 `<title>`, `<meta description>`, `<meta keywords>` 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
</boundaries>
<verification>
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 `<title>` zatwierdzony
- [ ] Brak regresji dla strony głównej i CMS pages
- [ ] Wszystkie acceptance criteria spełnione
</verification>
<success_criteria>
- 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)
</success_criteria>
<output>
After completion: `.paul/phases/19-frontend-meta-tags-fix/19-01-SUMMARY.md`
</output>

View File

@@ -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 <title>/<meta description>/<meta keywords> 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``<title>Sen i otulenie | shopPRO 1</title>` |
| AC-3: Strona produktu pokazuje własny title | Pass | Curl: `/kocyk-niemowlaka-...``<title>Kocyk niemowlaka - Szczeniak z balonikiem - Fuksja | shopPRO 1</title>` |
| 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 `<title>` 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*

View File

@@ -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 `<title>` | 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 `<meta description>` | 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 `<meta keywords>` | 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.

View File

@@ -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 <title>
* 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' ) )

View File

@@ -0,0 +1,110 @@
<?php
namespace Tests\Unit\Front;
use PHPUnit\Framework\TestCase;
require_once __DIR__ . '/../../../autoload/front/LayoutEngine.php';
/**
* Phase 19 — fix metatagów na stronach kategorii/produktu/artykułu.
*
* Bug: domyślne $page jest stroną główną. W gałęziach category/article/product
* w LayoutEngine::show() nadpisywany był tylko title, a meta_title homepage
* żył dalej w $page['language']['meta_title'] i wygrywał w substytucji [TITLE].
*
* Fix: applyEntityMetaToPage() zawsze nadpisuje meta_title/meta_keywords/
* meta_description encji (nawet pustymi/null), żeby nic z homepage nie wyciekało.
*/
class LayoutEngineMetaTagsTest extends TestCase
{
public function testHomepageMetaTitleDoesNotLeakWhenEntityHasOwnMetaTitle()
{
$page = $this->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,
],
];
}
}