Next.js
@blokcms/next connects a Next.js site (App Router, Next.js 16): every URL is a page in BlokCms, cached until editors publish, with unpublished changes in the visual editor and in share links. You write the components; the package does the rest. For TanStack Start, see TanStack Start.
Set up
npx create-next-app@latest my-site # or your existing app
cd my-site
npx @blokcms/cli init
npm run dev
init signs you in through the browser, asks which space has the content (or makes one with starter content), and then:
- writes
.env.localwith the space's keys (and keeps it out of git), and adds your dev server (http://localhost:3000, or the port yourdevscript sets) as a Local website of the visual editor; - adds the routes (below) and
blokcms/: a component for every block type in your space, a header and footer, and the site that ties them together; - installs
@blokcms/next,@blokcms/clientand@blokcms/react.
Files that are there already are left alone, so it's safe to run again. create-next-app's welcome page (app/page.tsx) is renamed: your home page comes from BlokCms now.
Going live
npx @blokcms/cli connect https://www.example.com
It adds the site as a Live website of the visual editor, makes its keys and the publish webhook, and prints the environment variables for your host. On Vercel, paste them all at once under Project → Settings → Environment Variables, then deploy. When editors publish, the site shows it within seconds.
| Option | |
|---|---|
--staging | A staging site: a Staging website of the visual editor. |
--drafts | A drafts site: every page shows unpublished changes, behind a password (DRAFTS_PASSWORD, made for you). No webhook: nothing is cached there. |
--origin https://example.com | Another address of the same site, so forms work from it too. Repeat it for more. |
--no-preview | No preview key: the site only ever shows what's published (editors preview on staging). |
--out .env.production | Write the variables to a file instead of printing them. |
Your code
One file says how the site looks. init writes it from your space's types; change it as you like.
// blokcms/site.tsx
"use client";
import { createSite } from "@blokcms/react";
import { HeroBlock, TextBlock } from "./blocks";
import { Layout } from "./layout";
export const Site = createSite({
blocks: { hero: HeroBlock, text: TextBlock }, // one component per block type
layout: Layout, // header and footer
pages: { page: "body" }, // a page shows its blocks field
});
A block component gets the block and its fields. Spread editable(block) on its root element, so editors can click it in the visual editor:
import { type BlockProps, editable, editableField } from "@blokcms/react";
export function HeroBlock({ block, fields }: BlockProps<{ headline?: string }>) {
return (
<section {...editable(block)}>
<h1 {...editableField(block, "headline")}>{fields.headline}</h1>
</section>
);
}
createSite | |
|---|---|
blocks | A component per block type (see SDKs for Blocks, RichText, BlokImage, BlokForm). |
layout | Around every page: gets globals, menus, entry (null on the page-not-found page), extra and children. LayoutProps<{ settings: { siteName?: string } }> types the globals. |
pages | How a content type's page renders: the key of its blocks field, or a component ({ entry, included }). Types not listed show every field that holds blocks. |
notFound | The page-not-found text, inside the layout. |
notice | Tells visitors of a share link or the drafts site that they see unpublished changes ({ kind }); null for none. |
Any component inside reads the globals, menus and page with useSite(). Globals and the page update while editors type in the visual editor. Sites set up before @blokcms/react had them import createSite and useSite from @blokcms/next/client, which still works.
The routes
Each is a few lines that call blok (blokcms/index.ts: createBlok({ Site })):
| File | |
|---|---|
app/[[...slug]]/page.tsx | blok.Page, generateMetadata, generateStaticParams: the published page at every URL. A page not found answers 404, an old address (a BlokCms redirect) redirects. |
app/blok-preview/[[...slug]]/page.tsx | blok.PreviewPage, generatePreviewMetadata: unpublished changes, for the visual editor, share links and a drafts site. Never cached, never indexed. |
proxy.ts | Sends the visual editor's requests to the preview route, so every other page stays static; on a drafts site it asks for the password. |
app/api/blokcms/route.ts | POST = blok.webhook: the publish webhook clears the cache. |
app/sitemap.ts, app/robots.ts, app/not-found.tsx | blok.sitemap, blok.robots, blok.NotFound. |
app/layout.tsx | Optional: generateMetadata = blok.generateLayoutMetadata, the site's default title and description from the Site settings global. |
With Cache Components (create-next-app's default), the page route also exports ensureStatic = "navigation" and instant = false: a page's first visit waits for the whole page, so a page not found answers 404 and an old address redirects. init adds them when next.config turns Cache Components on; remove them if you turn them off. Next.js reads route settings from the file itself, so they can't come from the package.
Options
export const blok = createBlok({
Site,
types: ["page", "blog_post"], // content types with pages (default: all)
metadata: (metadata, data) => ({ ...metadata, description: metadata.description ?? summaryOf(data) }),
load: async ({ page, client }) => ({
companies: (await client.listEntries({ type: "company", sort: "name" })).items,
}),
});
types | Content types that have pages. Entries of other types (team members listed on another page, say) are a page not found and stay out of the sitemap. |
include | Reference fields whose entries come along with a page and the globals (default: all). |
revalidate | How long content is kept without a publish webhook, in seconds (default: an hour). |
metadata | Changes a page's <head>: title, description, Open Graph image and canonical address come from its SEO field and the site's defaults. |
load | More data a page shows, read on the server with the page's client (published, or drafts in the visual editor) and cached with the page: useSite().extra. For lists editors pick, use a query field instead: it comes with the page. |
Environment variables
| Variable | |
|---|---|
BLOK_SPACE, BLOK_TOKEN | The space and its public key: published content, and forms sent from the browser. |
BLOK_PREVIEW_TOKEN, BLOK_PREVIEW_SECRET | Drafts for the visual editor and share links (read on the server only). Without them, the site shows what's published only. |
BLOK_WEBHOOK_SECRET | Checks the publish webhook's signature. |
SITE_URL | The site's address, for the sitemap and links in shared pages. |
BLOK_DRAFTS=1, DRAFTS_PASSWORD | A drafts site: every page shows unpublished changes, behind the password (any user name). |
The build reads the home page, so it needs them too (a missing one fails the build with its name).
Your own proxy
When the app has a proxy of its own, call BlokCms's first and use its answer when there is one:
import { blokProxy } from "@blokcms/next/proxy";
import { type NextRequest, NextResponse } from "next/server";
export async function proxy(request: NextRequest) {
const blok = await blokProxy(request);
if (blok) return blok;
// … your own
return NextResponse.next();
}