Building this site with Next.js and MDX

This site has two kinds of material that need to behave as one product: React components that define the interface, and essays that need durable URLs, metadata, typography, and code examples. I am keeping those essays beside the application code. In this repository, an article is an MDX page, a small data export, and ordinary source control—not a record fetched from a CMS and not a document rendered by a server runtime on every request.

That is a deliberately narrow architecture. The application currently uses Next.js 16.2.6, React 19.2.6, and @next/mdx 16.2.6. A Next.js build compiles the MDX into React components and produces a static export. Once deployed, an article request does not need a content API or application server to assemble the page. Build-time software still does real work; “static” describes the deployed result, not the authoring pipeline.

The configuration makes the constraint explicit

The center of the decision is in next.config.mjs. This is the configuration the site builds with:

import nextMDX from '@next/mdx'

/** @type {import('next').NextConfig} */
const nextConfig = {
  pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'mdx'],
  output: 'export',
  images: {
    unoptimized: true,
  },
  trailingSlash: true,
}

const withMDX = nextMDX({
  extension: /\.mdx?$/,
  options: {
    remarkPlugins: ['remark-gfm'],
    rehypePlugins: ['@mapbox/rehype-prism'],
  },
})

export default withMDX(nextConfig)

pageExtensions lets an MDX file participate in the App Router alongside JavaScript and TypeScript pages. output: 'export' tells Next.js to generate the deployable site without requiring the Next.js server after the build. trailingSlash matches the directory-shaped URLs in the exported output, and unoptimized images avoid depending on the runtime image optimization service.

Those settings reinforce one another. I cannot switch an article to request-time rendering casually while keeping the same deployment contract. Anything that needs a live server-only API has to be resolved at build time, moved to a browser-accessible service, or treated as a reason to change the architecture.

An article is both prose and a module

MDX is useful here because the article body stays readable as text while the page can use the same React presentation contract as the rest of the site. The top of every article exports its data and wraps the compiled body in ArticleLayout:

import { ArticleLayout } from '@/components/ArticleLayout'

export const article = {
  author: 'George Jeng',
  date: '2026-07-14',
  title: 'Building this site with Next.js and MDX',
  description: 'A description used by the article index and feed.',
}

export const metadata = {
  title: article.title,
  description: article.description,
}

export default function ArticlePage({ children }) {
  return <ArticleLayout article={article}>{children}</ArticleLayout>
}

The real description above this essay is longer, but the shape is the same. Next.js reads metadata for the route, while the site reads article for the visible title, publication date, article index, home-page list, and RSS feed. Deriving the route metadata from the article object prevents two copies of the title and description from drifting apart.

Discovery is convention-based rather than entered into a second registry. getAllArticles() uses fast-glob to find */page.mdx, imports each module, derives the slug from its directory, and sorts the results by date. Both the listing and the feed call that function. Renaming this directory therefore changes the public slug everywhere those consumers use it; there is no separate CMS identifier to synchronize.

let articleFilenames = await glob('*/page.mdx', {
  cwd: './src/app/articles',
})

let articles = await Promise.all(articleFilenames.map(importArticle))

return articles.sort((a, z) => +new Date(z.date) - +new Date(a.date))

This arrangement also clarifies what React is doing. React 19 is the component model that the compiled MDX joins. The prose does not become an independent content system; it becomes children passed into the existing layout. That layout handles the title, date, typography, and back navigation in one place. Because ArticleLayout is a client component, the exported page can still hydrate interactive behavior in the browser. A static export removes the request-time application server; it does not promise a page with no JavaScript.

What co-location buys me

The strongest authoring benefit is proximity. A change to a component and the essay that explains it can be reviewed in the same diff. Code samples can be checked against nearby source, and a broken import or invalid MDX expression can fail the build instead of quietly publishing a partial page. Git provides history, branching, and review without a separate content synchronization step.

The deployed shape is simple too. Article HTML and assets are produced ahead of time, so reading an essay does not depend on a database connection, a CMS response, or server-side React work at request time. That removes an entire class of runtime availability and caching questions. I am not attaching an invented performance number to that statement: the concrete benefit is fewer request-time dependencies, not a benchmark this repository does not contain.

There is also a useful degree of portability. The prose is stored as text, code fences remain recognizable outside the application, and the metadata is small enough to transform. It is not perfect portability, however. These files use JavaScript exports, the @/ import alias, React components, and Next.js route conventions. Moving to a different generator would be feasible, but it would require a conversion rather than a drag-and-drop copy.

What it costs

Keeping writing with code gives the build more responsibility. MDX compilation, syntax highlighting, module imports, article discovery, and the static export all have to compose successfully. A malformed article is now a software build failure. As the archive grows, every article also participates in work that is performed before deployment rather than being read on demand from a content store.

The authoring ergonomics are intentionally developer-shaped. Publishing means editing a repository, running the project, understanding the metadata contract, and sending the change through the same delivery path as code. That is coherent for the current repository, but it is less welcoming than a CMS editor with previews, scheduled publishing, media management, roles, and an approval workflow.

The static constraint can move complexity instead of eliminating it. Draft previews, search, comments, personalization, or content that must change immediately all need an explicit design. Some can run in the browser or during the build. Others would introduce an external service. If enough of them accumulate, preserving a serverless-looking deployment at all costs would be a poor trade.

The alternatives are valid

A CMS would separate editorial work from application releases. I would prefer that choice for a publication with several non-technical authors, frequent scheduling changes, a large managed media library, or permissions that should not map to repository access. The application could still generate static pages from CMS data, but it would gain a content schema, API credentials, preview integration, build hooks, and a plan for API failures during builds. Those are worthwhile costs when the editorial workflow needs them.

A server runtime would be the better foundation when article responses must be fresh per request, authenticated, personalized, or assembled from data that cannot be known during a build. It can also support server-owned search or preview behavior without rebuilding the whole site. In exchange, deployment must operate that runtime, observe it, patch it, and decide how its rendered responses are cached. The current article model does not require those capabilities, so adding the runtime would create responsibilities without changing what a reader receives.

For this site, the boundary is straightforward: writing remains source until a build turns it into the published site. Next.js supplies the route and build system, React supplies the shared component model, and MDX lets prose enter that model without hiding the document inside another platform. The static export is both the payoff and the constraint. It keeps the deployed reading path small, while making build discipline and code-oriented authoring part of the price.