B2B Content and Design in a Liquid Store

Shareable with BLACKROLL and project partners. Maintained by 22Bits. Written 2026-10-16, updated after the kickoff 2026-10-21. Part of Project: B2B Migration.

Can the B2B store use Craft CMS content, and what does it take to bring the D2C design into it? This page sets out what decides it, the options, and what a short spike has to prove. The decisions are T1 (content system) and T2 (design scope) in B2B Decisions and Workshop Plan.

Working assumption, needs clearance with Kristy and Sen. Content is managed with native Shopify content editing. Content from Craft CMS is only possible with a headless shop, and a headless shop was ruled out. The same goes for the design system: porting the D2C design into the B2B store as a Liquid theme means rebuilding the components, which is a lot of work. The kickoff deck still lists Craft CMS as a B2B integration, and a deep dive was agreed at the kickoff.

What decides this

  • The storefront is a Liquid theme. BLAC-6171: "Decision to avoid the complexity of a headless shop has been made"; BLAC-6179: "Theme, not headless"; ADR-035 driver "likely as a dedicated Shopify Liquid setup rather than fully integrating it into the main BLACKROLL® headless experience"; the DEPT spike: "stay Liquid ... Build the theme on Horizon". Confirmed again at the kickoff (PL1).

  • Content is decided too, on paper. BLAC-6181: "Decided: native Shopify content features and content management in Shopify", delivery BLACKROLL (Sen). The deck disagrees (Craft CMS as shared service on slide 6, under Integrations for 22Bits on slide 7). ADR-006 already noted "B2B has limited content". At the kickoff, the discovery's reason for a Liquid store was given as cost and low maintenance: B2B editors work in the Shopify store, not in the CMS.

  • The store is login-gated for dealers. BLAC-6173 "Login protection for the whole B2B shop"; BLAC-6192 "Only logged-in contacts of approved companies see products, prices and the cart". Shopify: "If you operate a dedicated B2B store, then you can restrict access so that only logged-in B2B customers can view your store" (B2B login). Two caveats on the same page: restricting access "doesn't redirect all visitors to a sign-in page", so a full login wall may need custom Liquid or an app, and "Prospective customers can't request access to your store through company account requests when store access is restricted", which affects the dealer application page. A gated store has no SEO, so hreflang and sitemaps matter only for the public pages (login, dealer application), and today's B2B host is deliberately noindex (BLAC-5819).

  • What Craft does today is bound to Nuxt. Preview targets render through the headless URL (content.blackroll.com/config/project/sections/*.yaml, previewTargets urlFormat '{url}', @headless alias in config/general.php:18); market scoping is a GraphQL directive the Nuxt client injects (@contentMarket, blackroll.com/.github/instructions/craft-cms.instructions.md); SEOmatic sitemaps and hreflang are rendered in Craft and passed through by Nuxt (blackroll.com/server/utils/seomaticSitemap.js). None of that exists for a Liquid store.

  • Liquid can't fetch at render time. The Shopify docs contain no statement that Liquid can or cannot call external HTTP at render time; the only documented way to "fetch and display data on a storefront from an external source" is an app proxy (app proxies). Treat server-side fetching from Liquid as not available.

The content options

Option

How content gets in

Editor preview

Market components and Shopify Markets

hreflang and sitemaps across two systems

Performance

Rough effort

What happens to Nuxt

0. Native Shopify content, no Craft (Jira's decision)

B2B editors use pages, blog, theme sections and metaobjects in the Shopify admin ("Metaobjects are created in the Content page of the Shopify admin", Liquid metaobjects); product content comes with the products via PIM and the gateway

Theme editor and theme preview links ("Visitor previews ... expire 2 days", "Merchant previews ... 30 days", adding themes); metaobject entries have a template preview ("click the eye icon to preview the template with your metaobject entry") and a DRAFT/ACTIVE status

Market and language localisation is native: "A merchant can also localize their store's content for a specific combination of market and language" (Markets); per-market theme content needs Advanced or Plus ("Per-market theme customization ... requires the Advanced plan or higher", requirements); "Liquid templates" and global theme settings cannot vary per market, section content and block visibility can (online store customizations). Our marketScope block has a direct equivalent: a section shown per market in the theme editor

One system. "Hreflang tags are generated automatically based on your market and language configuration" and "sitemap files are generated for all of your domains" (Markets SEO, sitemap). The B2B domain is its own store, so no cross-domain hreflang to blackroll.com, which fixes the BLAC-5819 class of bug by construction

Shopify's own rendering, nothing added

Low for engineering (theme sections and a few metaobject definitions); the cost moves to content editing, which Sen's team does in a second admin

Untouched. The legacy b2b.blackroll.com app on Vercel is replaced at the CH cutover; blackroll.com keeps a "Business customers" entry point linking to the B2B store

A. Metaobject sync from Craft

A small app or Craft webhook consumer (craftcms/webhooks is installed, config/webhooks.php) upserts entries with metaobjectUpsert (metaobjectUpsert); translations registered through the Translations API (translated content); theme sections loop over shop.metaobjects

Weak. The sync publishes what Craft publishes; Craft drafts cannot be previewed in the theme. The publishable capability gives DRAFT/ACTIVE on the Shopify side, but "If an entry is set to Draft, then you can't publish it to your store" and the docs name no way to preview API-set draft content beyond the eye icon (capabilities, creating entries). Editors would preview in Nuxt and trust the mapping

Craft sites (de, en, fr, nl, ch) map to Shopify locales; contentMarket and marketScope have no native counterpart, so each market-scoped block becomes either a separate entry with a "markets" list field read by the theme, or a per-market theme customization done by hand. Rich text must be converted from CKEditor HTML to Shopify's rich text JSON (data types); images are Cloudinary URLs today, Shopify wants file_reference or URL fields

Two systems: Craft pages would be in SEOmatic's sitemap for blackroll.com, their copies in the B2B store only if the definition has renderable, and onlineStore adds the template and URL ("Metaobjects with this capability are also included in the store's sitemap")

Theme-side limits: loops return 50 entries without paginate, 20 handle lookups and 50 definitions per render, "Exceeding the per-type limits renders a Liquid error" (metaobject limits); 40 fields per definition, 256 definitions on Plus; Admin API 1,000 points per second on Plus (rate limits). Fine for dozens of pages; the 50-definitions-per-render limit only bites if one page mixes more than 50 of the 85 contentBlocks types

High for what it buys: definitions, mapping, rich text and asset conversion, translation registration, a queue, monitoring, and a DEPT owner afterwards. Weeks, not days

Nuxt keeps rendering blackroll.com; it gains nothing and loses nothing

B. App proxy to a Craft-backed renderer

Shopify proxies /apps/<subpath>/* to our service; if it answers with Content-Type: application/liquid, "Shopify renders any Liquid code in the request body in the context of the shop using the shop's theme" (app proxies). The service could be a Nuxt server route that queries Craft and returns Liquid

Good: the proxy can pass Craft's preview token through, so an editor sees a draft inside the live theme

The service receives shop, logged_in_customer_id and the path; market and language must be derived from the request (no documented market parameter). The @contentMarket directive can be reused

Proxied pages are not Shopify resources; whether an application/liquid response gets hreflang or appears in sitemap.xml is not documented and must be tested; irrelevant behind the login gate, a problem for public pages

One extra hop per page view to Cloud Run; "App proxies don't support cookies" and the docs give no caching or timeout statement; one proxy route per app, prefix limited to /apps, /a, /community, /tools

Medium: a renderer service, signature check, Liquid templates for our block types. Days for a proof, weeks for all blocks. Creates a service DEPT must run

Nuxt (or a slice of it) becomes a content renderer for a second storefront; the handover then includes it

C. Client-side fetch in the theme

Theme JavaScript calls Craft's public GraphQL schema or a Nuxt API and renders blocks in the browser

Easy with a preview token in the URL

Same mapping problem as A for market scoping, done in JavaScript

None: content is invisible to crawlers; acceptable only because the store is gated

Slower first paint, layout shift, CORS and a public endpoint on Craft; no caching through Shopify

Low to medium for a few blocks, but every block is rebuilt in a second UI stack

Unchanged, plus one more consumer of the CMS API

D. Split by domain: shop on Shopify, content on Nuxt

The B2B store is ordering only; brand, knowledge and training content stay on blackroll.com (Craft via Nuxt), with links both ways; dealer-only content (price lists, assets) lives in Shopify metaobjects or pages

Unchanged for Craft content (Nuxt preview); Shopify preview for the rest

Markets stay where they are: Shopify Markets on both stores, marketScope on the content side

Each domain has its own sitemap and hreflang; nothing crosses; the B2B domain stays noindex except the login and application pages

No extra hop

Lowest. This is what option 0 implies. ADR-038's path-based proxy pattern does not apply: Shopify targets a domain as a whole, a path split of one domain between Shopify and Nuxt is not documented, and Shopify advises against a proxy in front of the storefront (avoid request proxies)

Untouched

E. Headless B2B on Nuxt

Storefront API with buyer identity ("Identifies a B2B buyer for the @inContext directive", BuyerInput)

As today

As today

As today

Contextualised responses cannot be cached ("If you cache these responses, other users could see another customer's B2B pricing", headless B2B)

Rejected by ADR-035, BLAC-6171 and BLAC-6179; listed to close it. This is the only option that would carry Craft content and the D2C components over directly

Would make the B2B store a second Nuxt app that DEPT also has to run

What we propose

0 plus D: the B2B store manages its content natively in Shopify, blackroll.com keeps everything that is brand, knowledge and training content, the two are linked by domain, and no Craft sync is built in this phase. Reasons:

  • it is what the Jira epic already says, and what fits a Liquid store;

  • the store is login-gated, so the SEO argument for a shared content model does not apply;

  • ADR-006 expected "limited content";

  • every sync option creates a service that DEPT must own after January.

If a concrete shared content type turns up (for example training content for dealers), option A for that one definition is the fallback, and only after the spike below proves preview and translation.

The design system

Kristy was open at the kickoff to reusing the D2C design system in the B2B store. What that means in a Liquid theme:

  • The D2C components are Vue components with Tailwind tokens and Storybook (blackroll.com/components/redesign/**, Figma). Nothing of it is Liquid, so nothing carries over: each component is rebuilt as a Shopify section, plus the source of its data.

  • A rebuilt copy is a second implementation of the same design. Every later change to Brand 2.0 has to be made in two places.

  • Each rebuilt component adds scope, so a reuse decision needs the exact list of components and a designer who owns the B2B look and feel.

Working assumption (T2): Brand 2.0 tokens, logo and typography on Horizon's B2B blocks, and a short list of components rebuilt only where a B2B page needs them. This keeps the store close to standard Shopify, which also fits the low-maintenance reason for choosing a Liquid store. Needs clearance with Kristy and Sen.

What a one-day spike in a Plus dev store needs to prove

  1. Gating and public pages: restrict the dev store to logged-in B2B customers and confirm which pages stay public (login, dealer application form), because those are the only pages where SEO and hreflang exist; check that the redirect to sign-in and the company account request still work with access restricted, since the Shopify help says neither is automatic.

  2. Metaobject round trip (half a day, only if option A is kept alive): create one definition by Admin API with translatable, publishable and storefront access; upsert one Craft entry with DE and FR translations through translationsRegister; render it in a Horizon section with the market and language context; then set it to DRAFT and record what an editor can and cannot see in a theme preview link. The output is a yes or no on editor preview.

  3. Market variation: make one section show different content for the CH and DE markets with the theme editor's market customisation and note the plan requirement.

  4. Sitemap and hreflang output of the dev store with a B2B domain, two markets and two languages: confirm no reference to blackroll.com and that noindex can be set for the gated pages.

  5. Rich text and images: convert one CKEditor block with an image to Shopify rich text plus a file reference and look at the loss.

  6. Numbers: Admin API cost of upserting 50 entries, and theme render with 50 entries in one loop.

If item 2 fails on preview, option A is dead and the decision is 0 plus D without a fallback. If the whole day goes into item 2 alone, that shows how much effort option A would take.