Last updated

Affiliate Hydration

Affiliate Hydration lets a single cached storefront page render for every visitor — attributed or anonymous — with affiliate-specific values (name, avatar, link URLs) filled in on the client by the FairShare SDK.

The SDK ships custom elements, data attributes, and inline sentinels that the theme author uses in place of the server-rendered {{ affiliate.name }} / {{ affiliate.avatar }} variables. The SDK swaps them at runtime with the resolved affiliate's values (or a fallback for anonymous visitors), and re-swaps them if the affiliate changes mid-session.

For the storefront-theme migration guide, see Affiliate Hydration Cheatsheet.


What's Included

ItemTypeDescription
<fluid-affiliate-name>ElementRenders the affiliate's full name
<fluid-affiliate-email>ElementRenders the affiliate's email address
<fluid-affiliate-initials>ElementRenders the affiliate's initials
<fluid-affiliate-avatar>ElementRenders the affiliate's avatar URL as text
<fluid-affiliate-my-site-url>ElementRenders the affiliate's MyHub URL as text
data-fluid-affiliate-srcAttributeFills the host element's src with an affiliate field
data-fluid-affiliate-hrefAttributeFills the host element's href with an affiliate field
data-fluid-affiliate-altAttributeFills the host element's alt with an affiliate field
data-fluid-affiliateAttributeFills the host element's textContent with an affiliate field
data-fluid-affiliate-base-urlAttributeOverrides the origin used to resolve my_site_url
data-fluid-affiliate-show-ifAttributeReveals the element when the named field or affiliate is present
data-fluid-affiliate-hide-ifAttributeHides the element when the named field or affiliate is present
__fluid_affiliate_username__SentinelRep-username placeholder swapped in URLs and JSON literals

Benefits

  • One cached page per storefront — The rendered HTML is affiliate-independent, so a single CDN artifact serves every rep and every anonymous visitor
  • Automatic swap — The SDK detects the elements and attributes on load and fills them with the resolved affiliate; no explicit initialize call
  • Reactive to rep switches — When the visitor navigates from /rep-a/ to /rep-b/, the SDK re-swaps every managed value
  • Anonymous fallback — Visitors without a rep prefix see fallback content (default text inside the element, "home" for URL segments)
  • Framework-agnostic — Works with plain Liquid themes, Alpine.js components, Turbo/Barba SPAs, and any other client-side layer

Quick Example

<!-- Text field: renders the affiliate's name, or "our team" for anonymous visitors -->
<p>Shop with <fluid-affiliate-name>our team</fluid-affiliate-name></p>

<!-- Attribute field: fills src + alt with affiliate values; hidden when there's no avatar -->
<img
  data-fluid-affiliate-src="avatar"
  data-fluid-affiliate-alt="name"
  data-fluid-affiliate-show-if="avatar"
  style="display:none"
/>

<!-- URL sentinel: browser gets a real /rep-a/shop link after the SDK swaps -->
<a href="/{{ username }}/shop">Shop this rep</a>

<fluid-affiliate-*> fields

Custom elements that render an affiliate field's value as text content. The SDK creates them in the light DOM (no Shadow DOM), so surrounding theme styles apply normally.

Usage

<p>Hi, <fluid-affiliate-name>friend</fluid-affiliate-name></p>
<span class="badge"><fluid-affiliate-initials>FA</fluid-affiliate-initials></span>
<a><fluid-affiliate-my-site-url>#</fluid-affiliate-my-site-url></a>

Elements

ElementFills withFallback (anonymous)
<fluid-affiliate-name>The affiliate's full nameThe content inside the tag
<fluid-affiliate-email>The affiliate's email addressThe content inside the tag
<fluid-affiliate-initials>The affiliate's initialsThe content inside the tag
<fluid-affiliate-avatar>The avatar URL as textThe content inside the tag
<fluid-affiliate-my-site-url>The affiliate's MyHub URL as textThe content inside the tag

Put a sensible default inside the tag — that's what anonymous visitors see. For attributed visitors, the SDK replaces the tag's textContent with the resolved value.

When NOT to use

For an attribute value (src, href, alt), a custom element can't help — HTML doesn't allow tags inside attribute values. Use a data attribute instead.


Data attributes

Markers on any element that tell the SDK to fill one of its HTML attributes (or its textContent) with an affiliate field. Use these for eagerly-fetched attributes (src, href, alt) where a custom element can't sit inside the attribute value.

Usage

<img data-fluid-affiliate-src="avatar" data-fluid-affiliate-alt="name" />
<a data-fluid-affiliate-href="my_site_url">View store</a>
<span data-fluid-affiliate="name">friend</span>

Attributes

AttributeFillsValid <field> values
data-fluid-affiliate-src="<field>"srcavatar (any URL field)
data-fluid-affiliate-href="<field>"hrefmy_site_url, avatar (any URL field)
data-fluid-affiliate-alt="<field>"altname (any text field)
data-fluid-affiliate="<field>"textContentAny field — same as using a custom element
data-fluid-affiliate-base-url="<url>"Override for my_site_url originAn absolute URL (e.g. https://staging.example.com)

<field> is one of name, email, avatar, initials, my_site_url.

data-fluid-affiliate-base-url

By default, the SDK resolves my_site_url against window.location.origin. Add data-fluid-affiliate-base-url on the same element to override:

<!-- Default: uses the current page's origin -->
<a data-fluid-affiliate-href="my_site_url">View store</a>

<!-- Explicit origin override -->
<a
  data-fluid-affiliate-href="my_site_url"
  data-fluid-affiliate-base-url="https://staging.example.com"
>View store</a>

Only affects my_site_url; other fields ignore this attribute.

Why not <img src="{{ affiliate.avatar }}">?

The browser fetches src before the SDK runs. If you put a raw affiliate value or sentinel in src, the browser makes a broken request to the placeholder URL before the SDK can swap. Always use data-fluid-affiliate-src for eagerly-fetched attributes.

Lazy attributes (<a href>, <form action>, text nodes) are safe to hold sentinels directly.


Visibility toggles

Reveal or hide an element based on whether the SDK has resolved an affiliate — or a specific affiliate field. Handles anonymous vs. attributed rendering from one HTML source.

Usage

<!-- Show only when a rep is attributed -->
<div data-fluid-affiliate-show-if="affiliate" style="display:none">
  Hi, <fluid-affiliate-name></fluid-affiliate-name>
</div>

<!-- Show only when the rep has an avatar -->
<img
  data-fluid-affiliate-src="avatar"
  data-fluid-affiliate-show-if="avatar"
  style="display:none"
/>

<!-- Show only for anonymous visitors -->
<div data-fluid-affiliate-hide-if="affiliate">
  <a href="/join">Become a rep</a>
</div>

Tokens

TokenElement visible when
affiliateAny affiliate resolved (not the anonymous / "home" fallback)
name, email, avatar, initials, my_site_urlThat specific field has a value

hide-if wins over show-if when both target the same element.

Why the inline style="display:none"

The SDK toggles the element after the page loads. Without pre-hiding, an anonymous visitor sees affiliate content flash before it disappears. The SDK removes display:none when the condition holds; leaves it in place when it doesn't. No flash either way.

For accessibility notes on aria-live and screen reader behaviour, see the themes cheatsheet — Accessibility section.


Sentinels

Inline placeholder strings that the SDK swaps document-wide with the resolved rep username. Use these for URL path segments (href, form action) or JSON literals embedded inside <script> tags where a custom element or data attribute can't fit.

Usage

<!-- Liquid source — the raw {{ username }} compiles to a sentinel under hydration -->
<a href="/{{ username }}/shop">Shop</a>
<form action="/{{ username }}/join">...</form>

Server-rendered HTML:

<a href="/__fluid_affiliate_username__/shop">Shop</a>
<form action="/__fluid_affiliate_username__/join">...</form>

At runtime, the SDK replaces every occurrence of __fluid_affiliate_username__ in href, action, text nodes, and JSON literals with the resolved rep username, falling back to "home" for anonymous visitors.

Liquid identifiers that emit sentinels

Liquid variableSentinel emittedUse for
{{ username }}__fluid_affiliate_username__Path segments in link/form URLs
{{ share_guid }}__fluid_affiliate_username__Sharing-link surfaces
{{ affiliate_guid }}__fluid_affiliate_username__Legacy alias for share_guid
{{ sharing_id }}__fluid_affiliate_username__Sharing link variants
{{ external_id }}Real value (not a sentinel)Backend-only surfaces (webhooks, non-cached)
{{ company.shop_page_url }}URL with sentinel baked inShop link (no theme edit needed)
{{ company.join_page_url }}URL with sentinel baked inJoin link (no theme edit needed)

Company URL helpers

{{ company.shop_page_url }} and {{ company.join_page_url }} already emit sentinel-bearing URLs under hydration:

<a href="{{ company.shop_page_url }}">Shop</a>
<!-- Renders to: <a href="/__fluid_affiliate_username__/shop">Shop</a> -->

The SDK swaps the sentinel client-side. No theme edit needed.

One case that requires care

Raw __fluid_affiliate_username__ sentinels in text nodes can only be swapped once — after the first substitution the original string is gone. For text nodes that need to survive a rep switch mid-session, prefer the <fluid-affiliate-name> custom element over the raw {{ username }} sentinel. The element re-renders on affiliate change; the sentinel doesn't.

For URL paths (/{{ username }}/shop), rep switching triggers a full page navigation anyway, so this rarely comes up in practice.


How the SDK finds them

On page load and on subsequent DOM mutations, the SDK scans the document for the custom elements, data attributes, and sentinels above. Two scan paths:

  1. Initial scan — Runs once after the SDK boots. Finds every marker in the initial HTML.
  2. MutationObserver — Watches for childList changes on document.body and re-scans newly-added subtrees. This is what makes SPA navigation (Turbo, Barba) work without explicit hooks — as long as the swap dispatches standard DOM mutations, the SDK picks up the new markers.

If your SPA library performs DOM swaps in a way that skips mutation events, call the SDK's rescan API after each swap:

document.addEventListener('turbo:load', function () {
  window.FairShareSDK?.rescanForSentinels?.();
});

The optional-chaining call is deliberate — if the SDK isn't loaded yet, the call is a no-op instead of a TypeError.


Fields that stay real (no hydration)

Not every affiliate field goes through hydration. These render with real, per-request values even under hydration:

VariableUnder hydration
{{ affiliate.web_rep_store_enabled }}Real (company-level setting, not per-rep)
{{ affiliate.logged_in_rep_for_store }}Real
{{ affiliate.sign_in_url }} / sign_out_urlReal placeholders ("#") — wire up via SDK auth API, not the Liquid value
{{ company.* }} (except URL helpers above)Real (not affiliate-dependent)

For sign_in_url / sign_out_url, the Liquid renders "#". Wire the click through a JS handler that calls the SDK's auth API — the SDK holds the real URLs even when the Liquid variable doesn't.


Mysite pages don't hydrate

Any template under /my/* (bio pages, favorites, personal links) renders with the real affiliate's values. Hydration is bypassed because mysite content is fundamentally per-rep. In a mysite template you can use {{ affiliate.name }} directly — the SDK isn't relied on for these pages.


Verifying hydration is on

Every response includes diagnostic headers you can use to confirm hydration is running:

curl -I -A "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36" \
     "https://<host>/<rep>/products/<slug>" \
  | grep -iE "x-hydration|x-credit|x-original-credit|x-cdn-cache|x-gcs-path"
HeaderMeaning
x-hydration: onHydration is active for this request
x-credit: _defaultCache key is collapsed — every rep for this page shares one artifact
x-original-credit: <rep>The rep from the request URL (preserved for analytics)
x-cdn-cache: HIT / MISSCache state at request time
x-gcs-path: <host>/latest/pages/_default/<page>/...Path to the cached artifact served for this request

Use a browser-shaped -A UA — many storefronts return 500 for bare-curl User-Agents.