Special-purpose Pages

A theme can ship pages via theme.pagesDir. Theme pages follow the same routing rules as user pages, and user pages always win on route conflicts.

This is useful for:

Provide Pages from the Theme

theme/index.js:

export default () => ({
	theme: {
		root: '.',
		pagesDir: './pages'
	}
})

theme/pages/:

pages/
  index.mdx
  menu.mdx
  friends.mdx
  contact.mdx
  404.mdx

Use Frontmatter to Choose a Layout

The simplest approach is to add a layout tag in frontmatter and branch in the template.

Example menu.mdx:

---
title: Menu
layout: menu
hidden: true
---

# Menu

This page uses a different layout.

In your template:

const MenuLayout = ({ ctx, PageContent }) => (
	<div class="menu-layout">
		<h1>{ctx.page.title}</h1>
		<PageContent />
	</div>
)

const DocLayout = ({ ctx, PageContent }) => (
	<div class="doc-layout">
		<aside>{/* nav */}</aside>
		<main>
			<PageContent />
		</main>
	</div>
)

export default function PageTemplate({ PageContent, ExtraHead, ctx }) {
	const layout = ctx.page.frontmatter?.layout
	return (
		<>
			<html>
				<head>
					<ExtraHead />
					<title>{ctx.page.title || ctx.site.name}</title>
				</head>
				<body>
					{layout === 'menu' ? (
						<MenuLayout ctx={ctx} PageContent={PageContent} />
					) : (
						<DocLayout ctx={ctx} PageContent={PageContent} />
					)}
				</body>
			</html>
		</>
	)
}

Route-based Switching

If you prefer “hard” routing rules, branch by route:

const route = ctx.page.routePath
if (route === '/menu') { /* ... */ }

Note: directory index routes include a trailing slash in routePath (e.g. /guide/).

Contact Forms and Other Interactive Pages

For pages like contact forms, render the page normally but supply client-only components (e.g. ContactForm.client.jsx) from your theme’s components/ and use them inside the page content.

Alternative: Special-purpose .html Pages

Special-purpose pages don’t have to be MDX. You can also create plain .html pages under pages/ (and under a theme’s pagesDir) for flows that fit better as standalone HTML (e.g. a dedicated landing page, an embedded form, or a micro-app).

These pages are included in the build and processed by Vite, and they still go through theme.template. The difference is that you’ll usually want to branch them into a “standalone” layout (no sidebar/ToC/footer, different <head>, etc.).

Common patterns:

Base and Asset Caveat

For theme source URLs (theme.sources) and static files under public/, you typically don’t need to apply withBase() — Methanol resolves those at build time. If you generate URLs in JS and hit Vite dev inconsistencies, withBase('/...') can be a practical workaround.