Пост как папка
Как устроены посты: одна папка на запись, строгая схема фронтматтера, черновики и медиа рядом с текстом.Каждый пост здесь — отдельная папка. Внутри лежит index.md и всё, что к нему относится: картинки, видео, исходники. Удалить пост — значит удалить одну папку, и ничего не останется висеть в public/.
Новый пост за три шага
- Создать папку внутри категории:
src/content/posts/<категория>/<слаг>/. - Положить в неё
index.mdс фронтматтером (шаблон ниже). - Запустить
npm run devи открытьhttp://localhost:4321— пост появится в ленте, в дереве слева и в RSS.
---
title: "Заголовок поста"
date: 2026-09-24
description: "Одна-две фразы для ленты, RSS и поисковиков."
tags: [astro, notes]
draft: false
---
Первый абзац. Заголовок первого уровня писать не нужно:
он берётся из title.
## Первый раздел
Структура и адреса
Дерево в сайдбаре строится рекурсивным обходом src/content/posts, поэтому в нём видны настоящие папки и файлы, включая картинки:
src/content/posts/
├── dev/
│ ├── content-collections/
│ │ └── index.md
│ └── neovim-as-a-blog/
│ ├── index.md
│ ├── palette.png
│ └── loop.mp4
└── notes/
├── ascii-art/
│ └── index.md
└── draft-idea/ ← черновик
└── index.md
- Адрес поста повторяет путь папки:
/posts/dev/content-collections/. - Категория — это просто папка. Новая категория появляется в дереве сама, настраивать нечего.
- Теги живут не в папках, а в фронтматтере: каждый тег получает страницу
/tags/<тег>/. - Удаление — удалить папку. Ни список постов, ни навигацию править не нужно.
Фронтматтер
Поля проверяются zod-схемой при сборке. Ошибка роняет npm run build с указанием файла и поля, а не выкатывает страницу с undefined в заголовке.
const posts = defineCollection({
loader: glob({ pattern: '**/index.md', base: './src/content/posts' }),
schema: z.object({
title: z.string().min(1),
date: z.coerce.date(),
description: z.string().min(1),
tags: z.array(z.string()).default([]),
draft: z.boolean().default(false),
}).strict(),
});
title— заголовок. Обязательно.date— дата публикации, по ней сортируется лента. Обязательно.description— описание для ленты, RSS и<meta>. Обязательно.tags— список тегов, по умолчанию пустой.draft—trueдля черновика, по умолчаниюfalse.
Схема строгая: лишнее поле — ошибка, поэтому опечатка вроде titel не проскочит молча. Если в строке есть двоеточие, её нужно взять в кавычки, поэтому title и description проще всегда писать в кавычках.
Черновики
draft: true делает пост черновиком:
- в дереве он помечен
~, как несохранённый файл; - в ленту, RSS и на страницы тегов не попадает;
- в
npm run devоткрывается как обычный пост, а в продакшен-сборке его страница не создаётся вовсе.
Картинки
Картинка кладётся в папку поста и вставляется относительным путём:

Подходят png, jpg, webp, avif, gif. При сборке картинки сжимаются в webp, получают размеры и ленивую загрузку, поэтому исходник можно класть большим. Текст в квадратных скобках — описание для тех, кто картинку не видит, оставлять его пустым не стоит. Картинка занимает ширину колонки текста.
Видео
Видео лучше гифок: весит в разы меньше и не портит цвета. Файл кладётся в папку поста, вставляется HTML-тегом:
<figure>
<video src="./loop.mp4" autoplay loop muted playsinline width="960" height="540"></video>
<figcaption>Подпись под видео</figcaption>
</figure>
autoplayработает только вместе сmuted, аplaysinlineнужен для iPhone.widthиheight— исходный размер ролика: браузер заранее оставит под него место, и текст не будет прыгать при загрузке.- Поддерживаются
mp4иwebm. Короткий зацикленный ролик без звука собирается так:
ffmpeg -i in.mov -t 4 -an -c:v libx264 -crf 28 -movflags +faststart loop.mp4