Blog

Wiedza o CRM, zarządzaniu firmą i technologii dla biznesu

← Wróć do bloga

Jak przenieśliśmy 200 stron na nowy CMS bez utraty pozycji w Google

Michał Stefaniak · 2026-01-15
Jak przenieśliśmy 200 stron na nowy CMS bez utraty pozycji w Google

Siteor to nasz system CMS. Budowaliśmy go latami - klienci stawiali na nim strony firmowe, blogi, landing page’e. Miał swoje API (strony, artykuły, edycja treści), ale z biegiem czasu stawał się ograniczeniem. Treść stron była rozbita na “paragrafy” - osobne bloki tekstu, obrazków, galerii, FAQ. API nie zwracało treści strony jako całości - trzeba było scraping’ować HTML z publicznej strony albo składać paragrafy ręcznie.

Kiedy budowaliśmy Intum - naszą platformę do zarządzania firmą - zdecydowaliśmy się wbudować w nią nowy moduł CMS. Nie fork starego Siteora. Nowy system jako część Intum, z pełnym REST API, szablonami Liquid i automatycznym SEO.

A potem musieliśmy przenieść na niego istniejące strony klientów. Około 200. Na kilkunastu domenach. Bez utraty tego co Google już zaindeksował.

Co mieliśmy do przeniesienia

Strony klientów na starym Siteorze. Strony firmowe z menu i podstronami. Blogi z dziesiątkami artykułów. Landing page’e per produkt i język. Kalkulatory, słowniki pojęć, formularze kontaktowe.

BitFaktura UA miała 90 artykułów blogowych i 100 stron (kalkulatory, integracje, słownik 79 pojęć). InHelp działał w 6 językach. Siteor.net miał ruch organiczny z pozycjami na frazy “kreator stron” i “cms”.

Największy problem techniczny: stary Siteor trzymał treść w paragrafach, nowy CMS ma jedno pole content z HTML. Nie dało się tego przenieść automatycznie 1:1 - API starego systemu zwracało strukturę stron i meta dane, ale nie dawało treści jako jednego bloku. Musieliśmy pobierać renderowany HTML z publicznych URL-i i czyścić go.

Canonical URL - pułapka przy blogach

W starym Siteorze URL artykułu wyglądał tak: /blog/jak-zrobic-fakture. W nowym CMS artykuły renderują się przez stronę-parent (Page z szablonem blog). I tu był problem: canonical wskazywał na /blog zamiast na /blog/jak-zrobic-fakture.

Co to znaczy w praktyce? Google widzi canonical na stronę listingu i myśli: “te wszystkie artykuły to duplicates jednej strony”. Gdybyśmy tego nie wyłapali, stracilibyśmy indeksację kilkudziesięciu artykułów.

Naprawka wymagała zmiany w kodzie CMS - logika generowania canonical musi wiedzieć, że renderuje artykuł (nie stronę) i ustawić canonical na URL artykułu. To samo z og:url. Prosta zmiana w kodzie, ale łatwa do przeoczenia przy migracji.

Structured data - czego stary system nie miał

Stary Siteor nie generował żadnych danych strukturalnych. Zero JSON-LD, zero Open Graph, zero Twitter Card. Każdy klient który chciał mieć schema musiał ręcznie wklejać skrypty w treść strony.

Nowy CMS generuje automatycznie:

  • BlogPosting z tytułem, datą, autorem i wydawcą - dla każdego artykułu
  • Open Graph meta tagi (og:title, og:description, og:image)
  • Twitter Card
  • AudioObject gdy artykuł ma powiązany plik mp3
  • VideoObject gdy artykuł ma film z YouTube

Do artykułu wystarczy dodać audio_url i video_id w polach dodatkowych. System sam generuje schema i meta tagi. BitFaktura UA ma 10 artykułów z audio - po dodaniu jednego pola w JSON, Google od razu widzi AudioObject, a przy udostępnianiu linku na Facebooku pojawia się informacja o audio.

Szablony Liquid zamiast paragrafów

W starym Siteorze zmiana nagłówka strony wymagała edycji layoutu - jednego wielkiego pliku HTML. Treść była rozbita na paragrafy (tekst, obrazek, galeria, formularz) - osobne obiekty w bazie, edytowane przez panel.

Nowy CMS ma trzy warstwy. Layout to owijka (head, nawigacja, footer). Strona (Page) to szablon z tagami Liquid - , {%- if html_lang == "en" -%}

Blog

CRM knowledge, business management, and technology insights

{% if image_url != blank %} {{ title }}
{%- elsif html_lang == "fr" -%}

Blog

Connaissances CRM, gestion d'entreprise et technologies

{% if image_url != blank %} {{ title }}
{%- else -%}

Blog

Wiedza o CRM, zarządzaniu firmą i technologii dla biznesu

{% if image_url != blank %} {{ title }}
{%- endif -%}
, . Artykuł dostarcza dane.

Szablon bloga BitFaktura UA to 150 linii HTML z Tailwind CSS. Ma listing artykułów z miniaturkami, widok artykułu z audio playerem i YouTube embedem, sekcję CTA na dole. Wszystko parametryzowane - zmiana CTA na blogu to edycja jednego pola na stronie-szablonie, nie 90 artykułów.

Tag <cms type="article"> w treści strony mówi systemowi: “tutaj renderuj artykuły”. W środku definiujesz szablon listingu (<list>) i widoku szczegółowego (<show>). System robi resztę - paginacja, routing, canonical.

Multilang - hreflang zamiast kopii stron

W starym Siteorze “wielojęzyczność” polegała na kopiowaniu stron. Strona polska to jedna strona, angielska to druga - bez powiązania. Google nie wiedział że /pricing i /cennik to ta sama treść w dwóch językach.

Nowy CMS ma master/slave pages. Strona polska jest masterem, angielska jest powiązanym tłumaczeniem. System automatycznie generuje tagi hreflang i x-default w <head>. Domeny mogą mieć przypisany locale - intum.pl serwuje po polsku, intum.com po angielsku.

InHelp działa w 6 językach na jednym CMS site. Jedna strona w panelu, sześć wersji, hreflang i canonical generowane automatycznie.

Pola dodatkowe (fields) zamiast kolumn

Każdy projekt ma inne potrzeby. BitFaktura UA chce audio player. InHelp chce CTA z przyciskiem rejestracji. Kolejny klient potrzebuje FAQ schema.

Zamiast dodawać kolumny do bazy per potrzebę, artykuły mają pole fields - JSON, w który szablon wrzuca co potrzebuje. System czyta te dane: audio_url generuje og:audio i AudioObject, video_id generuje og:video i VideoObject.

Większość artykułów ma puste fields. I to dobrze - to metadane, nie obowiązkowe pola. Nie zaśmiecaj ich duplikatami danych które już są w kolumnach modelu (tytuł, autor, data).

API - migracja programowa

Największa różnica: pełne REST API na wszystko. Strony, artykuły, layouty, assety, domeny. Stary Siteor miał API, ale ograniczone - strony bez treści (paragrafy osobno), brak endpointu do listowania paragrafów, edycja przez contenteditable endpoint.

Przy migracji pisaliśmy skrypty. Pobierz listę stron ze starego Siteora (API), pobierz renderowany HTML (HTTP), wyczyść, utwórz stronę w nowym CMS (API). Nie idealne - paragrafy trzeba było składać ręcznie - ale automatyzacja zaoszczędziła mnóstwo godzin.

API przydaje się na co dzień. Batch update meta tagów na 50 stronach? Pętla z PATCH. Dodanie audio do 10 artykułów? Jeden skrypt. Czyszczenie śmieciowych fields na 30 artykułach? Minuta.

Monitoring w GSC

Przed każdą migracją - baseline. Pozycje, kliknięcia, zaindeksowane strony w Google Search Console. Po migracji sprawdzanie codziennie.

Kilka razy złapaliśmy problemy: brakujący canonical, redirect na złą stronę, sitemap ze starymi URL-ami. Złapane zanim Google zdążył zareagować.

Kluczowa zasada: migruj domenę po domenie. Nie przenoś wszystkiego na raz. Jak coś się posypie na jednej domenie, reszta dalej działa.

Co byśmy zrobili inaczej

Wcześniej zaczęli od structured data. Pierwsze migracje nie miały BlogPosting schema - dodaliśmy dopiero po kilku tygodniach.

Lepiej planowali redirecty 301. Część starych URL-i miała inną strukturę. Konfiguracja na kilkunastu domenach to sporo ręcznej pracy.

Od razu trzymali fields minimalne. Przy pierwszych migracjach wrzucaliśmy do fields JSON wszystko co mogło się przydać - tłumaczenia, duplikaty danych z kolumn, klucze których szablon nie czytał. Potem musieliśmy to czyścić na 30 artykułach. Lepiej od początku wrzucać minimum.

I najważniejsze: nie odkładali migracji. Im dłużej stary system stoi, tym więcej treści się nazbiera i tym trudniejsza będzie przeprowadzka.

← Wróć do bloga