Przejdź do treści

Astro Content Collections: blog z walidacją Zod

Zbuduj typowany blog w Astro 7. Spójny schemat Zod, filtrowanie szkiców, referencje autorów i generowanie stron, z testami błędów przed publikacją.

Maciej Sala

Founder StriveLab

9 min czytaniaAktualizacja

Co dają i czym są Content Collections w blogu Astro?

Content Collections stanowią powiązane grupy wpisów, ładowane ze wskazanego źródła i przekazywane do szablonów za pośrednictwem dedykowanego API Astro. Wykorzystany loader glob() odpowiada za odczyt niezależnych plików, podczas gdy file() potrafi rozbić pojedynczy plik JSON na osobne rekordy. Zadeklarowany schemat Zod precyzuje oczekiwaną strukturę i typy pól, na podstawie których Astro automatycznie inferuje statyczne typy dla właściwości post.data.

Dla bloga najważniejsza korzyść to jedno miejsce definiujące kontrakt redakcyjny, ponieważ przykładowo, jeśli karta, artykuł i RSS korzystają z tego samego pola description, brak opisu można łatwo wykryć przed publikacją. Zapobiega to konieczności utrzymywania trzech osobnych parserów nagłówków frontmatter. Warto jednak pamiętać o ograniczeniach samej walidacji, ponieważ nawet poprawny składniowo ciąg znaków może przecież zawierać literówki, merytoryczne błędy czy treść skopiowaną z innego wpisu – schemat nie zweryfikuje jakości danych.

PotrzebaWłaściwy mechanizm
Wymagany tytuł i poprawna dataSchemat Zod
Podpowiedzi pól w kodzie szablonuTypy wygenerowane przez Astro
Wykrycie nieistniejącego pola użytego w kodzieastro check
Ukrycie szkiców i zaplanowanych wpisówFiltr danych używany w trasach i listach
Czytelny artykuł oraz poprawność faktówRedakcja i weryfikacja źródeł

Założenia i pliki przykładu

Przykład jest przeznaczony dla Astro 7, które korzysta z Zod 4 i importu z z astro/zod. Zachowaj konfigurację astro/tsconfigs/strict oraz wersję Node obsługiwaną przez tę linię Astro, co najmniej 22.12.0. Jeżeli dopiero zakładasz projekt, zacznij od poradnika o pierwszej stronie w Astro. Różnice migracyjne opisuje przewodnik migracji do Astro 7, a najważniejsze zmiany tej wersji omawiam w przeglądzie Astro 7.

Zbudujemy dwa adresy, czyli /blog/ z listą i /blog/pierwszy-wpis/ z artykułem. Plik konfiguracji leży bezpośrednio w src i nie będziemy używać tu starszej konwencji src/content/config.ts z type: 'content'. Podczas aktualizacji wyodrębniłem poniższe pliki do osobnego projektu z Astro 7.3.2 i TypeScriptem 6.0.3. astro check i astro build przeszły poprawnie, sprawdziłem także wariant MDX z integracją 8.0.1 oraz błędne dane opisane w sekcji testów.

Code
src/
  content.config.ts
  content/authors.json
  content/blog/pierwszy-wpis.md
  lib/blog.ts
  pages/blog/index.astro
  pages/blog/[...id].astro

Zaczynamy od Markdownu, żeby sam blog nie wymagał komponentów UI ani dodatkowych integracji. Wzorzec loadera obejmie też .mdx, ale przed dodaniem pierwszego takiego pliku zainstaluj zgodne z Astro 7 @astrojs/mdx w wersji 8 i zarejestruj mdx() w integrations w astro.config.mjs. Sam wzorzec glob() nie włącza obsługi MDX. Od Astro 7 pliki .md i .mdx domyślnie przetwarza Sätteri. Jeśli korzystasz z pluginów remark lub rehype, doinstaluj @astrojs/markdown-remark, żeby zachować dotychczasowy pipeline, albo przenieś pluginy na Sätteri. Osadzanie komponentów opisuję w poradniku o MDX w Astro.

Jeden schemat Zod dla wpisów i autorów

Poniższa konfiguracja stanowi wspólny fundament dla całego przykładu. Schemat wymusza redakcyjne limity długości dla tytułu i opisu, unifikuje tagi w ramach jednej listy oraz definiuje pole author jako relację do osobnej kolekcji. Warto zauważyć, że każdy nowy wpis traktowany jest domyślnie jako wersja robocza — jego oficjalna publikacja wymaga więc jawnego ustawienia draft: false.

Code
// src/content.config.ts
import { defineCollection, reference } from 'astro:content'
import { file, glob } from 'astro/loaders'
import { z } from 'astro/zod'
 
const authors = defineCollection({
  loader: file('src/content/authors.json'),
  schema: z.object({ name: z.string().trim().min(1) }),
})
 
const blog = defineCollection({
  loader: glob({ base: './src/content/blog', pattern: '**/*.{md,mdx}' }),
  schema: z.object({
    title: z.string().trim().min(1).max(100),
    description: z.string().trim().min(50).max(200),
    date: z.iso.datetime({ offset: true }).transform((value) => new Date(value)),
    author: reference('authors'),
    tags: z.array(z.enum(['astro', 'poradnik', 'seo'])).min(1).max(5),
    draft: z.boolean().default(true),
  }),
})
 
export const collections = { authors, blog }

Limity 100 i 200 znaków oczywiście nie są wymogiem Google, ale jakąś umowną wartością przyjętą w redakcji. Pozwalają ograniczyć przypadkowo długie pola, ale też trzeba pamiętać, że Google nie publikuje limitów znaków dla tytułu i opisu, a w wynikach może dobrać inny fragment strony jako snippet. Data ma tu postać pełnego timestampa zapisanego jako string. Najpierw walidujemy sam format tekstu, a dopiero po pozytywnej weryfikacji przekształcamy go w obiekt Date. To bardziej jednoznaczny kontrakt niż samo z.coerce.date(), które dopuszcza szerszy zestaw konwersji. Poza tym nie odrzucamy przyszłych dat, ponieważ przydadzą się do planowania publikacji.

Metoda z.object() domyślnie usuwa nieznane pola z wyniku parsowania i taka jest jej rola, ale nie wyłapie automatycznie literówki w nazwie opcjonalnej metadanej. Jeżeli zespół redakcyjny wymaga rygorystycznego odrzucania wszystkich nieoczekiwanych kluczy, warto rozważyć zastosowanie z.strictObject(), pamiętając jednak przy tym o wcześniejszej weryfikacji pól wstrzykiwanych automatycznie przez używany loader lub integracje Astro.

Frontmatter, który przechodzi tę walidację

Najpierw zapisz profil w src/content/authors.json. Klucz id identyfikuje wpis dla loadera file() i referencji autora.

Code
[{ "id": "jan-kowalski", "name": "Jan Kowalski" }]

Następnie dodaj src/content/blog/pierwszy-wpis.md. Ten opis spełnia zadany limit, data zawiera strefę, a autor odpowiada ID w JSON-ie.

Code
---
title: 'Pierwszy wpis z Content Collections'
description: 'Przykład bloga Astro z walidacją metadanych, autorem i kontrolą publikacji.'
date: '2026-04-24T09:00:00+02:00'
author: jan-kowalski
tags: [astro, poradnik]
draft: false
---
 
## Po co walidować metadane?

Jednolity schemat metadanych pozwala przechwycić brakujące lub błędne pola jeszcze przed oficjalną publikacją artykułu. Sama treść wpisu zaczyna się od nagłówka drugiego poziomu (##), ponieważ główny nagłówek <h1> generowany jest automatycznie przez szablon na podstawie metadanych. Zapobiega to powstawaniu podwójnych znaczników H1 na stronie.

Nazwa pliku (np. pierwszy-wpis.md) służy w tym loaderze do wygenerowania identyfikatora pierwszy-wpis. Należy pamiętać, że ID definiuje strukturę danych, podczas gdy ostateczny URL kształtuje się w warstwie routingu. Zmiana nazwy pliku na produkcji zmienia ten identyfikator, a w konsekwencji adres wpisu — dlatego po publikacji wymaga zaplanowania odpowiedniego przekierowania. W bardziej rozbudowanych architekturach logikę tworzenia ID można dostosować za pomocą funkcji generateId w loaderze.

Szkice i przyszłe publikacje pod kontrolą

Wszystkie publiczne widoki powinny używać tej samej funkcji pobierania wpisów. Poniższy helper odrzuca szkice, pomija przyszłe daty i sortuje od najnowszych. Przy jednakowych datach porządkuje wyniki według ID.

Code
// src/lib/blog.ts
import { getCollection } from 'astro:content'
 
export async function getPublishedPosts() {
  const now = Date.now()
  const posts = await getCollection('blog', ({ data }) =>
    !data.draft && data.date.getTime() <= now,
  )
  return posts.sort((a, b) =>
    b.data.date.getTime() - a.data.date.getTime() || a.id.localeCompare(b.id),
  )
}

W przypadku witryny statycznej ostateczny wynik renderowania zostaje utrwalony w momencie kompilacji. W związku z tym samo nadejście wyznaczonej daty nie spowoduje automatycznej publikacji wpisu, więc jeśli artykuł ma ukazać się jutro o 9:00, musisz zaplanować przebudowanie oraz ponowne wdrożenie aplikacji po tej godzinie. Kanał RSS i mapa witryny powinny opierać się na tym samym mechanizmie filtrowania, przy czym integracje generujące sitemapę na podstawie gotowych ścieżek wyjściowych muszą uwzględniać wyłącznie faktycznie zbudowane adresy. Pamiętaj również, że wersja robocza nadal pozostaje częścią kolekcji. Flaga draft: true nie zwalnia pliku z wymogów walidacji Zod i w związku z tym błędne pola w szkicu nadal zablokują proces synchronizacji. Wreszcie, ochrona poufnych materiałów wymaga ścisłej kontroli repozytorium oraz artefaktów wyjściowych (pamiętajmy, że sam filtr wykluczający wpis z listy nie stanowi mechanizmu autoryzacji).

Generowanie strony i rozwiązanie referencji autora

W src/pages/blog/[...id].astro pobieramy wpisy, tworzymy trasy i przekazujemy dane przez props. Typ CollectionEntry<'blog'> opisuje props szablonu, a getEntry() pobiera profil wskazany referencją.

Code
---
// src/pages/blog/[...id].astro
import { getEntry, render, type CollectionEntry } from 'astro:content'
import { getPublishedPosts } from '../../lib/blog'
 
export async function getStaticPaths() {
  return (await getPublishedPosts()).map((post) => ({
    params: { id: post.id }, props: { post },
  }))
}
 
interface Props { post: CollectionEntry<'blog'> }
const { post } = Astro.props
const author = await getEntry(post.data.author)
if (!author) throw new Error(`Brak autora dla wpisu ${post.id}`)
const { Content } = await render(post)
---
<html lang="pl">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>{post.data.title}</title>
    <meta name="description" content={post.data.description} />
  </head>
  <body><main><article>
    <h1>{post.data.title}</h1>
    <p>Autor: {author.data.name}</p>
    <time datetime={post.data.date.toISOString()}>
      {post.data.date.toLocaleDateString('pl-PL', { timeZone: 'Europe/Warsaw' })}
    </time>
    <Content />
  </article></main></body>
</html>

To kompletny, prosty dokument, bez zależności od niepokazanego layoutu. W projekcie z istniejącym layoutem przenieś do niego <html> i <head>, zachowując jawnie typowane propsy. reference('authors') zmienia identyfikator w obiekt wskazujący kolekcję i wpis. Bezpośrednie sprawdzanie powiązania z polem author zablokuje wdrożenie w przypadku braku profilu danego twórcy. Sprawdzian ten omija jednak autorów przypisanych do szkiców, ponieważ niepublikowane treści nie trafiają do funkcji getStaticPaths. Kompletnie spójny audyt relacji wymaga przejrzenia całej kolekcji wpisów przed odfiltrowaniem wersji roboczych. Astro 7 zgłasza w logu błąd nieprawidłowej referencji także dla szkiców, ale nie przerywa przez to builda.

Z kolei nazwa parametru jest dowolna, ale musi jedynie być spójna. Nazewnictwo pliku routingu wymusza strukturę parametrów: [...id].astro wymaga dostarczenia params.id, a [...slug].astro — params.slug. Składnia z wielokropkiem pozwala dodatkowo obsłużyć zagnieżdżone ścieżki. O finale adresu kanonicznego przesądza jednak mechanika loadera i zmapowana logika, a nie samo użycie słowa kluczowego id.

Lista artykułów bez drugiej konfiguracji

Strona listy używa tego samego helpera. Nie powielamy w niej schematu ani reguł publikacji.

Code
---
// src/pages/blog/index.astro
import { getPublishedPosts } from '../../lib/blog'
const posts = await getPublishedPosts()
---
<html lang="pl">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>Blog</title>
  </head>
  <body><main>
    <h1>Blog</h1>
    <ul>{posts.map((post) => <li>
      <a href={`/blog/${post.id}/`}>{post.data.title}</a>
      <p>{post.data.description}</p>
    </li>)}</ul>
  </main></body>
</html>

Na tym etapie działają lista, publiczny artykuł, autor oraz filtrowanie publikacji. To wystarczająca baza do dodania stylów i kolejnych wpisów. Rozwijaj model przez rozszerzanie istniejącego schematu, aby przy dodawaniu np. powiązanych artykułów nie usunąć przypadkiem pola draft używanego przez szablony.

Walidacja Zod i TypeScript w CI

Walidacja danych kolekcji odbywa się przy synchronizacji treści, m.in. podczas astro dev i astro build. Loader plikowy śledzi zmiany lokalnych plików w trybie developerskim. Zewnętrzne API wymaga własnego sposobu odświeżania; nie zakładaj, że zmiana w CMS od razu przebuduje stronę.

astro build nie zastępuje kontroli typów. Zainstaluj @astrojs/check i typescript@6, a w CI uruchom:

Code
npx astro check && npx astro build

Wersja TypeScriptu ma tu znaczenie, ponieważ TypeScript 7 z natywnym kompilatorem nie udostępnia jeszcze API, z którego korzysta astro check, więc polecenie kończy się błędem. @astrojs/check 0.9.10 obsługuje TypeScript 5 i 6, dlatego instalujemy szóstkę.

astro check sprawdza kod i korzysta z wygenerowanych typów kolekcji, a edytor może podpowiadać post.data.title, ale walidacja YAML w pliku Markdown jest osobną funkcją edytora. Jeśli po zmianie konfiguracji schematu typy TypeScript pozostają nieaktualne, uruchom polecenie astro sync i odśwież serwer językowy w swoim edytorze. Pamiętaj jednak, że udana kompilacja (green build) zapobiegnie błędnemu wdrożeniu tylko wtedy, gdy Twój pipeline CI/CD rygorystycznie uzależnia publikację od powodzenia tego etapu. Poniższe scenariusze testowe warto przeprowadzić na kopii pliku, ponieważ dają one znacznie większą pewność niż zwykła weryfikacja wizualna w przeglądarce.

Zmiana testowaOczekiwany wynik
Usunięcie title albo opis krótszy niż 50 znakówBłąd walidacji kolekcji
date: 'jutro rano'Błąd formatu daty
tags: [Astro] zamiast tags: [astro]Błąd wartości spoza enum
draft: true albo data daleko w przyszłościBrak trasy artykułu i linku na liście
Nieistniejący autor publicznego wpisuBuild zatrzymany podczas rozwiązywania referencji
post.data.titel w szablonieBłąd wykryty przez astro check
Usunięcie profilu autora szkicuPotrzebny osobny audyt relacji, jeśli chcesz blokować również takie zmiany

Kiedy rozszerzyć kolekcję o CMS, obrazy lub dane live?

Przy kilku autorach JSON jest prosty do przeglądu w Gicie. Gdy redakcja potrzebuje panelu, uprawnień i obiegu akceptacji, rozważ headless CMS w Astro. Sprawdź, czy wybrany pakiet faktycznie udostępnia loader Content Layer, czy tylko klienta API albo komponenty. API i wersje loaderów zależą od dostawcy. Dla lokalnych obrazów użyj helpera image() w funkcji schematu, jeżeli chcesz korzystać z metadanych i przetwarzania obrazów Astro. Zwykły z.string() sprawdza tekst, nie istnienie pliku. Dla zewnętrznych adresów waliduj URL i dozwolony protokół; samo startsWith('/') przepuści także adres zaczynający się od //.

Build-time collections pasują do treści aktualizowanych wraz z wdrożeniem. Live Content Collections pobierają dane podczas obsługi zapytania do kolekcji i wymagają świadomej obsługi błędów oraz cache. W Astro 7 loader danych live może dołączyć do wyniku podpowiedź dla cache, czyli tagi do unieważniania i czas ostatniej modyfikacji. Aby dane zmieniały się pomiędzy wizytami bez przebudowy strony, potrzebujesz renderowania na żądanie i odpowiedniego adaptera. Nie są konieczne do tego bloga; szerszy kontekst znajdziesz w omówieniu Astro 6.

Co Content Collections zmieniają w SEO?

Generalizując, spójne dane ułatwiają generowanie tytułów, opisów, RSS i danych strukturalnych, pomagając pośrednio w indeksacji. Szablon musi oczywiście osadzać metadane bezpośrednio w kodzie HTML, choć o ostatecznym uwzględnieniu konkretnych adresów w indeksie i tak decydują nieubłagane algorytmy Google. Przed samą publikacją upewnij się, że skonfigurowałeś docelową domenę, wdrożyłeś spójną politykę adresów kanonicznych oraz zweryfikowałeś poprawność mapy witryny i kodów odpowiedzi HTTP. Tworząc dane strukturalne BlogPosting, dbaj o to, by dokładnie odzwierciedlały widoczną treść artykułu. Ścieżki do obrazów zawsze przekształcaj w adresy bezwzględne za pomocą konstruktora new URL(path, site), co zapobiegnie błędom podwójnego doklejania domeny do istniejącego adresu URL. Dodatkowo, jeśli osadzasz JSON-LD przy użyciu dyrektywy set:html, zabezpiecz proces serializacji przed ryzykiem przedwczesnego zamknięcia znacznika <script> przez niezweryfikowane znaki (np. zamieniając < na sekwencję \u003c).

Szczegóły znajdziesz w poradniku o technicznym SEO w Astro. Schemat powinien wspierać te mechanizmy, a jego limity wynikać z potrzeb publikacji. Google nie określa preferowanej liczby słów. Skracaj powtarzalny kod po to, żeby czytelnik łatwiej wdrożył rozwiązanie, nie po to, by osiągnąć domniemany współczynnik tekstu do kodu.

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

Często zadawane pytania

Czy muszę pisać wszystko w TypeScript, żeby używać Content Collections?

Nie. Konfiguracja kolekcji może być również plikiem JavaScript, ale projekt nadal potrzebuje odpowiednich ustawień TypeScript. Najprościej zachować astro/tsconfigs/strict. Walidacja danych przez Zod i kontrola typów kodu to dwa odrębne mechanizmy; autocomplete frontmatteru w edytorze wymaga dodatkowego wsparcia edytora.

Czy Content Collections obsługują Markdown i MDX?

Tak. Loader glob może obejmować oba rozszerzenia, np. przez wzorzec **/*.{md,mdx}. Markdown działa bez dodatkowej integracji, natomiast MDX wymaga @astrojs/mdx. Komponenty React w MDX potrzebują dodatkowo integracji React. Sam loader nie instaluje obsługi tych formatów.

Czy astro build sprawdza typy i frontmatter?

Build synchronizuje treści i sprawdza dane względem schematu kolekcji, ale nie zastępuje pełnej kontroli TypeScript. W CI uruchamiaj astro check i astro build, a wdrożenie uzależnij od powodzenia obu poleceń. Schemat sprawdza tylko zdefiniowane reguły, nie jakość tekstu ani poprawność SEO.

Dlaczego szkic albo przyszły wpis pojawia się na stronie?

Pole draft i data nie sterują publikacją automatycznie. Musisz zastosować filtr przy budowaniu tras, listy wpisów, RSS i mapy strony. Na statycznej stronie upływ daty publikacji nie uruchomi nowego wdrożenia; potrzebujesz zaplanowanego buildu, aby wpis stał się dostępny.

Dlaczego trasa z kolekcji nie powstaje?

Sprawdź, czy glob znajduje plik, czy wpis przechodzi filtr publikacji i czy klucz params odpowiada nazwie parametru w pliku trasy. Dla [...id].astro użyj params.id. Nazwa [...slug].astro też jest poprawna, jeśli zwracasz params.slug. Samo zdefiniowanie kolekcji nie tworzy stron.

Kiedy potrzebuję Live Content Collections?

Gdy dane powinny być pobierane podczas obsługi żądania, np. dostępność produktu. Statyczny blog zwykle wystarczy przebudować po zmianie treści. Wariant live wymaga uwzględnienia renderowania na żądanie, adaptera, błędów źródła i cache; nie gwarantuje sam z siebie aktualności danych.

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
Przewodnik po projektach w Astro od architektury po utrzymanie

Strona internetowa może mieć świetny wynik PageSpeed, a mimo to wciąż mieć różne problemy, poczynając od blokowania publikacji artykułów, a kończąc na gubieniu zapytania z formularza. Stworzyłem ten przewodnik, by sprawnie prowadził przez wybór technologii, migrację, model treści, rendering, SEO i testy, aż po wdrożenie i utrzymanie. Szczegółowe rozwiązania znajdziesz w materiałach przypisanych do poszczególnych etapów.

Maciej Sala

Maciej Sala

Founder StriveLab

Pierwszy projekt w Astro 7 od instalacji do wdrożenia

W Astro jedna komenda tworzy projekt z gotowymi skryptami do pracy lokalnej i budowania strony. W tym przewodniku przygotujesz stronę w Astro 7, poznasz strukturę katalogów, sprawdzisz wynik produkcyjnego builda i skonfigurujesz pierwsze wdrożenie.

Maciej Sala

Maciej Sala

Founder StriveLab

Astro 6 i przewodnik po nowościach w tym Cloudflare Workers

Rynek frameworków podzielił się wyraźnie: Vercel + Next.js kontra Cloudflare + Astro. Astro 6 wprowadza dużo wartościowych nowości, które wyraźnie zmieniają model pracy. W tym wpisie pokazuję, co realnie zmieniło się z perspektywy technicznej i co musisz sprawdzić przed migracją.

Maciej Sala

Maciej Sala

Founder StriveLab

LH.pl – Cloud Server 1C4G