[Astro] Content Collections and Markdown Migration

2026-03-26 hit count image

Sharing how to manage blog content using Astro's Content Collections, and the process of migrating markdown files from Jekyll.

astro

Overview

In the previous post Astro Installation and Project Setup, we completed the basic project configuration. In this post, I’ll explain how to manage blog content using Content Collections, one of Astro’s core features, and the process of migrating markdown files from Jekyll.

What are Content Collections

Content Collections is a feature in Astro for structuring and managing content files like markdown and MDX. Key features include:

  • Type Safety: Validates frontmatter with Zod schemas to detect errors at build time
  • Automatic Type Generation: TypeScript types are automatically generated, supporting IDE autocompletion
  • Flexible Loader: Flexibly loads files using glob patterns
  • Built-in API: Easily query content with APIs like getCollection() and getEntry()

Schema Definition

The Content Collections schema is defined in the src/content.config.ts file. Here’s the schema used in this blog:

// src/content.config.ts
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';

const blog = defineCollection({
  loader: glob({ pattern: '**/*.md', base: './src/content' }),
  schema: z.object({
    title: z.string(),
    description: z.string(),
    lang: z.enum(['ja', 'ko', 'en']),
    category: z.string(),
    permalink: z.string(),
    image: z.string().optional(),
    comments: z.boolean().default(true),
    date: z.coerce.date(),
    published: z.boolean().default(true),
  }),
});

export const collections = { blog };

Schema Field Descriptions

Each field in the schema serves the following role:

FieldTypeRequiredDescription
titlestringOPost title
descriptionstringOSEO description (recommended under 160 characters)
langenumOLanguage code (ja, ko, en)
categorystringOCategory slug (e.g., astro, react)
permalinkstringOURL path (e.g., /astro/installation/)
imagestring-Featured image path
commentsboolean-Whether to show comments (default: true)
datedateOCreation date
publishedboolean-Whether to publish (default: true)

Key Points

glob Loader

loader: glob({ pattern: '**/*.md', base: './src/content' }),

Recursively loads all .md files under src/content/. You can freely organize subdirectory structures by category.

z.coerce.date()

date: z.coerce.date(),

Uses z.coerce.date() instead of z.date(). This automatically converts dates written as strings in frontmatter ('2026-04-15') into Date objects. It’s convenient because you don’t need to change the date format when migrating from Jekyll.

Restricting Languages with z.enum

lang: z.enum(['ja', 'ko', 'en']),

By restricting the lang field with enum, a build-time error occurs if an unsupported language code is entered.

Directory Structure and Naming Conventions

Directory Structure

Post files are organized into category-specific directories under src/content/.

src/content/
├── astro/
│   ├── 2026-04-01-migration-reason/
│   │   ├── index-ja.md
│   │   ├── index-ko.md
│   │   └── index-en.md
│   └── 2026-04-08-installation/
│       ├── index-ja.md
│       ├── index-ko.md
│       └── index-en.md
├── react/
│   └── 2026-03-03-react-19-migration/
│       ├── index-ja.md
│       ├── index-ko.md
│       └── index-en.md
├── jekyll/
│   └── ...
└── ...

Naming Conventions

The naming conventions for directories and files are as follows:

  • Directory name: YYYY-MM-DD-slug/ format including date and slug
  • File name: index-{lang}.md format to distinguish files by language
    • index-ja.md — Japanese
    • index-ko.md — Korean
    • index-en.md — English

The advantage of this structure is that multilingual files for a single post are grouped in the same directory, making management convenient.

Frontmatter Migration from Jekyll

Jekyll’s Frontmatter

Here’s the frontmatter format used in Jekyll:

---
layout: post
title: Jekyll Installation
description: Let's install Jekyll on Mac/Windows and create a basic project to start a Jekyll blog.
image: /assets/images/category/jekyll/install.jpg
categories: jekyll
date: 2018-09-08
---

Astro’s Frontmatter

Here’s the frontmatter format after migrating to Astro:

---
title: Jekyll Installation
description: Let's install Jekyll on Mac/Windows and create a basic project to start a Jekyll blog.
lang: en
category: jekyll
permalink: /jekyll/installation/
date: '2018-09-08'
published: false
comments: true
image: /assets/images/category/jekyll/install.jpg
---

Key Changes

Comparing Jekyll’s frontmatter with Astro’s frontmatter, here are the main differences:

ItemJekyllAstro
Layoutlayout: postNot needed (layout specified in page)
Categorycategories: jekyllcategory: jekyll (singular)
LanguageManaged by pluginlang: en (explicit)
URLAuto-generatedpermalink: /jekyll/installation/ (explicit)
Date2018-09-08'2018-09-08' (string recommended)

In Jekyll, the layout field specified which layout to use, but in Astro, layouts are imported in the page component ([...path].astro), so the layout field is not needed in frontmatter.

Category System

Blog categories are centrally managed in src/data/categories.ts.

// src/data/categories.ts
export type CategoryGroup =
  | 'frontend'
  | 'backend'
  | 'development'
  | 'service'
  | 'etc';

export interface Category {
  slug: string;
  group: CategoryGroup;
  image: string;
  isMainCategory: boolean;
  permalink: string;
  singlePage: boolean;
  searchJson: string;
  translations: Record<string, CategoryTranslation>;
}

Each category contains the following information:

  • slug: Identifier used in URLs and content directories
  • group: Classification such as frontend, backend, development, service, etc.
  • translations: Titles and descriptions in three languages — Japanese/Korean/English
{
  slug: 'astro',
  group: 'frontend',
  image: '/assets/images/category/astro/background.jpg',
  isMainCategory: true,
  permalink: '/astro/',
  singlePage: false,
  searchJson: 'astro',
  translations: {
    ja: {
      title: 'Astro',
      description: 'A record of migrating from Jekyll to Astro...',
      seeMore: 'もっと見る',
      noSearchResult: '検索結果がありません。',
    },
    ko: {
      title: 'Astro',
      description: 'A record of migrating from Jekyll to Astro...',
      seeMore: '자세히 보기',
      noSearchResult: '검색 결과가 없습니다.',
    },
    en: {
      title: 'Astro',
      description: 'A record of migrating from Jekyll to Astro...',
      seeMore: 'see more',
      noSearchResult: 'There is no search result.',
    },
  },
},

Content Queries

Content stored in Content Collections can be queried using the getCollection() API.

Basic Usage

The getCollection() API can be used as follows:

import { getCollection } from 'astro:content';

// Get all blog posts
const allPosts = await getCollection('blog');

// Filter only English + published posts
const enPosts = await getCollection('blog', ({ data }) => {
  return data.lang === 'en' && data.published !== false;
});

Filtering by Category

You can also use getCollection() to retrieve posts from a specific category:

// Get posts from a specific category
const astroPosts = await getCollection('blog', ({ data }) => {
  return (
    data.lang === 'en' && data.category === 'astro' && data.published !== false
  );
});

// Sort by date descending
const sortedPosts = astroPosts.sort(
  (a, b) => new Date(b.data.date).getTime() - new Date(a.data.date).getTime()
);

Using in Pages

getCollection() is used with getStaticPaths in dynamic routing pages ([...path].astro) as follows:

// src/pages/en/[...path].astro
export async function getStaticPaths() {
  const posts = await getCollection('blog', ({ data }) => {
    return data.lang === 'en' && data.published !== false;
  });

  return posts.map((post) => ({
    params: { path: post.data.permalink.slice(1, -1) },
    props: { post },
  }));
}

The key point is removing the leading and trailing / from the permalink field to use as params.path. This allows access via URLs like /en/astro/installation/.

Conclusion

In this post, we looked at how to manage blog content using Astro’s Content Collections.

  • Type-safe frontmatter management with Zod schemas in src/content.config.ts
  • Structuring multilingual posts with YYYY-MM-DD-slug/index-{lang}.md naming
  • Key conversion points from Jekyll frontmatter to Astro frontmatter
  • Central category management in categories.ts
  • Content querying and filtering with the getCollection() API

In the next post Multilingual (i18n) Implementation, we’ll cover how to implement a multilingual system supporting three languages — Japanese, Korean, and English.

Series Guide

This post is part of the Jekyll to Astro migration series.

  1. Why I Migrated from Jekyll to Astro
  2. Astro Installation and Project Setup
  3. Content Collections and Markdown Migration
  4. Multilingual (i18n) Implementation
  5. SEO Implementation
  6. Image Optimization — Custom rehype Plugin
  7. Comment System (Utterances)
  8. Ad Integration (Google AdSense)
  9. Search Implementation with Pagefind
  10. Layout and Component Architecture
  11. GitHub Pages Deployment
  12. Social Share Automation Script
  13. Troubleshooting and Tips

Was my blog helpful? Please leave a comment at the bottom. it will be a great help to me!

App promotion

You can use the applications that are created by this blog writer Deku.
Deku created the applications with Flutter.

If you have interested, please try to download them for free.



SHARE
Twitter Facebook RSS