Split content setup
This page is for developers who want to set up split content for their project.
If you would like to learn how to use split content in Plasmic Studio, please see this page.
You can set up split content for your pages and components to show different content for different users at different times. This is useful for running A/B tests, targeting specific segments of users, or scheduling content that shows up in the future.
This document describes the code changes necessary to show the right variation of your split content. If you haven’t, please first read about how to set up split content from within Plasmic Studio.
Example repos
See the following example repos, demonstrating rendering variations in Next.js:
Setup
First, you need a way to “pick” which variation to render for the current user.
- For A/B tests, it means throwing dice and picking a bucket.
- For targeted content, it means taking a user’s traits and matching them into a segment to show personalized content. After completing the setup on this page, don’t forget to register traits.
- For scheduled content, it means checking whether the current time unlocks any new content.
Most of the complication lies in the first step — picking the right variation to use. How and where this happens depends on your application and what framework you’re using.
In general, we recommend that you pick the right variation to use on the server side. This makes it possible for the server to generate the right html content to start with, thus avoiding a “flash of wrong content” where the variation is picked and applied after hydration. It also makes it possible to cache the generated content at the edge, allowing faster page loads after initial generation.
In these docs, we’ll use Next.js as an example — but feel free to ask in the community forum if you want help with a different environment.
Next.js has three places where you can dynamically choose which variation of a component you want to render. We recommend using an edge network with SSG for the best results.
- On the edge network, e.g. in Next.js middleware/proxy on Vercel
- On your server, e.g. in
getServerSidePropswith the Pages Router - On the client, e.g. in a React component
We’ll examine each of these.
Edge network - Next.js middleware/proxy
Next.js has a proxy (formerly middleware) feature, which allows you to write a function that can rewrite the response based on the request. This allows you to dynamically pick the variation to use based on the request, but can still cache the generated for that specific variation for reuse later. This is the best performing option since the edge rewrite is run on Vercel’s edge network, but it is also the most complicated to set up.
You should consider using this if:
- You prefer to statically generate and cache server-rendered html where possible, instead of forcing dynamic rendering on every request.
- You are already using incremental static regeneration (aka
revalidate). - You are set up with a catchall page for rendering Plasmic pages. A codegen specific catchall page is explained in the following instructions.
- You can quickly look up the custom traits to use for a request (either because it is part of the URL or set in cookies).
- You are deployed on Vercel or other hosting services that support running code on edge networks.
The high-level sketch of a page request looks like this:
- The user requests a page, like
/pricing. - Your edge rewrite code intercepts the request, and “rewrites” the response as a different URL, which includes the custom traits for the request (usually read off of request cookies). This rewritten URL is still mapped to your catchall page.
- Your catchall page parses the rewritten pathname, fetches the Plasmic page data, and picks a specific variation.
- Your catchall page component renders
<ClientPlasmicRootProvider />(or codegen equivalent) with that picked variation.
Next.js will cache the generated content, so you only have to generate a page once. The key of the cache is the page path, which includes the URL, and the custom traits. Note that one of the “custom traits” will be a random number, which is used for picking the right bucket to use in A/B tests.
Here’s how your proxy.ts might look:
import { NextRequest, NextResponse } from 'next/server';import { getMiddlewareResponse } from '@plasmicapp/loader-nextjs/edge';// Exclude paths that are definitely not Plasmic pages with variationsexport const config = {matcher: ['/:path((?!_next/|api/|favicon\\.ico|plasmic-host).*)']};export async function proxy(req: NextRequest) {// Only pick a variation for GET requestsif (req.method !== 'GET') {return;}const newUrl = req.nextUrl.clone();const PLASMIC_SEED = req.cookies.get('plasmic_seed');// Rewrite to a new pathname that encodes the custom traits for// this request, as well as some randomness for A/B testsconst { pathname, cookies } = getMiddlewareResponse({path: newUrl.pathname,traits: {// Add values for custom traits that you are using; these are// likely read off of the cookie.},cookies: {...(PLASMIC_SEED ? { plasmic_seed: PLASMIC_SEED.value } : {})}});// Rewrite the response to use this new pathnamenewUrl.pathname = pathname;const res = NextResponse.rewrite(newUrl);// Save anything that needs to be saved in a cookie -- specifically,// the custom trait that corresponds to the random seed. The same// random seed will be used to pick the A/B test bucket each time// the user visits, to ensure that a visitor will always see the// same A/B test bucket.cookies.forEach((cookie) => {res.cookies.set(cookie.key, cookie.value);});return res;}
Now you need to extend your catchall page to also deal with these rewritten pathnames that encode traits and a random seed:
import {ComponentRenderData,PlasmicComponent} from '@plasmicapp/loader-nextjs';import { notFound } from 'next/navigation';import { PLASMIC } from '../../plasmic-init';import { ClientPlasmicRootProvider } from '../../plasmic-init-client';import {generateAllPaths,getActiveVariation,rewriteWithoutTraits} from '@plasmicapp/loader-nextjs/edge';interface Params {catchall: string[] | undefined;}interface LoaderPageProps {params: Promise<Params>;}// We use revalidate so new A/B tests, segments, and schedules are picked up.export const revalidate = 60;async function getPageData(params: Promise<Params>): Promise<{ pagePath: string; componentData?: ComponentRenderData; variation?: any }> {const catchall = (await params).catchall;const rawPlasmicPath = catchall ? `/${catchall.join('/')}` : '/';// Parse the path, and extract the traits.const { path: plasmicPath, traits } = rewriteWithoutTraits(rawPlasmicPath);const componentData = await PLASMIC.maybeFetchComponentData(plasmicPath);if (!componentData) {return { pagePath: plasmicPath };}// Pick the variation to use based on the traits.const variation = getActiveVariation({splits: PLASMIC.getActiveSplits(),traits,path: plasmicPath});return { pagePath: plasmicPath, componentData, variation };}export default async function CatchallPage({ params }: LoaderPageProps) {const { componentData, variation } = await getPageData(params);if (!componentData || !variation) {notFound();}const pageMeta = componentData.entryCompMetas[0];return (<ClientPlasmicRootProviderprefetchedData={componentData}variation={variation}pageRoute={pageMeta.path}pageParams={pageMeta.params}><PlasmicComponent component={pageMeta.displayName} /></ClientPlasmicRootProvider>);}export async function generateStaticParams() {const pageModules = await PLASMIC.fetchPages();const paths = pageModules.flatMap((page) => generateAllPaths(page.path));return paths.map((path) => ({catchall: path === '/' ? undefined : path.substring(1).split('/')}));}
Cached page content considerations
This approach relies on your catchall page to lazily generate the page content for a specific set of custom traits. The generated content will then be cached by Next.js for those set of traits. There are a few considerations to be aware of here:
- Page load will be slower the first time someone visits with a new set of custom traits, because Next.js will be rendering that page at request time. One of the custom traits, specifically, is a “random seed” that is used to pick an A/B test bucket. By default, we use 16 different seed values, which means there will be at least 16 cache misses.
- The timestamp used for activating scheduled content is the time when your page is generated/revalidated.
That means if you are using scheduled content, then you must use
revalidate, so that the cached content is invalidated and re-generated after some timeout. If you specify a largerevalidate, then your server will be doing less work (fewer invalidations), but your scheduled content may not be activated at precisely the right time. For example, if yourrevalidateis300(5 minutes), then the page will only be regenerated once every ~5 minutes, so you may miss your scheduled time by at most 5 minutes. Therefore, you should pick arevalidatenumber that is right for your use case.
Server - Next.js dynamic rendering
If you are already rendering dynamically on the server for every request, you can pick the variation directly in your route. Most of the complications of the edge rewrite approach go away.
Only use dynamic rendering for deterministic, trait-based targeting, where the variation is a pure function of the traits you pass in (e.g. a segment derived from the logged-in user). A/B tests, or anything that relies on randomness, should use the edge rewrite approach instead — a random bucket has to be persisted (via the plasmic_seed cookie) to keep a visitor in the same bucket across visits, which the edge rewrite code handles cleanly and cacheably.
This also means dynamic rendering is Pages Router only: App Router Server Components cannot set cookies during render (cookies can only be set in a Server Action or Route Handler), so they cannot persist the seed at all.
getServerSideProps is simpler to set up, without requiring any change to your codebase organization.
import * as React from 'react';import { PlasmicComponent, ComponentRenderData, PlasmicRootProvider } from '@plasmicapp/loader-nextjs';import { GetServerSidePropsContext } from 'next';import { PLASMIC } from '@/plasmic-init';export async function getServerSideProps(context: GetServerSidePropsContext) {const { catchall } = context.params ?? {};const plasmicPath =typeof catchall === 'string' ? catchall : Array.isArray(catchall) ? `/${catchall.join('/')}` : '/';const plasmicData = await PLASMIC.fetchComponentData(plasmicPath);// This is the main new addition to your existing Plasmic-loading code.const variation = await PLASMIC.getActiveVariation({req: context.req,res: context.res,// These are based on whatever custom traits you have defined.// Often, this trait is derived from user info, such as in req.cookies// or from your application database.traits: {age: 24,color: 'red',isLoggedIn: true}});return {props: {plasmicData,variation}};}export default function CatchallPage(props: {plasmicData: ComponentRenderData;// eslint-disable-next-line @typescript-eslint/no-explicit-anyvariation?: any;}) {const { plasmicData, variation } = props;const pageMeta = plasmicData.entryCompMetas[0];return (<PlasmicRootProvider loader={PLASMIC} prefetchedData={plasmicData} variation={variation}><PlasmicComponent component={pageMeta.displayName} /></PlasmicRootProvider>);}
Server - other frameworks
Knowing that the page is going to be rendered on the server, it’s only a question of obtaining the variation object.
If you are not using Next.js you would have to implement a cookie-management logic, similarly to how it’s done in Next.js.
The functions getKnownValue and updateKnownValue should be used in order to save picked variations into cookies or any other form that you might want.
When you will have the functions for cookie management your resulting code should look approximately like this:
const variation = getActiveVariation({splits,traits,// Persist picked variations yourself (e.g. in cookies) via these callbacks.getKnownValue: (key) => getPlasmicSplitFromCookies(key),updateKnownValue: (key, value) => setPlasmicSplitIntoCookies(key, value)});
Client - React Component
You can pick the variant client-side, but this will degrade the performance of your application, since it will first render (flash) the base variant of the page/component, load the correct variant of the design from the Plasmic CDN, following by a render of the correct variant.
function MyPage() {const [loading, setLoading] = React.useState(false);const [variation, setVariation] = React.useState({});const getVariation = async () => {// This is async because it loads the bundle if it is not already loaded.const activeVariation = await PLASMIC.getActiveVariation({traits: {}});setVariation(activeVariation);setLoading(false);};React.useEffect(() => {getVariation();}, []);if (loading) {return <p>Loading...</p>;}return (<PlasmicRootProvider loader={PLASMIC} variation={variation}><PlasmicComponent component={'Homepage'} /></PlasmicRootProvider>);}