Skip to main content

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:

  1. Rendering — the @bildit-platform/nextjs SDK. Fetches your published scheduled content and renders it on your production website, server-side. This is what visitors actually see.
  2. Editing — the editor bridge. A small postMessage bridge 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 — not window.WEB_ID.

The most common mistake

The editor bridge makes slots editable; it does not render content for visitors. Rendering is the @bildit-platform/nextjs SDK. You set up both.

Free trial? Your live site will be blank — and that's expected

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 as slotId in 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 URLStep 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
Don't include a path

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.

VariableRequiredWhat it is
BILDIT_API_KEYAPI key for your web instance — from Configuration → API Keys (a free-trial key on a free plan).
BILDIT_API_URLBILDIT 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
Remote images

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 halfgetInitialData fetches your published content server-side and passes it to Providers. 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 posts INJECT_SCRIPT, it injects /scripts/bildit-cms-script.min.js and posts SCRIPT_INJECTED. The editor script self-configures — no window.WEB_ID / pageData and no toolbar div needed. On your public site nothing posts INJECT_SCRIPT, so visitors never load it.
force-dynamic is required

Without 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
Register every dependency your scheduled content uses

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.

Blank in production but fine locally?

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:

  1. Free trial. A free trial is blank on the live site by design — upgrade to go live.
  2. Scheduled content location. Does the scheduled content have a Location that matches the page path? No match → empty.
  3. BILDIT_API_URL. Is it the Functions base URL (not the admin host/CDN)?
  4. force-dynamic. Is export const dynamic = 'force-dynamic' in the layout?
  5. Env vars in production. Are BILDIT_API_KEY and BILDIT_API_URL set in your host's environment (not just .env.local)? Missing in prod → nothing renders.
  6. cmsDependencies. Render errors in the console usually mean scheduled content imports a module you didn't register.
  7. 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