A/B Split (Bundled Extension) Since 3.5.0
totalcms/ab-split — bundled with Total CMS. Adds an ab-split page feature that renders an alternate page template at the same URL for a percentage of visitors. The visitor’s bucket sticks via cookie so refreshes don’t re-bucket. Use it to test layout changes, copy variations, hero swaps, CTA wording — anything where you want to measure two renders against each other without changing the URL.
Enabling
Section titled “Enabling”- Go to Admin → Extensions, find A/B Split, click Enable (or run
tcms extension:enable totalcms/ab-split). - Open a page in Site Builder, tick
ab-splitunder Features. - Add the alternate template path (and optionally the percent split) to the page’s Page Data JSON field:
{"abTemplate": "pages/contact-b.twig","abPercent" : 50}
- The middleware bucket-decides on first visit, sets a
tcms_ab_<page-id>cookie, and renders either the page’s normal template (variant A) orabTemplate(variant B).
Per-page configuration
Section titled “Per-page configuration”Set inside the page’s Page Data JSON field. Both keys are read from page.data.*.
| Key | Type | Default | Description |
|---|---|---|---|
abTemplate | string | (required) | Path to the variant-B template, e.g. pages/contact-b.twig. Empty/missing → middleware no-ops, page renders normally. |
abPercent | int | 50 | Percentage of visitors to send to variant B. Clamped to 0–100. Use 100 to force every visitor onto B (useful for testing the alternate template), 0 to effectively disable the split. |
What variant B sees
Section titled “What variant B sees”The alternate template is rendered with the same page context as the original — so page.title, page.image, page.description, page.data.*, etc. all work. Only the template body differs. URL parameters are passed as params (matching the normal page render).
This means you can keep all your meta tags, social previews, and SEO bits in a shared layout and only swap the body content per variant.
{# pages/contact-b.twig — variant B template #}{% extends 'layouts/default.twig' %}
{% block content %}<h1>{{ page.title }}</h1><p>This is the alternate version.</p><a class="cta cta-bold" href="/signup">{{ page.data.cta ?? 'Get started' }}</a>{% endblock %}Bucketing
Section titled “Bucketing”Variant assignment is sticky per visitor per page:
- On first visit, the middleware random-buckets the visitor based on
abPercent. - It sets a cookie
tcms_ab_<page-id>toaorb. - Subsequent visits read the cookie — same visitor sees the same variant for 30 days.
- Different pages get different cookies (
tcms_ab_contact,tcms_ab_pricing, etc.) — the same visitor can be in different buckets on different pages.
Cookie details
Section titled “Cookie details”| Attribute | Value |
|---|---|
| Name | tcms_ab_<page-id> |
| Value | a or b |
| TTL | 30 days |
| Path | / |
| SameSite | Lax |
The 30-day TTL is hard-coded in this version. Long enough that returning visitors keep their variant across most analytics windows; not so long that you can’t easily reset by clearing cookies.
Failure modes
Section titled “Failure modes”abTemplateempty or missing → middleware does nothing; page renders normally.abTemplatepoints at a missing file or has a Twig error → middleware silently falls through to the normal page render. A/B tests breaking should never break the live page.abPercentnon-numeric or out-of-range → falls back to the 50/50 default.- Visitor with an existing cookie → respected. Bucket doesn’t change for 30 days regardless of subsequent
abPercentchanges. To reset, clear cookies.
Use cases
Section titled “Use cases”Common patterns this is good for:
- CTA copy A/B — same page layout, different button text or hero copy. The template difference is small, the data is the same.
- Layout test — completely different presentation of the same content. Variant B is a substantively different template.
- Soft launch — bucket 5% of traffic onto a new design (
abPercent: 5) before promoting it. - Author / segment preview — set
abPercent: 100temporarily to force everyone onto B, validate, then dial back.
What it’s NOT good for:
- Per-visitor analytics tracking — the middleware doesn’t emit any events or write any per-bucket counts. Pair it with your analytics provider (GA, Plausible, etc.) by reading the
tcms_ab_<page-id>cookie client-side and tagging events with the variant. - Multivariate testing — only two variants per page (A vs B). For more than two, you’d need a different mechanism.
- Per-collection-object splits — the middleware applies to builder pages, not to individual collection objects. Splits on
pages/blog.twig(collection-URL match) aren’t supported.
Disabling
Section titled “Disabling”Bundled extensions can’t be removed (they ship in the package), but they can be disabled. Go to Admin → Extensions → A/B Split and click Disable, or run:
tcms extension:disable totalcms/ab-splitWhen disabled, the ab-split name disappears from the page-features picker. Pages that already have ab-split checked will silently skip it (the runner logs a warning that the name is unknown but the page still renders normally).
Implementation notes
Section titled “Implementation notes”The middleware lives at resources/extensions/totalcms/ab-split/AbSplitMiddleware.php. It registers as a page middleware with name ab-split and a class implementing PageMiddlewareInterface. The container definition wires TwigEngine so it can render the alternate template.
Source: resources/extensions/totalcms/ab-split/
See also
Section titled “See also”- Page Features (Builder) — the middleware framework this plugs into
- Bundled Extensions — the bundled-extension concept and the full list
- Extension Points —
addPageMiddleware()API for shipping your own page features