DocumentationFramework guide

Markdown and layouts

Use page.md for guides, articles, and documentation. Nib parses frontmatter and renders Markdown to HTML during development and the production build.

Frontmatter

---
title: A documentation page
description: Explain one part of the project.
layout: docs
---

# A documentation page

GitHub-Flavored Markdown is rendered at build time.

Nib validates these built-in fields by default:

Field Purpose
title Page title and metadata.
description Search and social description.
draft Omit the page when true.
layout Select a React layout by flat filename.

Remark GFM supports tables, task lists, autolinks, and strikethrough.

Define a custom frontmatter schema when a content type needs more fields:

// src/content.ts
import { defineMarkdown, z } from '@briansunter/nib'

export const articleSchema = z.object({
  title: z.string(),
  description: z.string().optional(),
  draft: z.boolean().optional(),
  layout: z.string().optional(),
  tags: z.array(z.string()),
  published: z.coerce.date(),
  cover: z.string().optional(),
})

export const markdown = defineMarkdown({ schema: articleSchema })

Register markdown in nib.config.ts. Nib re-exports Zod 4 and infers the transformed output. You may instead provide another parse-compatible schema or a validate(value) function; choose one validation adapter per definition.

Use the definition's meta callback when typed frontmatter should set standard route metadata. Its file field is the Markdown source identity used by the compiler and diagnostics:

export const markdown = defineMarkdown({
  schema: articleSchema,
  meta: ({ frontmatter, file, defaults }) => ({
    ...defaults,
    type: 'article',
    image: frontmatter.cover,
    head: {
      elements: [{ tag: 'meta', attributes: { name: 'content-source', content: file } }],
    },
  }),
})

The deprecated path field remains an alias of file for compatibility; new code should use file because it names the source file, not the public route.

Create a layout

Create src/layouts/docs.tsx:

import type { ReactNode } from 'react'

export default function DocsLayout({ children }: { children: ReactNode }) {
  return (
    <main className="prose prose-invert">
      {children}
    </main>
  )
}

Select it with layout: docs. The layout receives the rendered article as children, so it can add navigation, a sidebar, or other static TSX around the article.

It can also receive typed frontmatter:

import { z, type PageLayoutProps } from '@briansunter/nib'
import { articleSchema } from '../content'

export default function ArticleLayout({
  Content,
  frontmatter,
}: PageLayoutProps<z.infer<typeof articleSchema>>) {
  if (!Content) throw new Error('Article layout requires Markdown content')
  return (
    <Content
      as="article"
      className="prose"
      data-tags={frontmatter?.tags.join(',')}
      data-pagefind-body=""
    />
  )
}

Content is bound to the route's compiled Markdown. The layout chooses its semantic root and static attributes without cloning or inspecting a child element. Nib rejects a render that drops or duplicates the body. Layouts may place client enhancements or React islands before, after, or around Content. Inline JSX inside page.md is not supported.

Generated data pages can use the same pipeline:

import { Content, markdownBody } from '@briansunter/nib'

const body = await markdownBody(project.bodyMarkdown, {
  file: `src/content/projects/${project.slug}.md`,
  profile: markdown,
})

export function ProjectProse() {
  return <Content body={body} as="section" className="prose" />
}

The profile is an ordinary defineMarkdown() definition, so plugin order, raw-HTML policy, source-located errors, and build behavior stay identical between file pages and generated prose.

Layout names are flat filenames. src/layouts/docs.tsx works; nested layout paths are intentionally unsupported.

For folder-based composition, create src/pages/layout.tsx or a nested src/pages/docs/layout.tsx. Nib wraps a page with every matching folder layout from root to leaf, then applies its optional named layout. Folder layouts receive the same PageLayoutProps, including validated frontmatter, the immutable route, and typed collections.

Markdown page versus TSX page

Choose a Markdown page when the route is primarily content. Choose a TSX page when it needs custom static component composition or a page-specific structure. Attach browser state and event handlers through an explicit client enhancement or React island. Both page types are server-rendered and become static HTML in dist/client.