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:
- writes
.env.localwith the space's keys (.dev.varsinstead when a Workers app has one) and keeps it out of git, and adds your dev server (the port yourdevscript orvite.configsets) as a Local website of the visual editor; - adds the routes (below) and
src/blokcms/: a component for every block type in your space, a header and footer, the site that ties them together, and the server function that reads pages; - installs
@blokcms/tanstack-start,@blokcms/clientand@blokcms/react.
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.tsx | Every 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.ts | blokServer.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" }] }),
});
types | Content types that have pages. Entries of other types 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). |
load | More data a page shows, read on the server with the page's client (published, or drafts in the visual editor): useSite().extra. |
head | Changes 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 Node hosts, from the host's settings; locally, Start loads
.envand.env.local. - On Cloudflare Workers,
nodejs_compat(which Start needs) puts vars and secrets inprocess.env. Locally, wrangler reads.dev.vars, or.envand.env.localwhen there's no.dev.vars. Whenwrangler.jsonclistssecrets.required, wrangler passes only those (andvars): list the BlokCms ones too.
On a drafts site, BlokCms's pages ask for the password when they load; the editor's frame doesn't need it.