Add adaptive copy to a page
SparkleTree adapts the words in your page. Point at copy you already have, a button label, a form heading, a paragraph, and it returns a version written for the visiting context: time of day, device, language, referring site, plus anything you declare yourself. You write the copy that ships with your app. SparkleTree replaces it when an adapted version is available and leaves yours alone when it is not. The same provider also plays whole campaign creative, a hero or a CTA, managed from your dashboard.
It changes text and a few theme colours. It does not change your markup, your layout, or your CSS, it ships no stylesheet, and it renders no iframe. If every request fails, your page still shows the copy you wrote.
What you need before the first request works
Section titled “What you need before the first request works”No account yet? The runnable examples stream from a built-in mock server with no configuration at all, so you can evaluate everything below before signing up at app.sparkletree.io.
| You need | Where it comes from |
|---|---|
A publishable key (st_pk_live_…) | Your SparkleTree dashboard. It encodes your organization id and API address. |
A campaign id (camp_…) | Campaign islands only. An island is one block of SparkleTree-managed creative, like the hero below, and the campaign lives in your dashboard. A surface id (a named screen or placement that decides what plays there) works instead. Fragments need no dashboard object at all. |
| Your site’s origin, registered | Requests from an unregistered origin fail CORS. See Origins, CSP and privacy. |
| A mint token endpoint (newer workspaces) | Workspaces created after 2026-08-09 refuse unsigned mints by default, so fragments serve fallbacks until you wire signed minting. See “Before you ship” below. |
Install the packages
Section titled “Install the packages”npm install @sparkletree/core @sparkletree/reactReact 18 or later, Node 18 or later, ESM only.
Wrap the part of your app that uses SparkleTree
Section titled “Wrap the part of your app that uses SparkleTree”One provider per page view. In Next.js, put it in your root layout.
import { SparkletreeProvider } from "@sparkletree/react";
export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body> <SparkletreeProvider publishableKey={process.env.NEXT_PUBLIC_SPARKLETREE_KEY!}> {children} </SparkletreeProvider> </body> </html> );}In a single-page app, call pageVariants.reset() (exported from
@sparkletree/react) when your router changes route. A route change is a
new page view, and the per-page-view experiment pinning needs to know.
The publishable key is safe in client-side source. It is an address, not a secret: it decodes locally to your organization id and API host, and grants nothing that your page’s own network requests do not already reveal. What bounds access is the origin registry, not the key.
Self-hosting the API? Pass apiBase and organizationId explicitly instead
of the key. Signed minting explains why
that is required rather than preferred.
Adapt copy you already have
Section titled “Adapt copy you already have”Name a piece of your own copy, say what it says today, and render the result. There is nothing to create in a dashboard first. The keys and fallback strings you write are the request:
import { useFragments, FragmentText } from "@sparkletree/react";
function SignupButton() { const f = useFragments({ context: { page: "pricing", intent: "signup" }, fields: { cta: { kind: "cta", fallback: "Start your free trial" } }, }); return ( <button onClick={() => startCheckout()}> <FragmentText field={f.cta} /> </button> );}One useFragments call can carry a whole form’s worth of fields, written
together so they agree with each other. See
Fragments for groups, kinds and swap rules.
Play a campaign: render a hero and a button
Section titled “Play a campaign: render a hero and a button”Campaign islands play whole creative managed from your dashboard. This is
where the campaignId comes in:
import { Hero, Cta } from "@sparkletree/react";
export function Landing() { return ( <> <Hero campaignId="camp_123" fallback={{ headline: "Software that meets the moment", body: "Adaptive creative for the whole funnel.", }} /> <Cta campaignId="camp_123" fallback={{ cta: "Start free" }} href="/signup" /> </> );}That is the whole integration. fallback is the only required prop besides the
campaign id.
Write the fallback as copy you would ship on its own
Section titled “Write the fallback as copy you would ship on its own”The text in fallback is what a visitor reads whenever the adapted version is
late, missing, or never coming: a slow network, a paused campaign, our service
down. Until then the block holds rather than flashing copy the stream is about
to replace. Your fallback is there but invisible, occupying its exact space, so
nothing moves whichever way the wait ends. There is no skeleton and no
spinner. See What your visitors see.
Write it as the only version of the copy. For some visitors it is.
Check the failure path before the happy path
Section titled “Check the failure path before the happy path”Point the provider at an unreachable host and load the page:
<SparkletreeProvider apiBase="https://127.0.0.1:9" organizationId="org_123">You should see your fallback copy, fully readable, with no adapted provenance stamped and no layout shift. That is the behaviour that runs during an incident, so it is the one worth verifying first.
Style it with your own CSS
Section titled “Style it with your own CSS”The npm components ship no stylesheet. They render class hooks and CSS custom
properties for campaign colours: .st-hero, .st-hero-headline, .st-cta,
.st-stream-text, .st-mark. Style them like any other element in your
design system.
Before you ship
Section titled “Before you ship”Two steps stand between “works on my machine” and production:
- Register your production origin. Every request from an unregistered origin fails CORS. See Origins, CSP and privacy.
- Wire signed minting. Reading copy is free and unauthenticated; writing new copy spends your credits, and workspaces created after 2026-08-09 refuse unsigned mints by default. Without a token endpoint your fragments serve fallbacks forever, silently in production and with a console warning in dev. Signed minting is a ~20-line backend route.
Where to go from here
Section titled “Where to go from here”- Fragments: adaptive copy for anything you can name, under keys you choose.
- What your visitors see: the behaviour rules that change what you write.
- Provider, components and useIsland: every prop.
- When things go wrong: slow networks, paused campaigns, outages.