Landing page dla Google Ads w Next.js — jak budować strony, które konwertują

Jak zaprojektować landing page w Next.js pod Google Ads: szybkość, message match, formularz, atrybucja, consent i techniczne elementy, które wpływają na wynik kampanii.

Opublikowano

31 grudnia 2025 12:15

Czytanie

8 min czytania

Aktualizacja

7 kwietnia 2026 10:47

Landing page dla Google Ads to nie jest zwykła podstrona "z formularzem", ale to miejsce, w którym spotykają się trzy warstwy naraz: trafność reklamy, doświadczenie użytkownika i poprawna atrybucja konwersji.

Ważne doprecyzowanie na start: Quality Score w Google Ads to przede wszystkim narzędzie diagnostyczne, a nie KPI sam w sobie. Google wprost mówi, że nie jest to bezpośredni input w aukcji, ale jednocześnie "ocena jakości w czasie rzeczywistim reklamy", nadal bierze pod uwagę trzy obszary: expected CTR, ad relevance i landing page experience. Czyli można powiedzieć, że nie optymalizujesz "cyferki 1-10", ale jakość strony nadal realnie wpływa na wynik kampanii.

Jako developer masz bezpośredni wpływ na sporą część tego układu: szybkość ładowania, mobile usability, message match, formularz, third-party scripts i to, czy po kliknięciu reklamy nie zgubisz gclid ani UTM-ów.

Krótka odpowiedź: Skuteczna landing page w Next.js pod Google Ads opiera się na 4 filarach: szybkim renderowaniu (SSG, czyli Static Site Generation, oznacza generowanie HTML podczas buildu i serwowanie go jako statycznego pliku./ISR, czyli Incremental Static Regeneration, pozwala odświeżać strony statyczne po czasie bez pełnego rebuildu., Core Web Vitals to zestaw metryk Google oceniających szybkość, responsywność i stabilność wizualną strony. LCP, czyli Largest Contentful Paint, mierzy czas wyrenderowania największego widocznego elementu na ekranie. < 2,5 s), silnym związkiem między reklamą a nagłówkiem strony, lekkim formularzu zbierającym tylko niezbędne dane oraz poprawnej atrybucji konwersji z zachowanym gclid i UTM-ami. Konwersję rejestruj dopiero po realnym sukcesie formularza, a third-party scripts ładuj z odpowiednią strategią (afterInteractive lub lazyOnload) żeby nie wpływać negatywnie na wydajność kampanii.

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:

Usefulness i relevance – treść strony musi faktycznie odpowiadać na to, czego użytkownik spodziewa się po reklamie i słowie kluczowym.

Ease of navigation – użytkownik powinien móc łatwo znaleźć to, po co przyszedł, dlatego wszystko musi być poukładane - bez chaosu, bez ukrytych informacji czy też bez agresywnych przeszkadzaczy.

Expectation match – reklama obiecuje jedno, a landing page powinien to dowieźć, ponieważ to dotyczy nagłówka, oferty, ceny, CTA i całej narracji.

Transparency i trust – kontakt, polityka prywatności, podstawowe informacje o firmie, jasne warunki i brak podejrzanych wzorców.

Mobile-friendliness i speed – na mobile doświadczenie musi być szybkie, czytelne i stabilne, więc tutaj dochodzą jeszcze kwestie Core Web Vitals oraz praktyczny UX, czyli User Experience, opisuje całe doświadczenie użytkownika podczas korzystania z produktu..

Na sam koniec dochodzi techniczna higiena, więc działająca strona, ten sam domenowy kontekst co reklama, brak błędów 4xx/5xx, sensowna obsługa redirectów i brak "martwych" variantów LP.

Architektura landing page w Next.js

Static Generation – często najlepsza opcja

Jeśli landing page nie wymaga personalizacji w czasie rzeczywistym, SSG albo ISR będą zwykle najlepszym wyborem - oferują przewidywalny TTFB, czyli Time to First Byte, mierzy czas od wysłania żądania do odebrania pierwszego bajtu odpowiedzi., łatwiejszy caching i mniej ruchomych części niż pełny SSR, czyli Server-Side Rendering, oznacza generowanie HTML na serwerze przy każdym żądaniu..

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: { slug: string };
}): Promise<Metadata> {
  const page = await getLandingPage(params.slug);
 
  return {
    title: page.metaTitle,
    description: page.metaDescription,
    robots: {
      index: page.indexable ?? false,
      follow: page.indexable ?? false,
    },
  };
}
 
export default async function LandingPage({
  params,
}: {
  params: { slug: string };
}) {
  const page = await getLandingPage(params.slug);
 
  return <LandingPageContent page={page} />;
}

Dlaczego noindex?

noindex ma sens tylko wtedy, gdy landing page jest naprawdę dedykowana dla kampanii tj. ma warianty A/B, duplikuje treść z innych URL-i (bardzo szkodliwe dla SEO, czyli Search Engine Optimization, to optymalizacja strony pod widoczność w wynikach wyszukiwania.), jest krótką stroną stricte leadową albo nie chcesz, żeby konkurowała z główną ofertą w wynikach organicznych. Jeśli LP jest wartościową, pełnoprawną stroną ofertową i ma dobrej jakości unikatową treść, to nie stososuj noindex. Problemem nie jest sam fakt, że strona jest "reklamowa", tylko to, czy wnosi coś sensownego poza kampanią.

SSR nadal bywa uzasadniony, jeśli musisz pokazać dynamiczną cenę, stan magazynowy, lokalizację, język, zgodność prawną per kraj albo personalizację pod segment. Najistotniejsze w tym wszystkim jest to, żeby LP była "statyczna z zasady", tylko żeby była szybka, stabilna i przewidywalna.

Optymalizacja wydajności – checklist

LCP < 2.5s

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

priority ma sens tylko dla zasobu, który faktycznie jest kandydatem na LCP. Jeśli oznaczysz tym pół strony, rozwodnisz priorytety ładowania.

CLS < 0.1

Code
// 1. Zawsze podawaj wymiary obrazków
<Image width={400} height={300} ... />
 
// 2. Zarezerwuj miejsce dla dynamicznych elementów
<div className="min-h-[60px]"> {/* cookie banner space */}
  <CookieBanner />
</div>
 
// 3. Font display swap + size-adjust
const font = Inter({
  subsets: ["latin"],
  display: "swap",
  adjustFontFallback: true, // Next.js automatycznie dopasuje fallback
});

INP < 200ms

Code
// 1. Lazy load poniżej folda
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]" />,
});
 
// 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/validate-and-send", {
    method: "POST",
    body: formData,
  });
  setLoading(false);
};

Samo async/await nie sprawia magicznie, że kod przestaje blokować main thread. Jeśli ciężka walidacja nadal wykonuje się synchronicznie w JS po stronie klienta, INP nadal dostanie po głowie. Jeśli naprawdę musisz robić ciężką pracę w przeglądarce, rozważ Web Workera.

Third-party scripts – nie zabijaj wydajności

Code
import Script from "next/script";
 
// Tag pomiarowy, który musi wystartować wcześnie
<Script src="https://www.googletagmanager.com/gtag/js?id=AW-XXX"
  strategy="afterInteractive" />
 
// Widget supportowy, który może poczekać
<Script src="https://widget.intercom.io/widget/xxx"
  strategy="lazyOnload" />

Nie wszystkie third-party możesz opóźniać tak samo agresywnie. Script odpowiedzialny za pomiar i remarketing zwykle musi pojawić się wcześniej niż widget czatu, heatmapa czy recenzje. Kolejność ładowania to część architektury konwersji, nie tylko kwestia "zielonego Lighthouse".

Struktura konwertującej landing page

Above the fold – pierwszy ekran

Code
// components/lp/HeroSection.tsx
export function HeroSection({ page }: { page: LandingPage }) {
  return (
    <section className="min-h-screen flex flex-col justify-center px-4 md:px-8">
      {/* 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. Social proof */}
      <div className="mt-6 flex items-center gap-2 text-sm text-gray-500">
        <span>⭐⭐⭐⭐⭐</span>
        <span>4.8/5 na podstawie 2,340 opinii</span>
      </div>
    </section>
  );
}

Ten snippet jest dobry jako struktura, ale nie kopiuj ślepo samych "ładnych liczb", a social proof działa tylko wtedy, gdy jest prawdziwy i możliwy do obrony.

Message match – klucz do Quality Score

Message match to spójność między tekstem reklamy, a treścią landing page, dlatego jeśli reklama mówi "Tanie ubezpieczenie OC online", a LP ma headline "Kompleksowe rozwiązania ubezpieczeniowe" – message match jest generalnie do kitu.

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

Praktycznie: twórz oddzielne landing pages albo przynajmniej oddzielne warianty hero/message match per ad group, jeśli grupy słów kluczowych reprezentują różne intencje.

Formularz – minimalizuj friction

Code
// components/lp/LeadForm.tsx
"use client";
 
import { 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 handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    setStatus("loading");
 
    const formData = new FormData(e.currentTarget);
 
    try {
      const response = await fetch("/api/leads", {
        method: "POST",
        body: formData,
      });
 
      if (!response.ok) {
        throw new Error("Request failed");
      }
 
      setStatus("success");
 
      // Track conversions
      adsConversions.lead(formId);
      analytics.formSubmit(formId, true);
    } catch {
      setStatus("error");
      analytics.formSubmit(formId, false);
    }
  };
 
  if (status === "success") {
    return (
      <div className="text-center p-8">
        <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">
      {/* Minimum pól = maksymalna konwersja */}
      <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>
  );
}

Najważniejsza zasada brzmi, byś zbierał/a tylko te dane, których naprawdę potrzebujesz na tym etapie lejka, ponieważ każde dodatkowe pole zwiększa tarcie, ale zbyt krótki formularz potrafi z kolei obniżyć jakość leadów. I teraz pamiętaj, że optymalny punkt trzeba sprawdzić na własnych danych.

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),
  • jasna informacja o polityce prywatności i zgodzie marketingowej, jeśli jest wymagana,
  • jeden główny CTA, a nie pięć równorzędnych ścieżek.

Atrybucja – nie zgub gclid i UTM-ów

Wiele landing pages jest "ładnych", ale słabych analitycznie. Klik z reklamy wpada z gclid, utm_source, utm_campaign, a potem te parametry znikają przy pierwszej nawigacji albo nie trafiają do CRM-a.

W minimum produkcyjnym zadbaj o trzy rzeczy:

  • zachowaj gclid i kluczowe UTM-y w hidden fields albo storage pierwszej sesji
  • odpal konwersję dopiero po realnym sukcesie formularza, nie po kliknięciu przycisku
  • jeśli redirectujesz na thank-you page, upewnij się, że nie gubisz parametrów albo danych sesji po drodze
Code
'use client'
 
import { useSearchParams } from 'next/navigation'
 
export function AttributionFields() {
  const searchParams = useSearchParams()
 
  return (
    <>
      <input
        type="hidden"
        name="gclid"
        value={searchParams.get('gclid') ?? ''}
      />
      <input
        type="hidden"
        name="utm_source"
        value={searchParams.get('utm_source') ?? ''}
      />
      <input
        type="hidden"
        name="utm_campaign"
        value={searchParams.get('utm_campaign') ?? ''}
      />
    </>
  )
}

Jeśli użytkownik może przejść kilka kroków przed wysłaniem formularza, lepiej zapisać first-touch attribution w cookie albo sessionStorage, a nie liczyć tylko na aktualny URL.

A/B Testing landing pages

Warianty URL

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 pamiętaj o dyscyplinie testowej i testuj jedną większą zmianę naraz, nie mieszaj równocześnie headline'u, formularza, koloru CTA i układu sekcji, ponieważ potem nie wiesz, co naprawdę wygrało i co ma znaczenie, a co nie ma znaczenia. Nie ma nic gorszego niż chaos wniosków z testów - niby coś z nim mamy, ale nie wiemy konkretnie co.

Google Optimize replacement

Google Optimize został zamknięty w 2023, aktualne lternatywy: Optimizely, VWO lub buduj własny mechanizm z Next.js Middleware + cookies:

Code
// middleware.ts – prosty A/B split
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
 
export function middleware(request: NextRequest) {
  if (request.nextUrl.pathname === '/lp/produkt') {
    const variant = request.cookies.get('ab-variant')?.value
 
    if (!variant) {
      const newVariant = Math.random() < 0.5 ? 'a' : 'b'
      const response = NextResponse.rewrite(
        new URL(`/lp/produkt-${newVariant}`, request.url),
      )
      response.cookies.set('ab-variant', newVariant, {
        maxAge: 60 * 60 * 24 * 30,
      })
      return response
    }
 
    return NextResponse.rewrite(new URL(`/lp/produkt-${variant}`, request.url))
  }
 
  return NextResponse.next()
}

W praktyce warto jeszcze dopilnować:

  • spójnego raportowania wariantu w analityce i CRM
  • utrzymania tego samego wariantu w całej sesji
  • zachowania query params po rewritach
  • wyłączenia testu po osiągnięciu istotności, a nie po pierwszym "ładnym" wyniku

Monitoring – pętla feedbacku

Po wdrożeniu landing page, monitoruj:

Google Ads → Insights & reports → Landing pages – kliknięcia, wyświetlenia, mobile-friendly click rate i zachowanie konkretnych URL-i.

Google Ads → Keywords – kolumna Landing page exp. oraz pozostałe komponenty jakości na poziomie słów kluczowych.

GA4 / CRM – conversion rate, lead quality, koszt leada, segmentacja po kampanii, ad group i device.

Core Web Vitals / RUM – mierz rzeczywiste LCP, INP i CLS dla ruchu z kampanii, nie tylko syntetyczny Lighthouse.

Search terms report – sprawdzaj, czy ruch, który wpada na LP, faktycznie odpowiada intencji i obietnicy strony.

FAQ

Czy landing page pod Google Ads powinna mieć noindex?

Nie ma jednej odpowiedzi — to zależy od przeznaczenia strony, ale generalnie noindex ma wtedy sens, gdy LP jest tylko związna 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 bezproblemowo być zarówno pod reklamy, jak i SEO. 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) lub ISR dają przewidywalny, niski TTFB, bo strona jest serwowana z CDN bez czasu serwera. Dla landing pages, gdzie priorytetem jest szybkość ładowania i Core Web Vitals, to zwykle najlepszy wybór. SSR ma sens tylko wtedy, gdy strona wymaga personalizacji w czasie rzeczywistym — dynamicznej ceny, stanu magazynowego, lokalizacji użytkownika lub zgodności prawnej per kraj.

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 kluczowe słowa z reklamy powtarzają się na stronie — to bezpośrednio wpływa na wskaźnik konwersji i ocenę landing page experience przez Google.

Jak nie zgubić gclid i UTM-ów przy przekierowaniu na thank-you page?

Parametry gclid i UTM-y z URL są dostępne tylko w momencie wejścia na stronę. Jeśli formularz przekierowuje na oddzielną stronę potwierdzenia, musisz wcześniej zapisać te wartości — w hidden fields formularza, sessionStorage lub cookie first-touch. Nie licz na to, że Google Ads automatycznie odtworzy atrybucję po przekierowaniu, jeśli parametry znikną z URL.

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

afterInteractive ładuje skrypt po hydratacji strony — 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 — odpowiedni dla widgetów czatu, narzędzi heatmap czy recenzji, które nie są krytyczne dla konwersji. Niepotrzebne wczesne ładowanie skryptów niszczy Core Web Vitals.

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

Google Optimize został zamknięty w 2023. Alternatywy to Optimizely lub VWO dla gotowych rozwiązań, albo własny mechanizm z Next.js Middleware (losuje wariant przy pierwszym wejściu, ustawia cookie i przepisuje URL na odpowiedni wariant strony). Kluczowe zasady testowania jest taka, by testować jedną zmianę naraz, utrzymuj ten sam wariant w całej sesji użytkownika i kończ test dopiero po osiągnięciu statystycznej istotności. Inaczej czeka Cię chaos.

Jak mierzyć jakość ruchu z kampanii, nie tylko liczbę konwersji?

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 słów kluczowych oraz raport Search Terms pomagają ocenić, czy ruch faktycznie odpowiada intencji strony.

Podsumowanie

Landing page dla Google Ads to projekt, w którym performance, UX, analityka i marketing spotykają się w łącząc w skuteczną całość. Jako developer nie "podbijasz wyniku Quality Score" w próżni, tylko budujesz stronę, która jest szybka, trafna, wiarygodna i poprawnie mierzy efekt kampanii - w ten sposób działasz pośrednio na QS, a jednocześnie odchaczasz i tak sprawy, które wymagają dopracowania.

Najlepsze landing pages w Next.js zwykle mają wspólne cechy, czyli szybki i przewidywalny render, mocną korelacją reklamy z intencją zapytania, odpowiednio domknięty formularz, kontrolę nad third-party scripts oraz poprawną atrybucję konwersji.

To jeden z nielicznych obszarów, w których odpowiednia decyzja techniczna naprawdę przekłada się na wynik biznesowy niemal od razu, skutkując: mniejszą ilością zmarnowanych kliknięć, lepszą jakością ruchu oraz wyższą szansą na konwersję z tego samego budżetu.

Źródła i dokumentacja

Pracuję z tym zawodowo.

Jeśli chcesz zbudować landing page, który realnie wspiera kampanie Google Ads, nie gubi atrybucji i domyka ruch na leady lub sprzedaż, skontaktuj się ze mną. Pomagam łączyć warstwę techniczną Next.js, UX, pomiar i performance z celem kampanii.

O autorze

Maciej Sala

Maciej Sala — project manager i frontendowiec z doświadczeniem w marketingu internetowym. Na co dzień pracuję z Reactem, Next.js i TypeScriptem, łącząc perspektywę produktową z praktycznym podejściem do kodu. Przez kilka lat związany z branżą gier wideo jako project manager i game designer.

Absolwent historii na Uniwersytecie Jagiellońskim i studiów podyplomowych z marketingu internetowego na Akademii Górniczo-Hutniczej w Krakowie. Poza pracą trenuje na siłowni, maluje figurki i realizuje własne projekty.

Biblioteka wiedzy

Czytaj dalej

Zobacz więcej wpisów
Astro.js vs Next.js — które narzędzie wybrać w 2026 roku?

Astro.js vs Next.js — które narzędzie wybrać w 2026 roku?

Fachowe porównanie Astro.js i Next.js z perspektywy developera pracującego na co dzień w Next.js. Architektura, wydajność, SEO, DX, koszty i konkretne use case — z benchmarkami i przykładami kodu.

Maciej Sala

Maciej Sala

Founder Strivelab