Rendering Weaverse Pages
This guide covers everything you need to know about rendering different page types in Weaverse, from basic concepts to advanced implementation patterns.
Core Concepts
Weaverse page rendering revolves around two key elements:
loadPage() function : Loads page data based on type and handle
WeaverseContent component : Renders the loaded page content
The loadPage() Function
The loadPage() function is called in your route loaders to fetch page data:
// In your route loader
export async function loader ({ params , context } : LoaderFunctionArgs ) {
const { handle } = params ;
const { weaverse } = context ;
const weaverseData = await weaverse . loadPage ({
type: "PRODUCT" , // Page type - determines content structure
handle: handle , // Page handle - identifies specific page
});
return { weaverseData };
}
The WeaverseContent Component
The WeaverseContent component renders the loaded page data. It automatically gets the weaverseData from your loader through the <WeaverseHydrogenRoot> wrapper:
import { WeaverseContent } from "~/weaverse" ;
export default function ProductPage () {
// No need to pass weaverseData explicitly - WeaverseHydrogenRoot handles it
return < WeaverseContent /> ;
}
How it works : The <WeaverseHydrogenRoot> component (typically in your root layout) automatically extracts weaverseData from the current route’s loader data and provides it to all child <WeaverseContent> components through React context. This eliminates the need to manually pass the data prop.
Supported Page Types
Weaverse supports the following page types:
E-commerce Pages
PRODUCT - Individual product pages
COLLECTION - Collection/category listing pages
Content Pages
PAGE - Custom pages created in Weaverse Studio
BLOG - Blog listing pages
ARTICLE - Individual blog post pages
Special Pages
INDEX - Homepage
CUSTOM - Custom pages created in Weaverse Studio (no specific handle needed)
Working with Locales
Weaverse provides flexible localization support, allowing you to serve content in multiple languages and markets. Understanding how locale detection works is key to implementing internationalization correctly.
Weaverse uses the standard language-country format for locales:
Format: language-country (lowercase, hyphen-separated)
Language: ISO 639-1 code (2 letters, e.g., sv, en, fr)
Country: ISO 3166-1 alpha-2 code (2 letters, e.g., se, us, ca)
Examples:
en-us - English in United States
sv-se - Swedish (sv) in Sweden (se)
fr-ca - French (fr) in Canada (ca)
de-de - German (de) in Germany (de)
ja-jp - Japanese (ja) in Japan (jp)
Important: Always use lowercase with hyphen separator. Formats like en_US, en-US, or EN-us are not supported.
Locale Detection Methods
Weaverse provides three methods for detecting the current locale, with a clear priority order:
1. Explicit locale Parameter (Recommended)
The most reliable method is to explicitly pass the locale parameter to loadPage():
// app/routes/products.$productHandle.tsx
export async function loader ({ params , context } : LoaderFunctionArgs ) {
const { productHandle } = params ;
const { weaverse } = context ;
// Explicitly specify the locale
const weaverseData = await weaverse . loadPage ({
type: "PRODUCT" ,
handle: productHandle ,
locale: "sv-se" , // Load Swedish content
});
return { weaverseData };
}
When to use explicit locale:
Custom routing logic determines the locale
Loading content for a different locale than the current page
Background jobs or server-side processes without request context
Cross-locale content comparison or translation workflows
2. Path Prefix Detection (Deprecated)
This method is deprecated and may be removed in a future version. Use explicit locale parameter for new implementations.
Weaverse can extract locale from the URL path prefix:
// URL: /sv-se/products/jacket
// Automatically detects locale: "sv-se"
const weaverseData = await weaverse . loadPage ({
type: "PRODUCT" ,
handle: productHandle ,
// locale auto-detected from path: /sv-se/...
});
3. Auto-detection from context.storefront.i18n (Default)
When no explicit locale parameter is provided, Weaverse reads from Hydrogen’s storefront context:
// Relies on Hydrogen's i18n configuration
const weaverseData = await weaverse . loadPage ({
type: "PRODUCT" ,
handle: productHandle ,
// Automatically uses context.storefront.i18n.pathPrefix
});
// Behind the scenes:
// const pathPrefix = context.storefront.i18n.pathPrefix; // e.g., "/sv-se"
// const locale = pathPrefix.slice(1); // "sv-se"
Basic Locale Usage
Auto-Detection Example
// app/routes/($locale).products.$productHandle.tsx
export async function loader ({ params , context } : LoaderFunctionArgs ) {
const { productHandle } = params ;
const { weaverse , storefront } = context ;
// Weaverse automatically detects locale from context.storefront.i18n
const [{ product }, weaverseData ] = await Promise . all ([
storefront . query ( PRODUCT_QUERY , {
variables: { handle: productHandle }
}),
weaverse . loadPage ({
type: "PRODUCT" ,
handle: productHandle ,
// locale auto-detected
}),
]);
return { product , weaverseData };
}
Explicit Locale Example
// app/routes/products.$productHandle.tsx
export async function loader ({ params , context } : LoaderFunctionArgs ) {
const { productHandle } = params ;
const { weaverse } = context ;
// Determine locale from custom logic
const locale = determineLocaleFromRequest ( context . request );
const weaverseData = await weaverse . loadPage ({
type: "PRODUCT" ,
handle: productHandle ,
locale , // Explicitly pass locale
});
return { weaverseData , locale };
}
function determineLocaleFromRequest ( request : Request ) : string {
// Custom logic: subdomain, header, cookie, etc.
const url = new URL ( request . url );
const subdomain = url . hostname . split ( "." )[ 0 ];
const localeMap : Record < string , string > = {
"se" : "sv-se" ,
"fr" : "fr-fr" ,
"de" : "de-de" ,
};
return localeMap [ subdomain ] || "en-us" ;
}
Advanced Locale Scenarios
Loading Multiple Locales
Load content in different locales simultaneously for comparison or translation workflows:
export async function loader ({ params , context } : LoaderFunctionArgs ) {
const { weaverse } = context ;
const { productHandle } = params ;
// Load same product in multiple locales
const [ englishData , frenchData , swedishData ] = await Promise . all ([
weaverse . loadPage ({
type: "PRODUCT" ,
handle: productHandle ,
locale: "en-us" ,
}),
weaverse . loadPage ({
type: "PRODUCT" ,
handle: productHandle ,
locale: "fr-ca" ,
}),
weaverse . loadPage ({
type: "PRODUCT" ,
handle: productHandle ,
locale: "sv-se" ,
}),
]);
return { englishData , frenchData , swedishData };
}
Dynamic Locale Selection
export async function loader ({ params , context , request } : LoaderFunctionArgs ) {
const { weaverse } = context ;
let locale = "en-us" ; // default
// Priority 1: URL parameter
if ( params . locale && isValidLocale ( params . locale )) {
locale = params . locale . toLowerCase ();
}
// Priority 2: User preference cookie
else if ( request . headers . get ( "cookie" )?. includes ( "locale=" )) {
const cookieLocale = getCookieValue ( request , "locale" );
if ( cookieLocale && isValidLocale ( cookieLocale )) {
locale = cookieLocale ;
}
}
// Priority 3: Accept-Language header
else {
const acceptLanguage = request . headers . get ( "accept-language" );
locale = parseAcceptLanguageHeader ( acceptLanguage ) || "en-us" ;
}
const weaverseData = await weaverse . loadPage ({
type: "PRODUCT" ,
handle: params . productHandle ,
locale ,
});
return { weaverseData , locale };
}
Locale Fallback Behavior
When a requested locale doesn’t exist, Weaverse provides fallback content:
Current Behavior:
If the specified locale doesn’t exist, Weaverse falls back to en-us content
This ensures users always see content, even if localization is incomplete
Example:
// Request Swedish content
const data = await weaverse . loadPage ({
type: "PRODUCT" ,
handle: "jacket" ,
locale: "sv-se" ,
});
// If sv-se doesn't exist:
// → Falls back to en-us content
// → Returns en-us page data
Coming Soon: Smart fallback will try language matches before falling back to the default (e.g., try sv-* for sv-se request before falling back to en-us).
Best Practices for Localization
Use Explicit locale Parameter
For maximum reliability and control, always explicitly pass the locale parameter: const weaverseData = await weaverse . loadPage ({
type: "PRODUCT" ,
handle: productHandle ,
locale: determinedLocale , // Explicit is better
});
Handle Missing Locales Gracefully
Check if the returned content matches the requested locale: const weaverseData = await weaverse . loadPage ({
type: "PRODUCT" ,
handle: productHandle ,
locale: requestedLocale ,
});
if ( weaverseData ?. page ?. locale !== requestedLocale ) {
console . warn ( `Locale ${ requestedLocale } not found, using fallback` );
}
Configure caching strategies to account for locale variations: const weaverseData = await weaverse . loadPage ({
type: "PRODUCT" ,
handle: productHandle ,
locale ,
strategy: {
maxAge: 3600 , // Cache for 1 hour per locale
staleWhileRevalidate: 86400 ,
},
});
Routes Without Weaverse Integration
Some routes in your Hydrogen theme may not use Weaverse’s page system at all. These typically include functional pages that rely heavily on Shopify’s built-in components and APIs:
Search pages - Use Shopify’s search API directly
Cart pages - Use Shopify’s cart components and forms
Account pages - Use Shopify’s customer account API
Policy pages - Often render Shopify’s policy content directly
Custom Page Approach
Weaverse provides two ways to create custom pages:
Option 1: PAGE Type with Handle
For specific custom pages with defined handles:
// Example: About page with specific handle
export async function loader ({ params , context } : LoaderFunctionArgs ) {
const { weaverse } = context ;
const weaverseData = await weaverse . loadPage ({
type: "PAGE" ,
handle: "about" , // Specific page handle created in Studio
});
return data ({ weaverseData });
}
Option 2: CUSTOM Type for Dynamic Routing
For catch-all routing where URL determines the page content:
// Example: Dynamic custom page routing
export async function loader ({ params , context } : LoaderFunctionArgs ) {
const { weaverse } = context ;
const weaverseData = await weaverse . loadPage ({
type: "CUSTOM" , // No handle needed - URL determines content
});
return data ({ weaverseData });
}
This approach allows you to create visually customizable pages using Weaverse’s editor while maintaining functional pages that use Shopify’s native components.
Real-World Example: Pilot Template Pattern
The official Pilot template demonstrates this mixed approach. Here are examples from actual implementation:
// Homepage with dynamic routing (from Pilot template)
// app/routes/($locale)._index.tsx
export async function loader ({ params , context } : LoaderFunctionArgs ) {
const { pathPrefix } = context . storefront . i18n ;
const locale = pathPrefix . slice ( 1 );
let type : PageType = "INDEX" ;
if ( params . locale && params . locale . toLowerCase () !== locale ) {
// Update for Weaverse: if it's not locale, it's probably a custom page handle
type = "CUSTOM" ;
}
const [ weaverseData , { shop }] = await Promise . all ([
context . weaverse . loadPage ({ type }),
context . storefront . query < ShopQuery >( SHOP_QUERY ),
]);
return { shop , weaverseData };
}
// Catch-all route for custom pages (from Pilot template)
// app/routes/($locale).$.tsx
export async function loader ({ context } : LoaderFunctionArgs ) {
const weaverseData = await context . weaverse . loadPage ({
type: "CUSTOM" ,
});
// Validate that the custom page exists
validateWeaverseData ( weaverseData );
return { weaverseData };
}
export default function CustomPage () {
return < WeaverseContent />;
}
This pattern shows how Pilot handles custom pages:
Homepage route (_index.tsx) detects when a URL parameter isn’t a locale and switches to "CUSTOM" type
Catch-all route ($.tsx) handles any remaining unmatched routes as custom Weaverse pages
Both routes use validateWeaverseData() to ensure the page exists before rendering
// Search page without Weaverse (from Pilot template)
// app/routes/($locale).search.tsx
export default function Search () {
const { searchTerm , products } = useLoaderData < typeof loader >();
// No WeaverseContent component - uses native Shopify search functionality
return (
< Section width = "fixed" verticalPadding = "medium" >
< Form method = "get" >
< input name = "q" placeholder = "Search our store..." />
</ Form >
< Pagination connection = { products } >
{ /* Native Shopify components */ }
</ Pagination >
</ Section >
);
}
// Account page without Weaverse (from Pilot template)
// app/routes/($locale).account.tsx
export default function Account () {
const { customer } = useLoaderData < typeof loader >();
// No WeaverseContent - uses Shopify's customer account components
return (
< Section >
< AccountDetails customer = { customer } />
< AccountOrderHistory orders = { orders } />
< AccountAddressBook addresses = { addresses } />
</ Section >
);
}
Basic Implementation
Here’s the standard pattern for implementing Weaverse page rendering in a React Router v7 route:
1. Route Loader
// app/routes/products.$productHandle.tsx
import type { LoaderFunctionArgs } from "react-router" ;
import { data } from "@shopify/remix-oxygen" ;
export async function loader ({ params , context } : LoaderFunctionArgs ) {
const { productHandle } = params ;
const { storefront , weaverse } = context ;
// Load both Shopify data and Weaverse page data in parallel
const [ productData , weaverseData ] = await Promise . all ([
storefront . query ( PRODUCT_QUERY , {
variables: { handle: productHandle }
}),
weaverse . loadPage ({
type: "PRODUCT" ,
handle: productHandle ,
}),
]);
if ( ! productData . product ) {
throw new Response ( "Product not found" , { status: 404 });
}
return data ({
product: productData . product ,
weaverseData ,
});
}
2. Route Component
import { useLoaderData } from "react-router" ;
import { WeaverseContent } from "~/weaverse" ;
export default function ProductPage () {
// WeaverseHydrogenRoot automatically provides weaverseData to WeaverseContent
return < WeaverseContent /> ;
}
Page Type Examples
Product Page
// app/routes/products.$productHandle.tsx
export async function loader ({ params , context } : LoaderFunctionArgs ) {
const { productHandle } = params ;
const { storefront , weaverse } = context ;
const [{ product }, weaverseData ] = await Promise . all ([
storefront . query ( PRODUCT_QUERY , {
variables: { handle: productHandle }
}),
weaverse . loadPage ({
type: "PRODUCT" ,
handle: productHandle ,
}),
]);
return data ({ product , weaverseData });
}
Collection Page
// app/routes/collections.$collectionHandle.tsx
export async function loader ({ params , context } : LoaderFunctionArgs ) {
const { collectionHandle } = params ;
const { storefront , weaverse } = context ;
const [{ collection }, weaverseData ] = await Promise . all ([
storefront . query ( COLLECTION_QUERY , {
variables: { handle: collectionHandle }
}),
weaverse . loadPage ({
type: "COLLECTION" ,
handle: collectionHandle ,
}),
]);
return data ({ collection , weaverseData });
}
Custom Page
// app/routes/pages.$pageHandle.tsx
export async function loader ({ params , context } : LoaderFunctionArgs ) {
const { pageHandle } = params ;
const { storefront , weaverse } = context ;
const [{ page }, weaverseData ] = await Promise . all ([
storefront . query ( PAGE_QUERY , {
variables: { handle: pageHandle }
}),
weaverse . loadPage ({
type: "PAGE" ,
handle: pageHandle ,
}),
]);
return data ({ page , weaverseData });
}
Blog Page
// app/routes/blogs.$blogHandle._index.tsx
export async function loader ({ params , context } : LoaderFunctionArgs ) {
const { blogHandle } = params ;
const { storefront , weaverse } = context ;
const [{ blog }, weaverseData ] = await Promise . all ([
storefront . query ( BLOG_QUERY , {
variables: { handle: blogHandle }
}),
weaverse . loadPage ({
type: "BLOG" ,
handle: blogHandle ,
}),
]);
return data ({ blog , weaverseData });
}
Homepage
// app/routes/($locale)._index.tsx
export async function loader ({ params , context } : LoaderFunctionArgs ) {
const { storefront , weaverse } = context ;
const { pathPrefix } = context . storefront . i18n ;
const locale = pathPrefix . slice ( 1 );
let type : PageType = "INDEX" ;
// Handle locale-based routing and custom pages
if ( params . locale && params . locale . toLowerCase () !== locale ) {
// If it's not a locale, it's probably a custom page handle
type = "CUSTOM" ;
}
const [{ shop }, weaverseData ] = await Promise . all ([
storefront . query ( SHOP_QUERY ),
weaverse . loadPage ({ type }),
]);
return data ({ shop , weaverseData });
}
Custom Pages
Custom pages are created in Weaverse Studio and can be rendered using the PAGE type:
Creating Custom Routes
For user-friendly URLs, create custom routes that map to Weaverse pages:
// app/routes/($locale).about.tsx - Maps /about to a Weaverse page
export async function loader ({ context } : LoaderFunctionArgs ) {
const { weaverse } = context ;
const weaverseData = await weaverse . loadPage ({
type: "PAGE" ,
handle: "about" , // This should match the page handle in Weaverse Studio
});
return data ({ weaverseData });
}
export default function AboutPage () {
return < WeaverseContent />;
}
Dynamic Custom Pages
Create a catch-all route for dynamic custom pages:
// app/routes/($locale).pages.$pageHandle.tsx
export async function loader ({ params , context } : LoaderFunctionArgs ) {
const { pageHandle } = params ;
const { storefront , weaverse } = context ;
// Load both Shopify page data and Weaverse page data in parallel
const [{ page }, weaverseData ] = await Promise . all ([
storefront . query ( PAGE_QUERY , {
variables: { handle: pageHandle }
}),
weaverse . loadPage ({
type: "PAGE" ,
handle: pageHandle ,
}),
]);
// Handle page not found (neither Shopify nor Weaverse page exists)
if ( ! page && ! weaverseData ) {
throw new Response ( "Page not found" , { status: 404 });
}
return data ({ page , weaverseData });
}
Error Handling
404 Error Handling
Always check if the page data exists and handle 404 cases:
export async function loader ({ params , context } : LoaderFunctionArgs ) {
const { handle } = params ;
const { weaverse } = context ;
const weaverseData = await weaverse . loadPage ({
type: "PRODUCT" ,
handle ,
});
// Check if Weaverse page exists
if ( ! weaverseData ) {
throw new Response ( "Page not found" , { status: 404 });
}
return data ({ weaverseData });
}
Fallback Rendering
Provide fallback content when Weaverse data is unavailable:
export default function ProductPage () {
const { product , weaverseData } = useLoaderData < typeof loader >();
return (
< div >
{ weaverseData ? (
< WeaverseContent />
) : (
< div >
{ /* Fallback content when Weaverse page doesn't exist */ }
< h1 > { product . title } </ h1 >
< p > { product . description } </ p >
</ div >
) }
</ div >
);
}
Error Boundaries
Use error boundaries for graceful error handling:
import { ErrorBoundary } from "~/components/ErrorBoundary" ;
export default function ProductPage () {
return (
< ErrorBoundary >
< WeaverseContent />
</ ErrorBoundary >
);
}
Parallel Data Loading
Always load Weaverse data in parallel with other API calls:
// ✅ Good: Parallel loading
const [ shopifyData , weaverseData , reviewsData ] = await Promise . all ([
storefront . query ( PRODUCT_QUERY , { variables }),
weaverse . loadPage ({ type: "PRODUCT" , handle }),
getProductReviews ( handle ),
]);
// ❌ Bad: Sequential loading
const shopifyData = await storefront . query ( PRODUCT_QUERY , { variables });
const weaverseData = await weaverse . loadPage ({ type: "PRODUCT" , handle });
const reviewsData = await getProductReviews ( handle );
Caching
Use proper cache headers for Weaverse pages:
import { routeHeaders } from "~/utils/cache" ;
export const headers = routeHeaders ;
export async function loader ({ context } : LoaderFunctionArgs ) {
// Your loader implementation
}
Preloading
Consider preloading critical page data:
// Preload homepage data
export function preload ({ context } : LoaderFunctionArgs ) {
return context . weaverse . loadPage ({ type: "INDEX" });
}
Troubleshooting
Common Issues
1. Page Type Mismatch
Problem : Wrong page type specified in loadPage()
// ❌ Wrong: Using PRODUCT type for a collection page
weaverse . loadPage ({ type: "PRODUCT" , handle: collectionHandle })
Solution : Use the correct supported page type
// ✅ Correct: Using COLLECTION type for a collection page
weaverse . loadPage ({ type: "COLLECTION" , handle: collectionHandle })
Problem : Using unsupported page types
// ❌ Wrong: These page types are not supported
weaverse . loadPage ({ type: "SEARCH" , handle: "search" })
weaverse . loadPage ({ type: "CART" , handle: "cart" })
weaverse . loadPage ({ type: "ACCOUNT" , handle: "account" })
Solution : Many functional pages don’t need Weaverse integration - use native Shopify components instead
// ✅ Correct: Search, cart, and account pages often don't use Weaverse at all
// These routes use Shopify's native components directly:
// Search page - uses Shopify's search API and components
export default function SearchPage () {
// Uses native Shopify search components, no WeaverseContent needed
}
// Cart page - uses Shopify's cart components and forms
export default function CartPage () {
// Uses native Shopify cart components, no WeaverseContent needed
}
// Account page - uses Shopify's customer account components
export default function AccountPage () {
// Uses native Shopify account components, no WeaverseContent needed
}
Alternative : If you want to customize these pages visually, create custom Weaverse pages
// ✅ Alternative: Use PAGE type with custom page created in Studio (optional)
weaverse . loadPage ({ type: "PAGE" , handle: "custom-search-page" })
weaverse . loadPage ({ type: "PAGE" , handle: "custom-cart-page" })
weaverse . loadPage ({ type: "PAGE" , handle: "custom-account-page" })
2. Missing Handle Parameter
Problem : Handle not provided or undefined
// ❌ Wrong: Handle is undefined
weaverse . loadPage ({ type: "PRODUCT" , handle: undefined })
Solution : Always validate handle exists
// ✅ Correct: Validate handle before using
invariant ( handle , "Missing product handle" );
weaverse . loadPage ({ type: "PRODUCT" , handle })
3. Component Not Rendering
Problem : Weaverse components not registered properly
Solution : Ensure components are registered in ~/weaverse/components.ts (or ~/app/weaverse/components.ts in Pilot template):
import type { HydrogenComponent } from "@weaverse/hydrogen" ;
import * as ProductInfo from "~/sections/product-info" ;
import * as Hero from "~/sections/hero" ;
export const components : HydrogenComponent [] = [
ProductInfo ,
Hero ,
// Add all your components here
];
Important : Always use namespace imports (* as ComponentName) and restart your dev server after registration.
4. TypeScript Errors
Problem : Type errors with WeaverseContent
Solution : Ensure proper type imports:
import type { WeaverseLoaderData } from "@weaverse/hydrogen" ;
export async function loader ({ context } : LoaderFunctionArgs ) {
const weaverseData : WeaverseLoaderData = await context . weaverse . loadPage ({
type: "PRODUCT" ,
handle ,
});
return data ({ weaverseData });
}
Debug Mode
Enable debug mode to troubleshoot page loading issues:
// In your weaverse.config.ts
export default defineConfig ({
debug: process . env . NODE_ENV === "development" ,
// ... other config
}) ;
Next Steps
For more advanced use cases, see the Migration Guide and Custom Routing documentation.