TanStack Start

@blokcms/tanstack-start connects a TanStack Start site (React): every URL is a page in BlokCms, read on the server for every request, with unpublished changes in the visual editor and in share links. You write the components; the package does the rest. It runs wherever Start does: Node hosts (Vercel, Netlify, your own server) and Cloudflare Workers.

Set up

npx @tanstack/cli create 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. The starter's welcome page (src/routes/index.tsx) is renamed to -index.before-blokcms.tsx, which the router skips: your home page comes from BlokCms now. Routes of your own (the starter's about.tsx, say) answer their URLs before BlokCms does; init lists them. The starter's root route has a header and footer of its own: keep that one or BlokCms's src/blokcms/layout.tsx, not both.

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 prints the environment variables for your host: on Vercel or Netlify, paste them into the project's environment variables; on Cloudflare Workers, write them to a file with --out and run npx wrangler secret bulk <file>. Then deploy. There's no publish webhook: pages are read for every request, so what editors publish shows within a minute. The options are the same as for Next.js (--staging, --drafts, --origin, --no-preview, --out).

Your code

One file says how the site looks. init writes it from your space's types; change it as you like.

// src/blokcms/site.tsx
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
});

createSite is the same as for Next.js: its options (blocks, layout, pages, notFound, notice) and useSite() are on the Next.js page, and the block components on SDKs. In the layout, link menu items with <Link {...linkTo(item.href)}> (from @blokcms/tanstack-start): a link to a part of a page gets its anchor as TanStack expects it, and links to other sites become plain links.

The routes

Each is a few lines that call blok (src/blokcms/index.ts) and blokServer (src/blokcms/server.ts):

File
src/routes/$.tsx, src/routes/index.tsxEvery URL, and the home page: loader, head, headers, the page and the page-not-found page. A page not found answers 404 with the site's layout around it; an old address (a BlokCms redirect) redirects with 308 (307 for temporary ones). The home page has a route of its own so <Link to="/"> type-checks.
src/routes/sitemap[.]xml.ts, src/routes/robots[.]txt.tsblokServer.sitemap and blokServer.robots: the published pages, and everything allowed (nothing on a drafts site).
// src/blokcms/index.ts
import { createBlok } from "@blokcms/tanstack-start";
import { createServerFn } from "@tanstack/react-start";
import { blokServer } from "./server";
import { Site } from "./site";

const read = createServerFn({ method: "GET" })
  .validator(blokServer.input)
  .handler(({ data }) => blokServer.read(data));

export const blok = createBlok({ Site, read });

The server function is your site's own: Start only compiles one that's declared in a file of yours, at the top. It reads pages on the server, where the keys are: the browser never gets the preview key, and Start leaves server.ts out of the browser's code. In the visual editor and from share links, the page's own URL carries a signed ticket, so the same route shows unpublished changes (never cached, never indexed): there's no separate preview route.

Options

// src/blokcms/server.ts
export const blokServer = createBlokServer({
  types: ["page", "blog_post"],      // content types with pages (default: all)
  load: async ({ page, client }) => ({
    companies: (await client.listEntries({ type: "company", sort: "name" })).items,
  }),
});

// src/blokcms/index.ts
export const blok = createBlok({
  Site,
  read,
  head: (head, page) => ({ ...head, meta: [...head.meta, { name: "author", content: "The park" }] }),
});
typesContent types that have pages. Entries of other types 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).
loadMore data a page shows, read on the server with the page's client (published, or drafts in the visual editor): useSite().extra.
headChanges a page's head tags: title, description, Open Graph and canonical link come from its SEO field and the site's defaults.

blok.loadPage(path) reads a page for a route of your own.

Caching

Start has no data cache like Next.js, so the site reads each page when it's asked for: the page, the globals and the menus, three requests to the Delivery API in parallel. They're answered from the Delivery API's edge cache, and they count towards your space's Delivery API requests. Client-side navigation reads through the same server function; TanStack Router keeps what it read for going back and forth.

Environment variables

The same as for Next.js, without BLOK_WEBHOOK_SECRET: BLOK_SPACE, BLOK_TOKEN, BLOK_PREVIEW_TOKEN, BLOK_PREVIEW_SECRET, SITE_URL, and for a drafts site BLOK_DRAFTS=1 and DRAFTS_PASSWORD. They're read from process.env:

On a drafts site, BlokCms's pages ask for the password when they load; the editor's frame doesn't need it.