Hybrid PLP Architecture
This application supports a hybrid PLP that is a combination of the Hydrogen application, the liquid storefront theme, and a static Preact app originally built by Search Spring and maintained by Birdy Grey. The Search Spring app controls the product grid rendering, while Hydrogen provides the page shell, cart functionality, and data orchestration.
Backgroundโ
In order to migrate the Core site over to Hydrogen more quickly, and because we knew were were planning on redesigning our PLP, we opted to proxy in the PLP pages that existed on the Core theme. These pages are a mix of Liquid templates and a script that inserts a Preact app that was initially built by Search Spring and currently maintained by the Site team. This application is hosted on Firebase and versioned here: Static Assets.
Implementationโ
We chose a partial proxy for the PLPs as we still wanted to maintain the Hydrogen header and footer and ensure a smooth UX from discovery to purchase. This presented a challenge, though. The Liquid PLPs relied heavily on javascript to run, but our partial proxy architecture strips out all of the javascript. To get around this we pull in the scoped CSS + HTML necessary for the Search Spring app to target and render and load the Search Spring script in Hydrogen. The Search Spring script used to execute a function to set context for the app, but because of how our CSP and how the script component works, we couldn't follow this same operation within Hydrogen. Because of this we set the context on a hidden element (<div>), and then within the Search Spring application we look for the element and manually set the context. The overall process at a high level is as follows:
- Server fetches Liquid theme HTML, processes it, and sends scoped CSS + HTML blocks
- Hydrogen component injects the HTML and loads Search Spring's client-side script
- Search Spring app targets Liquid theme HTML elements and renders product grid, filters, pagination in the injected container
- Hydrogen intercepts cart interactions (ATC buttons) to integrate with Hydrogen cart
- MutationObserver bridges the gap between Search Spring's DOM updates and Hydrogen's event handlers
Page Routeโ
The loader in the page route (app/routes/collections.$supercollection.tsx) decides which PLP type to render:
Data Loading Flow:
- Shopify Collection Check: Attempts to fetch Shopify collection data and proxied content in parallel
- Fallback to Builder: If no Shopify collection exists, loads Builder.io content
- Hybrid Content: Calls
hybridLoader()(app/lib/proxy/loaders.server.ts) which fetches the Liquid theme's HTML
Component Selection:โ
- If Shopify collection exists โ renders
<SearchSpringPlp /> - Otherwise โ renders
<BuilderPlp />(pure Builder.io content)
Hybrid Loader:โ
This server-side function fetches and processes Liquid theme content:
- Fetch HTML: Proxies the request to the Liquid theme store
- Extract Assets: Removes scripts, links, and styles from HTML
- Fetch Stylesheets: Downloads linked CSS files
- Scope Styles: Wraps CSS in
.hybridContainerto prevent style conflicts - Extract Content Block: Captures the
mainContentblock from Liquid comments - Returns scoped styles and HTML content
SearchSpringPLP Componentโ
The key responsibilities for this component (app/components/SearchSpringPlp/searchSpringPlp.component.tsx) are to set up the initial render and Search Spring script data. We try to minimize what we need to use from the Search Spring app by rendering the header and any data below the products directly from the Shopify collection data. This means that only the product grid, filters, and color bar are being rendered by the Search Spring script.
Initial Render:โ
- Renders
<CollectionHeader />with Shopify data - Injects proxied HTML via dangerouslySetInnerHTML
- Creates hidden context
<div>with SearchSpring config data - Loads SearchSpring script bundle
- Renders
<AccordionGroup />with Shopify data if present
SearchSpring Script Data:โ
- Collection metadata (name, handle, ID)
- Customer tags for swatch discount
- Template and asset paths
Effects:โ
- Loading Setup: Initializes SearchSpring when HTML is ready
- Cleanup: Removes observers/listeners on unmount
- Analytics: Collects product data from DOM after loading
- Swatch Pricing: Updates context data when cart changes
Diagram:โ
SearchSpring Helpers (helper.tsx -> handleLoadingSetup):โ
Script Injection:โ
- Manually creates SearchSpring script tag
- Appends to
<body>(bypasses React's script handling)
Add to Cart Listeners:โ
- Finds
.quick-atcbuttons in SearchSpring's rendered HTML - Attaches click handlers that use Hydrogen's cart actions
- Submits optimistic cart updates via fetcher
- Updates button text ("Adding...")
Pagination Scroll:โ
- Detects pagination clicks
- Scrolls to top on desktop
MutationObserver:โ
- Watches
#searchspring-contentfor DOM changes - Re-attaches ATC listeners when products are filtered/paginated
- Ensures cart integration works after SearchSpring re-renders
Script Load Handler:โ
- Waits for SearchSpring to load
- Removes loading spinner
- Initializes all listeners/observers
- Sets loading state to false
Testingโ
Testing changes for these pages can be cumbersome. You will need to run the Hydrogen codebase locally, and within searchSpringPlp.component.tsx and helper.tsx update the script url to point to either to a deployed preview (Firebase hosted preview link created with PR) or your local version of the Search Spring app (typically http://localhost:3333).
// Example searchSpringPlp.component.tsx update:
<Script
waitForHydration
src={`http://localhost:3333/searchspring/bundle.js`}
data-environment={isProduction() ? 'production' : 'staging'}
id="searchspring-context"
type="text/javascript"
/>
Known Issuesโ
- Pagespeed/Performance: Because we have to first inject the Liquid HTML, then run the script that injects the Search Spring app, then attach all of the event listeners, these pages are slow to load and not very performant. They are, however, highly visited and important in the customer journey.
- Reliance three codebases for UI: There is styling applied from all three aspects of this page - the Liquid theme, the Search Spring app, and the Hydrogen app. While it's fairly well scoped now, it means multiple places to update and more chances for incorrect styles to be applied/rendered.
- Reliance on hard-coded values for swatch ATB:
helper.tsxrelies an pulling data attributes on the ATB buttons from Search Spring to add swatches to bag. Some of these attributes, especially around production timelines, are hard-coded and require manual updating outside of our Builder Site Settings pattern. - Brittleness/Multiple points of failure: There is an overall brittleness to an architecture that has this many connection points. We've seen PLPs hang on the loading spinner, show incorrect data/badges, or completely fail due to vendor outages. Moving to the new API-driven architecture will give us better control over error handling and data fallbacks.