Next.js
Host Backmatter in a Next.js app with @backmatter/next.
@backmatter/next is the Next.js adapter. Install it in the app that renders the site. It re-exports @backmatter/core, and adds the config, the catch-all route, MDX rendering, and preview.
Install scaffolds the files. This page is what those files are for.
Configure
Wrap the Next config so the registry is generated when models, blocks, or layouts change.
import { withBackmatter } from "@backmatter/next/config";
export default withBackmatter({
// your Next config
});
withBackmatter turns on Cache Components. It also registers a loader that watches models/ and blocks/, so adding a file updates .backmatter/ without a separate process.
instrumentation.ts imports that registry on Node, and skips it on Edge:
export async function register() {
if (process.env.NEXT_RUNTIME === "nodejs") {
await import("backmatter/sync");
}
}
The client
Import cms from backmatter. withBackmatter maps that name to the generated .backmatter/ folder and writes the same paths into tsconfig.json. Until a workspace is connected, cms reads content/ from disk.
import { cms } from "backmatter";
const post = await cms.query.blog.get("hello");
defineModel, field, and createBackmatter come from the same package. Field kinds are Fields. The rest of the model is Core modules.
Routes
backmatter init writes one catch-all. It mounts every model at the route on that model.
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;
A model with route: "/blog/{slug}" serves content/blog/hello.mdx at /blog/hello. matchRoute tries the most specific template first. Pass render(doc, body) when the catch-all should wrap the body in your layout.
To own the URL, write the page and call cms from it:
import { notFound } from "next/navigation";
import { renderDoc } from "@backmatter/next/mdx";
import { cms } from "backmatter";
import { components } from "backmatter/mdx";
export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const doc = await cms.query.blog.get(slug);
if (!doc) notFound();
return renderDoc(doc, { components });
}
The [slug] param is the file name: content/blog/hello.mdx is get("hello"). With the content token set, that call reads the published document. Querying is how that call reads the file. Fetching & routes covers filesystem versus the content API.
MDX
renderDoc compiles a document body to React. Pass the components the document uses.
import { renderDoc } from "@backmatter/next/mdx";
return renderDoc(doc, { components });
A preview iframe that updates on the client uses MdxBody from @backmatter/next/mdx/client.
Preview
Preview shows the open document before it is published. The catch-all does this when BACKMATTER_PREVIEW_TOKEN is set. Helpers for a page you wrote yourself:
import { fetchPreviewDoc, subscribePreview } from "@backmatter/next/preview";
How preview stays on the changed documents is Live preview. The dev bar is that preview with the editor, chat, and publish on the page, served by the Studio mounted on the site.