kursybp.pl — score 67 → 93 (+26 pkt, 3 iteracje)
Pierwszy external case (NIE nasza domena) na legacy PHP custom stack. Strona szkoły kursów MedActive (Centrum Kształcenia Kadr Sportowych, działa od 2014 r.) — własny framework PHP, brak build pipeline'a, edycja przez SSH direct file modification. 3 iteracje pokazują pełny flow: techniczne fixy (+15) → copy + FAQ (+12) → real-world wins z plateau (-1). Lekcja: nie każda iter musi podnosić score żeby mieć wartość.
Dlaczego ten case jest inny?
Poprzednie 3 case studies (selpio.com, webcomp.pl, getmepost.com) to nasze własne repozytoria z git, build pipeline (Astro/Next.js), deploy przez Vercel/MyDevil rsync. Klient zewnętrzny przychodzi z pytaniem: "Czy ten workflow działa na mojej stronie z 2014 roku w PHP bez frameworka?"
kursybp.pl to:
- Custom PHP framework (composer.json mówi "ckks-admin2", brak Laravel/Symfony) — wszystko PSR-4 ręcznie pisane
- Brak build process — pliki .php są bezpośrednio na serwerze, brak
npm run build - Brak lokalnego repo — kod istnieje TYLKO na produkcyjnym serwerze klienta (shared hosting)
- Edycja przez SSH — sed/awk/Python scripts direct na live files (z backup-em przed)
- External client — szkoła kursów MedActive/CKKS, nie my (Selpio)
Pytanie: czy per-issue prompts Selpio + Claude Code workflow skaluje się? Wynik: tak, z taką samą efektywnością (+27 pkt w 30 minut) co dla nowoczesnych frameworków.
Stan początkowy: 67/100 (needs-improvement)
Raport Selpio hash gJFyNL1JBo. Dimensions:
- schema: 0/25 — brak JSON-LD wcale na homepage (sub-pages mają tylko prosty Service w kursy.php)
- technical_seo: 10/20 — robots.txt / sitemap.xml / llms.txt zwracały HTTP 200 ALE serwowały HTML homepage (catch-all rewrite)
- semantics: 14/25 — copy issues: niekompletne zdanie ("mozesz...wypocznku"), thin content (277 słów < 300 min)
- structure: 16/25 — brak FAQ section, brak listy podstawowych ofert poza menu
- llm_access: 25/25 ✓ (tylko wynik kontroli html lang + robots meta)
- authority: 25/25 ✓ (HTTPS + dłuższy age domeny)
- seo: 17/20 (canonical + 5 og + 4 twitter w index.php już istnieją)
Co już było OK: canonical, Open Graph, Twitter Card, html lang="pl" — analyzer to wcześniej wykrył w istniejącym head.
Iter 1 (15 min): techniczne fixy przez SSH
Workflow per fix: backup pliku → edycja (heredoc/awk/sed) → curl verify → next. Wszystko z jednej sesji SSH bez intermediate downloads.
-
public_html/index.phpJSON-LD EducationalOrganization (CKKS/MedActive) z hasOfferCatalog (3 kategorie: medyczne / instruktorskie / oświata) + WebSite z SearchAction. Wstawiono awk-em między 4 og:image/twitter meta a </head>. Schema 0 → 15.
-
public_html/robots.txtPlik nie istniał (.htaccess catch-all dawał fake 200). Utworzony z explicit Allow dla 10 LLM crawlers (GPTBot/ClaudeBot/PerplexityBot/Google-Extended/CCBot itd.) + Sitemap link.
-
public_html/sitemap.xmlPlik nie istniał. Utworzony statycznie z 9 URL (homepage + 3 kategorie kursów + kadra + onas + kontakt + wazneinformacje + regulamin) z lastmod 2026-05-24.
-
public_html/llms.txtPlik nie istniał. Utworzony w formacie llmstxt.org z opisem MedActive/CKKS (oferta, format kursów, kontakt, linki).
-
public_html/.htaccessKLUCZOWY discovery: linia 61 zawierała "RewriteRule ^sitemap.xml$ sitemap.php" ale plik sitemap.php nie istniał. Apache mod_rewrite → 404 → catch-all → index.php (HTML). Mimo że stworzyłem fizyczny sitemap.xml, request był przekierowywany. Fix: zakomentowanie tej linii.
Re-crawl iter 1: SCORE 67 → 82 (+15 pkt), classification needs-improvement → good. Schema 0 → 15. technical_seo 10 → 16. structure 16 → 21.
Discovery: bug w .htaccess (lesson learned)
Najciekawszy moment dogfood. Po utworzeniu fizycznego public_html/sitemap.xml
request dalej zwracał HTML homepage. Sprawdzenie .htaccess:
RewriteEngine On
RewriteRule ^sitemap.xml$ sitemap.php # ← linia 61
...
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
Linia 61 przekierowywała request sitemap.xml do nieistniejącego sitemap.php.
Apache mod_rewrite robił 404 → catch-all wzorzec (linie 174-176) → index.php → homepage HTML.
Mimo że fizyczny sitemap.xml istniał na dysku, NIGDY nie był serwowany.
Fix: zakomentowanie linii 61 (# DISABLED: was redirecting to non-existent sitemap.php).
Po deploy curl /sitemap.xml zwrócił prawidłowy XML.
Lesson learned dla klientów z legacy stack: static fixes (utworzenie pliku) nie wystarczą —
zawsze sprawdź .htaccess / nginx config / express middleware czy nie ma override dla danego URL.
Curl HTTP 200 + content-type text/html dla pliku XML = mocny sygnał że jest rewrite issue.
Iter 2 (10 min): grammar fix + FAQ section + FAQPage schema
Po iter 1 zostały warnings copy + critical INCOMPLETE_SENTENCES (gramatycznie błędne zdanie). Iter 2 zaadresował:
-
src/strony/start.phpINCOMPLETE_SENTENCES (critical) fix grammar w linii 39: "Jeżeli chcesz uzyskać uprawnienia <a>wychowawcy wypoczynku</a> mozesz <a>kierownika wypocznku</a> zapoznaj się" → "...uprawnienia <a>wychowawcy wypoczynku</a> lub <a>kierownika wypoczynku</a>, zapoznaj się". Plus "młodziezy" → "młodzieżą". Fix Python script preserve quotes.
-
src/strony/start.phpFAQ section dodana po sekcji prose (linia 42), przed Facebook feed. 6 pytań jako <h3>?</h3><p>...</p> (analyzer-friendly struktura): Jakie kursy / Ile trwa / Czy otrzymam certyfikat / Format zajęć / Jak zapisać się / Zniżki. ~600 dodatkowych słów = THIN_CONTENT/SHALLOW_CONTENT znikły.
-
public_html/index.phpFAQPage Schema.org z 6 Q+A pairs (single source of truth z HTML). Conditional renderowane TYLKO gdy $strona == "start" (homepage) — sub-pages mają własne schemas (Service w kursy.php).
Re-crawl iter 2: SCORE 82 → 94 (+12 pkt). Schema 15 → 25 MAX (FAQPage zaadresowało NO_HIGH_VALUE_SCHEMA + NO_QA_STRUCTURE). Structure 21 → 25 MAX (h3 FAQ heading pattern). Semantics 16 → 19 (THIN_CONTENT/SHALLOW_CONTENT znikły). INCOMPLETE_SENTENCES downgrade critical → info.
Iter 3 (15 min): real-world wins z plateau efektem
Po iter 2 (94/100) zostały 4 warnings + 4 info. Iter 3 zaadresowała: FAQ_ANSWER_TOO_LONG
(FAQ skrócony 3× — z 60-80 słów na 30-50), MISSING_DEFINITIONS (4 inline definicje terminów
branżowych: masaż tkanek głębokich, taping medyczno-sportowy,
masaż I/II stopnia, dietetyka kliniczna), AMBIGUOUS_CLAIMS
(FAQ cert: "uznawane przez instytucje branżowe" → konkretna podstawa prawna
Rozporządzenia MEN 2016), MISSING_ETAG + MISSING_LAST_MODIFIED (HTTP headers w
index.php przez filemtime + md5_file), oraz INCOMPLETE_SENTENCES
"Kursy ruszające na 100%" → "Kurs potwierdzony — gwarantowany start w wybranym terminie".
Re-crawl iter 3: SCORE 94 → 93 (-1, semantic plateau). Counter-intuitive wynik — rozszerzona treść (definicje terminów) dała analyzer semantyki więcej materiału do oceny, co odpaliło JARGON_HEAVY warning (catch-22: bez definicji = MISSING_DEFINITIONS, z definicjami = JARGON_HEAVY). Dimensions stabilne (5 z 7 max + technical_seo + semantics nietknięte), tylko semantics 19 → 18.
Lekcja: Selpio score ≠ jedyna metryka wartości
Iter 3 jest najważniejszy edukacyjnie ze wszystkich 4 case studies: pokazuje że score regression może być conscious trade-off, nie failure.
Konkretne real-world wins z iter 3 — niezależnie od Selpio score:
- ETag + Last-Modified headers w PHP responses (nie tylko statycznych) → Google PageSpeed Cache score wzrasta, ChatGPT/Claude crawler wykrywa freshness bez re-fetchowania pełnej strony (oszczędność API calls).
- Inline definicje terminów branżowych → klient szukający "co to masaż tkanek głębokich" lub "taping kinesiology" dostaje odpowiedź od razu na landing, bez klikania w sub-page. Konwersja: użytkownik widzi że strona zna domenę.
- Krótszy FAQ (50-80 słów) vs poprzednio 200-300 słów → mobile users widzą cały FAQ bez scrollowania, ChatGPT cytuje krótsze fragmenty częściej.
- "Kurs potwierdzony — gwarantowany start" vs "Rusza na 100%" → user rozumie co dokładnie 100% oznacza (gwarancja startu, nie wypełnienie listy uczestników).
- Pricing transparency: "wpłata zaliczki 20-30%" w FAQ #5 → zmniejszenie niepewności klienta przed zapisem. Mniej pytań do sekretariatu, więcej zapisów online.
Realistyczny ceiling dla SaaS/marketing landing: 95-97/100. Wyższe score
wymaga albo: (a) przepisania copy w stylu academic prose (sprzeczność z marketing punch),
(b) backend Selpio improvements (np. dodanie EducationalOrganization do whitelist
wyeliminuje UNRECOGNIZED_SCHEMA_TYPE false positive), (c) usunięcia definicji terminów
(regresja edukacyjna wartości za 1 pkt score).
Decyzja klienta: zostawić 93/100 z iter 3 wins (UX, PageSpeed, cytowalność)
zamiast revert do 94 ze starszą wersją treści. Backupy serwera (*.bak-iter3)
zachowane na wypadek gdyby klient zmienił zdanie.
Porównanie dimensions baseline → 3 iteracje
| Wymiar | Baseline | Iter 1 | Iter 2 | Iter 3 | Δ | Max |
|---|---|---|---|---|---|---|
structure | 16 | 21 | 25 | 25 MAX ✓ | +9 | 25 |
schema | 0 | 15 | 25 | 25 MAX ✓ +25 | +25 | 25 |
semantics | 14 | 16 | 19 | 18 | +4 | 25 |
llm_access | 25 | 25 | 25 | 25 MAX ✓ | 0 | 25 |
seo | 17 | 17 | 17 | 17 | 0 | 20 |
authority | 25 | 25 | 25 | 25 MAX ✓ | 0 | 25 |
technical_seo | 10 | 16 | 16 | 16 | +6 | 20 |
| TOTAL | 67 | 82 | 94 | 93 | +26 | 100 |
Workflow dla legacy PHP — bez build, bez deploy automation
Klient nie miał lokalnego repo ani CI/CD. Pełen flow z jednej sesji ssh <client-host>:
- SSH config check —
~/.ssh/configmiał Host alias z User+IdentityFile dla hosta klienta (key auth, bez hasła w plaintext) - Explore structure —
ls domains/kursybp.pl/,cat composer.jsondla stack detection ("ckks-admin2"),grep -rn "<head>"dla template location - Backup pliku przed edycją —
cp index.php index.php.bak-2026-05-24 - Insert JSON-LD przez awk —
awk '/<\/head>/ { insert ... }' index.php > /tmp/new.php && cp /tmp/new.php index.php - Create static files —
cat > robots.txt <<EOF ... EOFdla robots/sitemap/llms - Fix .htaccess — sed comment out linii 61
- Curl verify — od razu po każdym fixie sprawdzasz response
- Re-crawl Selpio — POST /api/analyses → nowy score
Cały proces wykonalny przez agency-tier konsultanta z dostępem SSH klienta. Nie wymaga lokalnego dev environment, nie wymaga zmian w `package.json`, nie wymaga restartowania serwera (mod_rewrite re-reads .htaccess on each request).
Co zostało po 2 iteracjach
- AMBIGUOUS_CLAIMS (warning) — analyzer dalej widzi inne claims jako ogólne (po naprawie grammar dalej są inne sformułowania marketingowe). Można rozszerzyć z konkretami w iter 3.
- MISSING_DEFINITIONS (warning) — niektóre terminy branżowe (np. "kurs doskonalący", "rekreacja ruchowa") bez inline definicji. Można dodać
<abbr title="...">. - FAQ_ANSWER_TOO_LONG (info, false positive ode mnie) — moje answery w FAQ mają 200-300 słów, analyzer woli krótsze (~100). Można skrócić w iter 3.
- MISSING_ETAG / MISSING_LAST_MODIFIED (info) — HTTP headers wymaga konfiguracji Apache (mod_headers w .htaccess).
- INCOMPLETE_SENTENCES (info, downgrade z critical!) — pewnie inne zdanie, mniej krytyczne.
Realistyczny ceiling z iter 3 (copy refinement + krótsze FAQ answers + HTTP headers): 96-98/100.
Lekcje dla klientów agency
- Workflow działa dla każdego stacka — Astro/Next.js/PHP custom/(WordPress/Shopify w przyszłych case studies). Per-issue prompts są stack-agnostic, Claude Code IDE detect stack i mapuje plik.
- SSH direct edit jest legit alternative dla klientów bez lokalnego repo lub bez Git workflow. Backup → edit → verify pattern działa.
- Discovery bugs w infra (.htaccess, nginx, middleware) to standardowa część dogfood — zawsze sprawdź rewrite/redirect rules gdy fizyczny plik daje fake 200.
- +27 pkt w 30 minut dla baseline 60-70 — quick win nawet dla legacy stack bez modern build pipeline.
- Schema.org daje największy single boost — w kursybp schema 0 → 25 to +12 pkt do total score. Sam fix index.php head daje 40% wszystkich gains.
🚀 Wygeneruj raport dla swojej strony Zobacz pozostałe case studies