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:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
||||
.vscode/ftp-kr.sync.cache.json
|
||||
temp/
|
||||
|
||||
@@ -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)*
|
||||
|
||||
@@ -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*
|
||||
|
||||
21
.paul/changelog/2026-05-13.md
Normal file
21
.paul/changelog/2026-05-13.md
Normal 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`
|
||||
@@ -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` |
|
||||
|
||||
@@ -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.
|
||||
|
||||
264
.paul/phases/19-frontend-meta-tags-fix/19-01-PLAN.md
Normal file
264
.paul/phases/19-frontend-meta-tags-fix/19-01-PLAN.md
Normal 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>
|
||||
147
.paul/phases/19-frontend-meta-tags-fix/19-01-SUMMARY.md
Normal file
147
.paul/phases/19-frontend-meta-tags-fix/19-01-SUMMARY.md
Normal 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*
|
||||
129
.paul/phases/19-frontend-meta-tags-fix/DIAGNOSTICS.md
Normal file
129
.paul/phases/19-frontend-meta-tags-fix/DIAGNOSTICS.md
Normal 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.
|
||||
@@ -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' ) )
|
||||
|
||||
110
tests/Unit/front/LayoutEngineMetaTagsTest.php
Normal file
110
tests/Unit/front/LayoutEngineMetaTagsTest.php
Normal 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,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user