Witryna oparta na WordPressie z Elementorem — wolne ładowanie, słabe Core Web Vitals, brak kontroli nad strukturą HTML i JSON-LD, rosnące trudności z obsługą wielojęzyczności. Brzmi znajomo. W przypadku trimsandfasteners.com, producenta i dystrybutora zamków błyskawicznych i pasmanteryjnych na rynki EN i PL, te problemy przekładały się bezpośrednio na słabą indeksację i ograniczone możliwości dalszego rozwoju serwisu.

Celem projektu taf-next było oddzielenie warstwy prezentacji od backendu CMS, przejęcie pełnej kontroli nad performancem i SEO, przy jednoczesnym zachowaniu całej treści zarządzanej w WordPressie przez redakcję. Poniżej opisuję dokładnie, co zbudowaliśmy i jakie decyzje techniczne podjęliśmy.

Architektura: Headless WordPress + Next.js App Router

WordPress na subdomenie wp.trimsandfasteners.com pełni wyłącznie rolę backendu — przechowuje treści, taksonomie, produkty i media. Dane pobierane są przez własne endpointy REST API (/wp-json/taf/v1/...) oraz standardowe WP REST API. Frontend zbudowany w Next.js 15 z App Routerem nie ma żadnego bezpośredniego kontaktu z bazą danych ani silnikiem PHP WordPress.

Warstwa Technologia Rola
FrontendNext.js 15 App Router + TypeScriptRendering, routing, SEO, interakcje
StylowanieTailwind CSSZero runtime CSS
CMS (backend)WordPress REST API + PolylangZarządzanie treścią, tłumaczenia
Wielojęzycznośćnext-intl + PolylangRouting EN/PL, locale detection
HostingVercel Edge NetworkCDN, ISR, edge middleware
SEOJSON-LD (FAQPage), OpenGraph, SitemapStructured data, indeksacja

Model renderowania — SSG, ISR i Server Components

Wybór trybu renderowania per-route to jedna z kluczowych decyzji w projekcie Next.js. Tu zastosowaliśmy trzy podejścia w zależności od charakteru danych:

SSG (Static Site Generation) dla stron kategorii, podkategorii i wpisów blogowych. Strony generowane w całości przy buildzie, serwowane z CDN Vercel bez żadnego opóźnienia serwerowego — TTFB poniżej 100ms globalnie. Idealny tryb dla treści, które zmieniają się rzadko, a są odwiedzane najczęściej.

ISR (Incremental Static Regeneration) dla danych dynamicznych — automatyczne odświeżanie cache bez pełnego rebuildu. Kiedy redaktor zaktualizuje opis produktu w WordPressie, zmiana pojawi się na froncie po ustalonym interwale bez konieczności triggerowania nowego deploymentu.

Server Components jako domyślny model renderowania — JavaScript wysyłany do przeglądarki ograniczony wyłącznie do interaktywnych wysp: popupy produktowe, FAQ accordion, filtry. Reszta strony nie wysyła żadnego JS do klienta — to bezpośrednie przełożenie na niższe INP i szybszy Time to Interactive.

Wielojęzyczność z Polylang — locale-aware routing

Witryna działa w dwóch językach (EN/PL) z pełną segmentacją URL: / dla angielskiego i /pl/ dla polskiego. Implementacja wielojęzyczności w headless WordPress z Polylangiem ma jedną nieoczywistą pułapkę: Polylang przydziela różne Post ID dla tłumaczeń — angielska wersja posta i polska wersja to osobne rekordy w bazie, powiązane relacją.

Wymagało to osobnej obsługi mapowania między wersjami językowymi przy każdym zapytaniu do API. Zaimplementowano [locale] dynamic segment z middleware zarządzającym detekcją języka — przeglądarka bez preferencji językowej dostaje EN, request z nagłówkiem Accept-Language: pl jest automatycznie przekierowany do /pl/.

Interaktywny katalog produktów — Zipper Popup z in-memory cache

Kluczowa funkcja serwisu: kliknięcie zamka błyskawicznego w katalogu otwiera modal z pełnymi danymi technicznymi. To jednocześnie najważniejszy element interaktywny i największe wyzwanie wydajnościowe — modal musi się otwierać natychmiast, a dane muszą być poprawne językowo.

Rozwiązanie składa się z trzech warstw:

In-memory cache po stronie klienta z kluczem ${productId}-${locale} — pierwsze otwarcie popupu wykonuje fetch do API route, kolejne są natychmiastowe z pamięci. Cache rozróżnia wersje językowe, eliminując problem serwowania treści EN po przełączeniu języka na PL.

Locale-aware API route (/api/zipper/[id]?lang=pl) z parametrem lang przekazywanym do endpointu WP. Route działa jako proxy między frontendem a WordPress — ukrywa strukturę backendu, umożliwia cache'owanie odpowiedzi i dodaje warstwę transformacji danych.

PL Override Map — dla 13 produktów z kategorii nylon Polylang nie posiadał przetłumaczonych opisów w bazie WP. Zamiast ingerować w bazę danych klienta, stworzono plik src/lib/pl-zipper-overrides.ts z polskimi tłumaczeniami aplikowanymi po stronie Next.js API route. Zmiana izolowana całkowicie po stronie frontendu — backend WP nienaruszony.

FAQ Accordion i JSON-LD Schema — structured data, której Elementor nie dawał

W poprzedniej implementacji Elementor generował FAQ accordion jako widget, ale nie emitował żadnych structured data. To oznaczało, że Google crawlował pytania i odpowiedzi jako zwykły tekst, bez możliwości wyświetlenia ich jako rich result w SERP.

Po migracji na Next.js:

  • Komponent FaqAccordion z animowanym rozwijaniem odpowiedzi zbudowany jako Client Component z useState.
  • Automatyczne generowanie schematu FAQPage według specyfikacji Schema.org osadzanego jako <script type="application/ld+json"> w initial HTML.
  • Pomimo że FaqAccordion jest Client Component ('use client'), Next.js App Router wykonuje SSR również dla komponentów klienckich — JSON-LD jest obecny w source przy pierwszym załadowaniu strony, indeksowalny przez Googlebot bez JavaScript.
  • Ten sam pattern zastosowano w komponencie PersonalizationContent na stronach personalizacji.

Przykładowa struktura emitowanego JSON-LD:

{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": [
    {
      "@type": "Question",
      "name": "What are coil (nylon) zippers...",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Coil zippers, also known as nylon zippers..."
      }
    }
  ]
}

Każda migracja URL-i to ryzyko utraty pozycji. Wszystkie historyczne URL-e z WordPressa zmigrowały do nowej struktury z zachowaniem wartości SEO:

Stary URL Nowy URL Kod
/coil-zippers-military//blog/coil-zippers-military/301
/nylon-zippers-for-sportswear//blog/nylon-zippers-for-sportswear/301
/zippers-in-the-furniture-industry//301
/blog/zippers-in-the-furniture-industry//301
/pl/blog/zamki-blyskawiczne-w-branzy-meblarskiej//pl/301

Przekierowania zdefiniowane w next.config.ts — obsługiwane na poziomie CDN Vercel przed trafieniem requestu do aplikacji. Zero narzutu serwerowego, zero konfiguracji po stronie WordPress, zero ryzyka zepsucia przy aktualizacjach WP.

Strategia serwowania obrazów — świadoma decyzja architektoniczna

Obrazy produktowe serwowane są bezpośrednio z origin (wp.trimsandfasteners.com), z pominięciem pośredniej transformacji po stronie CDN. To nie było zaniedbanie — to świadoma decyzja.

WordPress dostarcza pliki już w odpowiednio skompresowanych formatach (WebP/JPEG), a dodatkowy processing layer na ścieżce krytycznej popupów wyłącznie wydłużałby czas odpowiedzi. Zipper Popup to interakcja wrażliwa na latency — użytkownik oczekuje natychmiastowego otwarcia modala. Eliminacja zbędnej warstwy pośredniej redukuje liczbę hopów na ścieżce obrazu i skraca czas odpowiedzi przy otwieraniu modali produktowych.

Skonfigurowano remotePatterns w next.config.ts obejmując cały host WP, co umożliwiło ładowanie ikon aplikacji i mediów z różnych lokalizacji na serwerze.

Wyniki po wdrożeniu

Obszar Przed (WP + Elementor) Po (Next.js Headless)
TTFB stron kategoriiPHP + Elementor, zależny od serwera<100ms globalnie (CDN Edge)
Structured data (FAQ)Brak — Elementor widget bez JSON-LDFAQPage JSON-LD w initial HTML
JavaScript do przeglądarkiElementor bundle + jQueryTylko interaktywne wyspy (popup, FAQ)
WielojęzycznośćPolylang + WPML = złożoność w WPnext-intl + locale-aware routing
Indeksacja (GSC)Słaba — CSR Elementora niewidoczny dla botaZnaczący wzrost po migracji
CLSNiekontrolowany (Elementor layout shifts)CLS = 0 (statyczne SSG)

Po migracji z WordPress+Elementor na Next.js zanotowano znaczący wzrost indeksacji serwisu w Google Search Console. Strony wcześniej słabo indeksowalne — z powodu client-side renderingu Elementora i całkowitego braku structured data — zaczęły być poprawnie crawlowane i zaindeksowane. Przyczyniły się do tego przede wszystkim:

  • Czysty, semantyczny HTML generowany przez React Server Components — Googlebot dostaje gotowy markup bez potrzeby uruchamiania JavaScript.
  • JSON-LD structured data (FAQ schema) emitowane w initial HTML — wcześniej całkowicie nieobecne w implementacji Elementor.
  • Poprawna struktura URL po wdrożeniu przekierowań 301 — konsolidacja link equity ze starych URL-i do nowych.
  • Core Web Vitals w zielonych progach — LCP, CLS i INP w normie dzięki statycznemu generowaniu i eliminacji Elementor JS.

Wnioski — co daje headless dla serwisu B2B

trimsandfasteners.com to typowy serwis B2B w niszy produkcyjnej — katalog produktów, content marketing po angielsku i polsku, formularz zapytania. Dokładnie tam, gdzie klasyczny WordPress z Elementorem ma strukturalne ograniczenia, a headless Next.js daje największą przewagę:

  1. Structured data jako przewaga SEO. Elementor generuje widgety, nie schematy. Next.js daje pełną kontrolę nad każdym <script type="application/ld+json"> — FAQ, Product, BreadcrumbList — wszystkie w initial HTML, wszystkie indeksowalne.
  2. Wielojęzyczność bez kompromisów. Locale-aware routing z next-intl + middleware to rozwiązanie, które skaluje się bez wtyczek premium. Polylang po stronie WP zarządza treścią, next-intl zarządza routingiem — każda warstwa robi swoje.
  3. SSG jako fundament wydajności. Strony kategorii zamków błyskawicznych generowane raz, serwowane dla każdego kolejnego użytkownika z CDN — TTFB niezależny od obciążenia serwera WP.
  4. Izolacja frontendu od backendu. PL Override Map to idealny przykład — problem z tłumaczeniami rozwiązany po stronie Next.js bez dotykania bazy WP. Klient zarządza treścią w znajomym panelu, front ewoluuje niezależnie.

Migracja dała klientowi to, czego Elementor nie był w stanie zaoferować: pełną kontrolę nad rendered output, mierzalne wyniki SEO i fundamenty pod dalszy rozwój — nowe landing pages, integracje i funkcje bez dotykania WordPressa.