StriveLab
Strony internetowe
Usługi
RealizacjeO mnieBlogPorozmawiajmy
PL
EN
StriveLab
Strony internetowe
Usługi
RealizacjeO mnieBlogPorozmawiajmy
PL
EN

Next.js

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

React

Połączenie intuicyjności z wydajnością, które zapewnia bezproblemową skalowalność kodu.

Astro

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

Doradztwo produktowe

Połączenie perspektywy produktu, developera i marketingu w jednym miejscu

QA & Automation

Testy automatyczne komponentów i E2E w oparciu o Cypress.

SEO & Performance

Audyt techniczny i optymalizacja pod kątem SEO i GEO.

RealizacjeO mnieBlog
Porozmawiajmy
PL
EN

Nowoczesne strony internetowe dla firm, które myślą odważnie.

Przewiń do góry

Nazwa

StriveLab Maciej Sala

NIP

6772218995

REGON

524008527

E-mail

contact@strivelab.pl

Usługi główne
  • Tworzenie stron internetowych
  • Strony internetowe Next.js
  • Strony internetowe Astro
  • Strony internetowe React
Inne usługi
  • Usługi
  • SEO & Performance Sprint
  • QA & Stabilizacja
  • Konsultacje Product / Delivery
  • Aplikacje webowe Next.js
  • Współpraca ciągła
Strony
  • O mnie
  • Usługi
  • Realizacje
  • Blog

© 2026 StriveLab.pl

Polityka prywatności
AstroContent CollectionsZod

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.

OpublikujLinkedInFacebookWyślij
Autor
Maciej Sala
Opublikowano
1 maja 2026 16:31
Czytanie
5 min czytania
Aktualizacja
Wersja pierwotna

W skrócie

  • Content Collections dają typowany model treści, walidację frontmatteru i bezpieczne query do wpisów.
  • W Astro 6 konfiguracja kolekcji opiera się na src/content.config.ts, loaderach i astro/zod.
  • Build-time collections są najlepsze dla blogów i dokumentacji, a Live Content Collections dla danych zmiennych.
  • Największa wartość pojawia się, gdy schemat treści traktujesz jak kontrakt redakcyjny.

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.ts
import { 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.

Struktura plików

Dla powyższej konfiguracji struktura wygląda tak:

Code
src/
├── content/
│   └── blog/
│       ├── astro-6-przewodnik.mdx
│       ├── islands-architecture.mdx
│       └── content-collections-tutorial.mdx
├── content.config.ts
└── pages/
    └── blog/
        ├── index.astro         ← lista artykułów
        └── [...slug].astro     ← pojedynczy artykuł

Każdy plik MDX zaczyna się od frontmatteru YAML:

Code
---
title: "Mój pierwszy wpis"
description: "Krótki opis dla SEO i OG tags."
date: 2026-04-24
tags: ["astro", "poradnik"]
---
 
# Treść wpisu
 
Tutaj normalna treść Markdown/MDX.

Generowanie stron z kolekcji

Teraz dynamicznie generujemy stronę dla każdego artykułu:

Code
---
// src/pages/blog/[...slug].astro
import { getCollection, render } from 'astro:content';
import Layout from '../../layouts/Layout.astro';
 
export async function getStaticPaths() {
  const posts = await getCollection('blog', ({ data }) => !data.draft);
  return posts.map((post) => ({
    params: { slug: post.id },
    props: { post },
  }));
}
 
const { post } = Astro.props;
const { Content } = await render(post);
---
 
<Layout title={post.data.title} description={post.data.description}>
  <article>
    <header>
      <h1>{post.data.title}</h1>
      <time datetime={post.data.date.toISOString()}>
        {post.data.date.toLocaleDateString('pl-PL', {
          year: 'numeric',
          month: 'long',
          day: 'numeric',
        })}
      </time>
    </header>
 
    <Content />
  </article>
</Layout>

Kilka rzeczy wartych uwagi:

  • getCollection('blog', filterFn) — zwraca tablicę wszystkich entries z kolekcji, z opcjonalnym filtrem. Tu filtrujemy szkice.
  • post.id — Astro automatycznie generuje slug z nazwy pliku. astro-6-przewodnik.mdx → slug astro-6-przewodnik.
  • await render(post) — kompiluje MDX do komponentu Astro z nazwą Content, który renderuje treść.
  • Cała typografia (post.data.title, post.data.date) jest typowana — TypeScript wie, co Zod zwalidował.

Lista artykułów

Dla strony z listą:

Code
---
// src/pages/blog/index.astro
import { getCollection } from 'astro:content';
import Layout from '../../layouts/Layout.astro';
 
const posts = await getCollection('blog', ({ data }) => !data.draft);
const sorted = posts.sort(
  (a, b) => b.data.date.getTime() - a.data.date.getTime()
);
---
 
<Layout title="Blog — StriveLab">
  <h1>Blog</h1>
  <ul>
    {sorted.map((post) => (
      <li>
        <article>
          <a href={`/blog/${post.id}/`}>
            <h2>{post.data.title}</h2>
            <p>{post.data.description}</p>
            <time>{post.data.date.toLocaleDateString('pl-PL')}</time>
            <ul class="tags">
              {post.data.tags.map((tag) => <li>{tag}</li>)}
            </ul>
          </a>
        </article>
      </li>
    ))}
  </ul>
</Layout>

Proste, typowane, bez biblioteki do parsowania Markdown, bez ręcznego buildowania pipeline'u. Wszystko, czego potrzeba, jest w Astro.

Referencje między kolekcjami

Co, jeśli chcesz, żeby artykuły miały autora, a autorzy byli osobną kolekcją? Zod + Astro mają na to wbudowany mechanizm — reference().

Code
// src/content.config.ts
import { defineCollection, reference } from 'astro:content';
import { glob, file } from 'astro/loaders';
import { z } from 'astro/zod';
 
const authors = defineCollection({
  loader: file('src/content/authors.json'),
  schema: z.object({
    id: z.string(),
    name: z.string(),
    bio: z.string(),
    avatar: z.string(),
    linkedin: z.string().url().optional(),
  }),
});
 
const blog = defineCollection({
  loader: glob({ pattern: '**/*.mdx', base: './src/content/blog' }),
  schema: z.object({
    title: z.string(),
    description: z.string(),
    date: z.coerce.date(),
    author: reference('authors'),      // ← referencja
    relatedPosts: z.array(reference('blog')).optional(),
    tags: z.array(z.string()),
  }),
});
 
export const collections = { authors, blog };

I src/content/authors.json:

Code
[
  {
    "id": "maciej-sala",
    "name": "Maciej Sala",
    "bio": "Founder StriveLab, frontend dev specjalizujący się w Astro i Next.js.",
    "avatar": "/images/authors/maciej.jpg",
    "linkedin": "https://linkedin.com/in/maciej-sala"
  }
]

Teraz w artykule w frontmatterze wpisujesz tylko ID autora:

Code
---
title: "Mój wpis"
author: "maciej-sala"
---

A w szablonie:

Code
---
import { getCollection, getEntry, render } from 'astro:content';
 
const post = // ... pobierasz post
const author = await getEntry(post.data.author);
---
 
<article>
  <h1>{post.data.title}</h1>
 
  <div class="author">
    <img src={author.data.avatar} alt={author.data.name} />
    <div>
      <p>{author.data.name}</p>
      <p>{author.data.bio}</p>
    </div>
  </div>
</article>

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:

Code
// src/live.config.ts
import { defineLiveCollection } from 'astro:content';
import { z } from 'astro/zod';
 
const products = defineLiveCollection({
  loader: {
    name: 'products-api-loader',
    loadCollection: async () => {
      const response = await fetch('https://api.mystore.com/products');
      const data = await response.json();
      return { entries: data };
    },
    loadEntry: async ({ filter }) => {
      const response = await fetch(
        `https://api.mystore.com/products/${filter.id}`
      );
      return response.json();
    },
  },
  schema: z.object({
    id: z.string(),
    name: z.string(),
    price: z.number(),
    inventory: z.number(),
    updatedAt: z.coerce.date(),
  }),
});
 
export const liveCollections = { products };

W komponentach używasz tego przez getLiveCollection i getLiveEntry:

Code
---
import { getLiveCollection } from 'astro:content';
 
const { entries, error } = await getLiveCollection('products');
 
if (error) {
  console.error('Nie udało się pobrać produktów:', error);
}
---
 
{entries?.map((product) => (
  <div>
    <h3>{product.data.name}</h3>
    <p>{product.data.price} zł</p>
    <p>W magazynie: {product.data.inventory}</p>
  </div>
))}

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 Storyblok
import { 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:

Code
---
// src/layouts/BlogPost.astro
const { post, author } = Astro.props;
 
const jsonLd = {
  '@context': 'https://schema.org',
  '@type': 'BlogPosting',
  headline: post.data.title,
  description: post.data.description,
  datePublished: post.data.date.toISOString(),
  author: {
    '@type': 'Person',
    name: author.data.name,
    url: author.data.linkedin,
  },
  image: post.data.image ? `https://strivelab.pl${post.data.image}` : undefined,
};
---
 
<script
  type="application/ld+json"
  set:html={JSON.stringify(jsonLd)}
/>

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.
Notatka

Dobra schema Content Collections zmniejsza liczbę błędów redakcyjnych bardziej niż checklisty w Notion, bo niepoprawny wpis po prostu nie przejdzie builda.

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.

Skontaktuj się ze mną
Maciej Sala

O autorze

Maciej Sala

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.

Moje artykułyWięcej o mnie

Seria

Astro w praktyce 2026
  1. 1Astro 6 — przewodnik po nowościach: Cloudflare Workers, Live Content Collections, Fonts API i CSP
  2. 2Architektura wysp w Astro — czym są wyspy i dlaczego zero JS domyślnie zmienia zasady gry
  3. 3SEO w Astro — Core Web Vitals, dane uporządkowane i techniczny fundament rankingu w 2026
  4. 4Migracja bloga z WordPress na Astro — eksport treści, przekierowania 301 i zachowanie pozycji w Google

Spis treści

12 sekcji · 12 min

  • Czym są Content Collections1 min
  • Konfiguracja kolekcji1 min
  • Struktura plików1 min
  • Generowanie stron z kolekcji1 min
  • Lista artykułów1 min
  • Referencje między kolekcjami1 min
  • Zaawansowana walidacja Zod1 min
  • Live Content Collections w Astro 61 min
  • Integracja z istniejącym CMS1 min
  • SEO i structured data z Content Collections1 min
  • Kontrakt redakcyjny1 min
  • Podsumowanie1 min

Biblioteka wiedzy

Czytaj dalej

Zobacz więcej wpisów
SEO w Astro — Core Web Vitals, dane uporządkowane i techniczny fundament rankingu w 2026

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.

Maciej Sala

Maciej Sala

Founder Strivelab

1 maja 2026
Migracja bloga z WordPress na Astro — eksport treści, przekierowania 301 i zachowanie pozycji w Google

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.

Maciej Sala

Maciej Sala

Founder Strivelab

1 maja 2026
Architektura wysp w Astro — czym są wyspy i dlaczego zero JS domyślnie zmienia zasady gry

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.

Maciej Sala

Maciej Sala

Founder Strivelab

1 maja 2026