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 a 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 Next.js middleware for the best results.
- On an edge runtime, e.g. Next.js middleware on Vercel
- On your server, e.g. in Next.js
getServerSideProps
- On the client, e.g. in a React component
We’ll examine each of these.
Edge runtime - Next.js middleware
Next.js has a middleware feature, which allows you to write a middleware 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, but it is also the most complicated to set up.
You should consider using Next.js middleware if:
- You prefer to statically generate and cache server-rendered html (that is, you use
getStaticProps()
where possible, instead ofgetServerSideProps()
). - 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 structions.
- 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 Next.js middleware at the edge.
The high-level sketch of a page request looks like this:
- The user requests a page, like
/pricing
. - Your middleware 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.
- The
getStaticProps()
function of your catchall page parses the traits, fetches the Plasmic page data, and picks a specific variation. The fetched Plasmic page data and the variation to use is returned aspageProps
. - Your catchall page component renders
<PlasmicRootProvider />
or<PlasmicSplitsContext />
with the picked variation frompageProps
.
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 middleware.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 middleware(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 * as React from 'react';import Error from 'next/error';import { PlasmicComponent, ComponentRenderData, PlasmicRootProvider } from '@plasmicapp/loader-nextjs';import { GetStaticPaths, GetStaticProps } from 'next';import { PLASMIC } from '../init';import { generateAllPaths, getActiveVariation, rewriteWithoutTraits } from '@plasmicapp/loader-nextjs/edge';export default function CatchallPage(props: { plasmicData?: ComponentRenderData; variation?: any }) {const { variation, externalIds, plasmicData } = props;if (!plasmicData) {return <Error statusCode={404} />;}// Render Plasmic page with the variation picked in `getStaticProps()`return (<PlasmicRootProviderloader={PLASMIC}prefetchedData={plasmicData}variation={variation}><PlasmicComponent component={plasmicData.entryCompMetas[0].name} /></PlasmicRootProvider>);}export const getStaticProps: GetStaticProps = async (context) => {const { catchall } = context.params ?? {};const rawPlasmicPath =typeof catchall === 'string' ? catchall : Array.isArray(catchall) ? `/${catchall.join('/')}` : '/';// Parse the path, and extract the traits.const { path: plasmicPath, traits } = rewriteWithoutTraits(rawPlasmicPath);const plasmicData = await PLASMIC.maybeFetchComponentData(plasmicPath);if (!plasmicData) {// Non-Plasmic pagereturn { props: {} };}// Pick the variation to use based on the traitsconst variation = getActiveVariation({splits: PLASMIC.getActiveSplits(),traits,path: plasmicPath});return {props: {plasmicData,variation},// We use revalidate here, so that if new A/B tests or segments have// been defined and published, they will be automatically picked up// and used here.//// Also, for scheduled content to work, you must use revalidate, as// the "current time" is not part of the page cache key, and is// actually the time when this is run. So we rely on revalidate to// invalidate the cached page content and regenerate with a// new timestamp.revalidate: 60};};export const getStaticPaths: GetStaticPaths = async () => {const pageModules = await PLASMIC.fetchPages();const paths = pageModules.flatMap((page) =>generateAllPaths(page.path).map((path) => ({params: {catchall: path.substring(1).split('/')}})));return {paths,// We set `fallback:"blocking"` to generate page variations lazily.// Once a page for a specific set of traits has been generated, it// will be cached and reused.fallback: 'blocking'};};
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
getStaticProps()
is called. That means if you are using scheduled content, then you must userevalidate
, 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 yourrevalidate
is300
(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 arevalidate
number that is right for your use case.
Server - Next.js getServerSideProps()
If you are already dynamically rendering on the server for every request — by using getServerSideProps()
— then you can simply also pick the variation to use while you are rendering.
Most of the complications with using middleware goes away.
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;queryCache?: Record<string, unknown>;// eslint-disable-next-line @typescript-eslint/no-explicit-anyvariation?: any;}) {const { plasmicData, variation } = props;return (<PlasmicRootProvider loader={PLASMIC} prefetchedData={plasmicData} variation={variation}><PlasmicComponent component={'Homepage'} /></PlasmicRootProvider>);}
Server - other frameworks
Knowing that the page is going to be rendered in 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:
getActiveVariation(...getKnownValue: (key: string) => {return getPlasmicSplitFromCookies(key);},updateKnownValue: (key: string, value: string) => {return 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>);}
Have feedback on this page? Let us know on our forum.