Przejdź do treści

Jak połączyć Astro z Sanity CMS krok po kroku

Zbuduj ultraszybki blog, łącząc Astro z Sanity CMS. Sprawdź schematy treści, zapytania GROQ, obsługę Portable Text, webhooks oraz listę kontrolną wdrożenia.

Maciej Sala

Founder StriveLab

13 min czytaniaAktualizacja

Astro i Sanity CMS oraz powody, dla których warto połączyć te technologie

Kombinacja tych dwóch narzędzi jest mocna, ponieważ każde z nich robi coś innego i ich role się nie nakładają. Sanity dostarcza ustrukturyzowaną treść, a Astro zamienia ją w czysty HTML bez zbędnego JavaScriptu.

Sekret tkwi w (Astro Islands). Klasyczny framework typu wysyła do przeglądarki cały JavaScript potrzebny do zbudowania strony, nawet jeśli to zwykły artykuł, który nic nie robi po załadowaniu. Astro odwraca tę logikę: domyślnie renderuje wszystko do statycznego HTML-a i nie wysyła żadnego JavaScriptu. Interaktywność dokładasz tylko punktowo, używając do tego celu wyspy (np. menu mobilne czy formularz), które się , podczas gdy reszta strony pozostaje czystym, lekkim HTML-em.

Dla strony opartej na treści to naturalne dopasowanie, ponieważ większość artykułu nie potrzebuje interaktywności. Astro może pobrać dane podczas buildu i zamienić je w gotowe pliki HTML. Dobry wynik nadal zależy od całej strony: obrazów, fontów, CSS-u, skryptów analitycznych, reklam, formularzy i hostingu. Sam wybór frameworka nie zastępuje pomiaru danych terenowych ani testów na urządzeniach mobilnych.

Jak połączyć Astro z Sanity CMS krok po kroku

Sanity utrzymuje oficjalną integrację @sanity/astro, więc nie wymyślasz koła na nowo. Instalacja to jedna komenda:

Code
npx astro add @sanity/astro @astrojs/react

@astrojs/react jest potrzebny, jeśli chcesz korzystać z wizualnej edycji albo osadzić Sanity Studio na trasie w projekcie Astro. Warto przy okazji dograć pakiety pomocnicze:

Code
npm install astro-portabletext @sanity/image-url groq
  • astro-portabletext renderuje Portable Text (format tekstu sformatowanego z Sanity) do HTML-a,
  • @sanity/image-url buduje URL-e obrazów z transformacjami z -u Sanity,
  • groq eksportuje defineQuery, dzięki któremu Sanity TypeGen może powiązać zapytanie z wygenerowanym typem wyniku.

Konfigurację dodajesz w astro.config.mjs:

Code
// astro.config.mjs
import { defineConfig } from 'astro/config'
import react from '@astrojs/react'
import sanity from '@sanity/astro'
import { loadEnv } from 'vite'
 
const { PUBLIC_SANITY_PROJECT_ID, PUBLIC_SANITY_DATASET } = loadEnv(
  process.env.NODE_ENV,
  process.cwd(),
  '',
)
 
export default defineConfig({
  integrations: [
    sanity({
      projectId: PUBLIC_SANITY_PROJECT_ID,
      dataset: PUBLIC_SANITY_DATASET,
      apiVersion: '2026-03-01', // wymagane dla przewidywalnych zapytań
      useCdn: false, // false przy buildzie statycznym i draftach
      perspective: 'published', // bez draftów i nieopublikowanych zmian
    }),
    react(),
  ],
})

W produkcji projectId, dataset i tokeny trzymaj w zmiennych środowiskowych. Sam projectId nie jest sekretem, ale jeden spójny sposób konfiguracji ogranicza ryzyko pomyłek między środowiskiem testowym i produkcyjnym.

Code
PUBLIC_SANITY_PROJECT_ID="YOUR_PROJECT_ID"
PUBLIC_SANITY_DATASET="production"
SANITY_API_READ_TOKEN="token-tylko-do-odczytu"

Prefiks PUBLIC_ stosuj wyłącznie do wartości, które mogą znaleźć się w kodzie przeglądarki. Tokenu nigdy nie oznaczaj jako publiczny. loadEnv() jest tu potrzebne, ponieważ plik astro.config.mjs jest oceniany przed udostępnieniem zmiennych przez import.meta.env. Dla publicznego datasetu i zwykłego buildu token nie jest potrzebny; dodajesz go dla prywatnego datasetu lub podglądu draftów i używasz wyłącznie po stronie serwera. Produkcyjne zapytania warto wykonywać z jawną perspektywą published, natomiast podgląd z perspective: 'drafts', tokenem oraz useCdn: false.

Integracja wystawia gotowego klienta Sanity jako wirtualny moduł sanity:client, którego importujesz w dowolnym komponencie i nie musisz ręcznie konfigurować połączenia w każdym pliku. Dla TypeScript warto dodać też deklarację typów:

Code
// src/env.d.ts
/// <reference types="astro/client" />
/// <reference types="@sanity/astro/module" />

Gdzie mieszka Sanity Studio i schemat treści

Zanim przejdziemy dalej, trzeba rozstrzygnąć, gdzie będzie działać Sanity Studio, czyli aplikacja dla redakcji. Istnieją dwie drogi.

Klasycznym rozwiązaniem jest osobny projekt Studio. Uruchamiasz npm create sanity@latest, kreator zakłada projekt w Sanity (dostajesz projectId) i generuje aplikację Studio z folderem na schematy. Wtedy plik sanity/schemas/post.js z następnej sekcji trafia właśnie tam, a Studio wystawiasz osobno (np. npx sanity deploy daje darmowy hosting pod *.sanity.studio).

Studio osadzone w projekcie Astro. Bardzo wygodne, gdy chcesz mieć wszystko w jednym repozytorium. Integracja @sanity/astro montuje Studio na wskazanej trasie przez opcję studioBasePath (to do tego potrzebny był @astrojs/react):

Code
// astro.config.mjs, fragment integracji
sanity({
  // ...konfiguracja jak wyżej
  studioBasePath: '/admin',
}),

Przy tym wariancie w korzeniu projektu Astro tworzysz jeszcze sanity.config.ts, który definiuje Studio i rejestruje schematy:

Code
// sanity.config.ts
import { defineConfig } from 'sanity'
import { structureTool } from 'sanity/structure'
import post from './sanity/schemas/post'
 
export default defineConfig({
  projectId: 'YOUR_PROJECT_ID',
  dataset: 'production',
  plugins: [structureTool()],
  schema: { types: [post] },
})

Studio wykonuje uwierzytelnione żądania z przeglądarki, dlatego dodaj adresy frontendu w panelu Sanity: API → CORS Origins. Dla środowiska lokalnego będzie to zwykle http://localhost:4321, a dla produkcji pełna domena serwisu. Włącz Allow credentials wyłącznie dla domen, które kontrolujesz.

W statycznym buildzie odświeżenie podstrony takiej jak /admin/structure może zwrócić 404, ponieważ wewnętrzny router Studio działa po stronie przeglądarki. Rozwiąż to przez rewrite /admin/* do /admin na hostingu albo ustaw studioRouterHistory: 'hash'. Przy output: 'server' integracja obsługuje trasę catch-all automatycznie. Hash routing jest dobry dla statycznego Studio, ale Presentation Tool wymaga domyślnego trybu historii oraz serwerowego frontendu.

Dla małego bloga wariant osadzony może być najprostszy: panel działa pod /admin, a schematy pozostają w tym samym repozytorium co frontend. Osobne Studio daje natomiast niezależny cykl wdrożeń, osobną domenę i mniejsze powiązanie panelu redakcyjnego z frontendem. Warto je rozważyć nie tylko przy większym zespole, lecz także wtedy, gdy jeden dataset zasila kilka serwisów.

Minimalny model wpisu w Sanity

Zanim Astro zacznie pobierać dane, Sanity musi wiedzieć, jak wygląda wpis blogowy. Minimalny model powinien zawierać tytuł, slug, opis, datę publikacji, obraz główny i treść w Portable Text:

Code
// sanity/schemas/post.js
import { defineField, defineType } from 'sanity'
 
export default defineType({
  name: 'post',
  title: 'Wpis',
  type: 'document',
  fields: [
    defineField({
      name: 'title',
      title: 'Tytuł',
      type: 'string',
      validation: (rule) => rule.required(),
    }),
    defineField({
      name: 'slug',
      title: 'Slug',
      type: 'slug',
      options: { source: 'title', maxLength: 96 },
      validation: (rule) => rule.required(),
    }),
    defineField({
      name: 'excerpt',
      title: 'Opis SEO',
      type: 'text',
      rows: 3,
      validation: (rule) => rule.required().max(180),
    }),
    defineField({
      name: 'publishedAt',
      title: 'Data publikacji',
      type: 'datetime',
      validation: (rule) => rule.required(),
    }),
    defineField({
      name: 'mainImage',
      title: 'Obraz główny',
      type: 'image',
      options: { hotspot: true },
      fields: [
        defineField({
          name: 'alt',
          title: 'Tekst alternatywny',
          type: 'string',
          validation: (rule) => rule.required(),
        }),
      ],
    }),
    defineField({
      name: 'body',
      title: 'Treść',
      type: 'array',
      of: [
        { type: 'block' },
        {
          type: 'image',
          options: { hotspot: true },
          fields: [
            defineField({
              name: 'alt',
              title: 'Tekst alternatywny',
              type: 'string',
              validation: (rule) => rule.required(),
            }),
          ],
        },
      ],
      validation: (rule) => rule.required(),
    }),
  ],
})

To prosty schemat wystarczający do uruchomienia bloga. Przed produkcją ustal jednak model SEO i redakcji: autora, kategorie, kanoniczny URL, tytuł oraz opis Open Graph, zasady przekierowania po zmianie sluga i relacje między wersjami językowymi. Nie dodawaj wszystkich możliwych pól „na zapas”, ale zaprojektuj te wpływające na publiczny adres i migrację treści przed pierwszą publikacją.

Jak pobierać dane z Sanity CMS w Astro za pomocą GROQ

Tu wkracza , czyli język zapytań Sanity, który jest trochę jak dla grafu dokumentów JSON: filtrujesz, sortujesz, robisz projekcje i joiny w jednym zapytaniu. Sanity wystawia też , jeśli masz takie preferencje, ale GROQ jest natywny i zwykle zwięźlejszy. Wybór należy do Ciebie.

W Astro pobierasz dane bezpośrednio we komponentu, czyli w bloku między ---, który wykonuje się na serwerze podczas builda i nigdy nie trafia do przeglądarki:

Code
---
// src/pages/blog/index.astro
import { sanityClient } from 'sanity:client'
import BaseLayout from '../../layouts/BaseLayout.astro'
 
const posts = await sanityClient.fetch(
  `*[
    _type == "post" &&
    defined(slug.current) &&
    defined(publishedAt) &&
    dateTime(publishedAt) <= dateTime(now())
  ] | order(publishedAt desc) {
    title,
    "slug": slug.current,
    excerpt,
    publishedAt
  }`
)
---
 
<BaseLayout title="Blog">
  <h1>Blog</h1>
  <ul>
    {posts.map((post) => (
      <li>
        <a href={`/blog/${post.slug}`}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </a>
      </li>
    ))}
  </ul>
</BaseLayout>

To zapytanie GROQ czyta się tak: weź opublikowane dokumenty typu post, które mają slug i datę publikacji nieprzekraczającą bieżącej chwili, posortuj je od najnowszego i zwróć tylko potrzebne pola. Cała ta logika wykonuje się raz podczas buildu, a użytkownik dostaje gotowy HTML. Filtr daty jest ważny, ponieważ dokument może być opublikowany w Sanity, ale zaplanowany do pokazania dopiero później.

W SSG sam filtr nie uruchomi jednak nowego buildu o godzinie zapisanej w publishedAt. Jeśli publikujesz dokument dziś z datą ustawioną na jutro, dzisiejszy webhook zbuduje stronę bez tego wpisu, a jutro nie pojawi się nowe zdarzenie w Sanity. W związku z powyższym zaplanowane publikacje wymagają więc cyklicznego buildu na hostingu, osobnego harmonogramu wyzwalającego deploy o właściwej porze albo trasy renderowanej na żądanie.

Typowanie zapytań przez Sanity TypeGen

Samo użycie defineQuery nie tworzy jeszcze typów. Umieść zapytania w osobnym pliku .ts, uruchom ekstrakcję schematu i generowanie typów, a wygenerowany plik dodaj do zakresu include w tsconfig.json:

Code
// src/lib/queries.ts
import { defineQuery } from 'groq'
 
export const POSTS_QUERY = defineQuery(`
  *[_type == "post" && defined(slug.current)] | order(publishedAt desc) {
    _id,
    title,
    "slug": slug.current,
    publishedAt
  }
`)
Code
npx sanity schema extract
npx sanity typegen generate

TypeGen ogranicza ręczne dublowanie schematu, ale nie zastępuje obsługi wartości opcjonalnych. Build powinien nadal przerwać się czytelnym błędem, jeśli wpis nie ma pól wymaganych przez frontend.

Strona pojedynczego wpisu z getStaticPaths

Lista wpisów to dopiero połowa bloga, podczas gdy druga połowa to dynamiczna trasa src/pages/blog/[slug].astro, która podczas builda generuje osobną stronę dla każdego sluga z Sanity:

Code
---
// src/pages/blog/[slug].astro
import { sanityClient } from 'sanity:client'
import ArticleBody from '../../components/ArticleBody.astro'
import BaseLayout from '../../layouts/BaseLayout.astro'
import { urlFor } from '../../lib/sanityImage'
 
export async function getStaticPaths() {
  const posts = await sanityClient.fetch(
    `*[
      _type == "post" &&
      defined(slug.current) &&
      defined(publishedAt) &&
      dateTime(publishedAt) <= dateTime(now())
    ] {
      title,
      "slug": slug.current,
      excerpt,
      publishedAt,
      body[]{
        ...,
        _type == "image" => {
          asset->,
          alt
        }
      },
      mainImage {
        ...,
        alt
      }
    }`
  )
 
  return posts.map((post) => ({
    params: { slug: post.slug },
    props: { post },
  }))
}
 
const { post } = Astro.props
 
const imageUrl = post.mainImage
  ? urlFor(post.mainImage).width(1200).height(630).fit('crop').auto('format').url()
  : null
---
 
<BaseLayout title={post.title} description={post.excerpt}>
  <article>
    <h1>{post.title}</h1>
 
    {imageUrl && (
      <img
        src={imageUrl}
        alt={post.mainImage?.alt ?? ''}
        width="1200"
        height="630"
        loading="eager"
        fetchpriority="high"
      />
    )}
 
    <ArticleBody value={post.body} />
  </article>
</BaseLayout>

W tym wariancie getStaticPaths() pobiera wszystkie wpisy jednym zapytaniem GROQ i przekazuje je do stron przez props. Eliminuje to osobne żądanie dla każdego sluga, ale zwiększa rozmiar pojedynczej odpowiedzi oraz użycie pamięci podczas buildu. Dla małego i średniego bloga jest to rozsądny kompromis. Przy dużych treściach porównaj go z pobraniem samych slugów i osobnym zapytaniem parametryzowanym dla każdej strony albo z przetwarzaniem danych partiami.

Niezależnie od wybranego wariantu unikaj bezpośredniej interpolacji Astro.params.slug w zapytaniach GROQ. Zamiast tego skorzystaj z parametru $slug i przekaż { slug } jako drugi argument funkcji fetch(). Klient Sanity automatycznie sanitizuje parametry, co skutecznie chroni strukturę zapytania przed niepożądaną ingerencją wartości pochodzących z adresu URL.

Portable Text i obrazy z Sanity

Treść z Sanity nie jest Markdownem ani HTML-em, ponieważ domyślny edytor zapisuje ją jako Portable Text, czyli strukturalny JSON. Dzięki temu treść jest przenośna, ale musisz ją wyrenderować po stronie Astro.

Najprostszy wariant to komponent PortableText:

Code
---
import { PortableText } from 'astro-portabletext'
 
const body = post.body
---
 
<PortableText value={body} />

Ten wariant wystarcza tylko dla standardowych bloków tekstowych. Schemat z tego artykułu dopuszcza również obraz wewnątrz body, dlatego trzeba przypisać typ image do własnego komponentu:

Code
---
// src/components/ArticleBody.astro
import { PortableText } from 'astro-portabletext'
import SanityImage from './SanityImage.astro'
 
const components = {
  type: {
    image: SanityImage,
  },
}
---
 
<PortableText value={Astro.props.value} components={components} />

Komponent obrazu odbiera blok Portable Text jako node i może od razu wygenerować responsywne warianty z poprawnymi wymiarami:

Code
---
// src/components/SanityImage.astro
import { urlFor } from '../lib/sanityImage'
 
const { node } = Astro.props
const dimensions = node.asset?.metadata?.dimensions
const width = 800
const height = dimensions
  ? Math.round((width * dimensions.height) / dimensions.width)
  : undefined
---
 
{node.asset && (
  <img
    src={urlFor(node).width(width).auto('format').url()}
    srcset={[480, 800, 1200]
      .map((size) => `${urlFor(node).width(size).auto('format').url()} ${size}w`)
      .join(', ')}
    sizes="(min-width: 900px) 800px, calc(100vw - 32px)"
    alt={node.alt ?? ''}
    width={width}
    height={height}
    loading="lazy"
    decoding="async"
  />
)}

Tą samą metodą obsłużysz bloki kodu, filmy i linki wewnętrzne. Projekcja GROQ musi zwracać dane wymagane przez komponent. W głównym przykładzie robi to body[]{..., _type == "image" => {asset->, alt}}, dzięki czemu dostępne są również wymiary assetu. Nieznany typ bloku powinien być widoczny w testach, zamiast po cichu znikać z opublikowanego artykułu.

Z kolei dla obrazów użyj @sanity/image-url, żeby korzystać z transformacji Sanity CDN i nie wstawiać surowych assetów bez kontroli rozmiaru:

Code
// src/lib/sanityImage.js
import imageUrlBuilder from '@sanity/image-url'
import { sanityClient } from 'sanity:client'
 
const builder = imageUrlBuilder(sanityClient)
 
export function urlFor(source) {
  return builder.image(source)
}

Wtedy w komponencie możesz wygenerować obraz dopasowany do layoutu:

Code
<img
  src={urlFor(post.mainImage).width(1200).height(630).fit('crop').auto('format').url()}
  alt={post.mainImage.alt ?? ''}
  width="1200"
  height="630"
/>

Dla obrazu wyświetlanego w różnych szerokościach dodaj srcset i sizes, aby telefon nie pobierał pliku przygotowanego dla dużego ekranu:

Code
<img
  src={urlFor(image).width(800).auto('format').url()}
  srcset={[480, 800, 1200]
    .map((width) => `${urlFor(image).width(width).auto('format').url()} ${width}w`)
    .join(', ')}
  sizes="(min-width: 900px) 800px, calc(100vw - 32px)"
  alt={image.alt}
  width="800"
  height={Math.round(
    (800 * image.asset.metadata.dimensions.height) /
      image.asset.metadata.dimensions.width
  )}
  loading="lazy"
  decoding="async"
/>

Wymiary wymagają dereferencji asset-> w zapytaniu. Zachowują proporcje miejsca przed pobraniem grafiki i ograniczają CLS. Obrazu LCP nie ładuj leniwie; dla niego pozostaw loading="eager" oraz fetchpriority="high", ale nie stosuj tych atrybutów do wszystkich ilustracji w treści. To drobny szczegół, który ma jednak kluczowe znaczenie dla Core Web Vitals. W sytuacji, gdy umieścisz na stronie oryginalny obraz o szerokości 4000 px wprost z biblioteki mediów, witryna oparta na Astro co prawda nadal będzie działać sprawnie, jednak wskaźnik LCP może wyraźnie ucierpieć przez zbyt ciężki zasób.

Jeśli zależy Ci na wysokich pozycjach w wyszukiwarce Google, warto zadbać również o pełne SEO. Oficjalna integracja @astrojs/sitemap wymaga ustawienia produkcyjnego adresu w opcji site:

Code
import sitemap from '@astrojs/sitemap'
 
export default defineConfig({
  site: 'https://example.com',
  integrations: [sanity(/* ... */), react(), sitemap()],
})

Przy buildzie integracja generuje sitemap-index.xml oraz co najmniej jeden plik, na przykład sitemap-0.xml. Obejmuje również dynamiczne strony utworzone przez getStaticPaths(), ale nie potrafi automatycznie odkryć dynamicznych tras renderowanych wyłącznie w SSR. Link kanoniczny i tagi Open Graph generuj w layoutcie z pól wpisu. Obraz OG masz już gotowy dzięki transformacjom urlFor w rozmiarze 1200×630.

SSG czy SSR w Astro z Sanity CMS i który tryb wybrać?

Wybór sposobu renderowania wpływa na to, kiedy treść z Sanity trafia na stronę:

(domyślny) oznacza, że każde zapytanie GROQ wykonuje się raz, podczas builda, a strony serwowane są jako statyczne pliki. Idealny, gdy treść nie zmienia się co minutę i możesz pozwolić sobie na chwilę zwłoki między publikacją a przebudową (zwykle wyzwalaną z Sanity). To wybór dla większości blogów i stron firmowych, ponieważ jest najszybszy i najtańszy.

Renderowanie na żądanie () oznacza, że strona może zostać wyrenderowana podczas żądania. Nie gwarantuje to jednak natychmiastowej świeżości: wynik nadal może przechodzić przez cache Sanity, frameworka lub hostingu. Potrzebujesz adaptera zgodnego z platformą. Po SSR sięgasz przy podglądzie draftów, treści zależnej od cookies albo personalizacji, a zasady cache ustalasz osobno dla każdej z tych potrzeb.

W prostym modelu możesz zachować publiczny blog jako SSG i utworzyć osobne, serwerowe trasy podglądu z export const prerender = false. Aktualny oficjalny przewodnik Sanity po pełnym Visual Editing w Astro 7 stosuje jednak output: 'server', ponieważ ta sama trasa odczytuje przy każdym żądaniu ciasteczko trybu podglądu i wybiera perspektywę treści. Jeśli część tras nie potrzebuje tego mechanizmu, możesz prerenderować je jawnie przez export const prerender = true.

Przy okazji domknij regułę useCdn: dla zapytań wykonywanych w buildzie zostaw false, aby pobrać najświeższy opublikowany stan. Dla tras SSR z publiczną treścią zwykle ustaw true, natomiast dla perspektywy drafts zawsze używaj tokenu i useCdn: false. Token powinien mieć minimalne uprawnienia, na przykład Viewer, i nigdy nie może trafić do przeglądarki.

Visual Editing wymaga również skonfigurowania Presentation Tool, podania adresu Studio na potrzeby stega encodingu, przygotowania bezpiecznej ścieżki włączającej draft mode oraz dodania frontendu do listy CORS z obsługą poświadczeń (credentials). To pełnoprawna funkcja produkcyjna, a nie pojedyncza przełączana flaga w integracji.

Webhook po publikacji w Sanity

Statyczna strona nie zaktualizuje się sama po kliknięciu „Publish” w Sanity. Potrzebuje sygnału, który uruchomi nowy build i najprostszym jest wariant webhook z Sanity do platformy hostingowej:

  1. W panelu Sanity wejdź w projekt i sekcję API / Webhooks.
  2. Utwórz webhook z możliwie wąskim filtrem, na przykład _type == "post".
  3. Jako adres podaj build hook z Vercela, Netlify albo Cloudflare Pages.
  4. Zaznacz techniczne zdarzenia Create, Update i Delete: obejmują one pierwszą publikację, kolejne publikacje oraz wycofanie publikacji.
  5. Nie włączaj zdarzeń draftów ani wersji, jeśli każde naciśnięcie klawisza nie ma uruchamiać buildu. Domyślnie są pomijane.
  6. Sprawdź w logu prób, czy platforma zwraca kod z zakresu 2xx i rzeczywiście rozpoczyna wdrożenie.

Adres build hooka jest sekretem dającym możliwość uruchamiania wdrożeń, dlatego nie umieszczaj go w repozytorium ani publicznych logach. Jeśli webhook trafia najpierw do własnego endpointu, zweryfikuj podpis przy użyciu sekretu Sanity i obsłuż nagłówek idempotency-key, ponieważ dostarczenie ma semantykę co najmniej raz i może zostać powtórzone. Przy serii publikacji warto zastosować kolejkę lub debounce, aby kilka zmian nie uruchamiało konkurencyjnych buildów.

Ten mechanizm domyka podstawowy przepływ, ale publikacja nie kończy się na wysłaniu webhooka i dlatego monitoruj nieudane buildy, czas od publikacji do dostępności nowej wersji i stan ostatniego wdrożenia. Cały czas zachowaj możliwość ręcznego przebudowania serwisu oraz wycofania wadliwej publikacji.

Produkcyjna lista kontrolna Astro i Sanity

Przed uruchomieniem bloga sprawdź cały przepływ, a nie tylko lokalny rendering pojedynczego wpisu:

  1. Dostęp do danych: produkcja używa perspektywy published, token podglądu pozostaje po stronie serwera, a dataset ma świadomie wybrany tryb publiczny lub prywatny.
  2. Integralność treści: schemat wymaga kluczowych pól, zapytania pomijają przyszłe publikacje, a brak dokumentu lub nieprawidłowy slug kończy się kontrolowanym 404.
  3. Portable Text: każdy niestandardowy blok i adnotacja mają komponent, test oraz bezpieczne zachowanie awaryjne.
  4. Obrazy: grafiki mają tekst alternatywny, wymiary, srcset, sizes i odpowiednią strategię ładowania; obraz LCP nie jest ładowany leniwie.
  5. SEO po zmianie sluga: stary adres otrzymuje przekierowanie 301, link kanoniczny wskazuje aktualną stronę, a sitemapę generujesz dopiero z publicznych dokumentów.
  6. Publikacja: webhook ma wąski filtr, logi prób i monitoring buildu; zespół zna procedurę ręcznego wdrożenia oraz wycofania.
  7. Podgląd: Visual Editing nie ujawnia tokenu ani draftów, CORS dopuszcza tylko wymagane źródła, a trasa aktywująca draft mode weryfikuje sekret.

Na koniec opublikuj wpis testowy, zaktualizuj go i usuń. Sprawdź stronę listy, stronę artykułu, sitemapę, wynik 404 oraz wdrożenie po każdej operacji. Test pełnego cyklu wykrywa inne błędy niż samo uruchomienie astro build.

Ograniczenia integracji Astro z Sanity CMS

@sanity/astro ma inny zakres niż next-sanity i nie udostępnia identycznego modelu integracji. Integracja ta nie przypomina prostego przełącznika typu włącz podgląd na żywo. Pakiet next-sanity ściśle współpracuje z Next.js, korzystając z Live Content API oraz mechanizmów rewalidacji cache. Z kolei w Astro standardowy, najprostszy model opiera się na statycznym buforze i webhookach. Choć Visual Editing jest tam jak najbardziej możliwy, wymaga zupełnie osobnej konfiguracji: tokenu dostępu do wersji roboczych, specjalnego trybu podglądu, narzędzia Presentation Tool, kodowania stega oraz tras działających po stronie serwera.

Dla bloga czy strony firmowej nie jest to większy problem, ponieważ publikacja z kilkudziesięciosekundowym opóźnieniem po buildzie jest akceptowalna. Problem pojawia się, gdy budujesz aplikację, w której redakcja musi widzieć zmiany natychmiast, a frontend ma dużo logiki. Wtedy warto rozważyć Next.js z Payload albo Sanity z Next.js.

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

Często zadawane pytania

Czy Astro z Sanity nadaje się do dużego bloga?

Tak. W trybie statycznym Astro pobiera treść podczas builda i serwuje gotowy HTML. Przy setkach lub tysiącach wpisów trzeba jednak kontrolować rozmiar projekcji GROQ, czas i pamięć buildu, częstotliwość publikacji oraz limity platformy. Czasem lepsze są zapytania stronicowane, renderowanie na żądanie wybranych tras albo podział serwisu na mniejsze sekcje.

Czym jest GROQ i czy trudno się go nauczyć?

GROQ (Graph-Relational Object Queries) to język zapytań Sanity, projektowany pod grafy dokumentów JSON. Ma łagodną krzywą uczenia i szybko staje się produktywny, bo w jednym zapytaniu robisz filtrowanie, sortowanie, projekcje i joiny, które w REST wymagałyby kilku osobnych żądań. Sanity wystawia też GraphQL, jeśli wolisz.

Czy w Astro można osadzić Sanity Studio?

Tak. Integracja @sanity/astro pozwala zamontować Sanity Studio na wybranej trasie (np. /admin) za pomocą opcji studioBasePath. Wymaga to dodatkowo @astrojs/react. Dzięki temu panel edycyjny i strona żyją w jednym projekcie.

Czy zmiany w Sanity pojawiają się na stronie od razu?

W trybie statycznym nie, ponieważ treść aktualizuje się przy najbliższej przebudowie, zwykle wyzwalanej webhookiem z Sanity po zapisaniu zmiany. W trybie serwerowym (SSR) strona ponownie pobiera treść przy żądaniu, ale jej świeżość nadal zależy od ustawień cache Sanity i hostingu. Sanity ma Visual Editing dla Astro, ale wymaga osobnego setupu: draft mode, tokenów, Presentation Tool i serwerowo renderowanego podglądu.

Co to jest architektura wysp (Astro Islands) i dlaczego jest ważna dla wydajności?

Architektura wysp polega na tym, że Astro domyślnie renderuje całą stronę do statycznego HTML bez JavaScriptu. Interaktywność dodajesz tylko punktowo jako izolowane wyspy komponentów, które się nawadniają (hydrate) w przeglądarce. Dzięki temu strona contentowa ładuje się błyskawicznie, bo przeglądarka nie musi parsować i wykonywać kilobytów JS przy każdym wejściu.

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ą
LH.pl – Hosting Mango

Biblioteka wiedzy na temat Astro

Czytaj dalej

Zobacz więcej wpisów
Integracja Sanity CMS z Next.js od instalacji po Live Preview

Sanity to headless CMS, w którym schemat treści definiujesz w TypeScript. Schemat ż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 przez webhooki, Draft Mode i Visual Editing z Presentation Tool.

Maciej Sala

Maciej Sala

Founder StriveLab

Payload CMS czy Sanity: który headless CMS wybrać?

Payload i Sanity potrafią obsłużyć ten sam katalog treści, lecz przenoszą ciężar projektu w zupełnie inne miejsce. Payload oddaje zespołowi kod, bazę i odpowiedzialność za produkcję. Sanity dostarcza zarządzany Content Lake oraz rozbudowane środowisko współpracy, ale wiąże system z limitami i modelem usługi. Dobre porównanie nie kończy się na ekranie edytora. Musi objąć dzień publikacji, awarię, zmianę schematu, rachunek przy wzroście oraz możliwość wyprowadzenia danych.

Maciej Sala

Maciej Sala

Founder StriveLab

Astro i Headless CMS: Integracja Sanity, Storyblok, Strapi

Markdown w plikach projektu działa sprawnie, dopóki treść piszą osoby, które swobodnie pracują w Git. W sytuacji, kiedy klient chce sam zmienić cennik, a redaktor poprawia nagłówek w piątek po południu, repozytorium przestaje być wygodnym CMS-em. Wtedy najlepszy będzie headless CMS, czyli panel dla redakcji i API dla Astro.

Maciej Sala

Maciej Sala

Founder StriveLab

LH.pl – Cloud Server 1C4G