StriveLab
Strony internetowe
Usługi
RealizacjeO mnieBlogPorozmawiajmy
PL
EN

Astro

Ultraszybkie projekty, łączące lekkość ze skalowalnością.

Next.js

Elastyczne i wydajne narzędzia dla biznesu, które dotrzymają kroku Twojemu rozwojowi.

React

Połączenie intuicyjności z wydajnością, które zapewnia bezproblemową skalowalność kodu.

SEO & Performance

Audyt techniczny i optymalizacja pod kątem SEO i GEO.

Automatyzacja AI

Bezpieczne automatyzacje procesów i agenci AI w n8n, Make i Claude.

QA & Automation

Testy automatyczne komponentów i E2E w Cypress.

Doradztwo produktowe

Połączenie perspektywy produktu, developera i marketingu w jednym miejscu

StriveLab
Strony internetowe
Usługi
RealizacjeO mnieBlogPorozmawiajmy
PL
EN

Astro

Ultraszybkie projekty, łączące lekkość ze skalowalnością.

Next.js

Elastyczne i wydajne narzędzia dla biznesu, które dotrzymają kroku Twojemu rozwojowi.

React

Połączenie intuicyjności z wydajnością, które zapewnia bezproblemową skalowalność kodu.

SEO & Performance

Audyt techniczny i optymalizacja pod kątem SEO i GEO.

Automatyzacja AI

Bezpieczne automatyzacje procesów i agenci AI w n8n, Make i Claude.

QA & Automation

Testy automatyczne komponentów i E2E w Cypress.

Doradztwo produktowe

Połączenie perspektywy produktu, developera i marketingu w jednym miejscu

Astro

Ultraszybkie projekty, łączące lekkość ze skalowalnością.

Next.js

Elastyczne i wydajne narzędzia dla biznesu, które dotrzymają kroku Twojemu rozwojowi.

React

Połączenie intuicyjności z wydajnością, które zapewnia bezproblemową skalowalność kodu.

SEO & Performance

Audyt techniczny i optymalizacja pod kątem SEO i GEO.

Automatyzacja AI

Bezpieczne automatyzacje procesów i agenci AI w n8n, Make i Claude.

QA & Automation

Testy automatyczne komponentów i E2E w Cypress.

Doradztwo produktowe

Połączenie perspektywy produktu, developera i marketingu w jednym miejscu

RealizacjeO mnieBlog
Porozmawiajmy
PL
EN

Nowoczesne strony internetowe dla firm, które myślą odważnie.

Przewiń do góry

Nazwa

StriveLab Maciej Sala

NIP

6772218995

REGON

524008527

E-mail

contact@strivelab.pl

Usługi główne
  • Tworzenie stron internetowych
  • Strony internetowe Next.js
  • Strony internetowe Astro
  • Strony internetowe React
Inne usługi
  • Usługi
  • SEO & Performance Sprint
  • QA & Stabilizacja
  • Konsultacje Product / Delivery
  • Automatyzacja Procesów AI
  • Aplikacje webowe Next.js
  • Współpraca ciągła
Strony
  • O mnie
  • Usługi
  • Realizacje
  • Blog

© 2026 StriveLab.pl

Polityka prywatności
Next.jsBackend

Sanity CMS + Next.js — od instalacji po live preview i Visual Editing

Jak zintegrować Sanity CMS z Next.js App Router? Schema, GROQ queries, ISR, live preview, Visual Editing i deploy — kompletny setup headless CMS dla strony usługowej.

OpublikujLinkedInFacebookWyślij
Autor
Maciej Sala
Opublikowano
11 kwietnia 2026 08:20
Czytanie
3 min czytania
Aktualizacja
31 maja 2026 08:00

Sanity to headless CMS, w którym schemat treści definiujesz w TypeScript — żyje w repozytorium razem z kodem, a nie w GUI. Sanity hostuje backend i API za Ciebie, więc nie potrzebujesz własnego serwera. W tym przewodniku budujesz pełny setup: schema, zapytania GROQ, On-demand ISR (Incremental Static Regeneration) to mechanizm Next.js odświeżający konkretne strony natychmiast po zdarzeniu — np. webhookу z CMS-a — zamiast czekać na upływ czasu revalidate. Wywołujesz revalidateTag() lub revalidatePath() w Route Handlerze, a Next.js przebudowuje tylko dotknięte strony, serwując resztę z cache. przez webhooki, Draft Mode i Visual Editing z Presentation Tool.

Artykuł w skrócie

  • GROQ zamiast GraphQL — Sanity używa własnego języka zapytań; *[_type == "post"] to odpowiednik SELECT * FROM posts, ale typowany TypeScriptem.
  • On-demand ISR przez webhooki — revalidatePath() w Route Handlerze wywołanym przez webhook Sanity gwarantuje świeżość treści bez pełnego rebuild.
  • Draft Mode — podgląd nieopublikowanych treści przez specjalny cookie; tylko zalogowani redaktorzy widzą wersje robocze.
  • Visual Editing — Presentation Tool w Sanity umożliwia edycję treści bezpośrednio w podglądzie Next.js z podświetlaniem pól.
  • Studio opcjonalne — można używać Sanity API w Server Components bez osadzania Studio; /studio to wygodny panel admina, nie wymóg.

Dlaczego Sanity?

Dla stron usługowych w Next.js Sanity rozwiązuje konkretny problem, który początkowo może nie być taki oczywisty. Nietechniczny klient może samodzielnie edytować treści - teksty, zdjęcia, meta tagi, FAQ to sekcja najczęściej zadawanych pytań i odpowiedzi, często używana też do danych strukturalnych. - i to bez angażowania dewelopera przy każdej zmianie.

Notatka

Największą przewagą Sanity nie jest sam panel edycji, tylko Schema-as-code to podejście, w którym typy treści CMS-a (pola, walidacje, relacje) definiujesz jako pliki TypeScript w repozytorium zamiast klikać w panelu admina. Schemat trafia do gita razem z kodem frontendu, co ułatwia code review, refaktoring i utrzymanie spójności między CMS-em a aplikacją.. Jeśli typy treści żyją w repozytorium, łatwiej utrzymać spójność między CMS-em, frontendem, podglądem i walidacją danych.

Setup

Code
# Inicjalizacja Sanity w projekcie Next.js
npm create sanity@latest -- --project-id xxx --dataset production --template clean
npm install next-sanity @sanity/image-url @portabletext/react

Oczywiście możesz też użyć yarn zamiast npm.

Sanity Studio - czyli panel admina - działa jako route w Twojej aplikacji Next.js, czyli wchodzisz na /studio.

Zmienne środowiskowe

Code
# .env.local
NEXT_PUBLIC_SANITY_PROJECT_ID=twoje_project_id
NEXT_PUBLIC_SANITY_DATASET=production
SANITY_API_READ_TOKEN=twoj_token_z_manage_sanity_io
SANITY_WEBHOOK_SECRET=losowy_string_do_webhookow
SANITY_PREVIEW_SECRET=losowy_string_do_draft_mode

SANITY_API_READ_TOKEN generujesz w manage.sanity.io → API, czyli Application Programming Interface, definiuje sposób komunikacji między aplikacjami lub modułami. Tu chodzi o konwencje plików Next.js, które zamieniają eksportowaną funkcję w gotowy plik sitemap.xml lub robots.txt. → Tokens → dodaj token z uprawnieniami Viewer.

Konfiguracja klienta

Code
// lib/sanity/client.ts
import { createClient } from 'next-sanity'
 
const config = {
  projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!,
  dataset: process.env.NEXT_PUBLIC_SANITY_DATASET || 'production',
  apiVersion: '2026-04-11',
}
 
// Klient produkcyjny (CDN, publiczne dane)
export const client = createClient({
  ...config,
  useCdn: process.env.NODE_ENV === 'production',
})
 
// Klient do podglądu (token, brak CDN, drafty)
export const previewClient = createClient({
  ...config,
  useCdn: false,
  token: process.env.SANITY_API_READ_TOKEN,
  perspective: 'previewDrafts',
})

Przy Draft Mode w Next.js pozwala tymczasowo ominąć cache i renderować nieopublikowane lub robocze treści., preview i Visual Editing używaj previewClient — widzi najnowsze drafty zamiast danych z CDN, czyli Content Delivery Network, to rozproszona sieć serwerów dostarczająca zasoby z węzła najbliższego użytkownikowi; CDN do obrazów dodatkowo transformuje je w locie..

Pomocnik urlFor

Code
// lib/sanity/image.ts
import imageUrlBuilder from '@sanity/image-url'
import { client } from './client'
 
const builder = imageUrlBuilder(client)
 
export function urlFor(source: Parameters<typeof builder.image>[0]) {
  return builder.image(source)
}

Schema — definicja typów treści

Najpierw blockContent — typ wielokrotnego użytku dla pól Portable Text:

Code
// sanity/schemas/blockContent.ts
import { defineArrayMember, defineType } from 'sanity'
 
export const blockContentSchema = defineType({
  name: 'blockContent',
  type: 'array',
  of: [
    defineArrayMember({
      type: 'block',
      styles: [
        { title: 'Normalny', value: 'normal' },
        { title: 'H2', value: 'h2' },
        { title: 'H3', value: 'h3' },
        { title: 'Cytat', value: 'blockquote' },
      ],
      marks: {
        decorators: [
          { title: 'Pogrubienie', value: 'strong' },
          { title: 'Kursywa', value: 'em' },
          { title: 'Kod', value: 'code' },
        ],
        annotations: [
          {
            name: 'link',
            type: 'object',
            title: 'Link',
            fields: [{ name: 'href', type: 'url', title: 'URL' }],
          },
        ],
      },
    }),
    defineArrayMember({ type: 'image', options: { hotspot: true } }),
  ],
})

Następnie schema posta:

Code
// sanity/schemas/post.ts
import { defineType, defineField } from 'sanity'
 
export const postSchema = defineType({
  name: 'post',
  title: 'Artykuł',
  type: 'document',
  fields: [
    defineField({
      name: 'title',
      title: 'Tytuł',
      type: 'string',
      validation: (Rule) => Rule.required().max(100),
    }),
    defineField({
      name: 'slug',
      title: 'Slug (URL)',
      type: 'slug',
      options: { source: 'title', maxLength: 96 },
      validation: (Rule) => Rule.required(),
    }),
    defineField({
      name: 'excerpt',
      title: 'Zajawka',
      type: 'text',
      rows: 3,
      validation: (Rule) => Rule.max(200),
    }),
    defineField({
      name: 'mainImage',
      title: 'Zdjęcie główne',
      type: 'image',
      options: { hotspot: true },
    }),
    defineField({
      name: 'body',
      title: 'Treść',
      type: 'blockContent', // Portable Text
    }),
    defineField({
      name: 'publishedAt',
      title: 'Data publikacji',
      type: 'datetime',
    }),
    defineField({
      name: 'tags',
      title: 'Tagi',
      type: 'array',
      of: [{ type: 'string' }],
      options: { layout: 'tags' },
    }),
    defineField({
      name: 'seo',
      title: 'SEO',
      type: 'object',
      fields: [
        defineField({ name: 'metaTitle', title: 'Meta Title', type: 'string' }),
        defineField({
          name: 'metaDescription',
          title: 'Meta Description',
          type: 'text',
          rows: 2,
        }),
      ],
    }),
  ],
  orderings: [
    {
      title: 'Data publikacji',
      name: 'publishedAtDesc',
      by: [{ field: 'publishedAt', direction: 'desc' }],
    },
  ],
  preview: {
    select: { title: 'title', media: 'mainImage', date: 'publishedAt' },
    prepare({ title, media, date }) {
      return {
        title,
        media,
        subtitle: date
          ? new Date(date).toLocaleDateString('pl-PL')
          : 'Brak daty',
      }
    },
  },
})

Oba typy zarejestruj w konfiguracji Sanity:

Code
// sanity.config.ts (fragment)
import { postSchema } from './sanity/schemas/post'
import { blockContentSchema } from './sanity/schemas/blockContent'
 
export default defineConfig({
  // ...
  schema: { types: [postSchema, blockContentSchema] },
})

Wygeneruj typy TypeScript ze schemy, żeby uniknąć any w komponentach:

Code
npx sanity typegen generate

Tworzy sanity.types.ts — importuj stamtąd typy zamiast pisać je ręcznie.

Zapytania GROQ

GROQ to język zapytań Sanity służący do pobierania i filtrowania treści z datasetu. to język zapytań Sanity — podobny do GraphQL, ale prostszy. Każde zapytanie powinno deklarować cache tagi, żeby on-demand revalidation działała poprawnie:

Code
// lib/sanity/queries.ts
import { client } from './client'
import type { Post, PostSummary } from '@/sanity.types'
 
// Wszystkie opublikowane posty
export async function getPosts(): Promise<PostSummary[]> {
  return client.fetch(
    `*[_type == "post" && publishedAt < now()] | order(publishedAt desc) {
      _id,
      title,
      "slug": slug.current,
      excerpt,
      publishedAt,
      tags,
      mainImage
    }`,
    {},
    { next: { tags: ['posts'] } },
  )
}
 
// Wszystkie slugi (do generateStaticParams)
export async function getPostSlugs(): Promise<string[]> {
  const slugs = await client.fetch<{ slug: string }[]>(
    `*[_type == "post" && defined(slug.current)]{ "slug": slug.current }`,
    {},
    { next: { tags: ['posts'] } },
  )
  return slugs.map((s) => s.slug)
}
 
// Pojedynczy post po slug
export async function getPost(slug: string): Promise<Post | null> {
  return client.fetch(
    `*[_type == "post" && slug.current == $slug][0] {
      _id,
      title,
      "slug": slug.current,
      body,
      mainImage,
      publishedAt,
      tags,
      seo,
      "readingTime": round(length(pt::text(body)) / 5 / 200)
    }`,
    { slug },
    { next: { tags: ['posts', `post-${slug}`] } },
  )
}

Strona listingu blogowego

Code
// app/blog/page.tsx
import { getPosts } from '@/lib/sanity/queries'
import { urlFor } from '@/lib/sanity/image'
import type { PostSummary } from '@/sanity.types'
import Image from 'next/image'
import Link from 'next/link'
 
export const revalidate = 3600 // ISR — odśwież co godzinę (fallback)
 
export default async function BlogPage() {
  const posts = await getPosts()
 
  return (
    <main className="mx-auto max-w-4xl py-12">
      <h1 className="mb-8 text-3xl font-bold">Blog</h1>
      <div className="space-y-8">
        {posts.map((post: PostSummary) => (
          <Link
            key={post._id}
            href={`/blog/${post.slug}`}
            className="group block"
          >
            <article className="flex gap-6">
              {post.mainImage && (
                <Image
                  src={urlFor(post.mainImage).width(300).height(200).url()}
                  alt={post.title ?? ''}
                  width={300}
                  height={200}
                  className="rounded-lg object-cover"
                />
              )}
              <div>
                <h2 className="text-xl font-semibold group-hover:text-blue-600">
                  {post.title}
                </h2>
                <p className="mt-2 text-gray-600">{post.excerpt}</p>
                <time className="mt-2 block text-sm text-gray-400">
                  {new Date(post.publishedAt!).toLocaleDateString('pl-PL')}
                </time>
              </div>
            </article>
          </Link>
        ))}
      </div>
    </main>
  )
}

Strona pojedynczego posta

Code
// app/blog/[slug]/page.tsx
import { getPost, getPostSlugs } from '@/lib/sanity/queries'
import { urlFor } from '@/lib/sanity/image'
import { PortableText } from '@portabletext/react'
import type { Metadata } from 'next'
import Image from 'next/image'
import { notFound } from 'next/navigation'
 
type Props = { params: Promise<{ slug: string }> }
 
export async function generateStaticParams() {
  const slugs = await getPostSlugs()
  return slugs.map((slug) => ({ slug }))
}
 
export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { slug } = await params
  const post = await getPost(slug)
  if (!post) return {}
 
  return {
    title: post.seo?.metaTitle ?? post.title,
    description: post.seo?.metaDescription ?? post.excerpt,
    openGraph: post.mainImage
      ? { images: [urlFor(post.mainImage).width(1200).height(630).url()] }
      : undefined,
  }
}
 
export default async function PostPage({ params }: Props) {
  const { slug } = await params
  const post = await getPost(slug)
  if (!post) notFound()
 
  return (
    <article className="mx-auto max-w-3xl py-12">
      <h1 className="mb-4 text-4xl font-bold">{post.title}</h1>
      <div className="mb-8 flex items-center gap-4 text-sm text-gray-500">
        {post.publishedAt && (
          <time>{new Date(post.publishedAt).toLocaleDateString('pl-PL')}</time>
        )}
        {post.readingTime && <span>{post.readingTime} min czytania</span>}
      </div>
      {post.mainImage && (
        <Image
          src={urlFor(post.mainImage).width(800).height(450).url()}
          alt={post.title ?? ''}
          width={800}
          height={450}
          className="mb-8 rounded-xl object-cover"
          priority
        />
      )}
      {post.body && (
        <div className="prose prose-lg">
          <PortableText value={post.body} />
        </div>
      )}
    </article>
  )
}

On-demand ISR z webhookami Sanity

Zamiast czekać na rewalidację czasową — odśwież stronę natychmiast po edycji w Sanity:

Code
// app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache'
import { NextRequest, NextResponse } from 'next/server'
import { parseBody } from 'next-sanity/webhook'
 
export async function POST(req: NextRequest) {
  try {
    const { isValidSignature, body } = await parseBody(
      req,
      process.env.SANITY_WEBHOOK_SECRET,
    )
 
    if (!isValidSignature) {
      return NextResponse.json({ error: 'Invalid signature' }, { status: 401 })
    }
 
    const { _type, slug } = body
 
    if (_type === 'post') {
      revalidateTag('posts')
      if (slug?.current) revalidateTag(`post-${slug.current}`)
    }
 
    return NextResponse.json({ revalidated: true })
  } catch (error) {
    return NextResponse.json({ error: 'Bad Request' }, { status: 400 })
  }
}

W panelu Sanity: Manage → API → Webhooks → dodaj URL (https://twoja-domena.pl/api/revalidate) i secret.

Draft Mode — włączanie podglądu

Draft Mode to mechanizm Next.js przełączający rendering z cache'owanych danych na dane na żywo — czyli drafty z Sanity. Potrzebujesz dwóch route'ów: jeden włącza podgląd, drugi go wyłącza:

Code
// app/api/draft/enable/route.ts
import { draftMode } from 'next/headers'
import { redirect } from 'next/navigation'
import { NextRequest } from 'next/server'
 
export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url)
 
  if (searchParams.get('secret') !== process.env.SANITY_PREVIEW_SECRET) {
    return new Response('Invalid token', { status: 401 })
  }
 
  const draft = await draftMode()
  draft.enable()
  redirect(searchParams.get('redirect') || '/')
}
Code
// app/api/draft/disable/route.ts
import { draftMode } from 'next/headers'
import { redirect } from 'next/navigation'
 
export async function GET() {
  const draft = await draftMode()
  draft.disable()
  redirect('/')
}

W Sanity Studio skonfiguruj Presentation Tool z URL podglądu:

Code
// sanity.config.ts
import { presentationTool } from 'sanity/presentation'
 
export default defineConfig({
  // ...
  plugins: [
    presentationTool({
      previewUrl: {
        origin:
          process.env.SANITY_STUDIO_PREVIEW_URL || 'http://localhost:3000',
        draftMode: {
          enable: '/api/draft/enable',
        },
      },
    }),
  ],
})

Live Preview z Presentation Tool

W Draft Mode zamiast domyślnego klienta używasz previewClient — ten ma perspective: 'previewDrafts' i widzi nieopublikowane wersje robocze:

Code
// app/blog/[slug]/page.tsx (fragment z draft mode)
import { draftMode } from 'next/headers'
import { client, previewClient } from '@/lib/sanity/client'
 
export default async function PostPage({ params }: Props) {
  const { slug } = await params
  const { isEnabled } = await draftMode()
 
  const sanityClient = isEnabled ? previewClient : client
  const post = await sanityClient.fetch(
    `*[_type == "post" && slug.current == $slug][0]{...}`,
    { slug },
    { next: { tags: [`post-${slug}`] } },
  )
 
  if (!post) notFound()
  // ... reszta komponentu
}

previewClient ma perspective: 'previewDrafts' — zwraca nieopublikowane zmiany. Dla pełnego preview na żywo z aktualizacjami w czasie rzeczywistym (bez przeładowania strony) użyj VisualEditing razem z @sanity/preview-kit — dane są wtedy synchronizowane przez WebSocket w momencie każdej zmiany w edytorze.

Visual Editing — edycja treści na żywej stronie

Visual Editing pozwala klientowi kliknąć dowolny element na żywej stronie i edytować go bez otwierania panelu Studio:

Code
npm install @sanity/visual-editing
Code
// app/layout.tsx
import { VisualEditing } from 'next-sanity'
import { draftMode } from 'next/headers'
 
export default async function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  const { isEnabled } = await draftMode()
 
  return (
    <html lang="pl">
      <body>
        {children}
        {isEnabled && <VisualEditing />}
      </body>
    </html>
  )
}

Klient widzi stronę jak użytkownik, ale edytowalne elementy dostają overlay prowadzący do Sanity Studio lub Presentation Tool. Jeśli Studio jest osadzone w tej samej aplikacji, trzymaj jego route w osobnej grupie layoutów i nie renderuj VisualEditing nad panelem Studio.

Werdykt Labu

Sanity + Next.js to dojrzały stack dla stron, gdzie treść zmienia się regularnie, a klient musi edytować ją samodzielnie. Dzięki podejściu „schema-as-code”, struktura treści jest zawsze zsynchronizowana z kodem strony. Inteligentne odświeżanie (on-demand ISR) przez webhooki sprawia, że każda zmiana w Sanity jest widoczna na stronie niemal natychmiast, a Visual Editing pozwala edytować wszystko bezpośrednio na podglądzie, więc od razu widzisz efekt swojej pracy. Dla stron firmowych, blogów i portfeli to kompletny setup bez dedykowanego backendu i bez budowania panelu od zera.

  • Dlaczego Sanity?1 min
  • Setup1 min
  • On-demand ISR z webhookami Sanity1 min
  • Draft Mode — włączanie podglądu1 min
  • Live Preview z Presentation Tool1 min
  • Visual Editing — edycja treści na żywej stronie1 min
  • Werdykt Labu1 min

Często zadawane pytania

Źródła i data weryfikacjiZweryfikowano: 20 maja 2026

Informacje o API, planach, Visual Editing i integracji z App Routerem zweryfikowano na podstawie oficjalnej dokumentacji Sanity i Next.js:

Sanity docs, Sanity — Visual Editing, Sanity — Presentation Tool, Sanity pricing, next-sanity package, Next.js — Draft Mode, Next.js — On-Demand Revalidation.

Maciej Sala

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.

Moje artykułyWięcej o mnie

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

Skontaktuj się ze mną

Biblioteka wiedzy

Czytaj dalej

Zobacz więcej wpisów
Programmatic SEO z Next.js i AI — jak generować tysiące zoptymalizowanych stron
Programmatic SEO z Next.js i AI — jak generować tysiące zoptymalizowanych stron

Programmatic SEO w połączeniu z AI i Next.js ISR/SSG pozwala skalować produkcję treści bez proporcjonalnego wzrostu kosztów. Praktyczny przewodnik po architekturze, generowaniu treści i optymalizacji pod Google, ChatGPT i Perplexity.

Maciej Sala

Maciej Sala

Founder Strivelab

31 marca 2026
Next.js vs WordPress w 2026 — kiedy polecam jedno, a kiedy drugie
Next.js vs WordPress w 2026 — kiedy polecam jedno, a kiedy drugie

Next.js vs WordPress w 2026 — obiektywne porównanie dla firm, freelancerów i agencji. Wydajność, SEO, bezpieczeństwo, koszty, łatwość edycji — kiedy który wybrać i dlaczego.

Maciej Sala

Maciej Sala

Founder Strivelab

10 kwietnia 2026
Google Analytics 4 w Next.js App Router — konfiguracja z gtag i @next/third-parties
Google Analytics 4 w Next.js App Router — konfiguracja z gtag i @next/third-parties

Jak poprawnie wdrożyć GA4 w Next.js App Router: gtag, @next/third-parties, page_view przy client-side navigation, consent mode v2 i custom events bez chaosu w danych.

Maciej Sala

Maciej Sala

Founder Strivelab

18 października 2025
Poprzedni wpisNext.js vs WordPress w 2026 — kiedy polecam jedno, a kiedy drugieNext.js vs WordPress w 2026 — obiektywne porównanie dla firm, freelancerów i agencji. Wydajność, SEO, bezpieczeństwo, koszty, łatwość edycji — kiedy który wybrać i dlaczego.
Maciej Sala

Maciej Sala

Founder Strivelab

10 kwietnia 2026
Następny wpisGoogle Search Console + Next.js — indeksacja, błędy, performance i co z nimi robićJak korzystać z Google Search Console dla strony Next.js? Weryfikacja, sitemap, indeksacja, Core Web Vitals, crawl budget i najczęstsze problemy — praktyczny poradnik.
Maciej Sala

Maciej Sala

Founder Strivelab

11 kwietnia 2026