MDX в Astro: компоненты, шорткоды, картинки
MDX — это Markdown, в котором можно использовать JSX-компоненты прямо в тексте. Для контентных сайтов это очень удобно: пишешь обычный текст, а в нужных местах вставляешь интерактивные блоки, графики, формы или просто красивые сноски. В Astro MDX подключается через официальную интеграцию и почти не отличается от обычного Markdown — ровно до того момента, когда в нём появляются хитрые компоненты.
Базовая установка
Включается одной командой:
pnpm astro add mdxПосле этого Astro будет понимать .mdx-файлы наряду с .md. Можно класть их в src/pages — они станут страницами; в src/content/<collection>/ — войдут в коллекцию.
Минимальный пример:
---
title: Заметка про MDX
---
import Note from '../components/Note.astro';
# {frontmatter.title}
Это обычный Markdown. А вот <Note kind="info">это уже наш компонент</Note> внутри текста.В отличие от чистого Markdown, в MDX можно import, использовать выражения вроде {count} и обращаться к frontmatter.
Глобальные компоненты
Импортировать каждый компонент в каждом файле — раздражает. Решается провайдером компонентов: один раз указываешь, что слово Note в любом MDX — это твой Note.astro.
В Astro это делается на уровне layout. У меня в общем BlogLayout.astro:
---
import Note from '../components/Note.astro';
import Quote from '../components/Quote.astro';
import Image from '../components/Image.astro';
const components = { Note, Quote, img: Image };
---
<article>
<slot components={components} />
</article>А в самой статье просто пишу:
---
import BlogLayout from '../layouts/BlogLayout.astro';
---
# Заголовок
<Note>Подсказка прямо в тексте.</Note>
И дальше обычный текст.Перепрыгнуть стандартные теги тоже легко: ключ img в map components заменит дефолтный рендер картинки. Удобно для подключения нашей собственной обёртки с lazy-loading и WebP.
Шорткоды через ремарк-плагины
Иногда хочется писать в Markdown что-то простое — скажем, :youtube[abc123] — и получить вставку видео. В JSX-стиле это работало бы как <Youtube id="abc123" />, но я предпочитаю не пугать редакторов JSX. Решается через ремарк-плагин remark-directive и собственный обработчик.
// astro.config.mjs
import remarkDirective from 'remark-directive';
import { visit } from 'unist-util-visit';
function remarkYouTube() {
return (tree) => {
visit(tree, 'leafDirective', (node) => {
if (node.name === 'youtube') {
const id = node.children[0]?.value ?? '';
node.data = {
hName: 'iframe',
hProperties: {
src: `https://www.youtube.com/embed/${id}`,
allowfullscreen: true,
loading: 'lazy',
},
};
}
});
};
}
export default defineConfig({
markdown: { remarkPlugins: [remarkDirective, remarkYouTube] },
});Теперь в любой .md и .mdx можно писать :youtube[xxxx], и сразу получится блок с видео. Тот же приём работает для других «шорткодов»: твиты, схемы, заметки.
Картинки
Тут самая частая боль. В обычном Markdown  Astro обрабатывает через свой image-сервис, и всё хорошо. В MDX это тоже работает, но если ты вставляешь компонент <img> вручную — оптимизация не происходит, и картинка идёт «как есть».
Решение — импортировать изображения как обычные модули и использовать компонент Astro <Image />:
---
import { Image } from 'astro:assets';
import cover from '../assets/cover.png';
---
# Привет
<Image src={cover} alt="Обложка" widths={[640, 960, 1280]} sizes="(max-width: 800px) 100vw, 800px" />Импорт даст Astro метаданные (размеры, тип), и сборка отдаст оптимизированные версии. Для блогов с тысячами постов автоматически подключаю format="avif,webp" и quality={70} — экономия трафика без видимой потери качества.
Колоночная вёрстка и embeds
В MDX удобно делать «два столбца с примером и описанием». Я использую обёртку Cols.astro:
<Cols>
<Col>
Слева — описание метода.
</Col>
<Col>
```typescript
function calc(x: number) {
return x * 2;
}
```
</Col>
</Cols>Markdown внутри JSX корректно отрабатывает, если соблюдать пустые строки. Это, кстати, главная боль MDX: парсер чувствителен к отступам и пустым строкам, особенно когда вложенность большая.
Тёмная тема и подсветка кода
Astro умеет встроенную подсветку через Shiki — быстро, статически, никакого клиентского рантайма. По умолчанию в темах github-dark. Если хочется и светлую тему — задаётся в конфиге:
export default defineConfig({
markdown: {
shikiConfig: {
themes: { light: 'github-light', dark: 'github-dark' },
langs: ['typescript', 'tsx', 'bash', 'json'],
},
},
});Astro вставит две версии стилей и переключит их по классу html.dark. Не нужно делать клиентскую перерисовку.
Frontmatter и схемы
Если MDX живёт в коллекции, обязательно задавай schema на коллекцию. Опечатки в frontmatter ловятся ещё на этапе сборки и не доезжают до прода:
import { defineCollection, z } from 'astro:content';
const blog = defineCollection({
type: 'content',
schema: z.object({
title: z.string().min(3),
pubDate: z.coerce.date(),
cover: z.string().optional(),
tags: z.array(z.string()).max(5),
}),
});
export const collections = { blog };Я однажды поймал баг: в одной статье вместо tags: [astro, mdx] стояло tag: [...]. Сборка прошла, но фильтрация по тегам пропустила пост. С zod-схемой такое ловится сразу.
Что не стоит делать в MDX
- Гигантские компоненты с большой клиентской логикой. Если нужна интерактивность — лучше отдельный остров с импортом, а не куча useState внутри MDX.
- Сложную бизнес-логику (запросы, формы) — это хочется оставить в обычных
.astro-страницах. - Длинные импорты у каждого файла — если это типовой случай, заводи провайдера компонентов и убирай boilerplate.
Куда копать дальше
MDX — это «контент с компонентами», а не «компоненты с контентом». Если в статье больше 30% JSX — скорее всего она должна быть обычной .astro-страницей или React-компонентом. Всё остальное MDX отлично закрывает: техническая документация, справочные материалы, длинные обзорные статьи. Я в нём пишу почти весь блог уже два года, и обратно на чистый Markdown не хочется.