Metafield-Driven Two-Level FAQ Accordion Grafted Into a Help-Center App's Shell
The help-center page kept the app's shell. The answers inside it moved into a Shopify page metafield, and a two-level accordion we wrote renders them — deep-linkable, keyboard-operable and reusable elsewhere in the theme.
LUS Brands' FAQ corpus lived in a third-party help-center app's dashboard, invisible to the rest of the storefront. We left the app's page shell in place and rendered the answers from a Shopify page metafield instead: a two-level accordion that groups questions under categories three different ways, gives every answer its own anchor, and leaves the corpus in data the rest of the theme can read.
Fact Strip
- Client: LUS Brands — haircare DTC, loveurcurls.com
- Surfaces: Landing page, plus the storefront-wide content field the corpus lives in
- Templates served: One —
page.help-center - Complexity: High — roughly a thousand lines across the section and its two snippets
- Attribution — Deploi-modified vendor: the storefront runs Dawn 15.4.1, and this page is a third-party help-center app's template. The shell, its CSS class family and its own scripts are the app's. The metafield reader, the accordion and its two snippets are our modification, sitting inside that shell. We didn't build a help center. We changed what the help center renders.
- Status: Live — verified 2026-09-06
- Evidence: Moderate confidence — read from the theme code, plus three records from the client's own documentation
- Platform primitives: 2
The Problem
Content that lives in a third-party app's dashboard is not Shopify data. Liquid can't read it, the theme can't reuse it, and every copy change is a trip through someone else's admin. That was the position the LUS Brands help center was in. The FAQ corpus — how a curl cream differs from a curl gel, which routine suits which hair type — sat in the app's own store, one route away from a storefront that couldn't quote a line of it. LUS wanted it inside Shopify and structured rather than flat: sections containing categories containing questions, authored in the admin, available to any template that asked.
The Constraint
The page was already deployed and already the app's. Its markup, its CSS class family and its own scripts were already in the document and referenced by the app's own JavaScript. Rebuilding the route wasn't on the table — the redesign plan lists this page twice, once as a support page and once as a quick link, and both times as a styling item. New content rendering had to slot into the vendor's shell without disturbing what surrounded it.
Liquid supplied the second constraint. There is no groupBy. Grouping questions under categories has to be assembled from map, compact and uniq, and the running index that keeps panel ids unique has to be threaded through by the author, because Liquid won't carry a counter across a snippet boundary.
What We Built
The section reads one field: page.metafields.custom.faq_sections. Each entry supplies a name, an items list, and optionally a nested categories list. Everything below that is rendering.
Three authoring shapes are handled, because we didn't want the metafield dictating one. Explicit nested categories are used as given. Where they're absent, categories are derived from the data — categories = faqs | map: 'category' | compact | uniq — and questions with no category are emitted first, in their own leading group. Where neither is present, the entry renders flat.
Two local snippets do the work. faq-category-group.liquid is the outer accordion, one panel per category; help-center-faq-item.liquid is the inner one, one panel per question. Splitting them means one question renderer serves both nesting levels, so a question is marked up the same way whether it sits inside a category group or directly under a section.
Ids took the most care. Panel ids are composed from section.id, the section's index within the metafield, and a running faq_item_counter, so nothing collides when one page renders several sections and several categories. The counter enters faq-category-group.liquid as faq_item_counter_start and comes back incremented by category_faqs.size, so each group hands the next one a clean starting number. Every question also gets a second id, built from faq.question | handleize — the one a human or a campaign links to.
Answers render through metafield_tag, so rich text authored in the admin keeps its markup instead of arriving escaped.
The controller is inline and small. It keeps aria-expanded and aria-hidden in sync at both levels and scopes its lookups — ':scope > [data-if-pit-faq-trigger]' — so an outer category trigger never matches an inner question trigger. Nested accordions sharing a data attribute capture each other's clicks otherwise.
Two section settings expose the contact banner at the foot of the page: text, button label, URL. That's deliberately all the schema this section has. Settings are per-placement; the corpus isn't.
Why This Way
Putting the corpus in a Shopify metafield does two things at once. It makes the content renderable anywhere in the theme — the storefront's kids FAQ page reads the same field for its own audience — and it takes the vendor out of the copy-editing loop, so a wording change is an admin edit rather than a support ticket. Deriving categories from the data instead of demanding them keeps the field tolerant of whichever way someone authors it. Leaving the app's shell in place was the other half of the decision, and it was a scope call rather than an architectural one.
The cost is that the page is now two authorships in one document. Anyone maintaining it has to know which half is ours, and the vendor's chrome is still a dependency we don't control. We also carry index bookkeeping in Liquid that a real groupBy would carry for us — the counter threading works perfectly and reads like an apology.
Why Not an App
This one runs the other way. The usual build-versus-buy argument is about not installing something; here the app was already installed and already rendering the page, and the work took the content back off it into native Shopify data while leaving the shell where it was.
The reason to bother is ownership of the corpus, not features. An app's FAQ list is a fine FAQ list right up until you want the same answers in a product tab, a campaign page and a machine-readable format — at which point the content is in the wrong database. Grouping, anchors and accordion behavior are theme code here.
Implementation Notes
- The content source is read as a
.value | default:pair, so the section works whether the metafield resolves to a list of metaobject entries or raw JSON. The code deliberately never names the definition behind it. - The app's shell and its own scripts stayed in place; we changed only what renders inside it. We didn't rename around the vendor's markers and we didn't take ownership of them.
- Three grouping paths live in one section — explicit
categories, derived from each item'scategoryfield viamap | compact | uniq, or flat. The section picks per entry, not per page. faq_item_counteris threaded throughfaq-category-group.liquidasfaq_item_counter_startand returned incremented bycategory_faqs.size, which is what keeps panel ids unique across category groups on a single page.- Question anchors use
faq.question | handleize, so one answer can be linked directly from a campaign, an email or an AI-search surface instead of linking to the top of the page. - Answers render through
metafield_tagrather than as plain output, so rich-text formatting authored in the admin survives to the page intact. - The controller scopes its trigger lookup with
':scope > [data-if-pit-faq-trigger]', so category triggers and question triggers can share one attribute without the outer level swallowing the inner one.
Edge Cases
- Uncategorized questions inside an otherwise categorized section get their own leading accordion at the top of the group, rather than being silently dropped from the page.
- A section with a blank category list and a blank items list renders its heading and stops there, instead of shipping an empty accordion shell to the browser.
prefers-reduced-motiondisables both the chevron rotation and the panel height transitions, at both levels of the accordion.aria-hiddenandaria-expandedare kept in sync on both levels, and every panel isrole="region"with anaria-labeledbypointing at its own trigger.
Platform Primitives Used
- Metafields — one page-level field,
custom.faq_sections, carries the whole corpus as sections, categories and questions. An earlier product-level FAQ field is marked superseded in the client's own metafield records, which is part of why this one is page-scoped. - Liquid section schema and settings — kept deliberately thin: contact-banner text, button label and URL. Content is not a setting.
Where It Runs
One template mounts the section: page.help-center. What it renders is a page metafield rather than a set of section settings, which is why this build carries a global surface tag as well as a landing-page one — the accordion is a page, and the corpus behind it is storefront-wide data any template can read.
What This Demonstrates
Getting content into a structure machines can read starts with getting it into a structure at all, which is why this is filed under structured data for AI answers. Worth saying plainly: this section doesn't emit FAQPage markup. It builds the thing that would make emitting it honest.
How We Know
One section file and two snippets, read directly from the live theme, plus three records from the client's own documentation: two entries in the redesign page-scope sheet listing the help center as a styling item, and a metafield export showing an earlier FAQ field marked superseded. Documented from the code; no client-side record of the decision survives. The scope records tell us the page was in the redesign as styling only — which is why the accordion was grafted into the app's shell rather than taking over the route — but nobody wrote the reasoning down, so we're reading it off the implementation.
Related Builds
- Product card with award badges, per-variant content payload and per-card structured data — Nudestix. The same capability, opposite direction: markup emitted per card rather than content restructured per page.
- WebMCP agent commerce tools — Three Ships. Machine-addressable storefront data, a step further on.
- Product information tabs — LUS Brands. The same metafield-to-accordion pattern on the product page.
The Buy-vs-Build Question
An FAQ app is a reasonable purchase and a poor home. The question was never whether the app renders a decent accordion — it's where the answers live, who can reuse them, and what happens when you need them in a format you don't control. We've written up that trade in Structured data and schema markup: buy or build?
Ready to Get Your Content Back?
If your best answers are locked in someone else's dashboard, moving them is usually a smaller job than it looks — and it's the job that makes every surface after it easier. Contact us today to talk through what your content would look like as Shopify data.