Render BILDIT Scheduled Content on Your Next.js Site
This guide takes a Next.js (App Router) site from zero to scheduled content rendering on your production website. Follow the steps in order — each one builds on the previous, and the order is chosen so nothing silently fails along the way.
How it works: two parts
Your BILDIT setup has two distinct pieces, and you need both. On Next.js, both live in your app code — this guide sets up each:
- Rendering — the
@bildit-platform/nextjsSDK. Fetches your published scheduled content and renders it on your production website, server-side. This is what visitors actually see. - Editing — the editor bridge. A small
postMessagebridge in your root layout (Step 6) that, when the page is opened in the BILDIT Live Editor, injects the self-configuring editor script (bildit-cms-script.min.js) so your slots become editable. On your public site nothing injects it, so visitors never download it.
Plain-HTML sites instead use a
window.WEB_ID+ script tag (see Visual Editor Setup). Next.js sites use the editor bridge below — notwindow.WEB_ID.
The editor bridge makes slots editable; it does not render content for visitors. Rendering is
the @bildit-platform/nextjs SDK. You set up both.
On a free trial, your content shows in the Live Editor but won't appear on your live site until you upgrade. If you've wired everything up and your live site is still blank, that's why — preview in the Live Editor, then upgrade when you're ready to go live.
Before you start
Have these ready first — skipping them is the cause of most "it's blank" surprises:
- A web instance in the VEE with at least one published scheduled content item. New here? Create a web instance and your first scheduled content.
- That scheduled content must have a Location assigned (for example
/) and a web slot — the slot name is what you'll use asslotIdin Step 8. The SDK matches scheduled content by the request path, so scheduled content with no location never appears on your production site. - Your API key and Functions URL — Step 2 shows where to find them.
- A Next.js App Router project.
Step 1 — Install the SDK
npm install @bildit-platform/nextjs @bildit-platform/nextjs-api
(Use yarn add or pnpm add if you prefer.) You need both packages: @bildit-platform/nextjs (provider + components) and @bildit-platform/nextjs-api (the data connector). These are public npm packages — no access token or .npmrc setup required.
Step 2 — Get your API key and Functions URL
Two values from your VEE connect the SDK to your content:
- API key — in the BILDIT CMS, select your website → Configuration → API Keys and copy your key. On a free trial this is a free-trial key (see the callout above).
- Base URL — your BILDIT API endpoint,
https://admin.bildit.co. The SDK appends/remote-webbanners_v1_4, so don't include a path.
You'll put both into environment variables next.
Step 3 — Set your environment variables
Set these before writing the code that reads them. In .env.local:
# Your website's API key (free-trial key on a free plan)
BILDIT_API_KEY=your-api-key
# Your BILDIT base URL. The SDK appends /remote-webbanners_v1_4 — don't include a path.
BILDIT_API_URL=https://admin.bildit.co
BILDIT_API_URL is just the base URL (e.g. https://admin.bildit.co) — the SDK appends /remote-webbanners_v1_4 itself. Don't add a path, and don't point it at the script CDN.
| Variable | Required | What it is |
|---|---|---|
BILDIT_API_KEY | ✅ | API key for your web instance — from Configuration → API Keys (a free-trial key on a free plan). |
BILDIT_API_URL | ✅ | BILDIT base URL the SDK fetches from — https://admin.bildit.co. |
Step 4 — Configure next.config
Add the SDK to transpilePackages:
/** @type {import('next').NextConfig} */
const nextConfig = {
transpilePackages: ['@bildit-platform/nextjs'],
}
export default nextConfig
If your scheduled content uses remote images, also configure images.remotePatterns — see Next.js Cache & Image Configuration.
Step 5 — Add middleware
Create middleware.ts in your project root:
import { enhanceMiddlewareWithBildit } from '@bildit-platform/nextjs'
import { NextRequest, NextResponse } from 'next/server'
async function customMiddleware(request: NextRequest) {
// Surface the request path so scheduled content can be matched by location.
const requestHeaders = new Headers(request.headers)
requestHeaders.set('x-pathname', request.nextUrl.pathname)
return NextResponse.next({ request: { headers: requestHeaders } })
}
// enhanceMiddlewareWithBildit adds VEE preview-date support.
export const middleware = enhanceMiddlewareWithBildit(customMiddleware)
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
}
x-pathname is how the SDK matches scheduled content to the current page (its assigned location). enhanceMiddlewareWithBildit forwards the VEE preview date so scheduled previews work in the Visual Editor.
Step 6 — Fetch content and wire the editor bridge in your root layout
Your root layout does two jobs: server-fetch published content (the render half) and host the
editor bridge (the edit half). It's a server component and must be dynamic so it can read
per-request headers. In app/layout.tsx:
import Script from 'next/script'
import { headers } from 'next/headers'
import { getPreviewDateFromHeaders } from '@bildit-platform/nextjs'
import { RemoteConnector } from '@bildit-platform/nextjs-api'
import Providers from '@/app/components/Providers'
// Required: read the path + preview date per request (no static caching).
export const dynamic = 'force-dynamic'
async function getInitialData() {
// Not wired up yet? With no key/URL there's nothing to fetch — render empty slots and
// skip the SDK call (avoids an "Invalid URL" from an empty baseURL).
if (!process.env.BILDIT_API_KEY || !process.env.BILDIT_API_URL) {
return []
}
try {
const headersList = await headers()
const pathname = headersList.get('x-pathname') || '/'
const previewDate = getPreviewDateFromHeaders(headersList)
const connector = new RemoteConnector({
key: process.env.BILDIT_API_KEY,
baseURL: process.env.BILDIT_API_URL,
})
const result = await connector.getWebBanners({
location: pathname,
date: previewDate,
mode: 'csr',
tomorrow: true,
source: 'live',
})
return result.data || []
} catch (error) {
console.error('[BILDIT] Failed to load scheduled content:', error)
return []
}
}
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const banners = await getInitialData()
return (
<html lang="en">
<head>
{/* BILDIT Live Editor bridge — makes the site editable in the Live Editor.
The SDK below only RENDERS content; this bridge handles editing. On the public
live site no parent posts INJECT_SCRIPT, so nothing loads. Inside the Live Editor
iframe the parent posts INJECT_SCRIPT and we inject the editor script. The
downloaded script sets window.WEB_ID / pageData itself — you don't set them. */}
<Script
id="bildit-editor-bridge"
strategy="beforeInteractive"
dangerouslySetInnerHTML={{
__html: `
window.parent.postMessage({ type: 'IFRAME_READY', success: true }, '*');
window.addEventListener('message', function (event) {
if (!event.data || event.data.type !== 'INJECT_SCRIPT') return;
if (window.__adminScriptInjected) return;
window.__adminScriptInjected = true;
var s = document.createElement('script');
s.src = '/scripts/bildit-cms-script.min.js';
s.onload = function () {
window.parent.postMessage({ type: 'SCRIPT_INJECTED', success: true }, '*');
};
s.onerror = function () {
window.parent.postMessage({ type: 'SCRIPT_INJECTED', success: false, error: 'Failed to load editor script' }, '*');
};
document.body.appendChild(s);
});
`,
}}
/>
</head>
<body>
<Providers banners={banners}>{children}</Providers>
</body>
</html>
)
}
This does two things:
- Render half —
getInitialDatafetches your published content server-side and passes it toProviders. The guard (return []when the env vars aren't set) keeps an unconfigured project from throwing an "Invalid URL". - Edit half (the editor bridge) — the inline script posts
IFRAME_READY; when the Live Editor postsINJECT_SCRIPT, it injects/scripts/bildit-cms-script.min.jsand postsSCRIPT_INJECTED. The editor script self-configures — nowindow.WEB_ID/pageDataand no toolbardivneeded. On your public site nothing postsINJECT_SCRIPT, so visitors never load it.
force-dynamic is requiredWithout export const dynamic = 'force-dynamic', Next.js statically caches the layout and never reads the per-request headers — so location matching and preview dates silently stop working.
Add the editor script
The bridge injects /scripts/bildit-cms-script.min.js, so that file must exist in your project. In the
BILDIT CMS, open your website and click Download script, then save it as
public/scripts/bildit-cms-script.min.js. Each website has its own script (it self-configures with
that site's settings) — download yours rather than reusing another site's. Without it, your site still
renders published content via the SDK; you just can't edit on-site.
Step 7 — Add the provider
The provider is a client component. Create app/components/Providers.tsx:
'use client'
import React, { Suspense } from 'react'
import { BilditProvider } from '@bildit-platform/nextjs'
import cmsDependencies from '@/app/utils/cmsDependencies'
interface ProvidersProps {
children: React.ReactNode
banners: any[]
}
export default function Providers({ children, banners }: ProvidersProps) {
return (
<Suspense>
<BilditProvider banners={banners} extraDependenciesConfig={cmsDependencies}>
{children}
</BilditProvider>
</Suspense>
)
}
And the dependency map it references, app/utils/cmsDependencies.ts:
import React from 'react'
import jsxRuntime from 'react/jsx-runtime'
import NextImage from 'next/image'
import NextLink from 'next/link'
import * as NextScript from 'next/script'
// Modules the BILDIT engine resolves when rendering your scheduled content as code.
// Register every module your scheduled content imports, or it fails to render.
const cmsDependencies = {
react: { module: React },
'react/jsx-runtime': { module: jsxRuntime },
'next/image': { module: { default: NextImage, __esModule: true } },
'next/link': { module: { default: NextLink, __esModule: true } },
'next/script': { module: NextScript },
}
export default cmsDependencies
Scheduled content that imports a module not listed in cmsDependencies (a custom component, an icon library, etc.) will throw a render error. Add it here.
Step 8 — Place your slots
Add a <SlotPlaceholder> wherever scheduled content should appear. slotId must match the web slot assigned to the scheduled content in the VEE:
import { SlotPlaceholder } from '@bildit-platform/nextjs'
export default function Home() {
return (
<main>
<SlotPlaceholder slotId="slot1" />
<SlotPlaceholder slotId="slot2" />
</main>
)
}
You can pass fallback (or children) to render default content when no scheduled content is assigned to that slot.
Step 9 — Run and verify
npm run dev
Open a route whose path matches published, located scheduled content. It should render in its slot. If it doesn't, jump to If your production site is blank.
Step 10 — Deploy
Deploy to your host (e.g. Vercel) as usual. The @bildit-platform/* packages are public, so the build installs them with no extra auth.
The one thing to remember: Vercel injects its own env vars at build time and does not read your local .env.local — so set BILDIT_API_KEY and BILDIT_API_URL on Vercel too. Via the CLI:
vercel # FIRST TIME ONLY — links the project
# Each command prompts for the value — paste it at the prompt:
vercel env add BILDIT_API_KEY production
vercel env add BILDIT_API_URL production # https://admin.bildit.co
vercel --prod # redeploy so the build picks up the vars
(Or add them in Vercel → Project → Settings → Environment Variables and redeploy.) Then hit Verify on your website in BILDIT to confirm the editor can reach it.
Almost always means BILDIT_API_KEY / BILDIT_API_URL are set locally but missing in your host's environment. Add them there and redeploy.
If your production site is blank
Scheduled content shows in the Live Editor but not on your production site? Check these in order — each maps to a real, easy-to-miss cause:
- Free trial. A free trial is blank on the live site by design — upgrade to go live.
- Scheduled content location. Does the scheduled content have a Location that matches the page path? No match → empty.
BILDIT_API_URL. Is it the Functions base URL (not the admin host/CDN)?force-dynamic. Isexport const dynamic = 'force-dynamic'in the layout?- Env vars in production. Are
BILDIT_API_KEYandBILDIT_API_URLset in your host's environment (not just.env.local)? Missing in prod → nothing renders. cmsDependencies. Render errors in the console usually mean scheduled content imports a module you didn't register.- Caching. The SDK caches responses briefly (~5 min); after flipping a key or schedule, give it a moment or redeploy.
The full reference is in API Reference & Troubleshooting.
Next steps
- API Reference & Troubleshooting — every prop, param, and gotcha.
- Next.js Cache & Image Configuration — cache clearing + remote images.
- Creating Scheduled Content — authoring content in the VEE.
- Example Project — a complete reference repo.