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:
@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:
astro-portabletextrenderuje Portable Text (format tekstu sformatowanego z Sanity) do HTML-a,@sanity/image-urlbuduje URL-e obrazów z transformacjami z -u Sanity,groqeksportujedefineQuery, dzięki któremu Sanity TypeGen może powiązać zapytanie z wygenerowanym typem wyniku.
Konfigurację dodajesz w astro.config.mjs:
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.
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:
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):
Przy tym wariancie w korzeniu projektu Astro tworzysz jeszcze sanity.config.ts, który definiuje Studio i rejestruje schematy:
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:
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:
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:
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:
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:
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:
Komponent obrazu odbiera blok Portable Text jako node i może od razu wygenerować responsywne warianty z poprawnymi wymiarami:
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:
Wtedy w komponencie możesz wygenerować obraz dopasowany do layoutu:
Dla obrazu wyświetlanego w różnych szerokościach dodaj srcset i sizes, aby telefon nie pobierał pliku przygotowanego dla dużego ekranu:
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:
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:
- W panelu Sanity wejdź w projekt i sekcję API / Webhooks.
- Utwórz webhook z możliwie wąskim filtrem, na przykład
_type == "post". - Jako adres podaj build hook z Vercela, Netlify albo Cloudflare Pages.
- Zaznacz techniczne zdarzenia
Create,UpdateiDelete: obejmują one pierwszą publikację, kolejne publikacje oraz wycofanie publikacji. - Nie włączaj zdarzeń draftów ani wersji, jeśli każde naciśnięcie klawisza nie ma uruchamiać buildu. Domyślnie są pomijane.
- 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:
- 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. - 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.
- Portable Text: każdy niestandardowy blok i adnotacja mają komponent, test oraz bezpieczne zachowanie awaryjne.
- Obrazy: grafiki mają tekst alternatywny, wymiary,
srcset,sizesi odpowiednią strategię ładowania; obraz LCP nie jest ładowany leniwie. - SEO po zmianie sluga: stary adres otrzymuje przekierowanie 301, link kanoniczny wskazuje aktualną stronę, a sitemapę generujesz dopiero z publicznych dokumentów.
- Publikacja: webhook ma wąski filtr, logi prób i monitoring buildu; zespół zna procedurę ręcznego wdrożenia oraz wycofania.
- 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.


