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.

  1. On an edge runtime, e.g. Next.js middleware on Vercel
  2. On your server, e.g. in Next.js getServerSideProps
  3. On the client, e.g. in a React component

We’ll examine each of these.

Edge runtime - Next.js middleware

Next.js has a new 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 of getServerSideProps()).
  • 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:

  1. The user requests a page, like /pricing.
  2. 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.
  3. 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 as pageProps.
  4. Your catchall page component renders <PlasmicRootProvider /> or <PlasmicSplitsContext /> with the picked variation from pageProps.

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:

Copy
import { NextRequest, NextResponse } from 'next/server';
import { getMiddlewareResponse } from '@plasmicapp/loader-nextjs/edge';
// Exclude paths that are definitely not Plasmic pages with variations
export const config = {
matcher: ['/:path((?!_next/|api/|favicon\\.ico|plasmic-host).*)']
};
export async function middleware(req: NextRequest) {
// Only pick a variation for GET requests
if (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 tests
const { 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 pathname
newUrl.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:

Copy
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 (
<PlasmicRootProvider
loader={PLASMIC}
prefetchedData={plasmicData}
variation={variation}
>
<PlasmicComponent component={plasmicData.entryCompMetas[0].name} />
</PlasmicRootProvider>
);
}
export const getStaticProps: GetStaticProps = async (context) => {
const { catchall } = context.params ?? {};
const rawPlasmicPath = `/${(catchall as string[]).join('/')}`;
// Parse the path, and extract the traits.
const { path: plasmicPath, traits } = rewriteWithoutTraits(rawPlasmicPath);
const plasmicData = await PLASMIC.maybeFetchComponentData(plasmicPath);
if (!plasmicData) {
// Non-Plasmic page
return { props: {} };
}
// Pick the variation to use based on the traits
const 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 use revalidate, so that the cached content is invalidated and re-generated after some timeout. If you specify a large revalidate, 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 your revalidate is 300 (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 a revalidate 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.

Copy
export async function getServerSideProps(context: GetServerSidePropsContext) {
// 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
}
});
const plasmicData = await PLASMIC.fetchComponentData('MyPage');
return {
props: {
plasmicData,
variation
}
};
}
export function CatchallPage({ plasmicData, variation }) {
return (
<PlasmicRootProvider
loader={PLASMIC}
prefetchedData={plasmicData}
variation={variation}
>
<PlasmicComponent component={'Homepage'} />
</PlasmicRootProvider>
);
}

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.

Copy
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>
);
}

Advanced topics

Understanding the variation object

The getActiveVariation function returns an object that identifies the content that should be activated based on internal IDs defined by Plasmic, as well as by external IDs defined by the user. But in some cases, you may want to get a more detailed object that explains what is the content that is being activated. For this, you can use the describeVariation function.

Copy
// It can also be imported from `@plasmicapp/loader-splits
import { describeVariation } from '@plasmicapp/loader-nextjs/edge';
...
const variation = getActiveVariation({
splits: PLASMIC.getActiveSplits(),
traits,
path: plasmicPath
});
const detailedVariation = describeVariation(
PLASMIC.getActiveSplits(),
variation
);

The detailed variation is an object with the same keys as the variation object, but the value is an object with the following keys:

FieldOptionalDescription
nameNoThe name of the split defined in Studio.
descriptionYesThe description of the split defined in Studio.
pagesPathsNoAn array of strings listing the Plasmic page paths that directly use the specific split. This does not account for changes activated through components.
typeNoIf the variation is an original value or an override.
chosenValueNoThe value that was chosen for the split. It’s equal to the original value in the variation object.
externalIdGroupYesThe external id group specified in the Studio.
externalIdValueYesThe external id specified to this variation in the Studio.
Was this page helpful?

Have feedback on this page? Let us know on our forum.