Web Development

Astro Content Collections

Pelajari cara mengelola konten secara terstruktur dengan Astro Content Collections — mulai dari schema definition, collection queries, dynamic routes, MDX integration, hingga RSS feed generation.

1. Pengenalan Content Collections

Content Collections adalah fitur Astro untuk mengelola konten lokal (Markdown, MDX, data files) secara terstruktur. Collections menyediakan validasi schema, type-safe queries, dan API yang konsisten untuk mengakses konten Anda.

Bayangkan Anda punya ratusan file Markdown untuk blog, dokumentasi, atau portofolio. Tanpa collections, Anda harus membaca file secara manual, parse frontmatter, dan mengelola datanya sendiri. Dengan Content Collections, Astro melakukan semua itu — plus validasi dan type safety.

Mengapa Content Collections?

FiturTanpa CollectionsDengan Collections
Schema ValidationManual, raw frontmatterOtomatis dengan Zod
Type SafetyString everywhereTypeScript types ter-generate
Queryimport.meta.glob + filtergetCollection() API
ErrorSilent failureBuild error jika schema invalid
Sorting/FilteringManual array manipulationBuilt-in query functions
💡 Tips

Content Collections adalah alasan utama mengapa banyak developer memilih Astro untuk website konten-heavy seperti blog, dokumentasi, dan marketing sites. Fitur ini menghilangkan sakit kepala dalam mengelola konten Markdown.

2. Project Setup

Shell — Membuat Project Astro
# Membuat project Astro baru
npm create astro@latest my-blog
cd my-blog

# Pilih template "Blog" saat setup
# Atau tambahkan collections manual ke project yang ada

# Jalankan development server
npm run dev
# Server berjalan di http://localhost:4321

# Struktur folder Content Collections:
# my-blog/
# ├── src/
# │   ├── content/           ← Content Collections
# │   │   ├── config.ts      ← Schema definitions
# │   │   ├── blog/          ← Blog collection
# │   │   │   ├── post-1.md
# │   │   │   ├── post-2.mdx
# │   │   │   └── post-3.md
# │   │   └── docs/          ← Docs collection
# │   │       ├── getting-started.md
# │   │       └── api-reference.md
# │   └── pages/
# │       ├── index.astro
# │       └── blog/
# │           ├── index.astro
# │           └── [...slug].astro
# └── astro.config.mjs

3. Schema Definition

Schema mendefinisikan struktur data frontmatter untuk setiap collection. Astro menggunakan Zod untuk validasi schema dan TypeScript type inference.

TypeScript — src/content/config.ts
// src/content/config.ts
import { defineCollection, z } from "astro:content";

// Definisi schema untuk collection "blog"
const blogCollection = defineCollection({
  type: "content",    // "content" untuk Markdown/MDX, "data" untuk JSON/YAML
  schema: z.object({
    title: z.string().min(5).max(100),
    description: z.string().min(20).max(300),
    pubDate: z.coerce.date(),       // Auto-parse string ke Date
    updatedDate: z.coerce.date().optional(),
    author: z.string().default("Admin"),
    category: z.enum([
      "tutorial",
      "berita",
      "tips",
      "opini",
    ]),
    tags: z.array(z.string()).min(1).max(10),
    image: z.object({
      src: z.string(),
      alt: z.string(),
    }).optional(),
    draft: z.boolean().default(false),    // Jangan publish jika true
    featured: z.boolean().default(false),
    readingTime: z.number().min(1).optional(),
  }),
});

// Definisi schema untuk collection "docs"
const docsCollection = defineCollection({
  type: "content",
  schema: z.object({
    title: z.string(),
    description: z.string(),
    section: z.string(),
    order: z.number().default(0),        // Urutan dalam sidebar
    badge: z.enum(["new", "updated", ""]).optional(),
  }),
});

// Definisi schema untuk collection "authors" (data collection)
const authorsCollection = defineCollection({
  type: "data",
  schema: z.object({
    name: z.string(),
    bio: z.string(),
    avatar: z.string().url(),
    social: z.object({
      twitter: z.string().optional(),
      github: z.string().optional(),
      website: z.string().url().optional(),
    }).optional(),
  }),
});

// Export semua collections
export const collections = {
  blog: blogCollection,
  docs: docsCollection,
  authors: authorsCollection,
};
Markdown — Contoh Frontmatter
---
title: "Tutorial Astro Content Collections"
description: "Pelajari cara mengelola konten dengan Astro Content Collections"
pubDate: 2026-06-29
author: "Budi Santoso"
category: "tutorial"
tags: ["astro", "web-development", "tutorial"]
image:
  src: "/images/blog/astro-collections.png"
  alt: "Astro Content Collections"
draft: false
featured: true
readingTime: 8
---

## Apa itu Content Collections?

Content Collections adalah fitur terbaru dari Astro yang memudahkan pengelolaan konten Markdown. Dengan collections, Anda bisa mendefinisikan schema untuk frontmatter dan mendapatkan type safety secara otomatis.

### Keunggulan

- **Validasi otomatis** — error di-build time jika frontmatter invalid
- **Type safety** — TypeScript tahu tipe data setiap field
- **Query yang powerful** — filter, sort, dan paginate dengan mudah

4. Collection Queries

Astro menyediakan API getCollection() dan getEntry() untuk mengakses konten secara type-safe.

TypeScript — Mengakses Collections
// src/pages/blog/index.astro
---
import { getCollection } from "astro:content";

// Ambil semua post yang bukan draft
const posts = await getCollection("blog", ({ data }) => {
  return !data.draft;    // Filter: hanya yang draft=false
});

// Sort berdasarkan tanggal terbaru
const sortedPosts = posts.sort(
  (a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
);

// Group berdasarkan category
const featured = sortedPosts.filter((p) => p.data.featured);
const recent = sortedPosts.slice(0, 10);
---

<h1>Blog</h1>

<section>
  <h2>Featured</h2>
  {featured.map((post) => (
    <article>
      <h3><a href={`/blog/${post.slug}`}>{post.data.title}</a></h3>
      <p>{post.data.description}</p>
      <time>{post.data.pubDate.toLocaleDateString("id-ID")}</time>
    </article>
  ))}
</section>

<section>
  <h2>Terbaru</h2>
  {recent.map((post) => (
    <article>
      <h3><a href={`/blog/${post.slug}`}>{post.data.title}</a></h3>
      <span>{post.data.category}</span>
    </article>
  ))}
</section>
TypeScript — getEntry dan Referensi
// src/pages/blog/[...slug].astro
---
import { getCollection, getEntry } from "astro:content";
import BlogPost from "../../layouts/BlogPost.astro";

// Generate static paths untuk semua post
export async function getStaticPaths() {
  const posts = await getCollection("blog", ({ data }) => !data.draft);
  return posts.map((post) => ({
    params: { slug: post.slug },
    props: { post },
  }));
}

const { post } = Astro.props;

// Render konten Markdown
const { Content, headings } = await post.render();

// Ambil data author dari collection terpisah
const author = await getEntry("authors", post.data.author);
---

<BlogPost title={post.data.title} description={post.data.description}>
  <article>
    <header>
      <h1>{post.data.title}</h1>
      <p>Oleh: {author?.data.name ?? "Admin"}</p>
      <time>{post.data.pubDate.toLocaleDateString("id-ID")}</time>
      <div class="tags">
        {post.data.tags.map((tag) => <span class="tag">{tag}</span>)}
      </div>
    </header>

    {/* Table of Contents */}
    <nav class="toc">
      <h2>Daftar Isi</h2>
      <ul>
        {headings.map((h) => (
          <li style={`padding-left: ${h.depth - 1}rem`}>
            <a href={`#${h.slug}`}>{h.text}</a>
          </li>
        ))}
      </ul>
    </nav>

    {/* Konten Markdown yang di-render */}
    <div class="prose">
      <Content />
    </div>
  </article>
</BlogPost>

5. Dynamic Routes

TypeScript — Category Pages
// src/pages/blog/category/[category].astro
---
import { getCollection } from "astro:content";

export async function getStaticPaths() {
  const posts = await getCollection("blog", ({ data }) => !data.draft);

  // Ambil semua unique categories
  const categories = [...new Set(posts.map((p) => p.data.category))];

  return categories.map((category) => ({
    params: { category },
    props: {
      category,
      posts: posts
        .filter((p) => p.data.category === category)
        .sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()),
    },
  }));
}

const { category, posts } = Astro.props;
---

<h1>Kategori: {category}</h1>

{posts.map((post) => (
  <article>
    <h2><a href={`/blog/${post.slug}`}>{post.data.title}</a></h2>
    <p>{post.data.description}</p>
  </article>
))}
TypeScript — Tag Pages
// src/pages/blog/tag/[tag].astro
---
import { getCollection } from "astro:content";

export async function getStaticPaths() {
  const posts = await getCollection("blog", ({ data }) => !data.draft);

  // Kumpulkan semua tag unik
  const allTags = posts.flatMap((p) => p.data.tags);
  const uniqueTags = [...new Set(allTags)];

  return uniqueTags.map((tag) => ({
    params: { tag },
    props: {
      tag,
      posts: posts.filter((p) => p.data.tags.includes(tag)),
    },
  }));
}

const { tag, posts } = Astro.props;
---

<h1>Tag: #{tag}</h1>
<p>{posts.length} artikel ditemukan</p>

{posts.map((post) => (
  <article>
    <h2><a href={`/blog/${post.slug}`}>{post.data.title}</a></h2>
  </article>
))}

6. MDX Integration

Astro mendukung MDX (Markdown + JSX) yang memungkinkan Anda menggunakan komponen Astro/React langsung di dalam file Markdown.

Shell — Install MDX
# Install MDX integration
npx astro add mdx
# Atau manual:
npm install @astrojs/mdx
MDX — Menggunakan Komponen di MDX
---
title: "Tutorial dengan Komponen Interaktif"
description: "Contoh MDX dengan komponen"
pubDate: 2026-06-29
category: "tutorial"
tags: ["astro", "mdx"]
---

import Callout from "../../components/Callout.astro";
import CodeSandbox from "../../components/CodeSandbox.astro";
import Counter from "../../components/Counter.tsx";

## Pengenalan MDX

MDX memungkinkan Anda menggunakan komponen Astro langsung di Markdown!

<Callout type="info">
  Ini adalah komponen Callout yang di-import dari Astro.
  Bisa digunakan langsung di dalam MDX!
</Callout>

### Contoh Interaktif

<Counter client:visible initialCount={0} />

<CodeSandbox id="abc123" />

## Lanjutkan dengan konten Markdown biasa...

Paragraf biasa dengan **bold**, *italic*, dan `code`.

7. RSS Feed Generation

Shell — Install RSS Plugin
# Install RSS integration
npm install @astrojs/rss
TypeScript — src/pages/rss.xml.ts
// src/pages/rss.xml.ts
import rss from "@astrojs/rss";
import { getCollection } from "astro:content";
import type { APIContext } from "astro";

export async function GET(context: APIContext) {
  const posts = await getCollection("blog", ({ data }) => !data.draft);

  // Sort berdasarkan tanggal terbaru
  const sortedPosts = posts.sort(
    (a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
  );

  return rss({
    title: "Blog BeebaneLabs",
    description: "Tutorial teknologi dan IT terbaru",
    site: context.site!,
    items: sortedPosts.map((post) => ({
      title: post.data.title,
      pubDate: post.data.pubDate,
      description: post.data.description,
      link: `/blog/${post.slug}/`,
      categories: [post.data.category, ...post.data.tags],
      author: post.data.author,
    })),
    customData: "<language>id-ID</language>",
    stylesheet: "/rss/styles.xsl",
  });
}
Astro Config — Site URL
// astro.config.mjs
import { defineConfig } from "astro/config";
import mdx from "@astrojs/mdx";
import sitemap from "@astrojs/sitemap";

export default defineConfig({
  site: "https://beebanelabs.pages.dev",
  integrations: [mdx(), sitemap()],
});

8. Teknik Lanjutan

Custom Renderers & Remark Plugins

TypeScript — Custom Remark Plugin
// astro.config.mjs
import { defineConfig } from "astro/config";
import mdx from "@astrojs/mdx";
import remarkToc from "remark-toc";
import remarkGfm from "remark-gfm";
import rehypeSlug from "rehype-slug";
import rehypeAutolinkHeadings from "rehype-autolink-headings";

export default defineConfig({
  markdown: {
    remarkPlugins: [
      remarkGfm,           // GitHub Flavored Markdown
      [remarkToc, { heading: "daftar isi" }],  // Auto-generate TOC
    ],
    rehypePlugins: [
      rehypeSlug,          // Tambah ID ke headings
      rehypeAutolinkHeadings,  // Auto-link headings
    ],
    shikiConfig: {
      theme: "dracula",    // Syntax highlighting theme
      langs: ["javascript", "typescript", "python", "html", "css"],
    },
  },
  integrations: [mdx()],
});

Data Layer & External APIs

TypeScript — Content Layer API (Astro 5)
// src/content/config.ts (Astro 5+ Content Layer)
import { defineCollection, z } from "astro:content";
import { glob, file } from "astro/loaders";

// Local Markdown files
const blog = defineCollection({
  loader: glob({ pattern: "**/*.{md,mdx}", base: "./src/content/blog" }),
  schema: z.object({
    title: z.string(),
    pubDate: z.coerce.date(),
    tags: z.array(z.string()),
  }),
});

// JSON data files
const authors = defineCollection({
  loader: file("./src/data/authors.json"),
  schema: z.object({
    id: z.string(),
    name: z.string(),
    bio: z.string(),
  }),
});

// External API (fetch data dari CMS/API eksternal)
const products = defineCollection({
  loader: async () => {
    const response = await fetch("https://api.example.com/products");
    const data = await response.json();
    return data.map((item: any) => ({
      id: item.id.toString(),
      ...item,
    }));
  },
  schema: z.object({
    id: z.string(),
    name: z.string(),
    price: z.number(),
  }),
});

export const collections = { blog, authors, products };

9. Best Practices

✅ Astro Content Collections Best Practices
  • Selalu definisikan schema — validasi di build time mencegah error di production
  • Gunakan coercez.coerce.date() otomatis parse string ke Date
  • Draft system — gunakan field draft: true untuk post yang belum siap
  • Image optimization — gunakan astro:assets untuk gambar di content
  • MDX untuk interaktivitas — gunakan MDX saat butuh komponen di konten
  • Filter di query — lakukan filtering langsung di getCollection()
  • RSS selalu tersedia — buat rss.xml untuk setiap blog
  • Sitemap — gunakan @astrojs/sitemap untuk SEO
  • Type export — export type dari schema untuk digunakan di komponen
  • Content Layer API — gunakan untuk data eksternal (CMS, API)

10. Quiz: Uji Pemahamanmu!

Setelah membaca tutorial Astro Content Collections, jawablah 5 pertanyaan berikut:

Pertanyaan 1: Apa yang digunakan Astro untuk validasi schema Content Collections?

a) Joi
b) Yup
c) Zod
d) AJV

Pertanyaan 2: Apa fungsi getCollection()?

a) Membuat collection baru
b) Mengambil semua entry dari collection
c) Menghapus collection
d) Meng-update schema

Pertanyaan 3: Tipe collection apa yang digunakan untuk file Markdown?

a) type: "data"
b) type: "content"
c) type: "file"
d) type: "markdown"

Pertanyaan 4: Integrasi apa yang memungkinkan penggunaan JSX di dalam file MDX?

a) @astrojs/react
b) @astrojs/mdx
c) @astrojs/vue
d) @astrojs/svelte

Pertanyaan 5: Apa fungsi method post.render() pada content collection?

a) Menghapus post dari collection
b) Mengambil data JSON dari post
c) Merender konten Markdown ke komponen Astro & headings
d) Mengirim post ke RSS feed
🔍 Zoom
100%
🎨 Tema