BackmatterPricingDocs
Contents

Fetching & routes

How a Next host loads and routes documents.

The Next.js app owns the URLs and the UI. Backmatter owns the documents: a type and a slug, each pointing at a file. Querying is how a page loads that file.

@backmatter/next fetches those documents. A [slug] segment is the file slug, whether the file is read from content/ on disk or from the published content API.

app/[...backmatter]/page.tsx  (scaffolded)   or   app/blog/[slug]/page.tsx  (yours)
        │
        ▼
 cms  ←  .backmatter/index.ts (generated from models/*.ts + backmatter.config.ts)
        │
        ├─ FS mode  → content/blog/{slug}.mdx   (@backmatter/core path match)
        └─ API mode → published documents from Backmatter
        │
        ▼
 { frontmatter, body }  →  renderDoc / MdxBody  →  site CSS + blocks/*.tsx

The public site does not read GitHub. The catch-all mounts every model at its route, or you write the route and cms fills the data. Both use the same generated client.

Core building blocks

PieceRole
defineModel / path + route templatesWhere files live; how the slug binds; which URL the catch-all serves
.backmatter/index.ts → cmsGenerated host client: listDocs / getDoc / query, typed from models/*.ts
createCatchAll(cms)Page + generateStaticParams for app/[...backmatter]/page.tsx
FS loaderScan/read content/ using model paths
API loaderThe same documents, published, with the content token
Preview helpersClient helpers for unsaved edits on your preview page (live preview)

@backmatter/core does not fetch HTTP or render React. It defines identity, paths, parse, validate. @backmatter/next is the Next-facing fetch layer.

Catch-all (zero-code pages)

backmatter init writes one file:

// app/[[...backmatter]]/page.tsx (Next 16)
import { createCatchAll } from "@backmatter/next/routes";
import { cms } from "backmatter";
import { components } from "backmatter/mdx";
import { blockLoaders } from "backmatter/blocks";
import { blockFallbacks } from "backmatter/block-fallbacks";
import { layouts } from "backmatter/layouts";
import { sources } from "backmatter/sources";

const route = createCatchAll(cms, {
  components,
  blocks: blockLoaders,
  fallbacks: blockFallbacks,
  layouts,
  sources,
});
export const generateStaticParams = route.generateStaticParams;
export const generateMetadata = route.generateMetadata;
export default route.Page;

instrumentation.ts (project root or src/ when using src/app) imports backmatter/sync inside if (process.env.NEXT_RUNTIME === "nodejs"). The catch-all does not import block modules.

backmatter init also writes proxy.ts (Next 16; the old middleware.ts name is deprecated). The handler is export { proxy } from "@backmatter/next/proxy". Next requires export const config with the matcher to live in that file, not be re-exported.

Each model declares a route. route: "/blog/{slug}" serves the blog file at /blog/hello. A model with external: true serves a URL whose record comes from outside the repo, such as a product in a catalog. External sources is that path. Set route: false for data and globals so the catch-all ignores them. matchRoute tries the most specific template first. render(doc, body) wraps the body in your layout. Anything the catch-all does not match falls through to your own routes or notFound().

Dynamic slug routes (hand-written)

Index + detail when you want full control of a URL:

app/blog/page.tsx
import { cms } from "backmatter";
const posts = await cms.query.blog.findMany();
// → links to /blog/{slug}

// app/blog/[slug]/page.tsx
const { slug } = await params;
const doc = await cms.query.blog.get(slug);
if (!doc) notFound();
return renderDoc(doc, { components: blocks });

The [slug] param is the document slug in that type. get, findMany, where, and include are Querying.

Static vs dynamic rendering

  • generateStaticParams — listDocs at build time → pre-render known slugs (great for marketing sites).
  • force-dynamic / ISR — fetch per request or revalidate on commit (CMS-driven sites).

Both use the same getDoc / listDocs. Backmatter does not force one Next caching mode.

Dual source (FS vs API)

ModeWhenSource
FilesystemLocal next dev without tokenscontent/ on disk via model paths
APIStaging and productionPublished documents, with the content token

Same route code. Switch with BACKMATTER_API_URL and BACKMATTER_CONTENT_TOKEN. Locally, with no token, the site reads the files in the repo. In production it reads the published documents. Editing in the CMS and editing the file in git stay the same pages.

Rendering

YouBackmatter
Layout, CSS, SEOLoads the frontmatter and body
Components in blocks/Compiles the body with renderDoc or MdxBody

renderDoc / MdxBody compile the body string with GFM. No default tag styles — wrap the result in site CSS. Pass components only when the doc uses custom MDX blocks.

Preview

The catch-all serves the public URL. With BACKMATTER_PREVIEW_TOKEN set, that same URL shows the editor's unsaved copy of the open document. Visitors still get the published file.

A page you wrote yourself uses @backmatter/next/preview (fetchPreviewDoc and subscribePreview) and the preview token. The content token stays on published reads. Live preview is that path. The dev bar puts the CMS editor on the page you are browsing.