Docs / Websites

Personalisation

Studio has two levels of “different visitors see different things”, both authored in the editor and both decided at render time by your renderer with @maravilla/studio-content. Neither needs code per campaign:

  • a Switcher holds several versions of a part of a page, each a full set of blocks with a rule;
  • a page variation keeps several complete versions of a whole page behind one URL.

What decides which version a visitor gets is a rule. Rules are built with the rule builder, can use reusable segments, and can be checked against personas before anything goes live.

Rules

A rule is a set of conditions in a group that reads All of or Any of. Groups nest (“Germany AND (mobile OR signed in)”) and can be turned around with NOT. Each condition is three choices — what you ask about, how you compare, and the value — and the value control fits the question: countries with presets such as DACH, EU or Nordics, weekday chips, an hour range, segments, or the values of a trait you declared.

The segment 'DACH customers' in the Studio editor: a rule builder with an All of group of two conditions, Segment is in DACH visitors and Segment is in Customers, and a plain-language summary line above
A rule in the builder. The line at the top reads it back in plain words; this one is a segment built from two other segments.

Test shows whether the rule matches for a persona of your choice, with ✓ or ✗ on every condition, so “why doesn’t my variant show?” has an answer on screen.

The rule builder's Test panel: the persona 'Corporate buyer — desktop, Switzerland, signed in' is selected, a green 'Matches — this visitor sees it' verdict, the segments the persona is in, and a green tick beside each condition
Testing a rule against a persona: the verdict, the segments that persona belongs to, and the result of every condition.

Code shows the same rule as text. It is REL, the expression language the rest of the platform uses for permissions and automations, so a rule means the same thing in the browser, on the server, at the edge and in email:

['DE', 'AT', 'CH'].contains(geo.country) && device.type == 'mobile'
segments.contains('customers') && !segments.contains('churned')
time.hour != null && time.hour >= 18 || utm.campaign == 'autumn'

Anything the visual view cannot draw stays exactly as written. Two things worth knowing if you write rules by hand: comparisons are exact (countries are upper-case ISO codes), and an error anywhere makes the whole rule not match — so a comparison on a value that may be unknown is written with a guard, time.hour != null && time.hour >= 18. The builder writes the guard for you.

What a rule can ask about

where it comes fromknown
geo.country, geo.regionthe visitor’s network address, set by the platform edgeon the server and in the browser
device.type, device.osthe browseron the server and in the browser
localethe pageeverywhere
referrer.host, utm.*, query.*the link the visitor followedeverywhere
time.hour, time.weekday, time.datethe visitor’s own clockin the browser
user.authenticated, user.rolesyour site or applicationon the server and in the browser
traits.*your site or application (declared in Studio)on the server and in the browser
segmentsthe segments the visitor belongs to—

The builder warns when a condition cannot work where it is used — a time-of-day condition on a whole-page variation, for example, which is decided before the browser’s clock is known.

Segments, traits and personas

The Personalization app holds the reusable pieces.

The Personalization app: navigation for Segments, Personas and Traits, and a list of segments with their keys and rules, including DACH customers built from two other segments
Segments with their keys and rules. Personas and traits live alongside.

Segments define a group of visitors once — “DACH visitors”, “Customers”, “Evening visitors” — and a rule then says Segment is in Customers. A visitor can be in several. A segment may be built from others (“DACH customers” is in DACH visitors and in Customers; “Prospects” is NOT in Customers); they are resolved in order, and a circular definition matches nobody and is flagged. Every segment has a key that rules use, so it can be renamed freely.

Traits are facts about a visitor that Studio cannot see by itself — a plan, interests, the campaign that first brought them. Declare one with its kind of value (one of a list, several from a list, text, number, yes/no) and how it is filled:

  • your site sets it — the site passes it in from its own data;
  • from the visitor’s account — read from the signed-in visitor’s profile on the server;
  • learned from what they do — the browser learns it from rules you write in plain terms: when the visitor views a page → add the page’s tags, when they arrive from a campaign → remember the first one, when the site reports newsletter_signup → set to yes.

The builder then offers the trait with the right operators and values.

Personas are example visitors: a country, a device, a time of day, a language, signed in or not, trait values. They don’t target anyone; they let you look. Each persona shows the segments it falls into as you edit it.

A persona 'Anna — mobile, Germany, evening' in the editor: segments this visitor is in (DACH visitors, Evening Visitors, German Speakers) and fields for country, region, language, device type, operating system and hour of day
A persona. The form comes from the same signals the rule builder offers; its segments update as you change it.

Step by step: an offer for a campaign, a welcome for returning visitors

This walkthrough builds a block that greets three kinds of visitor differently: people who first came from the autumn campaign get the campaign offer, people on their second visit or later get Welcome back, and everyone else gets the default. Nothing here needs a developer once the site has the visitor profile switched on (see What the browser learns).

1. Declare a trait the browser learns. In Personalization → Traits, create a trait called First campaign. Set Kind of value to Text, and under How it’s filled choose Learned from what they do. Add one rule: When the visitor arrives from a campaign, From the campaign name (utm_campaign), Then remember the first one. Its key — the name the site and rules use — comes from the label: first_campaign.

The trait 'First campaign' in Studio: under How it's filled, 'Learned from what they do' is selected, with one rule — When the visitor arrives from a campaign, From the campaign name (utm_campaign), Then remember the first one — and below it the Personal data checkbox and the Advanced section with the key first_campaign
A learned trait: the browser remembers the campaign that first brought the visitor here.

Other rules work the same way: when the visitor views a page → add the page’s tags collects interests from what they read (make the trait Several from a list and list the allowed topics), → count it counts views of the pages you name with a path such as /pricing*, and when the site reports an event → set it to yes records something the site tells the profile about, such as a newsletter sign-up.

2. Make segments from it. In Segments, create Autumn campaign with the rule First campaign is autumn. Create a second segment, Returning visitors, with Visit count ≥ 2 — the visit history needs no trait, the profile keeps it on its own.

The segment 'Autumn campaign': its membership rule reads 'First campaign is autumn', built in the rule builder from the Campaigns · First campaign signal, the operator is and the value autumn
A segment built on the learned trait. The builder lists traits under the group you gave them.

3. Add the Switcher. On the page, add a Switcher with three variants: Autumn campaign (rule: Segment is in Autumn campaign), Returning visitors (Segment is in Returning visitors) and a default without a rule. The first variant whose rule matches wins, so put the more specific one first.

4. Check it. Open the page as a new visitor — a private window is easiest. The site asks once whether it may remember the visit; before the visitor says yes, nothing is stored and they see the default.

The demo page for a first-time visitor: the third block reads 'First time here? Start with the tour.' and a prompt at the bottom asks 'May we remember what you read here, so the next pages fit your interests? Stored only in this browser.' with the buttons No thanks and Yes
A first visit: the default variant, and the question nothing is stored without.

After Yes, open the page again with ?utm_campaign=autumn at the end of the address. The block switches to the campaign offer at once, and it stays when you remove the parameter: the browser has learned First campaign, and from the next page on the server renders the offer straight away.

The same page after arriving from the autumn campaign: the third block now reads 'Your autumn offer is waiting — You came in from the autumn campaign, 15% off until the end of October.'
The same page once the campaign is learned.

A visit counts as a new one after 30 minutes without a page view, so Welcome back appears on the visitor’s next visit (for visitors who did not come from the campaign — the campaign variant is first). To start over as a new visitor, use a new private window.

View as: the page as one visitor sees it

In the page editor, View as picks a persona — or adjusts country, device, time and sign-in on the fly — and every Switcher on the page shows what that visitor gets, on the live page in the editor.

The View as menu in the editor's top bar: Editing — no persona, three personas with avatars and descriptions, and an Adjust section with country, device, time and signed-in controls
Pick a persona, or adjust a few signals without making one.
The editor viewing the demo page as Anna: the live page shows the German-language variant 'Willkommen — Lieferung in die ganze DACH-Region'
Viewing as Anna, a visitor from Germany: the page shows the DACH variant, exactly as the site renders it for her.

Select a block inside a Switcher and the design panel shows its variants as tabs, with the rule of the one you are on. The selection toolbar on the page switches variants too. Click a variant the persona doesn’t get and Studio says so — not what Anna sees — with a button to go back to theirs. Choose Editing — no persona to return to plain editing.

A heading inside the DACH visitors variant selected on the page; the design panel shows the Switcher's variant tabs (DACH visitors, Mobile, Evening, Default) and the variant's audience rule in the rule builder
A variant's rule, edited next to the page it changes.

The Personalize panel lists every Switcher on the page with the variant each persona gets — who sees which variant, and which page version they land on — and checks the setup: two variants that both match one persona (only the first ever shows), variants nobody can reach, rules that can’t work where they are used, missing segments.

The Personalize panel: under 'Who sees what', a card per Switcher listing which variant Anna, the corporate buyer and the first-time visitor each get, and a check warning that for Anna the DACH visitors, Mobile and Evening variants all match so only DACH visitors shows
Who sees what, and a check that caught an overlap: for Anna three variants match, so the later two can never show for her.

Open preview as opens the site preview rendered for the chosen persona. It works only inside a signed Studio preview session; a visitor who adds ?persona= to a URL gets the ordinary page.

Page variations: a whole page

Sometimes it is not one block but the page: a landing page with a different structure for returning customers, a home page for a campaign week. A page variation keeps several complete versions — each with its own title, SEO and blocks — behind one URL, with a rule each. loadPageVariation in the loader picks one the same way Switchers are picked, before the page renders; the URL does not change. Page variations are decided on the server, so they use the signals the server knows; for time of day use a Switcher inside the page.

Use a Switcher when part of a page differs; a page variation when the page’s structure differs. Both can nest.

In the renderer

The loader builds the visitor context once and hands it to the helpers:

import { contextFromRequest, resolveSegments } from '@maravilla/studio-content/personalize';

const base = { ...contextFromRequest(request), locale };  // country, device, referrer, utm, query
const { segments } = resolveSegments(segmentNodes, base);  // studio:Audience nodes, cached briefly
const visitor = { ...base, segments };

contextFromRequest reads the country the platform edge sets (X-Country-Code; CF-IPCountry and X-Vercel-IP-Country work too), the device from the User-Agent, the referrer and the campaign parameters. The same visitor goes to loadPageVariation({ ctx }) and to the framework helper that resolves Switchers — useVariantContent in React and Vue, variantContent in Svelte. Rules run as small compiled functions in plain JavaScript: no eval, nothing extra to download.

Time-of-day rules are the one thing the server doesn’t decide: the server renders without a clock, and the browser re-picks just after load, only when the pick actually changes.

Caching

A page whose rules read the country is still cacheable — per country. cachePolicyFor(contentRuleRefs(content, segmentNodes)) tells you what the response must declare for the rules this page actually carries: Vary: X-Country-Code for country rules, private for rules on device, referrer, visit, user or traits. Segments are expanded to what they read, so a page targeting “DACH visitors” varies on the country only. A page without rules caches like any other, and a persona preview is never cached. The example sites wire this up in their route’s headers.

What the browser learns

A learned trait and the visit history (first visit, visit count) live in the visitor’s browser, in a small profile the site creates with @maravilla/studio-content/profile. Nothing is stored until the visitor agrees; after that, every page view and every event the site reports runs the learn rules, and the page re-picks its variants on the spot. A compact copy goes into a first-party cookie, so from the next page on the server renders the right variant straight away — including whole-page variations. Traits marked as personal data never leave the browser.

Switching it on is two calls in the site, once — after that, authors add learned traits in Studio without touching code:

// server: once per site
import { createPersonalization } from '@maravilla/studio-content/personalization';
const pz = createPersonalization({ query, previewSecret });
const visitor = await pz.visitor(request, locale);        // what a rule can know about this visitor
const headers = await pz.cacheHeaders(content, visitor);  // what a shared cache must do for this page
const catalog = await pz.browserCatalog();                // what the browser re-picks with

// page: React and Vue (Svelte: personalizedContent)
const view = usePersonalizedContent(content, { visitor, editing, catalog, page });

// your consent banner or consent manager
setPersonalizationConsent(true);
getVisitorProfile()?.track('newsletter_signup');          // an event a learn rule names

Everything has a way out. extend adds what only your site knows — the signed-in user, its own traits, a customer-data platform’s segments — and account hands over the visitor’s account for traits filled from it. The catalogue can come from your own loaders instead of SQL, consent from your consent manager, and every building block underneath (contextFromRequest, resolveSegments, profileFromRequest, createProfile, useVariantContent) stays available if you would rather compose them yourself.

The same framework-free store re-picks variants in React, Svelte and Vue — the framework helpers are thin wrappers around it — and segments built on the clock or on what the browser learned — Evening visitors, Returning visitors — match there too. Astro renders each page once without a browser step, so there a learned trait applies from the visitor’s second page. The cookie is written by the visitor’s browser: good for choosing which version someone sees, never for deciding what they may see.

Where the application comes in

On a plain website the context is what the request says. An application knows more — the visitor is signed in, has a plan, has not finished onboarding — and this is the seam where the two meet:

  1. The app authenticates the visitor with its own means (its own database, its own sessions).
  2. It adds what it knows to the context: { ...visitor, user: { authenticated: true }, traits: { plan: 'pro', onboarding: 'done' } }.
  3. Those traits are declared in Studio, and authors write rules against them with no deployment: a “Welcome back” hero for signed-in visitors, a pricing block hidden for the enterprise plan.
  4. A block placed by an author may render the app’s data: a RecentProjects block registered by the app takes its heading and empty-state copy from Studio and its rows from the app’s database.

The app owns identity and data; Studio owns the words and the structure, including the decision of which structure this visitor gets. Keep the traits few and stable — they are the vocabulary authors write rules in.

For agents

The Studio MCP server exposes all of it: listing signals and declaring traits — including how each is filled, learn rules and all — creating and checking segments and personas, explaining a rule for a persona clause by clause, listing a page’s personalisation with its checks, showing which variant every persona sees, adding and editing variants, and previewing a page as a persona. An assistant can set up a campaign’s segments and variants and check them the same way an author would.

Pilot documentation. Studio is in a partner pilot. Screens and YAML shown here are current as of the version noted on each page; tell your Maravilla contact when something does not match what you see.