Przejdź do treści

Landing page pod Google Ads w Next.js i wysoka konwersja

Poznaj techniczne kulisy budowania landing page w Next.js. Zobacz, jak utrzymać atrybucję kampanii, poprawnie wdrożyć consent i zwiększyć skuteczność kampanii Google Ads.

Maciej Sala

Founder StriveLab

12 min czytaniaOpublikowano 31 grudnia 2025 (Aktualizacja 1 sierpnia 2026)

Quality Score jest narzędziem diagnostycznym, a nie KPI kampanii ani bezpośrednim składnikiem aukcji. Google wylicza go w skali od 1 do 10 na poziomie zapytania na podstawie oczekiwanego CTR, trafności reklamy i . Oceny tych obszarów w czasie aukcji są odrębne od liczby widocznej w panelu. Optymalizuj stronę pod użytkownika i wynik biznesowy, zamiast próbować mechanicznie podnieść Quality Score.

Jako deweloper masz bezpośredni wpływ na sporą część tego układu: szybkość ładowania, wygodę na urządzeniach mobilnych, message match, formularz, third-party scripts oraz to, czy przekierowania nie zgubią gclid ani UTM-ów.

Co Google ocenia w Landing Page Experience?

Google nie publikuje dokładnego algorytmu, ale możemy dowiedzieć się sporo z ogólnych wskazówek, które znajdziemy w oficjalnej dokumentacji:

  1. Treść zgodna z intencją użytkownika. Strona musi odpowiadać na to, czego użytkownik spodziewa się po reklamie i zapytaniu.

  2. Prosta nawigacja do właściwej informacji. Użytkownik powinien szybko znaleźć ofertę, warunki, cenę oraz sposób kontaktu lub zakupu.

  3. Spójność reklamy ze stroną docelową. Nagłówek, oferta, cena, warunki i CTA powinny realizować obietnicę widoczną przed kliknięciem.

  4. Przejrzyste informacje budujące zaufanie. Pokaż dane firmy, kontakt, politykę prywatności oraz jasne warunki bez interfejsów wymuszających wybór.

  5. Sprawne działanie na urządzeniach mobilnych. Widok powinien być czytelny, stabilny i responsywny. Core Web Vitals nie są osobnym komponentem Quality Score, lecz pomagają kontrolować techniczną jakość .

Na koniec dochodzi higiena techniczna. Strona musi działać dla Google AdsBot i użytkowników, nie może zwracać błędów HTTP, a przekierowania nie powinny gubić parametrów. Domena finalnego miejsca docelowego musi odpowiadać domenie widocznej w reklamie. Niedziałający adres, przekierowanie do innej domeny lub rozbieżność między adresem wyświetlanym i finalnym mogą doprowadzić do odrzucenia reklamy.

Architektura landing page w Next.js pod Google Ads

Static Generation jako najlepsza opcja dla landing page

Jeśli landing page nie wymaga personalizacji w czasie rzeczywistym, albo będą zwykle dobrym punktem wyjścia. Statyczny wynik ułatwia użycie cache i ogranicza pracę wykonywaną dla każdego żądania. Nie gwarantuje jednak niskiego TTFB ani dobrych Core Web Vitals, ponieważ wynik zależy też od hostingu, konfiguracji cache, dokumentu i zasobów klientowych.

app/lp/[slug]/page.tsx
Code
// app/lp/[slug]/page.tsx
import type { Metadata } from "next";
import { getAllLandingPages, getLandingPage } from "@/lib/cms";
import { LandingPageContent } from "./LandingPageContent";
 
// Statyczna generacja
export async function generateStaticParams() {
  const pages = await getAllLandingPages();
  return pages.map((page) => ({ slug: page.slug }));
}
 
export async function generateMetadata({
  params,
}: {
  params: Promise<{ slug: string }>;
}): Promise<Metadata> {
  const { slug } = await params;
  const page = await getLandingPage(slug);
 
  return {
    title: page.metaTitle,
    description: page.metaDescription,
    robots: {
      index: page.indexable ?? false,
      follow: true,
    },
    alternates: {
      canonical: page.canonicalUrl,
    },
  };
}
 
export default async function LandingPage({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const page = await getLandingPage(slug);
 
  return <LandingPageContent page={page} />;
}

Dlaczego landing page z kampanii może mieć noindex?

noindex może mieć sens, gdy landing page powiela inną stronę albo nie powinna pojawiać się w wynikach organicznych. Przy równoważnych wariantach testu Google zaleca wskazanie głównego adresu przez rel="canonical" zamiast automatycznego dodawania noindex. Pełnoprawnej oferty nie wykluczaj automatycznie z indeksowania, jeśli ma unikatową treść i realizuje również cel organiczny. Sam noindex nie poprawia skuteczności Google Ads.

Renderowanie zależne od żądania bywa uzasadnione, jeśli musisz pokazać dynamiczną cenę, stan magazynowy, lokalizację, język, wymagania dla danego kraju albo personalizację segmentu. Taką odpowiedź również można częściowo buforować. Liczy się szybkie i przewidywalne działanie, a nie statyczny tryb wybrany bez związku z wymaganiami.

Optymalizacja wydajności landing page: lista kontrolna

Progi 2,5 sekundy dla LCP, 200 ms dla INP i 0,1 dla CLS oznaczają ocenę „good” w danych terenowych na 75. percentylu. Analizuj osobno urządzenia mobilne i komputery. Pojedynczy test Lighthouse jest diagnostyką laboratoryjną, a nie zamiennikiem danych RUM lub CrUX.

LCP do 2,5 sekundy

Code
// 1. Hero image jako element LCP
import Image from "next/image";
 
<Image
  src="/lp/hero-product.webp"
  alt="Produkt XYZ"
  width={1200}
  height={600}
  preload
  sizes="100vw"
  quality={75}
/>;
 
// 2. next/font obsługuje preload fontów automatycznie
import { Inter } from "next/font/google";
 
const inter = Inter({
  subsets: ["latin", "latin-ext"],
  display: "swap",
});

W Next.js 16 właściwość priority jest przestarzała na rzecz preload. Stosuj ją tylko wtedy, gdy konkretny obraz jest elementem LCP niezależnie od typowego wariantu widoku. Nie łącz preload z loading ani fetchPriority na tym samym obrazie. Jeśli ustawisz quality={85}, wartość 85 musi znajdować się w images.qualities w konfiguracji Next.js 16, dlatego przykład używa domyślnego 75.

CLS do 0,1

Code
// 1. Zawsze podawaj wymiary obrazków
<Image width={400} height={300} ... />
 
// 2. Zarezerwuj miejsce dla dynamicznych elementów w układzie
<div className="min-h-[120px]">
  <DynamicOffer />
</div>
 
// 3. Font display swap + size-adjust
import { Inter } from "next/font/google";
 
const font = Inter({
  subsets: ["latin"],
  display: "swap",
  adjustFontFallback: true, // Next.js automatycznie dopasuje fallback
});

INP do 200 ms

Code
// components/lp/DeferredSections.tsx
"use client";
 
import { useEffect, useRef, useState } from "react";
import dynamic from "next/dynamic";
 
const Testimonials = dynamic(() => import("./Testimonials"), {
  loading: () => <div className="h-[400px]" />,
});
 
const FAQ = dynamic(() => import("./FAQ"), {
  loading: () => <div className="h-[300px]" />,
});
 
// 1. Załaduj sekcje dopiero tuż przed wejściem w obszar widoczny
export function DeferredSections() {
  const [shouldRender, setShouldRender] = useState(false);
  const markerRef = useRef<HTMLDivElement>(null);
 
  useEffect(() => {
    const marker = markerRef.current;
    if (!marker) return;
 
    const observer = new IntersectionObserver(([entry]) => {
      if (entry.isIntersecting) {
        setShouldRender(true);
        observer.disconnect();
      }
    }, { rootMargin: "200px" });
 
    observer.observe(marker);
    return () => observer.disconnect();
  }, []);
 
  return (
    <div ref={markerRef}>
      {shouldRender && <><Testimonials /><FAQ /></>}
    </div>
  );
}
 
// 2. Nie wrzucaj ciężkiej pracy do handlera submit
// ❌ Źle
const handleSubmit = async () => {
  const validated = heavyValidation(formData); // blokuje main thread
  await sendForm(validated);
};
 
// Dobrze: handler jest lekki, cięższa walidacja trafia na serwer
const handleSubmit = async () => {
  setLoading(true);
  await fetch("/api/leads", {
    method: "POST",
    body: formData,
  });
  setLoading(false);
};

Samo async/await nie sprawia magicznie, że kod przestaje blokować główny wątek. Jeśli ciężka walidacja nadal wykonuje się synchronicznie w JS po stronie klienta, i tak na tym ucierpi. Jeśli naprawdę musisz robić ciężką pracę w przeglądarce, rozważ Web Workera.

Third-party scripts bez zabijania wydajności

Code
import Script from "next/script";
 
// Google tag dla świadomie wybranego advanced consent mode
<Script
  src="https://www.googletagmanager.com/gtag/js?id=AW-XXX"
  strategy="afterInteractive"
/>
 
<Script id="google-tag-config" strategy="afterInteractive">
  {`
    gtag('js', new Date());
    gtag('config', 'AW-XXX');
  `}
</Script>
 
// Widget supportowy, który może poczekać
<Script
  src="https://widget.intercom.io/widget/xxx"
  strategy="lazyOnload"
/>

Przykład zakłada, że funkcja gtag i domyślny stan consent powstały wcześniej. W advanced consent mode tag Google może zostać załadowany przy domyślnych wartościach denied i wysyłać sygnały bez identyfikatorów cookie zgodnie z ustawieniami produktu. W basic consent mode zablokuj tag do czasu zgody. Nie kopiuj obu wariantów jednocześnie i nie pozwól, aby osobna integracja GTM uruchomiła drugi tag.

Consent Mode nie jest banerem zgody i nie rozstrzyga wymagań prawnych. Przekazuje tagom Google decyzję zapisaną przez CMP. Wersja druga rozszerzyła istniejące sygnały o ad_user_data i ad_personalization; konfiguracja reklam i analityki zwykle obejmuje też ad_storage oraz analytics_storage.

Jeżeli świadomie wybierasz advanced consent mode, ustaw wartości domyślne przed komendami config i event. W App Routerze taki skrypt umieść w głównym layoucie. Poniższy fragment pokazuje kolejność, ale nadal wymaga połączenia z CMP:

app/layout.tsx
Code
import Script from 'next/script'
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="pl">
      <body>
        {children}
        <Script id="consent-defaults" strategy="beforeInteractive">
          {`
            window.dataLayer = window.dataLayer || [];
            function gtag(){dataLayer.push(arguments);}
            gtag('consent', 'default', {
              ad_storage: 'denied',
              analytics_storage: 'denied',
              ad_user_data: 'denied',
              ad_personalization: 'denied',
              wait_for_update: 500
            });
          `}
        </Script>
      </body>
    </html>
  )
}

Po wyborze użytkownika CMP powinien wywołać gtag('consent', 'update', {...}) na tej samej stronie przed nawigacją. Musi też zapisać wybór, odtworzyć właściwy stan podczas kolejnej wizyty i umożliwić jego zmianę. wait_for_update jest krótkim buforem dla asynchronicznego CMP, a nie zgodą ani trwałym magazynem preferencji. W basic consent mode tagi Google pozostają zablokowane do czasu uzyskania zgody, więc sam powyższy fragment nie implementuje tego wariantu.

Struktura landing page, która konwertuje

Above the fold, czyli pierwszy ekran landing page

Code
// components/lp/HeroSection.tsx
export function HeroSection({ page }: { page: LandingPage }) {
  return (
    <section className="px-4 py-16 md:px-8 md:py-24">
      {/* 1. Headline odpowiadający na intencję reklamy */}
      <h1 className="text-4xl md:text-5xl font-bold">
        {page.headline}
      </h1>
 
      {/* 2. Subheadline z value proposition */}
      <p className="text-xl text-gray-600 mt-4 max-w-2xl">
        {page.subheadline}
      </p>
 
      {/* 3. CTA widoczny od razu */}
      <div className="mt-8 flex gap-4">
        <a
          href="#formularz"
          className="bg-blue-600 text-white px-8 py-4 rounded-lg text-lg font-semibold"
          data-testid="hero-cta"
        >
          {page.ctaText}
        </a>
      </div>
 
      {/* 4. Dowód społeczny pokazuj tylko z prawdziwego źródła */}
      {page.reviewSummary && (
        <p className="mt-6 text-sm text-gray-600">{page.reviewSummary}</p>
      )}
    </section>
  );
}

Nie ustawiaj min-h-screen wyłącznie po to, aby hero wypełniało ekran. Na niższym urządzeniu może to odsunąć argumenty i formularz bez korzyści dla użytkownika. Dowód społeczny pokazuj wyłącznie wtedy, gdy pochodzi z prawdziwego źródła, jest aktualny i można wyjaśnić sposób obliczenia oceny.

Message match między reklamą i stroną

Message match to spójność między tekstem reklamy a treścią landing page. Jeśli reklama mówi „Tanie ubezpieczenie OC online”, a strona ma nagłówek „Kompleksowe rozwiązania ubezpieczeniowe”, użytkownik nie otrzymuje szybkiego potwierdzenia, że trafił we właściwe miejsce.

Code
Reklama: "Kurs React dla początkujących, start w 7 dni"
     ↓
LP headline: "Naucz się React od zera w 7 dni"  ← ✅ silny message match
LP headline: "Akademia programowania online"      ← ❌ kiepski message match

Jeżeli grupy zapytań reprezentują różne potrzeby, przygotuj osobne warianty dla różnych intencji. Nie oznacza to automatycznie jednej strony na każde hasło. Łącz zapytania, które mogą otrzymać tę samą uczciwą obietnicę, ofertę i CTA.

Formularz z niskim oporem przed wypełnieniem

components/lp/LeadForm.tsx
Code
// components/lp/LeadForm.tsx
"use client";
 
import { useRef, useState } from "react";
import { analytics } from "@/lib/analytics";
import { adsConversions } from "@/lib/ads-tracking";
 
export function LeadForm({ formId }: { formId: string }) {
  const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle");
  const submissionIdRef = useRef<string | null>(null);
 
  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    setStatus("loading");
 
    const formData = new FormData(e.currentTarget);
    const submissionId = submissionIdRef.current ?? crypto.randomUUID();
    submissionIdRef.current = submissionId;
 
    try {
      const response = await fetch("/api/leads", {
        method: "POST",
        body: formData,
        headers: {
          "Idempotency-Key": submissionId,
        },
      });
 
      if (!response.ok) {
        throw new Error("Request failed");
      }
 
      setStatus("success");
 
      // Rejestruj konwersję dopiero po potwierdzeniu zapisu przez backend
      adsConversions.lead(formId);
      analytics.formSubmit(formId, true);
    } catch {
      setStatus("error");
      analytics.formSubmit(formId, false);
    }
  };
 
  if (status === "success") {
    return (
      <div className="text-center p-8" role="status">
        <h3 className="text-2xl font-bold text-green-600">Dziękujemy!</h3>
        <p>Skontaktujemy się w ciągu 24 godzin.</p>
      </div>
    );
  }
 
  return (
    <form onSubmit={handleSubmit} id="formularz" className="space-y-4">
      {/* Zbieraj wyłącznie dane potrzebne na tym etapie */}
      <div>
        <label htmlFor="email" className="block text-sm font-medium">
          Email *
        </label>
        <input
          id="email"
          name="email"
          type="email"
          required
          autoComplete="email"
          className="w-full p-3 border rounded-lg"
        />
      </div>
 
      <div>
        <label htmlFor="phone" className="block text-sm font-medium">
          Telefon
        </label>
        <input
          id="phone"
          name="phone"
          type="tel"
          autoComplete="tel"
          className="w-full p-3 border rounded-lg"
        />
      </div>
 
      <button
        type="submit"
        disabled={status === "loading"}
        className="w-full bg-blue-600 text-white p-4 rounded-lg text-lg font-semibold"
      >
        {status === "loading" ? "Wysyłam..." : "Wyślij zapytanie"}
      </button>
 
      {status === "error" && (
        <p className="text-red-600 text-sm" role="alert">
          Wystąpił błąd. Spróbuj ponownie.
        </p>
      )}
    </form>
  );
}

Zbieraj dane potrzebne na danym etapie, ponieważ każde dodatkowe pole może zwiększać opór. Zbyt krótki formularz może jednak obniżyć jakość leadów lub przenieść pracę kwalifikacyjną na zespół sprzedaży. Zakres pól testuj na pełnym lejku, uwzględniając sprzedaż, odrzucenia i czas obsługi.

Do tego dochodzą rzeczy, których często brakuje w pierwszej wersji:

  • walidacja i sanityzacja po stronie serwera,
  • ochrona antyspamowa (honeypot, rate limit, captcha jeśli trzeba),
  • idempotency key albo inny mechanizm chroniący przed podwójnym zapisem leada,
  • jasna informacja o polityce prywatności i zgodzie marketingowej, jeśli jest wymagana,
  • jeden główny CTA.

Endpoint formularza powinien działać po stronie serwera, na przykład w Route Handlerze /api/leads. Dzięki temu sekrety CRM-u nie trafiają do przeglądarki, a backend może wykonać walidację, ograniczenie liczby żądań i deduplikację.

app/api/leads/route.ts
Code
import { NextRequest, NextResponse } from 'next/server'
import { leadSchema } from '@/lib/leads/schema'
import {
  checkRateLimit,
  createLeadOnce,
  isAllowedOrigin,
} from '@/lib/leads/server'
 
export async function POST(request: NextRequest) {
  if (!isAllowedOrigin(request)) {
    return NextResponse.json({ error: 'Invalid origin' }, { status: 403 })
  }
 
  const rateLimit = await checkRateLimit(request)
  if (!rateLimit.allowed) {
    return NextResponse.json({ error: 'Too many requests' }, { status: 429 })
  }
 
  const submissionId = request.headers.get('idempotency-key')
  if (!submissionId) {
    return NextResponse.json(
      { error: 'Missing submission ID' },
      { status: 400 },
    )
  }
 
  const parsed = leadSchema.safeParse(
    Object.fromEntries(await request.formData()),
  )
 
  if (!parsed.success) {
    return NextResponse.json({ error: 'Invalid form data' }, { status: 400 })
  }
 
  await createLeadOnce({ submissionId, data: parsed.data })
  return NextResponse.json({ accepted: true }, { status: 202 })
}

Funkcje z @/lib/leads/server są warstwą zależną od Twojej infrastruktury. createLeadOnce() musi mieć unikalne ograniczenie w bazie albo kolejce, a rate limit powinien korzystać ze współdzielonego magazynu, jeśli aplikacja działa na wielu instancjach. Sprawdzenie nagłówka Origin ogranicza część żądań z obcych witryn, ale nie zastępuje ochrony antyspamowej. Captcha może być dodatkową warstwą uruchamianą po wykryciu ryzyka, zamiast obowiązkowym tarciem dla każdego.

Odpowiedź 2xx powinna oznaczać, że lead został trwale przyjęty albo bezpiecznie zapisany do kolejki. Dopiero wtedy wysyłaj zdarzenie konwersji. Przy zakupach i innych zdarzeniach z identyfikatorem przekaż do Google Ads unikalny transaction_id, aby ograniczyć duplikaty.

Atrybucja na landing page: identyfikatory kliknięcia i UTM

Landing page musi być dobry analitycznie. Klik z reklamy może zawierać gclid, a w części scenariuszy również gbraid lub wbraid. Adres może też zawierać ręczne parametry utm_source, utm_campaign i inne UTM-y. Przy włączonym automatycznym tagowaniu oraz poprawnie wdrożonym tagu Google dane kliknięcia mogą zostać zapisane w cookie pierwszej strony dla pomiaru Google Ads. Identyfikatory i UTM-y zatrzymuj we własnym przepływie tylko wtedy, gdy mają trafić do CRM-u, importu konwersji offline albo własnych raportów.

Podstawowy przepływ obejmuje cztery elementy:

  1. Włącz auto-tagging i sprawdź, czy landing page akceptuje nieznane parametry URL oraz czy przekierowania ich nie usuwają.
  2. Zapisz potrzebne identyfikatory kliknięcia i UTM-y w polach formularza lub kontrolowanym magazynie pierwszej strony, jeśli mają trafić do CRM-u albo własnych raportów.
  3. Konwersję rejestruj po potwierdzeniu trwałego przyjęcia formularza, a nie po samym kliknięciu przycisku.
  4. Przy przejściu między domenami skonfiguruj linker dla obu domen. Nie wystarczy ręcznie skopiować samego gclid.
Code
'use client'
 
import { useSearchParams } from 'next/navigation'
 
export function AttributionFields() {
  const searchParams = useSearchParams()
 
  return (
    <>
      {['gclid', 'gbraid', 'wbraid'].map((name) => (
        <input
          key={name}
          type="hidden"
          name={name}
          value={searchParams.get(name) ?? ''}
        />
      ))}
      <input
        type="hidden"
        name="utm_source"
        value={searchParams.get('utm_source') ?? ''}
      />
      <input
        type="hidden"
        name="utm_campaign"
        value={searchParams.get('utm_campaign') ?? ''}
      />
    </>
  )
}

Powyższy komponent odczytuje tylko parametry z aktualnego URL-a. Jeśli formularz jest wieloetapowy albo użytkownik może nawigować przed konwersją, zapisz potrzebne wartości po wejściu i powiąż je po stronie serwera z rekordem leada. Sposób przechowywania musi respektować consent oraz politykę prywatności. W statycznie generowanej trasie umieść komponent używający useSearchParams pod granicą Suspense, ponieważ bez niej produkcyjny build App Routera zakończy się błędem.

Parametry adresu są danymi wejściowymi kontrolowanymi przez użytkownika. Ogranicz ich długość, akceptuj oczekiwany format i nie używaj ich do autoryzacji ani decyzji o cenie. Zachowaj też pierwotną wartość oddzielnie od późniejszych zmian, jeśli model atrybucji wymaga rozróżnienia pierwszego i ostatniego kontaktu.

W nowych wdrożeniach Google zaleca enhanced conversions for leads i Data Manager. Sam gclid nadal może być kluczem dopasowania, ale można go uzupełnić prawidłowo zebranymi i zahashowanymi danymi własnymi. Od 15 czerwca 2026 roku nowe przesyłanie konwersji offline oraz enhanced conversions for leads powinno korzystać z Data Manager API; dotychczasowy import przez Google Ads API jest blokowany poza ograniczonym dostępem przejściowym. Sprawdź istniejący pipeline, zamiast zakładać, że starsza integracja nadal działa.

Przesyłanie danych własnych wymaga zgodności z zasadami Google, właściwej podstawy przetwarzania, poprawnej normalizacji i hashowania oraz przekazania stanu ad_user_data. Nie wysyłaj pól, których Google nie wymaga, ani danych bez udokumentowanego celu i retencji.

Konwersja formularza a jakość leada

Samo wysłanie formularza jest sygnałem pośrednim. Jeżeli część zgłoszeń stanowi spam, pomyłki albo kontakty spoza grupy docelowej, strategia stawek może optymalizować kampanię w kierunku tanich, lecz bezwartościowych formularzy.

Rozdziel etapy całego lejka sprzedaży i nadaj im stabilne identyfikatory:

  • lead_submitted oznacza formularz trwale przyjęty przez backend, bez duplikatu technicznego.

  • lead_qualified oznacza zgłoszenie spełniające uzgodnione kryteria sprzedażowe w CRM.

  • sale albo właściwy etap przychodowy zawiera czas konwersji, wartość i walutę, jeśli te dane są dostępne i poprawne.

Ustal z osobą prowadzącą kampanię, które działania są główne dla ustalania stawek, a które pozostają obserwacyjne. Przed importem sprawdź strefę czasową, format danych, okno konwersji i sposób aktualizacji wartości. Monitoruj raport diagnostyczny enhanced conversions oraz odsetek rekordów dopasowanych, zamiast oceniać integrację wyłącznie po braku błędu API.

A/B testing landing pages w Google Ads

Warianty URL dla testów A/B

Najprostsze podejście to oddzielne strony dla każdego wariantu.

Code
/lp/kurs-react          → wariant A (headline: "Naucz się React")
/lp/kurs-react-v2       → wariant B (headline: "Zostań React Developerem")

To działa, ale wymaga dyscypliny. Przed uruchomieniem zapisz hipotezę, główną metrykę, minimalną wielkość próby, planowany czas oraz warunki zatrzymania. Możesz testować pojedynczy element albo cały wariant doświadczenia. W drugim przypadku poznasz zwycięski pakiet zmian, ale nie ustalisz wpływu każdego elementu osobno. Nie kończ testu przy pierwszym korzystnym odczycie.

Mechanizm eksperymentu po zamknięciu Google Optimize

Google Optimize został zamknięty w 2023 roku. Możesz użyć platformy eksperymentalnej albo przygotować podział ruchu przez Next.js Proxy i cookie:

proxy.ts
Code
// proxy.ts: prosty podział ruchu A/B
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
 
export function proxy(request: NextRequest) {
  if (request.nextUrl.pathname === '/lp/produkt') {
    const cookieValue = request.cookies.get('ab-variant')?.value
    const variant =
      cookieValue === 'a' || cookieValue === 'b' ? cookieValue : null
 
    if (!variant) {
      const newVariant = Math.random() < 0.5 ? 'a' : 'b'
      const url = request.nextUrl.clone()
      url.pathname = `/lp/produkt-${newVariant}`
      const response = NextResponse.rewrite(url)
      response.cookies.set('ab-variant', newVariant, {
        maxAge: 60 * 60 * 24 * 30,
        path: '/',
        httpOnly: true,
        sameSite: 'lax',
        secure: process.env.NODE_ENV === 'production',
      })
      return response
    }
 
    const url = request.nextUrl.clone()
    url.pathname = `/lp/produkt-${variant}`
    return NextResponse.rewrite(url)
  }
 
  return NextResponse.next()
}
 
export const config = { matcher: '/lp/produkt' }

W praktyce warto jeszcze dopilnować:

  • spójnego raportowania wariantu w analityce i CRM,
  • utrzymania tego samego wariantu przez cały okres testu dla danego użytkownika,
  • opisania celu i retencji cookie zgodnie z przyjętą polityką prywatności,
  • zachowania query params po rewritach,
  • zdefiniowania metryki, minimalnej próby, czasu i reguły zakończenia przed startem,
  • ustawienia canonical na główny URL, jeśli warianty mają osobne adresy,
  • usunięcia mechanizmu i adresów testowych po zakończeniu eksperymentu,
  • kontroli sample ratio mismatch, ruchu botów oraz technicznych błędów wariantu,
  • analizy jakości leadów i przychodu, jeśli formularz jest metryką pośrednią.

Proxy jest dobrym miejscem na podział ruchu, ale oba warianty muszą przedstawiać tę samą promowaną usługę i spełniać zasady Google Ads. Nie pokazuj wersji zgodnej z polityką wyłącznie AdsBotowi, a innej użytkownikom. Taki mechanizm może zostać uznany za cloaking. Jeżeli warianty mają osobne, indeksowalne URL-e, wskaż główną wersję przez rel="canonical". Przy przekierowaniu użyj statusu tymczasowego, nie stałego.

Monitoring landing page i pętla feedbacku

Po wdrożeniu landing page, monitoruj:

Google Ads, raport Landing pages. Sprawdzaj kliknięcia, wyświetlenia, skuteczność i stan konkretnych URL-i. Nazwy kolumn oraz położenie raportu mogą zmieniać się wraz z interfejsem Google Ads.

Google Ads, widok Search keywords. Dodaj kolumnę Landing page exp. oraz pozostałe komponenty Quality Score na poziomie haseł używanych w kampanii.

Połącz dane GA4 oraz CRM. Mierz współczynnik konwersji, jakość leadów, koszt pozyskania oraz segmentację po kampanii, grupie reklam i urządzeniu.

Core Web Vitals i RUM. Mierz rzeczywiste , INP i dla ruchu z kampanii zamiast polegać na samym Lighthouse.

Raport wyszukiwanych haseł w Google Ads. Sprawdzaj, czy ruch, który trafia na LP, odpowiada intencji i obietnicy strony.

Diagram
Pętla skutecznej landing page dla Google Ads
Kampanie, landing page, tracking konwersji, GA4 i GTM w jednym procesie.
Google Ads i Analityka

Często zadawane pytania

Co jest najważniejsze na landing page pod Google Ads?

Najważniejsze są message match z reklamą, szybkość ładowania, czytelny formularz lub CTA, poprawna atrybucja kliknięcia i brak zbędnych rozpraszaczy.

Czy wynik Quality Score jest najważniejszym KPI?

Nie. Quality Score jest wskaźnikiem diagnostycznym. Ważniejsze są koszt pozyskania, współczynnik konwersji, jakość leadów i to, czy strona pomaga domykać cel kampanii.

Dlaczego Next.js ma sens dla landing page kampanii?

Next.js pomaga kontrolować wydajność, metadata, tracking, server-side rendering i optymalizację obrazów, czyli elementy wpływające na doświadczenie użytkownika i skuteczność kampanii.

Czy landing page pod Google Ads powinna mieć `noindex`?

Nie ma jednej odpowiedzi, bo to zależy od przeznaczenia strony, ale generalnie noindex ma wtedy sens, gdy LP jest tylko związana z kampanią, ma warianty A/B, duplikuje treść z innych URL-i lub nie wnosi wartości poza kontekstem kampanii. Jeśli jednak strona jest pełnoprawną ofertą produktową, może obsługiwać reklamy oraz ruch organiczny. Nie traktuj noindex jako domyślnego odruchu dla każdej LP, tutaj wiele zależy od kontekstu.

Dlaczego SSG jest lepsze niż SSR dla landing pages?

Static Generation (SSG) może dać przewidywalny, niski TTFB, ponieważ gotowa odpowiedź może być serwowana z CDN. ISR łączy cache z okresową regeneracją, dlatego jego zachowanie zależy również od konfiguracji hostingu. SSR ma sens przy personalizacji albo danych zależnych od żądania, ale także może korzystać z cache. Wybór sposobu renderowania nie gwarantuje sam w sobie dobrych Core Web Vitals.

Co to jest message match i dlaczego ma znaczenie?

Message match to spójność między tekstem reklamy a nagłówkiem landing page. Jeśli reklama obiecuje „Kurs React dla początkujących, start w 7 dni”, a LP ma headline „Akademia programowania online”, użytkownik czuje dysonans i często opuszcza stronę. Silny message match oznacza, że obietnica, oferta i warunki z reklamy są spójne ze stroną. Może to poprawić konwersję i landing page experience, ale nie gwarantuje określonego wyniku.

Jak nie zgubić identyfikatorów kliknięcia i UTM-ów?

Przy poprawnie wdrożonym tagu Google Ads lub Conversion Linkerze identyfikator kliknięcia jest zapisywany w cookie pierwszej strony, więc standardowa konwersja na tej samej domenie nie wymaga ręcznego dopisywania gclid do adresu thank-you page. Jeśli dane mają trafić do CRM, własnej analityki albo późniejszego importu konwersji, obsłuż również gbraid, wbraid i potrzebne parametry UTM. Zapisuj je oraz przekazuj zgodnie z decyzją użytkownika i polityką retencji. Przy przejściu między domenami skonfiguruj linker Google zamiast polegać wyłącznie na ręcznym kopiowaniu parametrów.

Kiedy użyć `strategy="lazyOnload"` zamiast `strategy="afterInteractive"` dla skryptów?

afterInteractive ładuje skrypt po hydratacji strony i jest odpowiedni dla tagów pomiarowych i remarketingowych, które muszą być gotowe względnie szybko. lazyOnload ładuje skrypt dopiero po załadowaniu całej strony w tle i sprawdza się przy widgetach czatu, narzędziach heatmap czy recenzji, które nie są krytyczne dla konwersji. Strategia ładowania nie zastępuje jednak Consent Mode ani decyzji CMP o tym, czy dany skrypt może się uruchomić.

Czy Consent Mode v2 zastępuje baner zgody?

Nie. Consent Mode przekazuje tagom Google stan decyzji użytkownika, ale nie zbiera zgody i nie rozstrzyga, kiedy jest ona prawnie wymagana. Potrzebujesz odpowiednio skonfigurowanego CMP lub własnego mechanizmu, który ustawia stan domyślny przed tagami i aktualizuje go po wyborze użytkownika.

Jak testować A/B landing pages bez Google Optimize?

Google Optimize został zamknięty w 2023 roku. Możesz użyć platformy eksperymentalnej albo własnego mechanizmu z Next.js Proxy, który przypisuje wariant przy pierwszym wejściu, ustawia cookie i wykonuje rewrite. Przed startem zdefiniuj hipotezę, główną metrykę, minimalną próbę i czas testu. Utrzymuj wariant dla użytkownika, kontroluj podział próby i uwzględnij jakość leadów, a nie samą liczbę wysłanych formularzy.

Jak mierzyć jakość ruchu z kampanii, a nie wyłącznie konwersje?

Poza samą liczbą leadów warto śledzić koszt leadu per kampania i ad group, współczynnik konwersji per wariant LP, jakość leadów w CRM (np. procent zakwalifikowanych do sprzedaży) oraz Core Web Vitals mierzone w RUM dla ruchu z kampanii. Kolumna Landing page exp w Google Ads na poziomie zapytań oraz raport Search Terms pomagają ocenić, czy ruch faktycznie odpowiada intencji strony.

O autorze

Maciej Sala

Maciej Sala — konsultant technologiczny produktów cyfrowych i web developer z bogatym doświadczeniem w marketingu internetowym oraz SEO. Na co dzień pracuje z Reactem, Next.js i TypeScriptem, a ostatnio także z Astro i narzędziami do automatyzacji procesów AI. Sprawnie łączy perspektywę produktową z praktycznym podejściem do kodu. Przez kilka lat był związany z branżą gier wideo jako project manager i game designer. Absolwent historii na Uniwersytecie Jagiellońskim oraz studiów podyplomowych z marketingu internetowego na AGH w Krakowie. Po godzinach trenuje na siłowni, maluje figurki i rozwija własne projekty.

Pomagam przekładać takie tematy na konkretne wdrożenia w frontendzie, SEO, analityce i procesie produktowym.

Skontaktuj się ze mną

Biblioteka wiedzy na temat Next.js

Czytaj dalej

Zobacz więcej wpisów
Porównanie Astro oraz Next.js pod kampanie płatne PPC

Landing page pod PPC ma jedno zadanie, czyli zamienić płatne kliknięcie w mierzalną akcję. I tyle. Każda zbędna sekunda ładowania, każdy niepotrzebny skrypt czy każda rozproszona narracja działają jak opór aerodynamiczny na kadłubie myśliwca, ponieważ palą paliwo, a Ty płacisz za nie z budżetu mediowego.

Maciej Sala

Maciej Sala

Founder StriveLab

Google Ads Conversion Tracking w Next.js

Kompletny przewodnik po implementacji śledzenia konwersji Google Ads w Next.js. Od podstawowego gtag, przez enhanced conversions, po server-side tracking z Google Ads API.

Maciej Sala

Maciej Sala

Founder StriveLab

Google Ads Remarketing w React i dynamiczne listy

Jak wdrożyć remarketing Google Ads w aplikacji React? Dynamiczny remarketing ecommerce, listy odbiorców na podstawie zachowań użytkowników i integracja z listami odbiorców w GA4.

Maciej Sala

Maciej Sala

Founder StriveLab