Skip to main content

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:
  1. loadPage() function: Loads page data based on type and handle
  2. 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.

Locale Format

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: 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

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
});
Ensure locales are properly formatted before passing to loadPage():
function normalizeLocale(input: string): string {
  return input.toLowerCase().trim().replace("_", "-");
}

const locale = normalizeLocale(params.locale || "en-us");
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:
  1. Homepage route (_index.tsx) detects when a URL parameter isn’t a locale and switches to "CUSTOM" type
  2. Catch-all route ($.tsx) handles any remaining unmatched routes as custom Weaverse pages
  3. 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>
  );
}

Performance Best Practices

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.