lenec ru

← все посты

MDX в Astro: компоненты, шорткоды, картинки

10K

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 ![alt](src.jpg) 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 не хочется.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.