Integrate Weaverse into an existing Shopify Hydrogen project — analyze codebase, convert existing components to Weaverse sections, set up SDK, configure routes, and preserve coding style. For projects not yet using Weaverse.
Purpose: You are integrating Weaverse into a Hydrogen project that does NOT currently use it. This skill guides you through the full process: analysis → SDK setup → component conversion → route migration → verification.
Key principle: Respect the existing project. Match their coding style, patterns, and conventions. Don't restructure what already works — extend it with Weaverse.
# Always fetch latest docs before making decisions
node scripts/search_weaverse_docs.mjs "existing hydrogen integration"
node scripts/get_weaverse_page.mjs "migration-advanced/existing-hydrogen-integration"
node scripts/search_shopify_docs.mjs "createHydrogenContext"
Before touching any code, read and document the project's current state.
# Check package.json
cat package.json | grep -E '"react-router"|"@remix-run"|"@shopify/hydrogen"|"@weaverse"'
| What you find | Weaverse version to install |
|---|---|
react-router + @shopify/[email protected]+ | @weaverse/hydrogen@latest (v5, React Router v7) |
@remix-run/react | @weaverse/hydrogen@4 (v4, Remix) |
If the project uses Remix (pre-2025.5.0):
Read these files and note their patterns:
| File | What to observe |
|---|---|
package.json | Dependencies, scripts, package manager (npm/yarn/pnpm/bun) |
app/root.tsx | Layout structure, providers, global styles |
app/entry.server.tsx | CSP setup, rendering approach |
server.ts (or server/index.ts) | Context creation, middleware |
app/routes/** | Route structure, loader patterns, naming convention |
app/components/** | Component patterns, props, styling approach |
app/sections/** | If sections exist — how they're structured |
app/styles/** or tailwind.config.* | Styling approach (Tailwind, CSS modules, styled-components, etc.) |
vite.config.ts | Build plugins, aliases |
.env or .env.example | Existing env vars (DO NOT read secrets) |
tsconfig.json | Path aliases, strictness settings |
Note and preserve these patterns:
forwardRefexport default vs named exports vs export let~/) vs relative, namespace imports vs default⚠️ CRITICAL: Your converted Weaverse components MUST match these conventions. If the project uses
functiondeclarations, don't useconst X = () =>. If they use Tailwind, don't introduce CSS modules.
Find components that are good candidates for Weaverse sections:
Good candidates:
Skip (don't convert):
# Use whatever package manager the project already uses
# React Router v7 (Hydrogen 2025.5.0+)
npm install @weaverse/hydrogen@latest
# Remix (legacy)
npm install @weaverse/hydrogen@4
Add to .env:
WEAVERSE_PROJECT_ID="provided-by-user"
WEAVERSE_API_KEY="provided-by-user"
Also add to .env.example (without real values) and update TypeScript env types:
// Add to the Env type in env.d.ts or wherever the project defines it
WEAVERSE_PROJECT_ID: string;
WEAVERSE_API_KEY: string;
WEAVERSE_HOST?: string;
app/weaverse/ DirectoryCreate these 5 files. Match the project's existing code style (exports, naming, quotes, semicolons).
app/weaverse/schema.server.ts — Theme Schemaimport type { HydrogenThemeSchema } from "@weaverse/hydrogen";
import pkg from "../../package.json";
export let themeSchema: HydrogenThemeSchema = {
info: {
version: pkg.version,
author: "Your Store Name",
name: "Your Theme Name",
},
settings: [
// Start minimal — add more as you convert components
{
group: "Colors",
inputs: [
{
type: "color",
name: "colorPrimary",
label: "Primary Color",
defaultValue: "#000000",
},
{
type: "color",
name: "colorBackground",
label: "Background Color",
defaultValue: "#ffffff",
},
{
type: "color",
name: "colorText",
label: "Text Color",
defaultValue: "#1a1a1a",
},
],
},
{
group: "Typography",
inputs: [
{
type: "range",
name: "headingBaseSize",
label: "Heading Base Size",
configs: { min: 14, max: 32, step: 1, unit: "px" },
defaultValue: 28,
},
{
type: "range",
name: "bodyBaseSize",
label: "Body Base Size",
configs: { min: 12, max: 20, step: 1, unit: "px" },
defaultValue: 16,
},
],
},
],
};
app/weaverse/style.tsx — Global StylesRead the project's existing global styles (CSS files, Tailwind config, root layout) and create CSS variables that integrate with their existing setup.
import { useThemeSettings } from "@weaverse/hydrogen";
export function GlobalStyle() {
let settings = useThemeSettings();
if (!settings) return null;
return (
<style
id="weaverse-global-style"
key="weaverse-global-style"
suppressHydrationWarning
dangerouslySetInnerHTML={{
__html: `
:root {
--color-primary: ${settings.colorPrimary};
--color-bg: ${settings.colorBackground};
--color-text: ${settings.colorText};
--heading-base: ${settings.headingBaseSize}px;
--body-base: ${settings.bodyBaseSize}px;
}
`,
}}
/>
);
}
app/weaverse/components.ts — Component Registryimport type { HydrogenComponent } from "@weaverse/hydrogen";
// Import converted sections here — start empty, add as you convert
// MUST use namespace imports: import * as X, NOT import X from
export let components: HydrogenComponent[] = [
// Sections will be added during Phase 3
];
app/weaverse/index.tsx — WeaverseContentimport { WeaverseHydrogenRoot } from "@weaverse/hydrogen";
import { components } from "./components";
export function WeaverseContent() {
return (
<WeaverseHydrogenRoot
components={components}
errorComponent={GenericError}
/>
);
}
Use the project's existing error component if they have one, otherwise create a minimal GenericError.
app/weaverse/csp.ts — Content Security Policyexport function getWeaverseCsp(request: Request, context: any) {
let url = new URL(request.url);
let weaverseHost = context.env?.WEAVERSE_HOST || "https://weaverse.io";
let isDesignMode = url.searchParams.get("weaverse_design_mode") === "true";
let weaverseHosts = [
new URL(weaverseHost).host,
"weaverse.io",
"*.weaverse.io",
"shopify.com",
"*.shopify.com",
"*.myshopify.com",
];
let updatedCsp: Record<string, string[] | string | boolean> = {
frameAncestors: weaverseHosts,
defaultSrc: ["'self'", "data:", ...weaverseHosts],
scriptSrc: ["'self'", "'unsafe-inline'", ...weaverseHosts],
styleSrc: ["'self'", "'unsafe-inline'", ...weaverseHosts],
connectSrc: ["'self'", ...weaverseHosts],
};
if (isDesignMode) {
updatedCsp.frameAncestors = ["*"];
}
return updatedCsp;
}
Modify the existing context file (usually server.ts or app/lib/context.ts):
// ADD these imports
import { WeaverseClient } from "@weaverse/hydrogen";
import { themeSchema } from "~/weaverse/schema.server"; // adjust path alias
import { components } from "~/weaverse/components";
// INSIDE createAppLoadContext, after hydrogenContext is created:
return {
...hydrogenContext,
weaverse: new WeaverseClient({
...hydrogenContext,
request,
cache,
themeSchema,
components,
}),
};
Don't restructure their context creation — just spread the Weaverse client into their existing return object.
app/root.tsx — Wrap with withWeaverseimport { withWeaverse } from "@weaverse/hydrogen";
import { GlobalStyle } from "~/weaverse/style";
// Keep their existing Layout/App components untouched
// Only change: wrap the default export
// BEFORE:
// export default function App() { ... }
// AFTER:
function App() {
// ... their existing App code, unchanged
}
export default withWeaverse(App);
Add <GlobalStyle /> inside <head> in their Layout component.
app/entry.server.tsx — Add Weaverse CSPimport { getWeaverseCsp } from "~/weaverse/csp";
// Inside handleRequest, merge Weaverse CSP with existing:
const { nonce, header, NonceProvider } = createContentSecurityPolicy({
...getWeaverseCsp(request, context),
shop: {
checkoutDomain: context.env?.PUBLIC_CHECKOUT_DOMAIN || context.env?.PUBLIC_STORE_DOMAIN,
storeDomain: context.env?.PUBLIC_STORE_DOMAIN,
},
});
Merge with their existing CSP config — don't replace it.
This is the most important phase. Convert the user's existing components into Weaverse-editable sections while preserving their exact visual output and behavior.
For each convertible component:
Original component (before):
// app/components/hero-banner.tsx
interface HeroBannerProps {
heading: string;
subtitle: string;
backgroundImage: string;
alignment: "left" | "center";
}
function HeroBanner({ heading, subtitle, backgroundImage, alignment }: HeroBannerProps) {
return (
<section className="relative bg-cover bg-center" style={{ backgroundImage: `url(${backgroundImage})` }}>
<div className={`flex flex-col ${alignment === 'center' ? 'items-center text-center' : 'items-start'}`}>
<h1 className="text-4xl font-bold">{heading}</h1>
<p className="text-xl mt-4">{subtitle}</p>
</div>
</section>
);
}
export default HeroBanner;
Converted component (after):
// app/sections/hero-banner.tsx (or keep original path, just add schema)
import type { HydrogenComponentProps } from "@weaverse/hydrogen";
import { createSchema } from "@weaverse/hydrogen";
interface HeroBannerProps extends HydrogenComponentProps {
heading: string;
subtitle: string;
backgroundImage: string;
alignment: "left" | "center";
}
function HeroBanner({ heading, subtitle, backgroundImage, alignment, children, ...rest }: HeroBannerProps) {
return (
<section {...rest} className="relative bg-cover bg-center" style={{ backgroundImage: `url(${backgroundImage})` }}>
<div className={`flex flex-col ${alignment === 'center' ? 'items-center text-center' : 'items-start'}`}>
<h1 className="text-4xl font-bold">{heading}</h1>
<p className="text-xl mt-4">{subtitle}</p>
</div>
{children}
</section>
);
}
export default HeroBanner;
export let schema = createSchema({
type: "hero-banner",
title: "Hero Banner",
settings: [
{
group: "Content",
inputs: [
{ type: "text", name: "heading", label: "Heading", defaultValue: "Welcome" },
{ type: "textarea", name: "subtitle", label: "Subtitle" },
{ type: "image", name: "backgroundImage", label: "Background Image" },
{
type: "toggle-group",
name: "alignment",
label: "Alignment",
configs: {
options: [
{ value: "left", label: "Left" },
{ value: "center", label: "Center" },
],
},
defaultValue: "center",
},
],
},
],
presets: {
heading: "Welcome",
alignment: "center",
},
});
Key changes:
HydrogenComponentProps (adds children, loaderData, and Weaverse internals){...rest} on root element (required for Studio interaction){children} if the component can accept nested sectionsschema export with createSchema()When creating schemas for converted components:
| Original prop type | Schema input type |
|---|---|
string (short text) | text |
string (long text) | textarea or richtext |
string (URL) | url |
string (image path) | image |
string (hex color) | color |
number | range (with appropriate min/max/step) |
boolean | switch |
string (enum) | select or toggle-group |
| Object with Shopify data | product, collection, blog, etc. |
Group settings logically — match what makes sense for the component, typically:
If the original component fetches data (e.g., a featured collection), convert the data fetching to a Weaverse loader:
import type { ComponentLoaderArgs } from "@weaverse/hydrogen";
type SectionData = {
collectionHandle: string;
count: number;
};
export let loader = async ({ weaverse, data }: ComponentLoaderArgs<SectionData>) => {
if (!data?.collectionHandle) return null;
return weaverse.storefront.query(COLLECTION_QUERY, {
variables: { handle: data.collectionHandle, first: data.count ?? 8 },
});
};
Use the project's existing GraphQL queries and fragments — don't rewrite them. If they have a graphql/ directory with shared queries, import from there.
Update app/weaverse/components.ts:
import * as HeroBanner from "~/sections/hero-banner";
import * as FeaturedCollection from "~/sections/featured-collection";
// ... other converted components
export let components: HydrogenComponent[] = [
HeroBanner,
FeaturedCollection,
// ...
];
MUST use import * as X — not import X from. This is the most common mistake.
For each route that should be Weaverse-editable:
// BEFORE — original route
import { useLoaderData } from "react-router"; // or "@remix-run/react"
import HeroBanner from "~/components/hero-banner";
import ProductGrid from "~/components/product-grid";
export async function loader({ context, params }: LoaderFunctionArgs) {
const productData = await context.storefront.query(PRODUCT_QUERY, { ... });
return { product: productData.product };
}
export default function Homepage() {
const { product } = useLoaderData<typeof loader>();
return (
<div>
<HeroBanner heading="Sale" subtitle="Big deals" />
<ProductGrid products={product} />
</div>
);
}
// AFTER — Weaverse-enabled route
import { WeaverseContent } from "~/weaverse";
export async function loader({ context, params }: LoaderFunctionArgs) {
// Keep existing data loading
const productData = await context.storefront.query(PRODUCT_QUERY, { ... });
// ADD Weaverse page loading
const weaverseData = await context.weaverse.loadPage({ type: "INDEX" });
return {
product: productData.product,
weaverseData,
};
}
export default function Homepage() {
// Render Weaverse content — it will use the registered components
return <WeaverseContent />;
}
| Route | type value |
|---|---|
Homepage (_index) | "INDEX" |
| Product page | "PRODUCT" (with handle: params.productHandle) |
| Collection page | "COLLECTION" (with handle: params.collectionHandle) |
| Collections list | "ALL_PRODUCTS" |
| Blog | "BLOG" |
| Article | "ARTICLE" (with handle: params.articleHandle) |
| Custom page | "PAGE" (with handle: params.pageHandle) |
| Custom template | "CUSTOM" |
You don't have to convert all routes at once. Start with:
For routes not yet converted, keep them as-is. Weaverse doesn't interfere with non-Weaverse routes.
If the project has shared layout components (header, footer, nav):
root.tsx LayoutDon't convert primitive/shared components. They stay in app/components/ and are used inside Weaverse sections.
If the project uses:
Add to the project's type definitions:
// Wherever the project defines AppLoadContext
interface AppLoadContext {
// ... existing properties
weaverse: import("@weaverse/hydrogen").WeaverseClient;
}
After integration, verify:
npm run dev starts without errorsnpm run build completes without errors| Mistake | Fix |
|---|---|
import X from instead of import * as X | Always namespace import for component registration |
Forgetting {...rest} on root element | Required for Weaverse Studio drag/drop interaction |
Not rendering {children} in container components | Required for nested sections |
| Overriding existing CSP instead of merging | Spread Weaverse CSP into existing config |
| Rewriting component JSX "to make it better" | Preserve the exact original rendering |
| Converting ALL components at once | Convert sections first, keep primitives as shared components |
Using inspector in schema | Use settings — inspector is deprecated |
Creating app/weaverse/ outside app/ | Must be inside the app directory |
| Forgetting env vars in type definitions | Add WEAVERSE_* to the Env type |
| File | Action |
|---|---|
package.json | Add @weaverse/hydrogen dependency |
.env | Add WEAVERSE_PROJECT_ID, WEAVERSE_API_KEY |
app/weaverse/schema.server.ts | CREATE — theme schema |
app/weaverse/style.tsx | CREATE — global styles component |
app/weaverse/components.ts | CREATE — component registry |
app/weaverse/index.tsx | CREATE — WeaverseContent wrapper |
app/weaverse/csp.ts | CREATE — CSP configuration |
server.ts (or context file) | MODIFY — add WeaverseClient to context |
app/root.tsx | MODIFY — wrap with withWeaverse, add <GlobalStyle /> |
app/entry.server.tsx | MODIFY — merge Weaverse CSP |
app/sections/*.tsx | CREATE/MODIFY — converted components |
| Route files | MODIFY — add weaverse.loadPage(), render <WeaverseContent /> |
| Type definitions | MODIFY — add WEAVERSE_* env vars, weaverse context |
.gitignore | VERIFY — .react-router/ is ignored (RRv7 projects) |