There are many ways to host blogs with Next.js but I needed something fast & simple: plain MDX files, first‑party support, and zero extra content pipelines. No Contentlayer (which is unmaintained). No next-mdx-remote. No heavy weighted CMS systems. next-mdx-remote TL;DR Next.js’s official MDX integration lets you import .mdx as components and export metadata alongside content. See doc here: https://nextjs.org/docs/pages/guides/mdx I used @next/mdx with the App Router and kept indexing simple by importing metadata directly from each MDX file. No extra build steps, no content database, and the sitemap pulls dates straight from the MDX front‑matter. Next.js’s official MDX integration lets you import .mdx as components and export metadata alongside content. See doc here: https://nextjs.org/docs/pages/guides/mdx .mdx metadata See doc here: https://nextjs.org/docs/pages/guides/mdx See doc here: https://nextjs.org/docs/pages/guides/mdx https://nextjs.org/docs/pages/guides/mdx I used @next/mdx with the App Router and kept indexing simple by importing metadata directly from each MDX file. @next/mdx No extra build steps, no content database, and the sitemap pulls dates straight from the MDX front‑matter. Why MDX + App Router? Content is code: The App Router treats a folder as a route and page.mdx as a component. You get layouts, streaming, and RSC benefits for free. First‑party MDX: The official plugin is maintained with Next.js and plays nicely with routing, metadata, and bundling. Lower cognitive load: For a small product site, I don’t want a content compiler, watcher, or a GraphQL layer. A few MDX files and some imports are enough. The Core Setup Content is code: The App Router treats a folder as a route and page.mdx as a component. You get layouts, streaming, and RSC benefits for free. Content is code: The App Router treats a folder as a route and page.mdx as a component. You get layouts, streaming, and RSC benefits for free. page.mdx First‑party MDX: The official plugin is maintained with Next.js and plays nicely with routing, metadata, and bundling. First‑party MDX: The official plugin is maintained with Next.js and plays nicely with routing, metadata, and bundling. Lower cognitive load: For a small product site, I don’t want a content compiler, watcher, or a GraphQL layer. A few MDX files and some imports are enough. The Core Setup Lower cognitive load: For a small product site, I don’t want a content compiler, watcher, or a GraphQL layer. A few MDX files and some imports are enough. The Core Setup Add the official MDX plugin and let Next treat MD/MDX as pages. Add the official MDX plugin and let Next treat MD/MDX as pages. next.config.js next.config.js import createMDX from '@next/mdx'; const withMDX = createMDX({ // Add remark/rehype plugins if/when needed options: { remarkPlugins: [], rehypePlugins: [], }, }); /** @type {import('next').NextConfig} */ const nextConfig = { ... pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'md', 'mdx'], }; export default withMDX(nextConfig); import createMDX from '@next/mdx'; const withMDX = createMDX({ // Add remark/rehype plugins if/when needed options: { remarkPlugins: [], rehypePlugins: [], }, }); /** @type {import('next').NextConfig} */ const nextConfig = { ... pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'md', 'mdx'], }; export default withMDX(nextConfig); Optionally customize how MDX renders components (I kept it minimal for now): Optionally customize how MDX renders components (I kept it minimal for now): mdx-components.tsx mdx-components.tsx import type { MDXComponents } from 'mdx/types'; export function useMDXComponents(components: MDXComponents = {}): MDXComponents { return { ...components, }; } import type { MDXComponents } from 'mdx/types'; export function useMDXComponents(components: MDXComponents = {}): MDXComponents { return { ...components, }; } Type the metadata you export from MDX so TS understands it when imported elsewhere. Type the metadata you export from MDX so TS understands it when imported elsewhere. src/types/mdx.d.ts src/types/mdx.d.ts declare module '*.mdx' { import type { ComponentType } from 'react'; const MDXComponent: ComponentType<any>; export default MDXComponent; export const metadata: { title?: string; description?: string; date?: string; author?: string; tags?: string[]; }; } declare module '*.mdx' { import type { ComponentType } from 'react'; const MDXComponent: ComponentType<any>; export default MDXComponent; export const metadata: { title?: string; description?: string; date?: string; author?: string; tags?: string[]; }; } Create a post as a route. In the App Router, a folder is your slug and page.mdx is the page. Create a post as a route. In the App Router, a folder is your slug and page.mdx is the page. page.mdx src/app/blog/how-to-export-ig-followers-tutorial/page.mdx src/app/blog/how-to-export-ig-followers-tutorial/page.mdx export const metadata = { title: 'How to Export Instagram Followers (CSV, Excel, JSON)', description: 'Step-by-step guide…', date: '2025-08-28', }; import Image from 'next/image'; export const metadata = { title: 'How to Export Instagram Followers (CSV, Excel, JSON)', description: 'Step-by-step guide…', date: '2025-08-28', }; import Image from 'next/image'; Build a simple index page by importing metadata straight from MDX modules. Build a simple index page by importing metadata straight from MDX modules. src/app/blog/page.tsx src/app/blog/page.tsx import Link from 'next/link'; import { metadata as igExport } from './how-to-export-ig-followers-tutorial/page.mdx'; const posts = [ { slug: 'how-to-export-ig-followers-tutorial', title: igExport?.title ?? 'How to Export Instagram Followers', description: igExport?.description, date: igExport?.date, }, ]; export default function BlogIndexPage() { // Render cards linking to /blog/[slug] } import Link from 'next/link'; import { metadata as igExport } from './how-to-export-ig-followers-tutorial/page.mdx'; const posts = [ { slug: 'how-to-export-ig-followers-tutorial', title: igExport?.title ?? 'How to Export Instagram Followers', description: igExport?.description, date: igExport?.date, }, ]; export default function BlogIndexPage() { // Render cards linking to /blog/[slug] } Keep your sitemap honest by importing the same metadata for lastModified. Keep your sitemap honest by importing the same metadata for lastModified. lastModified src/app/sitemap.ts src/app/sitemap.ts import type { MetadataRoute } from 'next'; import { metadata as igExportPost } from './blog/how-to-export-ig-followers-tutorial/page.mdx'; import { getURL } from '@/utils/get-url'; export default function sitemap(): MetadataRoute.Sitemap { return [ // …other routes { url: getURL('blog/how-to-export-ig-followers-tutorial'), lastModified: igExportPost?.date ? new Date(igExportPost.date) : new Date(), changeFrequency: 'weekly', priority: 0.7, }, ]; } import type { MetadataRoute } from 'next'; import { metadata as igExportPost } from './blog/how-to-export-ig-followers-tutorial/page.mdx'; import { getURL } from '@/utils/get-url'; export default function sitemap(): MetadataRoute.Sitemap { return [ // …other routes { url: getURL('blog/how-to-export-ig-followers-tutorial'), lastModified: igExportPost?.date ? new Date(igExportPost.date) : new Date(), changeFrequency: 'weekly', priority: 0.7, }, ]; } The Aha Moments (and a few gotchas) MDX as modules: You can import both the rendered component and named exports (metadata) from any .mdx file. That made the blog index and sitemap trivial. Keep it typed: The *.mdx module declaration means TS won’t complain when you do import { metadata } from 'some-post/page.mdx'. Less is more: I didn’t reach for Contentlayer because I don’t need filesystem crawling or transformations. With a handful of posts, a tiny array is fine. MDX as modules: You can import both the rendered component and named exports (metadata) from any .mdx file. That made the blog index and sitemap trivial. metadata .mdx Keep it typed: The *.mdx module declaration means TS won’t complain when you do import { metadata } from 'some-post/page.mdx'. *.mdx import { metadata } from 'some-post/page.mdx' Less is more: I didn’t reach for Contentlayer because I don’t need filesystem crawling or transformations. With a handful of posts, a tiny array is fine. Contentlayer vs. Native MDX What Contentlayer gives you: What Contentlayer gives you: Schemas and types: Define required fields and get generated TypeScript. Build fails if a post is missing a title or date. Content graph: Read files from a content/ directory, compute slugs/paths, and query everything in one place. Computed fields: Derive readingTime, slug, canonical URLs, etc., at build time. Good for docs sites: Multiple document types (Guides, API refs, Changelogs) with strict structure. Schemas and types: Define required fields and get generated TypeScript. Build fails if a post is missing a title or date. title date Content graph: Read files from a content/ directory, compute slugs/paths, and query everything in one place. content/ Computed fields: Derive readingTime, slug, canonical URLs, etc., at build time. readingTime slug Good for docs sites: Multiple document types (Guides, API refs, Changelogs) with strict structure. Native MDX strengths (why I chose it here): Native MDX strengths (why I chose it here): Zero ceremony: No schema layer, no background watcher — just .mdx files and imports. Co‑location: The post lives at app/blog/[slug]/page.mdx, same place users will visit. Good enough typing: A tiny *.mdx module declaration plus optional Zod to validate metadata if you want stricter checks. Zero ceremony: No schema layer, no background watcher — just .mdx files and imports. .mdx Co‑location: The post lives at app/blog/[slug]/page.mdx, same place users will visit. app/blog/[slug]/page.mdx Good enough typing: A tiny *.mdx module declaration plus optional Zod to validate metadata if you want stricter checks. *.mdx metadata