Przejdź do treści

Połączenie Sanity CMS z Next.js — od instalacji po live preview i Visual Editing

Sanity CMS w Next.js App Router — od schematu przez GROQ i ISR po Visual Editing na żywo. Setup, który redakcja pokocha, a deweloper nie przeklnie.

Maciej Sala

Founder StriveLab

8 min czytaniaOpublikowano 11 kwietnia 2026 (Aktualizacja 31 maja 2026)

Dlaczego Sanity CMS w projekcie Next.js?

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, czyli teksty, zdjęcia, meta tagi, , bez każdorazowego angażowania w proces dewelopera.

Największą zaletą Sanity jednak, nie jest sam panel edycji, tylko podejście . Jeśli typy treści żyją w repozytorium, łatwiej wtedy utrzymać spójność między CMS-em, frontendem, podglądem i walidacją danych.

Setup Sanity CMS w Next.js App Router

Komenda npm create sanity@latest jest interaktywna — zapyta o organizację w Sanity Cloud (lub utworzy nową), nazwę projektu i dataset (domyślnie production). W tle dzieje się dwie rzeczy: Sanity tworzy dla Ciebie darmowy projekt w swojej infrastrukturze (backend, API, CDN), a lokalnie w repozytorium powstaje konfiguracja Studio, którą za chwilę osadzisz w aplikacji Next.js.

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

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

Zmienne środowiskowe dla Sanity i Next.js

Podział na zmienne z prefiksem NEXT_PUBLIC_ i bez niego nie jest przypadkowy. Te pierwsze trafiają do kodu po stronie przeglądarki, więc mogą zawierać tylko dane, które i tak są publiczne — identyfikator projektu i dataset nie są tajemnicą, bo i tak widać je w publicznych żądaniach do API Sanity. Token (SANITY_API_READ_TOKEN) i sekrety webhooków zostają bez prefiksu, czyli istnieją tylko po stronie serwera — nigdy nie trafią do bundla JS wysyłanego do użytkownika.

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 → → Tokens → dodaj token z uprawnieniami Viewer.

Konfiguracja klienta Sanity w Next.js

Potrzebujesz dwóch klientów Sanity, nie jednego. Powód jest prosty: każdy z nich służy innemu czytelnikowi. Klient produkcyjny czyta z CDN i widzi tylko opublikowane treści — to ma być szybkie i publiczne, więc nie wymaga tokenu. Klient do podglądu działa odwrotnie: omija CDN, łączy się z API z autoryzacją i widzi nieopublikowane wersje robocze. Rozdzielenie ich na dwa obiekty, zamiast jednej funkcji z flagą isDraft, sprawia, że token i logika podglądu nigdy nie trafiają przypadkiem do kodu renderującego publiczną stronę.

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: 'drafts',
})

Przy , preview i Visual Editing używaj previewClient — widzi najnowsze drafty zamiast danych z .

urlFor do obrazów z Sanity CMS

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 w Sanity: definicja typów treści

Zamiast definiować edytor tekstu w każdej schemie od zera, tworzymy jeden, reużywalny typ blockContent. To on określa, jakie formatowanie (nagłówki, pogrubienie), linki czy obrazy będą dostępne w polach typu Portable Text w całym projekcie. Dzięki temu edytor treści jest spójny, a zmiany wprowadzasz w jednym miejscu.

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. Kilka pól ma sens nie tylko dla redaktora, ale i dla SEO: excerpt ma limit 200 znaków, bo dłuższy tekst i tak zostałby obcięty jako meta description. Pole seo jest osobnym obiektem, a nie płaskimi polami metaTitle/metaDescription na poziomie dokumentu — dzięki temu w Studio widać jasno, które dane są treścią, a które metadanymi do wyszukiwarek, i można je w przyszłości rozbudować (np. o noindex albo kanoniczny URL) bez zaśmiecania głównej listy pól. Konfiguracja preview na końcu schemy nie wpływa na frontend — decyduje tylko o tym, jak dokument wygląda na liście w Sanity Studio, żeby redaktor odróżnił posty od siebie bez klikania w każdy z nich.

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 do Sanity CMS

to język zapytań Sanity, jest dość podobny do GraphQL, ale prostszy. Każde zapytanie powinno deklarować cache tagi, żeby odświeżać pamięć podręczną na żądanie i żeby zadziałało 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}`] } },
  )
}

Listing bloga w Next.js pobierany z Sanity

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 posta w Next.js z treścią z Sanity

Strona pojedynczego posta różni się od listingu jedną kluczową rzeczą: musi istnieć dla każdego slugu osobno, zanim ktokolwiek wejdzie na stronę. Funkcja generateStaticParams mówi Next.js, jakie sluga wygenerować podczas builda — dzięki temu każdy artykuł trafia do statycznego HTML zamiast renderować się od zera przy każdym wejściu. generateMetadata działa na tej samej zasadzie, tylko dla SEO: dla każdego posta osobno ustawia tytuł, opis i obrazek Open Graph na podstawie pól zapisanych w Sanity, więc redaktor zmieniający seo.metaTitle w CMS-ie zmienia też to, co zobaczy Google i osoba udostępniająca link.

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 w Next.js

Standardowa rewalidacja czasowa (np. revalidate = 3600) w Next.js odświeża stronę co określony czas, co oznacza, że zmiany w CMS-ie mogą być widoczne z opóźnieniem. Lepszym podejściem jest On-demand ISR, czyli odświeżanie na żądanie. Dzięki webhookom, Sanity po każdej zmianie treści wysyła sygnał do Twojej aplikacji Next.js, która natychmiast przebudowuje tylko te strony, których dotyczyła edycja. To gwarantuje, że treść jest zawsze aktualna, bez zbędnego przebudowywania całej witryny.

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.

Sanity wysyła do tego endpointu nie cały dokument, a jego skrócony payload — typ dokumentu (_type) i kluczowe pola, w tym slug, którego dotyczyła zmiana. Stąd warunek if (_type === 'post'): webhook reaguje na każdą zmianę w projekcie, ale rewalidować chcesz tylko cache postów. Weryfikacja isValidSignature jest równie istotna — bez niej każdy, kto odgadnie URL endpointu, mógłby wywołać rewalidację (albo zalać Twój serwer żądaniami) bez dostępu do Sanity. Sekret z .env.local gwarantuje, że żądanie naprawdę przyszło z Twojego projektu.

Draft Mode w Next.js do podglądu treści Sanity

Domyślnie Next.js agresywnie cache'uje dane, aby strony ładowały się błyskawicznie. To świetne dla użytkowników, ale problematyczne dla redaktorów, którzy nie widzą swoich zmian od razu. Draft Mode rozwiązuje ten problem: to specjalny tryb podglądu, który pozwala tymczasowo ominąć cache i renderować stronę na podstawie najświeższych, nawet nieopublikowanych danych z CMS-a. Mechanizm działa w oparciu o bezpieczne ciasteczko (cookie), które Next.js ustawia po wejściu na specjalny, zabezpieczony tokenem adres URL. Dzięki temu redaktorzy mogą pracować na wersjach roboczych, podczas gdy zwykli użytkownicy wciąż otrzymują zoptymalizowaną wersję strony. Do jego obsługi potrzebne są dwa proste route handlery: jeden do włączania, a drugi do wyłączania trybu.

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 Sanity Presentation Tool

W trybie podglądu (Draft Mode) musisz przełączyć się na previewClient. W przeciwieństwie do domyślnego klienta, który korzysta z szybkiego CDN i pokazuje tylko opublikowane treści, previewClient łączy się bezpośrednio z API Sanity. Dzięki opcji perspective: 'drafts' pobiera najświeższe, nawet nieopublikowane wersje robocze, pozwalając redaktorom widzieć zmiany na żywo przed publikacją.

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: 'drafts' — zwraca nieopublikowane zmiany. Dla pełnego preview na żywo z aktualizacjami w czasie rzeczywistym (bez przeładowania strony) użyj defineLive z next-sanitysanityFetch subskrybuje wtedy zmiany przez i odświeża dane w momencie każdej edycji. To aktualny wzorzec dla App Routera, który zastąpił starsze @sanity/preview-kit.

Visual Editing w Sanity: edycja treści na żywej stronie

Visual Editing zaciera granicę między stroną a CMS-em, ponieważ zamiast szukać pola w panelu, redaktor klika tekst lub obrazek bezpośrednio na podglądzie strony, a edytor Sanity otwiera się dokładnie w tym miejscu. To sprawia, że praca jest kontekstowa i intuicyjna, co znacząco przyspiesza wprowadzanie zmian i redukuje pomyłki, zwłaszcza u osób nietechnicznych.

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.

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

Często zadawane pytania

Kiedy Sanity ma sens przy stronie w Next.js?

Sanity ma sens, gdy klient lub zespół chce samodzielnie edytować treści, a projekt potrzebuje elastycznego modelu danych, live preview i integracji z nowoczesnym frontendem.

Czy Sanity wymaga własnego backendu?

Nie. Sanity hostuje backend i API, a w projekcie definiujesz schematy treści oraz pobierasz dane przez GROQ lub klienta Sanity.

Co daje Visual Editing w Sanity?

Visual Editing pozwala edytować treści w kontekście strony, co ułatwia pracę redakcyjną i zmniejsza liczbę poprawek między CMS-em a gotowym widokiem w Next.js.

Ile kosztuje Sanity?

Sanity posiada darmowy plan, ale limity zapytań, CDN, bandwidth i assetów zmieniają się wraz z cennikiem. Generalnie, przy małych stronach darmowy plan może wystarczyć, ale przy większym ruchu lub wielu redaktorach, powinieneś sprawdzić aktualne limity przed wyceną. Przy małej stronie warto też rozważyć, czy w ogóle potrzebujesz Next.js — z Astro w trybie statycznym treść pobierasz raz podczas builda, więc liczba zapytań do Sanity (i koszt) bywa niższa. Pokazujemy to w artykule Astro z Sanity — przewodnik po ultra-szybkim blogu.

Sanity vs Strapi — co wybrać?

Sanity działa w chmurze i jest gotowe do użycia od razu, co oznacza mniej technicznej pracy po Twojej stronie. Oferuje nowoczesne funkcje, jak edycja treści bezpośrednio na stronie (Visual Editing), co jest bardzo wygodne dla klientów. Strapi z kolei instalujesz na własnym serwerze, co daje Ci pełną kontrolę nad danymi i infrastrukturą, ale z drugiej strony wymaga więcej konfiguracji. Sanity to wybór, jeśli cenisz wygodę i nowoczesny interfejs dla redaktorów, a Strapi, jeśli priorytetem jest pełna kontrola nad hostingiem i danymi.

Czy mogę użyć Sanity bez Studio w Next.js?

Tak, Studio to opcjonalny panel admina. Możesz korzystać z Sanity API bezpośrednio z Server Components, bez osadzania Studio w aplikacji, ale Studio w /studio to najwygodniejsze rozwiązanie dla klienta.

O autorze

Maciej Sala

Maciej Sala — Product Manager i Frontend 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 rozwijam 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
Jak połączyć Astro z Sanity CMS? Przewodnik po ultra-szybkim blogu

Jeśli budujesz bloga, portfolio albo stronę firmową, gdzie liczy się każdy punkt w Google, połączenie Astro z Sanity jest jednym z najlepszych wyborów , jakie możesz zrobić w 2026 roku. Sanity dostarcza ustrukturyzowaną treść, a Astro zamienia ją w czysty HTML bez zbędnego JavaScriptu. Strona ładuje się, zanim użytkownik zdąży mrugnąć, a Lighthouse 100/100 jest w zasięgu dzięki decyzjom architektonicznym podjętym już na samym początku.

Maciej Sala

Maciej Sala

Founder StriveLab

Payload CMS czy Sanity? Który Headless CMS wybrać w 2026 roku?

Payload czy Sanity? Samodzielne hostowanie czy SaaS ? Świetny edytor kolaboracyjny czy pełna kontrola nad danymi i przewidywalne koszty przy skali? Wybór CMS-a to poważna decyzja architektoniczna i jego zmiana np. w połowie projektu jest dużo droższa niż zmiana warstwy widoku. Po podjęciu decyzji żyjesz z jej konsekwencjami przez przynajmniej kilka następnych miesięcy.

Maciej Sala

Maciej Sala

Founder StriveLab

Migracja z WordPressa do Astro lub Next.js i Headless CMS

Przy migracji z WordPressa do Astro lub Next.js najczęstsza obawa brzmi: czy redakcja straci wygodny panel? Nie, jeśli frontend połączysz z Headless CMS-em takim jak Sanity, Payload albo Storyblok. Poniżej pokazuję, jak zaplanować taką architekturę i przenieść treść bez zmuszania zespołu do pracy w Markdownie.

Maciej Sala

Maciej Sala

Founder StriveLab