Server-side rendering eliminates the flash of default content that breaks conversions.
What you are looking at
This is a server component. The headline, subheading, and CTA above are not static strings or client-side injections. They were fetched from POST /api/assemble/ssr at render time, and the slot values were mapped directly to the existing HTML elements before the page was sent to the browser.
The pattern
const heroSlots = assembly.find(c => c.component === 'HeroStatement')?.slots ?? {};
const ctaSlots = assembly.find(c => c.component === 'CtaStrip')?.slots ?? {};
const headline = heroSlots.headline as string ?? 'Default headline';
const ctaLabel = (ctaSlots.primary as { label: string })?.label ?? 'Get started';
// These render as plain HTML: no script, no attribute, no DOM swap.
<h1>{headline}</h1>
<a href={...}>{ctaLabel}</a>Try it
Add UTM params and reload to see the intent change:
[
{
"component": "AnnouncementBar",
"slots": {
"text": "Spectare now measures its own lift.",
"link": {
"label": "See how it works",
"href": "/how-it-works"
}
}
},
{
"component": "HeroStatement",
"slots": {
"headline": "Ship personalised HTML on first paint",
"subheading": "Server-side rendering eliminates the flash of default content that breaks conversions."
}
},
{
"component": "AtomCard",
"slots": {
"atomId": 590,
"variant": "full",
"_atomId": 590,
"title": "Add Spectare to Next.js with a server-side fetch",
"summary": "On a Next.js server component, call the SSR endpoint with your org API key and render the returned assembly.",
"content": "On a Next.js server component, call the SSR endpoint with your org API key and render the returned assembly. Zero flicker: the personalised HTML ships in the first response.\n\n```ts\n// app/page.tsx (Server Component)\nconst res = await fetch('https://spectare.ai/api/assemble/ssr', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${process.env.SPECTARE_API_KEY}`,\n },\n body: JSON.stringify({ orgSlug: 'your-slug', pageUrl: '/' }),\n});\nconst { assembly, intent } = await res.json();\n// Pass to <PersonalisedSection initialAssembly={assembly} initialIntent={intent} />\n```\n\nThe endpoint runs qualify and assemble in one server-to-server call, so the first paint is already personalised. Find your API key in the Spectare admin under Settings.",
"stats": []
}
},
{
"component": "AtomCard",
"slots": {
"atomId": 709,
"variant": "card",
"_atomId": 709,
"title": "Tell Spectare which zones your page places",
"summary": "The SSR assembly endpoint accepts a zones array in the request body.",
"content": "The SSR assembly endpoint accepts a zones array in the request body. Pass the zone names your page actually renders, for example [\"hero\", \"cta\"], and the assembly engine composes only for those zones. No unrendered component earns impression credit, and the AI does not spend its budget selecting content your layout will never show.\n\nSpectare's own pricing page and WordPress developer guide now declare their zones this way. Both pages place only a hero and a cta, so their assemblies compose for two zones instead of five. The result is a tighter, faster assembly that matches what the visitor actually sees.\n\nWithout a zones declaration the engine defaults to the full set configured for your organisation. That is safe, but it means the assembly may select atoms for zones that never render on this particular page. Declaring zones is the more honest contract between your layout and the personalisation layer.\n\nPass zones as an optional third argument to getServerAssembly or include it in the request body to /api/assemble/ssr. The field is optional and backward compatible; pages that do not declare zones continue to work without changes.",
"stats": [
{
"label": "Cache key suffix format",
"value": ":z:body+proof (zones sorted)"
},
{
"label": "Warmer behaviour",
"value": "Scoped cells never warmed; costs stay flat"
},
{
"label": "Undeclared request overhead",
"value": "Zero; byte-identical to previous behaviour"
}
]
}
}
]Ready to add server-side personalisation to your stack?
Live conversion data, updated every 8s