Shopify Builds>LUS Brands>Quiz results: a 34-rule, priority-ordered recommendation engine running client-side from metaobject data

Quiz Results: A 34-Rule, Priority-Ordered Recommendation Engine Running Client-Side From Metaobject Data

The half of the Curl Quiz that turns answers into products. Liquid renders every possible recommendation once into hidden templates; a matcher in the browser runs 34 priority-ordered rules and clones the survivors into three result groups.

Answers that only exist in the browser cannot be matched in Liquid, but product tiles have to be server-rendered to carry a real price, stock state and quick add. So Liquid pre-renders every possible recommendation into hidden templates, and a client-side matcher runs 34 priority-ordered required/any/blocked rules over nine answers, de-duplicates by product and clones the survivors into three result groups.

Fact Strip

  • Client: LUS Brands
  • Surface: The quiz results page — one template, page.quiz
  • Rule model: 34 logic-rule entries, 13 product recommendations, 3 result groups
  • Complexity: High
  • Attribution: Deploi-authored. The section, the matcher and the rule model are ours; the product tiles are the theme's own card-product snippet on a Dawn 15.4.1 base.
  • Status: Live, verified 2026-09-06
  • Evidence: Strong — including an independent validation run, 26 of 26 checks passing
  • Integration: Klaviyo, for the completion event

The Problem

A quiz is only worth building if the answers produce a routine someone will buy — a foundation system plus targeted add-ons, not a bestsellers grid with a personalized headline on it.

The mapping here is genuinely conditional, and not in a way a scoring model handles well. Fragrance preference decides which three-step system a shopper is shown at all. Porosity crossed with strand thickness decides which moisture product is right — medium porosity with fine strands is a different answer from medium porosity with coarse strands. And the scalp scrub has to be suppressed entirely for anyone who answered "I only use fragrance-free products," no matter what else they said.

That mapping was still being negotiated between the brand team and a Curl Quiz 3.0 specification while the theme was in development. It could not be hard-coded, because it was not yet finished.

The Constraint

Recommendations have to react to answers that exist only in the browser. Liquid runs once, server-side, before a shopper has clicked anything, so the match itself cannot happen there.

The product cards, though, have to be real. A routine tile with a stale price, the wrong currency for a Canadian shopper, or a sold-out product presented as available is worse than no recommendation. That means genuine {% render 'card-product' %} markup — correct price, correct currency, availability, variant picker, quick add — and only Liquid produces that.

Shopify Functions were not an option: they operate on carts and checkouts, not on merchandising logic for a storefront page. And the rule set had to stay editable by the brand team without a deploy, which rules out expressing the conditions in the section's JavaScript.

So the constraint is a split one — the data must be matched where Liquid cannot reach, and rendered where only Liquid can.

What We Built

A hybrid render, split across the request boundary.

At page load, Liquid renders every recommendation the rule set can produce, once each, into <template data-recommendation-template="<recommendation_key>"> elements inside a hidden container. Each template holds a full {% render 'card-product' %} with quick_add: 'standard' — the storefront's real card, not a stand-in.

The matching side is projected out of the same metaobject entries the templates were rendered from, so the matcher and the tiles are built from one source rather than two.

At runtime the controller listens for the curl-quiz:results event published by the quiz section, flattens the nine answers into a Set of option keys, sorts the 34 rules by priority ascending, and evaluates each against three predicates: every entry in requiredOptions must be present, at least one entry in anyOptions must be present when that list is non-empty, and nothing in blockedOptions may be present. A rule carrying neither required nor any options is treated as non-matching, so a half-configured rule contributes nothing rather than matching everyone.

Accepted recommendations are de-duplicated twice — once by recommendation key, once by product ID — so two different rules that both resolve to Moisture Milk produce one tile. Survivors are cloned out of their templates into the matching [data-result-group] container, sorted by each recommendation's sortOrder, and truncated to that group's data-product-limit. Groups that end up empty are hidden. If nothing matched at all, the empty state renders and the Build Your Bundle call to action steps aside with it.

Deep links are treated as a first-class entry point rather than an afterthought. The section reads the cq_* parameters itself on load and renders a routine without the quiz ever running when cq_view=results is present — or when document.referrer is empty, the signature of a pasted link. The back control restores the quiz, re-shows it, and rewrites the URL to cq_view=quiz.

Completion is also an email event. On a valid address the section identifies the shopper to Klaviyo and tracks a "Quiz Completed" event carrying a resultslink — a fully-qualified URL rebuilt from the answers with cq_view=results appended — so the flow email links back to exactly the routine the shopper saw.

Why This Way

Cloning pre-rendered Liquid templates is the decision the page rests on. Price, currency, sold-out state and quick-add behavior stay server-rendered and correct, while the branching stays instant and local — no Section Rendering API round trip per answer combination, and no second source of truth for product data in JavaScript.

Priority ordering rather than first-match-wins is what makes the rule set additive. Foundation rules sit at 10–13, challenge rules at 100–105, goal rules at 200–205, the porosity-and-strand moisture chain at 300–305, hold rules at 400–405, scalp and fragrance rules at 500–505. A new rule slots into a band without disturbing its neighbors.

Suppression is data. The fragrance-free exclusion is a blockedOptions list on an entry, not a condition in the section, so the brand team changed it without us.

Two things that buys are two somebody keeps holding. The priority bands are a convention, not a constraint — nothing stops a new rule landing in the wrong one, and whoever adds it has to know the scheme exists. And every recommendation the rules can reach must already exist as a rendered template, so adding a product to a routine means a new metaobject entry, not just a rule.

Why Not an App

This is the half of a guided-selling purchase that quiz platforms actually charge for, and it displaces a second category as well: the personalization and recommendation apps that promise to learn the same mapping from behavior.

Neither fits the shape of the problem. The mapping here is not statistical, it is editorial — a brand team decided that medium porosity with fine strands means Moisture Milk, and that decision needs to be written down, reviewed and changed on purpose. Expressed in Shopify metaobjects it is the client's data: authored, reviewed and changed in their own admin, and exportable with the rest of it.

The tiles matter too. Because the results clone the theme's own card component, they inherit every card feature the storefront already has — secondary image, hover video, quick add — rather than reproducing a vendor's card markup that has to be re-styled every time the brand changes.

Implementation Notes

  • The rule logic was verified rather than eyeballed. A validation script re-implements the same required/any/blocked matcher and the same key-then-product de-duplication against a snapshot of the live quiz data; its report records 26 of 26 checks passing.
  • Those checks split into 19 configuration-integrity assertions — question count, per-question option counts, a valid minimum and maximum on each — and seven behavioral cases, each recording which rules fired, at what priority, and what came back.
  • The behavioral cases pin the conditional logic that mattered most, including the two-sided scalp case: flaking plus fragrance-free must exclude the scalp scrub, while flaking plus normal fragrance must include it. goal_growth alone returns two products from one rule.
  • A generated expectation table lists, per answer option, every rule it can trigger with its priority and products — the artifact the brand team signs off against. It also records the options that deliberately trigger nothing alone.
  • An earlier configuration audit checked the live rule data against the Curl Quiz 3.0 specification: eleven checks passed, and three items went back to the brand team as content decisions rather than code changes.

Edge Cases

  • A recommendation whose product reference is empty renders a labelled placeholder card reading "Product reference not selected yet" instead of collapsing the grid — the state a half-configured metaobject entry produces.
  • Two rules resolving to the same product collapse by product ID, which stops Moisture Milk appearing three times for a low-porosity, frizz-prone shopper whose goal is frizz control.
  • On zero matches every group is hidden, the "No routine matched yet" empty state shows, and the Build Your Bundle call to action is suppressed, so the page never offers a bundle above nothing.
  • The email confirmation resolves from either the live payload or the value the quiz section wrote to sessionStorage, so refreshing a results page keeps the "sent to" confirmation. An invalid stored value is ignored.
  • The success message is merchandiser-authored with an [email] placeholder, substituted at runtime for both the [email] and {{ email }} spellings, case-insensitively.
  • Group DOM order is re-sorted at render time by re-appending group elements in data-sort-order order, so a merchandiser reordering the groups in a metaobject reorders the page with no theme change.
  • initCardHoverVideos is re-invoked against the cloned cards where the theme exposes it, because content cloned out of a <template> misses the page-load hook the originals got.

Platform Primitives Used

  • Metaobjects — the logic rules, product recommendations and result-group definitions, all owned and edited by the client.
  • Liquid section schema and settings — the section's own settings, which supply fallbacks for the group limits and the surrounding copy.
  • Cart AJAX API — reached through the quick-add control the cloned card-product tiles carry, so a routine can be added without leaving the results.

Integrations in Play

  • Klaviyo — the email platform this section identifies a shopper to on a valid address, and sends a "Quiz Completed" event to, carrying a link back to the exact routine.

Where It Runs

One template, page.quiz, mounted below the quiz section on the same page. The two are separate sections that communicate over a window-level event, so either can be moved, reordered or placed on its own page in the theme editor without a code change to the other.

What This Demonstrates

  • Guided selling: quizzes, finders and routine builders — the recommendation half, with the routing expressed as client-owned Shopify data.
  • Product recommendations and cross-sell — a routine assembled from an editorial rule set, not behavioral similarity.
  • Email and SMS capture — a completion event carrying a reconstructable results link into the email platform.

How We Know

Three theme files carry this build — the section and its two stylesheets — taken from the live theme alongside the quiz page template. Six client working-session records from May 2026 document the mapping being requested as a client artifact, and the decision that results should trigger an email.

The rule behavior is better evidenced than most of this corpus, because it was checked rather than described. A validation harness re-derives the matcher against a snapshot of the live data and records 26 of 26 checks passing, and a generated expectation table states the intended outcome for every answer option.

Related Builds

The Buy-vs-Build Question

Buying covers the questionnaire easily. It covers this half badly. A vendor's recommendation engine keeps the answer-to-product mapping in its own dashboard, which means the rules cannot be exported with the catalog, reviewed alongside it, or validated against a fixture the way this one was. Where that line falls is argued in the product quiz and finder decision.

Ready to Turn a Quiz Into a Routine?

We build recommendation rules your team owns, in your own Shopify data, with the matching verified against a fixture before it goes live. Contact us today to talk through the routine your catalog should be recommending.

More builds