Page Builder live preview sends Link fields with allowText as empty (link_type: "Any") to /slice-simulator even though the editor shows the field filled.
I'm running a multi-tenant SaaS on Cloudflare Workers (via OpenNext) where each tenant has their own Prismic repository. Slices are shared across all tenants via a sync-slices push.
One of my slices, hero_simple, has a cta_link field configured with allowText: true. When editing the slice in Page Builder for a tenant's repository, the live preview iframe pointed at my /slice-simulator route receives the field with link_type: "Any" and empty text — even though I filled in both URL and display text in the editor and the field appears filled.
All other field types in the same slice (RichText for heading and body, Select for background_theme) serialize correctly. Only Link with allowText is affected.
Preview URL configured in Prismic: https://<tenant>.nyvia.dk/slice-simulator
Slice-simulator route at app/slice-simulator/page.tsx:
import { SliceSimulator, SliceSimulatorParams, getSlices } from "@prismicio/next";
import { SliceZone } from "@prismicio/react";
import { components } from "../../slices";
export default async function SliceSimulatorPage({ searchParams }: SliceSimulatorParams) {
const { state } = await searchParams;
const slices = getSlices(state);
return (
<SliceSimulator>
<SliceZone slices={slices} components={components} />
</SliceSimulator>
);
}
Field added via CLI:
prismic field add link cta_link --to-slice hero_simple --allow-text --allow-target-blank --label "Call-to-action"
Slice model excerpt in slices/HeroSimple/model.json:
"cta_link": {
"type": "Link",
"config": {
"label": "Call-to-action",
"allowTargetBlank": true,
"allowText": true
}
}
How I debugged this — I added raw output of slice.primary.cta_link at the top of the slice component to see exactly what the simulator receives:
const { cta_link } = slice.primary;
return (
<div className="flex gap-5">
<PrismicNextLink field={cta_link} linkResolver={linkResolver} />
<a href={cta_link.link_type === "Web" && cta_link.url ? cta_link.url : ""}>
{cta_link.text}
</a>
<p>{cta_link.link_type}</p>
<p>{cta_link.link_type === "Web" && cta_link.url ? cta_link.url : ""}</p>
<p>{cta_link.text}</p>
</div>
);
In production (published document served via Prismic CDN), the rendered output is:
hej
hej
Web
https://www.youtube.com/
hej
In Page Builder live preview (same document, same field, filled with same values in the editor), the rendered output is:
<div class="flex gap-5">
<a href=""></a>
<a href=""></a>
<p>Any</p>
<p></p>
<p></p>
</div>
The PrismicNextLink renders as an empty <a href="">. link_type is "Any", url is missing, text is empty — even though the same field returns the correct values from the production API.
Expected — the live preview /slice-simulator should receive the same field shape the production API returns:
{
"link_type": "Web",
"url": "https://www.youtube.com/",
"text": "hej"
}
Actual — what simulator receives:
{
"link_type": "Any"
}
Environment:
@prismicio/client 7.21.8
@prismicio/next 2.2.4
@prismicio/react 3.4.1
Next.js 16.2.10 (App Router)
Runtime: Cloudflare Workers via @opennextjs/cloudflare 1.20.1
Slice files managed via Prismic CLI (prismic slice, prismic field commands) — no Slice Machine
Additional context — because the Link field's text and url never reach the simulator, <PrismicNextLink> renders empty and CTA buttons visually disappear in live preview. Content editors see broken previews for any slice with a Link-with-text CTA, which makes the editing workflow confusing (they think their content is broken).
Workaround — splitting the field into a separate KeyText for label + plain Link (without allowText) for URL makes it work. KeyText serializes correctly through the postMessage → lz-string → simulator pipeline. But this defeats the purpose of allowText and forces a slice model refactor plus content re-entry for every tenant already onboarded.
Is this a known limitation of the Page Builder to slice-simulator state serialization, or should this work?