Skip to content
kobiakov.dev
Назад до блогу

Як створити блог без обслуговування за допомогою MDX і Next.js 15

Як створити блог із контентом у git за допомогою MDX, gray-matter і Next.js App Router — без бази даних.

Vladyslav Kobiakov2 хв читання
Як створити блог без обслуговування за допомогою MDX і Next.js 15

Це технічна стаття для розробників. Якщо ви керуєте салоном, рестораном або ремісничим бізнесом, краще почніть звідси: скільки насправді коштує сайт і що насправді потрібно вашому сайту.

Багато невеликих сайтів надмірно ускладнюють блог. Ви встановлюєте CMS, налаштовуєте вебхуки, розгортаєте базу даних і витрачаєте на інфраструктуру більше часу, ніж на тексти. MDX із Next.js App Router усуває все це: контент зберігається в репозиторії, версіонується разом із кодом і не має жодних runtime-залежностей.

Чому MDX, а не headless CMS

Headless CMS на кшталт Sanity або Contentful — правильний вибір, коли контент публікує нетехнічна команда. Для особистого блогу розробника вона створює зайві зв’язки:

  • Затримка холодного старту — кожен рендер сторінки звертається до зовнішнього API.
  • Залежність від доступності — збій CMS робить недоступним і ваш блог.
  • Перемикання контексту — доводиться писати в браузерному редакторі замість IDE.

MDX змінює цю модель. Контент — це файл .mdx. Публікація — це git push. Обслуговувати CMS не потрібно.

Структура проєкту

content/
  blog/
    nextjs-15-architecture.mdx
    trading-bot-design.mdx
lib/
  mdx.ts          ← parse frontmatter + content
app/[locale]/blog/
  page.tsx         ← listing
  [slug]/page.tsx  ← detail

Обробка MDX-файлів за допомогою gray-matter

Основа системи — одна утиліта, яка читає .mdx-файли з диска та перетворює їх на вашу модель даних:

import fs from 'fs'; import path from 'path'; import matter from 'gray-matter'; const BLOG_DIR = path.join(process.cwd(), 'content/blog'); export async function getMdxPosts() { const files = fs.readdirSync(BLOG_DIR).filter((f) => f.endsWith('.mdx')); return files .map((file) => { const raw = fs.readFileSync(path.join(BLOG_DIR, file), 'utf8'); const { data, content } = matter(raw); const slug = file.replace(/\.mdx$/, ''); return { slug: data.slug ?? slug, title: data.title ?? 'Untitled', excerpt: data.excerpt ?? '', content, coverImage: data.coverImage ?? null, date: new Date(data.date ?? Date.now()), author: data.author ?? 'Admin', tags: data.tags ?? [], published: data.published ?? true, }; }) .filter((p) => p.published) .sort((a, b) => b.date.getTime() - a.date.getTime()); }
ts

gray-matter відокремлює YAML-frontmatter від основного тексту. Решта — звичайний TypeScript; на цьому етапі спеціальний MDX runtime не потрібен.

Рендеринг MDX в App Router

Для сторінки статті Next.js рекомендує @next/mdx або легший next-mdx-remote. Оскільки файли завантажуються під час запиту, а не статично імпортуються, next-mdx-remote підходить краще:

// app/[locale]/blog/[slug]/page.tsx import { MDXRemote } from 'next-mdx-remote/rsc'; import { getMdxPostBySlug } from '@/lib/mdx'; import { notFound } from 'next/navigation'; export default async function BlogPostPage({ params, }: { params: Promise<{ slug: string; locale: string }>; }) { const { slug } = await params; const post = await getMdxPostBySlug(slug); if (!post) notFound(); return ( <article className="prose prose-invert max-w-3xl mx-auto py-16 px-6"> <h1>{post.title}</h1> <MDXRemote source={post.content} /> </article> ); }
ts

MDXRemote з експорту /rsc є React Server Component: він компілює та рендерить MDX на сервері без клієнтського JavaScript.

Схема frontmatter

Узгоджена структура frontmatter запобігає нетиповим ситуаціям на сторінці зі списком статей:

--- title: 'Post Title' description: 'One-sentence summary used for SEO meta description.' date: '2026-05-01' author: 'Vladyslav Kobiakov' tags: ['nextjs', 'performance'] coverImage: '/projects/blog/mdx-blog.webp' published: true ---
yaml

Для чернеток залишайте published: false — фільтр у getMdxPosts виключить їх без змін у маршрутизації.

Статична генерація з generateStaticParams

Для максимальної продуктивності попередньо рендерте всі статті під час збірки:

export async function generateStaticParams() { const posts = await getMdxPosts(); return posts.map((post) => ({ slug: post.slug })); } export const revalidate = false; // fully static
ts

Разом із next/image для обкладинок і CDN перед Vercel це скорочує Time to First Byte до кількох мілісекунд.

Додавання власних MDX-компонентів

Одна з найбільших переваг MDX — можливість вбудовувати React-компоненти в текст. Зіставте назви компонентів із реалізаціями в одному місці:

const components = { CodeBlock: ({ code, lang }: { code: string; lang: string }) => ( <SyntaxHighlighter language={lang}>{code}</SyntaxHighlighter> ), Callout: ({ children }: { children: React.ReactNode }) => ( <div className="border-l-4 border-primary pl-4 my-6 text-muted-foreground"> {children} </div> ), }; // Pass to MDXRemote <MDXRemote source={post.content} components={components} />
ts

Тепер будь-який .mdx-файл може використовувати <Callout> або <CodeBlock> безпосередньо в тексті.

Компроміси

Що ви отримуєте:

  • Жодної бази даних, CMS API або вебхуків.
  • Контент версіонується в git: чернетки — це гілки, перевірка — pull request.
  • Повний набір інструментів IDE: автодоповнення, перевірка правопису, рефакторинг.

Від чого відмовляєтеся:

  • Немає візуального редактора для нетехнічних учасників.
  • Немає вбудованого керування медіафайлами — використовуйте Cloudinary або public/.
  • Для публікації потрібне повторне розгортання — прийнятний компроміс для сайту, який підтримує одна людина.

Висновок

Для сайту, який підтримує один розробник, блог на MDX має правильний рівень складності. Налаштування вміщується у два файли, масштабується до сотень статей без бази даних і залишає роботу з текстом у звичних інструментах. Єдина витрата на обслуговування — час від часу запускати npm update для gray-matter і next-mdx-remote.

Працюєте над чимось подібним?

Я створюю швидкі маркетингові сайти та системи онлайн-запису для малого бізнесу по всій Європі — як незалежний розробник, віддалено. Відповідаю протягом 24 годин.

Зв’язатися
Share: