Astro Content Collections — typowany blog z walidacją Zod od podstaw
Jak zbudować w Astro typowany system treści z walidacją frontmatteru? Content Collections, schematy Zod, glob loader, referencje między kolekcjami i Live Content Collections w Astro 6.
Content Collections to jedno z tych narzędzi Astro, które z pozoru wygląda na gadget, a w praktyce okazuje się niezastąpione. Dopóki masz 10 artykułów, ręczne parsowanie frontmatteru działa. Przy 30 zaczyna być niewygodne. Przy 80+ (tyle mam na StriveLab) po prostu nie da się tego utrzymać bez walidacji.
W tym artykule pokażę, jak zbudować typowany system treści w Astro — od zera do produkcyjnego setupu, z walidacją Zod, referencjami między kolekcjami i wydajnym queryingiem.
Czym są Content Collections
Content Collections w Astro to typowany system ładowania i walidacji treści z plików lokalnych, API lub źródeł live. to zestrukturyzowana, walidowana grupa treści — najczęściej artykułów blogowych, dokumentacji, stron produktowych, opisów członków zespołu. Astro daje Ci API do:
Ładowania treści z dowolnego źródła (lokalny Markdown/MDX, zdalny CMS, JSON, YAML, TOML, API).
Walidacji schematu (Zod) w build time — błędny frontmatter oznacza, że build się wywala, a nie produkcja.
Typowanego queryingu — w VS Code autocomplete pokazuje, jakie pola ma artykuł, bo TypeScript je zna.
Generowania stron dynamicznie na podstawie kolekcji (getStaticPaths).
W Astro 6 Content Collections są podzielone na dwa tryby: build-time (klasyczny, domyślny — dla blogów, dokumentacji) i live (runtime, dla danych zmieniających się w czasie rzeczywistym). W tym artykule skupimy się głównie na build-time, a Live Content Collections omówię w osobnej sekcji.
Konfiguracja kolekcji
Wszystkie kolekcje definiujesz w jednym pliku: src/content.config.ts. Oto minimalny przykład dla bloga:
Code
// src/content.config.tsimport { defineCollection, reference } from 'astro:content';import { glob } from 'astro/loaders';import { z } from 'astro/zod';const blog = defineCollection({ loader: glob({ pattern: '**/*.mdx', base: './src/content/blog', }), schema: z.object({ title: z.string().max(80), description: z.string().min(50).max(160), date: z.coerce.date(), author: z.string().default('Maciej Sala'), tags: z.array(z.string()), image: z.string().optional(), draft: z.boolean().default(false), seo_aeo_geo: z.boolean().default(false), }),});export const collections = { blog };
Co tu się dzieje:
glob — loader, który przechodzi po plikach .mdx w src/content/blog i traktuje każdy jako osobny entry w kolekcji.
schema: z.object({...}) — definicja pól, jakie musi mieć frontmatter każdego pliku. Zod waliduje typy i wartości.
z.coerce.date() — Zod weźmie "2026-04-24" jako string i skonwertuje do obiektu Date. W szablonie masz gotowy obiekt do formatowania.
z.string().default('Maciej Sala') — jeśli autor nie jest podany, Astro użyje default. Przydatne dla solo-bloga.
draft: z.boolean().default(false) — pole do filtrowania szkiców.
Jeśli spróbujesz zbudować projekt z plikiem MDX, który nie ma title albo description krótszego niż 50 znaków, build się wywali z czytelnym komunikatem. W pipeline CI/CD to oznacza, że nigdy nie zdeployujesz artykułu bez kompletnych metadanych — a to istotne zarówno dla SEO, jak i dla czystości OG tags w social mediach.
---title: "Mój pierwszy wpis"description: "Krótki opis dla SEO i OG tags."date: 2026-04-24tags: ["astro", "poradnik"]---# Treść wpisuTutaj normalna treść Markdown/MDX.
Generowanie stron z kolekcji
Teraz dynamicznie generujemy stronę dla każdego artykułu:
Referencje działają również dla tablic — możesz zdefiniować relatedPosts: z.array(reference('blog')) i trzymać listę powiązanych artykułów. Zod waliduje, że ID-ki naprawdę istnieją w kolekcji — literówka w referencji wyłapuje się w build time.
Zaawansowana walidacja Zod
Zod daje Ci dużo więcej niż z.string(). Oto wzorce, które regularnie używam:
Code
const blog = defineCollection({ loader: glob({ pattern: '**/*.mdx', base: './src/content/blog' }), schema: z.object({ // Długość tytułu idealna dla SEO title: z.string().min(30).max(80), // Meta description w granicach, które Google wyświetla description: z.string().min(50).max(160), // Data w przeszłości date: z.coerce.date().refine( (d) => d <= new Date(), 'Data publikacji nie może być w przyszłości' ), // Enum tagów — błąd w build, jeśli ktoś wpisze nieistniejący tag tags: z.array( z.enum(['astro', 'next-js', 'react', 'seo', 'poradnik', 'javascript']) ).min(1).max(5), // URL obrazu musi być lokalny lub HTTPS image: z.string().refine( (url) => url.startsWith('/') || url.startsWith('https://'), 'Obraz musi być lokalny lub HTTPS' ).optional(), // Czas czytania w minutach readingTime: z.number().int().positive().optional(), // Opcjonalny tytuł na social media (może być inny niż główny) ogTitle: z.string().max(60).optional(), }),});
Każde z tych pól wyłapuje inną klasę błędów. Enum tagów jest szczególnie wartościowy — przy 80+ artykułach bez niego szybko by się pojawiły duplikaty typu „Next.js", „NextJS" i „nextjs", które rozjeżdżają filtry.
Live Content Collections w Astro 6
Astro 6 wprowadza stabilne Live Content Collections dla danych, które muszą być świeże. Konfiguracja żyje w osobnym pliku src/live.config.ts:
W odróżnieniu od build-time collections, każde żądanie do strony przechodzi do API i pobiera świeże dane. Używaj tego do rzeczy, które realnie muszą być aktualne — stan magazynu, ceny, oferta w czasie rzeczywistym.
Integracja z istniejącym CMS
Community zbudowało loadery dla popularnych headless CMS: Sanity, Contentful, Storyblok, Strapi, Notion. Instalujesz pakiet, podajesz klucze API, dostajesz typowaną kolekcję.
Code
// src/content.config.ts — z loaderem Storyblokimport { defineCollection } from 'astro:content';import { storyblokLoader } from '@storyblok/astro';import { z } from 'astro/zod';const articles = defineCollection({ loader: storyblokLoader({ accessToken: process.env.STORYBLOK_TOKEN, contentTypes: ['article'], version: 'published', }), schema: z.object({ title: z.string(), body: z.string(), seo: z.object({ title: z.string(), description: z.string(), }), }),});
W praktyce dla małych i średnich blogów rekomenduję pozostanie przy MDX w repo — wersjonowanie w Git, brak zewnętrznych zależności, pełna kontrola. Dla zespołów z edytorami/copywriterami, którzy nie mają dostępu do repo — wtedy CMS ma sens.
SEO i structured data z Content Collections
Content Collections świetnie integrują się z generowaniem structured data. W layoucie dla artykułu:
Ponieważ wszystko jest typowane, TypeScript łapie literówki i brakujące pola zanim strona wejdzie na produkcję. To element, który opisuję szerzej w artykule o SEO w Astro.
Kontrakt redakcyjny
Największa wartość Content Collections pojawia się wtedy, gdy schema nie jest tylko typem dla developera, ale kontraktem dla autora treści.
title i description powinny być wymagane, bo bez nich nie ma dobrego SEO.
date warto walidować jako z.coerce.date(), żeby nie przepuścić tekstu przypadkiem.
tags powinny mieć ograniczoną listę dozwolonych wartości, jeśli budujesz strony tagów.
canonical i redirectFrom trzymaj w schemacie, jeśli robisz migracje albo aktualizacje slugów.
checked zostaw jako pole redakcyjne, nie jako sygnał jakości automatycznej.
Podsumowanie
Content Collections to moja ulubiona funkcja Astro. Różnica między projektem z walidacją a bez walidacji jest zauważalna już przy 20 artykułach, a przy większych blogach staje się koniecznością. Zod jako schema validator jest dostatecznie elastyczny, żeby obsłużyć wszystkie realistyczne scenariusze, a integracja z TypeScript daje autocomplete i łapanie błędów w IDE.
Jeśli planujesz blog lub dokumentację w Astro i chcesz mieć pewność, że Twoje metadane są spójne — Content Collections są punktem startowym. Jeśli potrzebujesz pomocy w setupie lub migracji istniejącego projektu, skontaktuj się ze mną.
Często zadawane pytania
Nie. Content Collections działają też w JavaScript (`content.config.js`), ale tracisz autocomplete i inferencję typów. Rekomenduję TypeScript — to zero kosztu wejścia, a zysk jest ogromny.
Pracuję z tym zawodowo.
Jeśli chcesz przełożyć ten temat na lepszą architekturę frontendu, uporządkować React lub Next.js i podnieść jakość pracy zespołu, skontaktuj się ze mną. Pomagam zamieniać wiedzę z artykułów w praktyczne decyzje technologiczne.
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.
SEO w Astro — Core Web Vitals, dane uporządkowane i techniczny fundament rankingu w 2026
Jak zbudować stronę w Astro, która dominuje w SEO — Core Web Vitals, sitemap, robots.txt, metadane, dane uporządkowane i GEO/AEO. Przewodnik techniczny z konkretnymi implementacjami.
Migracja bloga z WordPress na Astro — eksport treści, przekierowania 301 i zachowanie pozycji w Google
Kompletny przewodnik po migracji bloga z WordPress na Astro. Eksport przez REST API i WXR, mapowanie URL, przekierowania 301, migracja obrazów do astro:assets i monitoring pozycji w Google.
Architektura wysp w Astro — czym są wyspy i dlaczego zero JS domyślnie zmienia zasady gry
Architektura wysp to fundament Astro. Wyjaśniam, czym są wyspy, jak działa selektywna hydracja, kiedy daje realną przewagę i gdzie jest jej granica — z przykładami kodu i benchmarkami.