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:

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
--stagingA staging site: a Staging website of the visual editor.
--draftsA drafts site: every page shows unpublished changes, behind a password (DRAFTS_PASSWORD, made for you). No webhook: nothing is cached there.
--origin https://example.comAnother address of the same site, so forms work from it too. Repeat it for more.
--no-previewNo preview key: the site only ever shows what's published (editors preview on staging).
--out .env.productionWrite 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
blocksA component per block type (see SDKs for Blocks, RichText, BlokImage, BlokForm).
layoutAround every page: gets globals, menus, entry (null on the page-not-found page), extra and children. LayoutProps<{ settings: { siteName?: string } }> types the globals.
pagesHow 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.
notFoundThe page-not-found text, inside the layout.
noticeTells 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.tsxblok.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.tsxblok.PreviewPage, generatePreviewMetadata: unpublished changes, for the visual editor, share links and a drafts site. Never cached, never indexed.
proxy.tsSends 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.tsPOST = blok.webhook: the publish webhook clears the cache.
app/sitemap.ts, app/robots.ts, app/not-found.tsxblok.sitemap, blok.robots, blok.NotFound.
app/layout.tsxOptional: 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,
  }),
});
typesContent 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.
includeReference fields whose entries come along with a page and the globals (default: all).
revalidateHow long content is kept without a publish webhook, in seconds (default: an hour).
metadataChanges a page's <head>: title, description, Open Graph image and canonical address come from its SEO field and the site's defaults.
loadMore 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_TOKENThe space and its public key: published content, and forms sent from the browser.
BLOK_PREVIEW_TOKEN, BLOK_PREVIEW_SECRETDrafts for the visual editor and share links (read on the server only). Without them, the site shows what's published only.
BLOK_WEBHOOK_SECRETChecks the publish webhook's signature.
SITE_URLThe site's address, for the sitemap and links in shared pages.
BLOK_DRAFTS=1, DRAFTS_PASSWORDA 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();
}