Targeting on a comma separated list of strings

Please fill out as many of the following questions as possible so we can help you promptly! If you don’t know how to answer or it does not apply, feel free to remove it or leave it blank.

Builder content link

Builder public api key

f52a80d474894fdda7032afe9176c0e6

What are you trying to accomplish
I am trying to create a Custom targeting attribute where a comma separated list of customer numbers can be entered. Like the functionality of selecting multiple items from an enum but without the enum, because we have thousands of custommer numbers.
Is that possible ?

Hi!

My name is Veronika and I’m a Customer Engineer here at Builder.

Yes, this is definitely possible. Builder’s built-in targeting does exact string matching, so you can’t use it directly for comma separated lists, but here’s a clean workaround that works well for large customer lists.

Instead of using a custom targeting attribute, add a custom text field called allowedCustomers to your Page model (Models → Page → + New Field, type: Text). Editors can then paste a comma-separated list directly on each page, e.g. 1042, 9999, 5500.

On the code side, fetch the content without passing the customer number to Builder’s API (since it can’t do comma matching server-side), then handle the filtering yourself on the client:

// In your page component — fetch without customerNumber in userAttributes
const content = await builder
  .get('page', {
    userAttributes: {
      urlPath: '/your-page-path',
    },
  })
  .toPromise();
// In your render component — check the allowedCustomers field
function matchesCustomerTargeting(
  content: BuilderContent | null,
  customerNumber: string,
): boolean {
  const allowedCustomers = content?.data?.allowedCustomers as string;

  if (!allowedCustomers) return true;

  const numbers = allowedCustomers.split(',').map((n: string) => n.trim());
  return numbers.includes(customerNumber);
}

export function RenderBuilderContent({ content, model, customerNumber = '' }) {
  const isPreviewing = useIsPreviewing();
  const passesTargeting = isPreviewing || matchesCustomerTargeting(content, customerNumber);

  if ((content && passesTargeting) || isPreviewing) {
    return <BuilderComponent content={content} model={model} />;
  }
  return <DefaultErrorPage statusCode={404} />;
}

The customerNumber you pass in comes from wherever your app stores the logged in user (cookie, session, JWT, etc).

This approach scales well with thousands of customer numbers since you’re just storing a plain text string in Builder, no enum setup needed. Hope that helps!

Hi Veronika
Thanks for the quick answer.
I’m totally new to builder and not verry strong in frontend code, so I’ll have to take your code at send it to one of my colleagues that is a lot better at that stuff than me.
It looks to me like your code would either show the page if the customer number were right or return an error page? What if we have 2 pages a default one and one for specific customer numbers would it be possible to show the default one to all the customers that does not have the right customer number and the specific one for the customers with the right numbers?

Another question is the same functionality possible for the Variant Container (Personalize), so that for example Variant 2 would be shown for only specific customer numbers?

@veronikapilipenko have you seen my question?

Hi!

Great questions. I can go ahead and take a loom video for you today for that first approach/ workaround if that’s helpful in terms of how I built this out and walk you through the steps.

1. Default page + specific page for certain customers

Yes! Instead of returning a 404 for non matching customers, you can publish two pages in Builder with the same URL, one default (leave allowedCustomers empty) and one targeted (fill in the customer numbers). Then in your code, fetch all pages for that URL and pick the right one:

const allContent = await builder.getAll('page', {
  userAttributes: { urlPath: '/your-page-path' },
  limit: 20,
});

// Find the page specifically targeting this customer
const specificPage = allContent.find((page) => {
  const allowed = page.data?.allowedCustomers as string;
  if (!allowed) return false;
  return allowed.split(',').map((n) => n.trim()).includes(customerNumber);
});

// Fall back to the default page if no specific match
const defaultPage = allContent.find((page) => !page.data?.allowedCustomers);

const content = specificPage ?? defaultPage ?? null;

Note that we intentionally don’t pass customerNumber to Builder’s API here because Builder does exact string matching server side, so it can’t match a single customer number against a comma separated list. So we fetch all pages for that URL and handle the matching ourselves in code.

So customers with a matching number see the targeted page, everyone else sees the default so no error page will be needed.

2. Variant Container (Personalize)

The Personalize container works differently from the page level approach. Instead of server-side API filtering, it uses builder.setUserAttributes() which stores attributes in a cookie and does the matching on the client side. This means Builder’s exact string matching works fine here and we don’t need the comma separated workaround.

To set it up, call setUserAttributes at the global level in a "use client" component, before React renders:

"use client"
import { builder } from "@builder.io/sdk";

// Must be outside the component at the global context
builder.setUserAttributes({ customerNumber: "1042" });

export default function MyPage() {
  return <BuilderComponent content={content} model={model} />;
}

Then in the Personalize container in the editor, set Variant 2’s targeting rule to customerNumber is 1042.

This works well if each variant targets a single customer number or a small fixed set.

However, if you need to target a large chunk of customer numbers per variant, the Personalize container isn’t the right fit, the page level allowedCustomers approach from my previous reply would be the better solution in that case.

Hope that helps. Let me know if you have any other questions!