@weaverse/next 0.1.0-alpha.4 → 0.1.0-alpha.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +502 -121
- package/dist/{chunk-O2TG2NYL.mjs → chunk-6PMZ77RA.mjs} +59 -3
- package/dist/chunk-6PMZ77RA.mjs.map +1 -0
- package/dist/index.d.mts +51 -8
- package/dist/index.d.ts +51 -8
- package/dist/index.js +126 -19
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +69 -22
- package/dist/index.mjs.map +1 -1
- package/dist/{types-DvwaU20C.d.mts → seo-kwaliD-K.d.mts} +49 -2
- package/dist/{types-DvwaU20C.d.ts → seo-kwaliD-K.d.ts} +49 -2
- package/dist/server.d.mts +3 -3
- package/dist/server.d.ts +3 -3
- package/dist/server.js +112 -0
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +56 -1
- package/dist/server.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-O2TG2NYL.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -1,165 +1,546 @@
|
|
|
1
1
|
# @weaverse/next
|
|
2
2
|
|
|
3
|
-
Next.js App Router integration for
|
|
3
|
+
Next.js App Router integration for Weaverse-powered Hydrogen storefronts.
|
|
4
4
|
|
|
5
|
-
This package
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
This package lets a Next App Router storefront render Weaverse page data, run server-side section loaders, connect to Weaverse Studio, and refresh loader-backed Studio edits without depending on React Router.
|
|
6
|
+
|
|
7
|
+
It is the Next equivalent of the useful parts of `@weaverse/hydrogen`, but the app creates the request-scoped server client explicitly because Next does not inject a custom `context` object into every route loader.
|
|
8
|
+
|
|
9
|
+
## Current status
|
|
10
|
+
|
|
11
|
+
- Package status: alpha (`0.1.0-alpha.x`).
|
|
12
|
+
- Target runtime: Next App Router.
|
|
13
|
+
- Peer dependencies: `next >=14`, `react >=19`, `react-dom >=19`.
|
|
14
|
+
- Studio bridge: supported through the dedicated Next Studio bridge script (`/static/studio/next/*`) plus Next-native runtime callbacks.
|
|
15
|
+
- Resource-picker revalidation: supported through per-item loader revalidation, not full-page `router.refresh()` on the happy path.
|
|
16
|
+
- Low-risk Hydrogen parity helpers: custom-page sitemap fetching, Next metadata conversion, and resource-picker type aliases are available.
|
|
17
|
+
|
|
18
|
+
## Quick answers for the team
|
|
19
|
+
|
|
20
|
+
### 1. What is this package?
|
|
21
|
+
|
|
22
|
+
`@weaverse/next` is the Weaverse adapter for Next App Router storefronts. It provides:
|
|
23
|
+
|
|
24
|
+
- a server client that fetches Weaverse page/theme data from the Weaverse public API;
|
|
25
|
+
- a component registry and renderer for Weaverse page trees;
|
|
26
|
+
- hooks and provider equivalents for theme settings, page data, commerce context, and Weaverse runtime access;
|
|
27
|
+
- Studio script connection and runtime binding;
|
|
28
|
+
- a per-item loader revalidation route handler for resource-picker/live-preview edits.
|
|
29
|
+
|
|
30
|
+
### 2. Are `loadPage` / `loadThemeSettings` implemented or only typed?
|
|
31
|
+
|
|
32
|
+
They are implemented in `@weaverse/next/server`.
|
|
33
|
+
|
|
34
|
+
Use `createWeaverseNextServerClient()` to create a request-scoped client, then call:
|
|
35
|
+
|
|
36
|
+
- `weaverse.loadPage({ type: 'INDEX' | 'PRODUCT' | 'COLLECTION', handle? })`
|
|
37
|
+
- `weaverse.loadThemeSettings()`
|
|
38
|
+
|
|
39
|
+
Implementation lives in [`src/server/server-client.ts`](./src/server/server-client.ts). It:
|
|
40
|
+
|
|
41
|
+
- resolves `projectId` from Studio query params, config, or env;
|
|
42
|
+
- POSTs page requests to `/api/public/project`;
|
|
43
|
+
- POSTs theme settings requests to `/api/public/project_configs`;
|
|
44
|
+
- runs registered component loaders and attaches `loaderData`;
|
|
45
|
+
- forces `no-store` for design/revision preview reads;
|
|
46
|
+
- returns safe client-facing configs including `requestInfo`.
|
|
47
|
+
|
|
48
|
+
### 3. Is the POC bootstrapped from Shopify's Hydrogen preview docs?
|
|
49
|
+
|
|
50
|
+
Yes. The POC started as a Next App Router app plus Shopify Hydrogen preview setup:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npx create-next-app@latest weaverse-hydrogen-next-poc --ts --app --eslint --tailwind --no-src-dir --import-alias "@/*" --use-npm --yes
|
|
54
|
+
npx @shopify/hydrogen@preview setup
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
See the POC findings doc:
|
|
58
|
+
|
|
59
|
+
- https://github.com/Weaverse/weaverse-hydrogen-next-poc/blob/main/findings.md
|
|
60
|
+
|
|
61
|
+
### 4. Where does the POC load page + Weaverse data?
|
|
62
|
+
|
|
63
|
+
Reference POC files:
|
|
64
|
+
|
|
65
|
+
- Server client helper: https://github.com/Weaverse/weaverse-hydrogen-next-poc/blob/main/app/weaverse-next/server.ts
|
|
66
|
+
- Home route load: https://github.com/Weaverse/weaverse-hydrogen-next-poc/blob/main/app/page.tsx
|
|
67
|
+
- Product route load: https://github.com/Weaverse/weaverse-hydrogen-next-poc/blob/main/app/products/%5Bhandle%5D/page.tsx
|
|
68
|
+
- Collection route load: https://github.com/Weaverse/weaverse-hydrogen-next-poc/blob/main/app/collections/%5Bhandle%5D/page.tsx
|
|
69
|
+
- Client wrapper/renderer: https://github.com/Weaverse/weaverse-hydrogen-next-poc/blob/main/app/weaverse-next/wrapper.tsx
|
|
70
|
+
- Studio script connector: https://github.com/Weaverse/weaverse-hydrogen-next-poc/blob/main/app/weaverse-next/studio-connect.tsx
|
|
71
|
+
- Per-item revalidation route: https://github.com/Weaverse/weaverse-hydrogen-next-poc/blob/main/app/api/weaverse/revalidate/route.ts
|
|
72
|
+
|
|
73
|
+
The route pattern is:
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
// app/page.tsx
|
|
77
|
+
import { loadWeaverseHomePage } from './weaverse-next/server'
|
|
78
|
+
import { WeaverseHome } from './weaverse-next/wrapper'
|
|
79
|
+
|
|
80
|
+
export default async function Home(props: {
|
|
81
|
+
searchParams: Promise<Record<string, string | string[] | undefined>>
|
|
82
|
+
}) {
|
|
83
|
+
let weaverseData = await loadWeaverseHomePage(props.searchParams)
|
|
84
|
+
return <WeaverseHome data={weaverseData} />
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 5. Is there a global app load context like Hydrogen/Pilot?
|
|
89
|
+
|
|
90
|
+
Not automatically from the framework.
|
|
91
|
+
|
|
92
|
+
Hydrogen/React Router injects `context.weaverse` into route loaders. Next App Router does not have that loader-context mechanism. The Next equivalent is a small app-owned server helper that creates the request-scoped Weaverse server client at the route boundary.
|
|
93
|
+
|
|
94
|
+
In the POC that helper is `getWeaverseServerClient(...)` in `app/weaverse-next/server.ts`. It plays the same role as Hydrogen's global load context, but explicitly:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
let weaverse = await getWeaverseServerClient(searchParams, pathname)
|
|
98
|
+
let page = await weaverse.loadPage({ type: 'INDEX' })
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
So the mental model is still `weaverse.loadPage(...)`; only the injection mechanism changes.
|
|
10
102
|
|
|
11
103
|
## Entrypoints
|
|
12
104
|
|
|
13
105
|
| Import | Use in | Purpose |
|
|
14
106
|
| --- | --- | --- |
|
|
15
|
-
| `@weaverse/next` | Client
|
|
16
|
-
| `@weaverse/next/server` | Server only |
|
|
107
|
+
| `@weaverse/next` | Client and shared code | Component registry, provider, renderer, hooks, Studio runtime/bind components, schema re-exports. |
|
|
108
|
+
| `@weaverse/next/server` | Server only | Real Weaverse API client, config resolution, page URL normalization, per-item revalidation route handler. |
|
|
17
109
|
|
|
18
|
-
|
|
19
|
-
> root entry stays free of server-only fetch assumptions and safe to import from
|
|
20
|
-
> client components.
|
|
110
|
+
The server API lives under `@weaverse/next/server` so client components can safely import the root entry without pulling server-only fetch code into the client bundle.
|
|
21
111
|
|
|
22
112
|
## Package structure
|
|
23
113
|
|
|
24
114
|
```text
|
|
25
115
|
packages/next/src/
|
|
26
|
-
index.ts
|
|
27
|
-
server.ts
|
|
28
|
-
client.ts
|
|
29
|
-
loader.ts
|
|
30
|
-
registry.ts
|
|
31
|
-
renderer.tsx
|
|
32
|
-
provider.tsx
|
|
33
|
-
hooks.ts
|
|
34
|
-
runtime.ts
|
|
35
|
-
studio-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
116
|
+
index.ts # root public exports
|
|
117
|
+
server.ts # server public exports
|
|
118
|
+
client.ts # createWeaverseNextClient for client/runtime state
|
|
119
|
+
loader.ts # runWeaverseComponentLoaders
|
|
120
|
+
registry.ts # component registration for @weaverse/react
|
|
121
|
+
renderer.tsx # WeaverseNextRenderer
|
|
122
|
+
provider.tsx # WeaverseNextProvider
|
|
123
|
+
hooks.ts # Next adapter hooks
|
|
124
|
+
runtime.ts # WeaverseNextRuntime + Studio binding
|
|
125
|
+
studio-connect.tsx # root Studio script loader
|
|
126
|
+
studio-bridge.tsx # page-level Studio runtime binder
|
|
127
|
+
studio-router.ts # Next router -> Studio internal callbacks
|
|
128
|
+
use-weaverse-next-studio.tsx # Next-native Studio binder using next/navigation
|
|
129
|
+
revalidate-item.ts # client per-item loader revalidation
|
|
130
|
+
request-info.ts # Studio-compatible request info
|
|
131
|
+
seo.ts # Next Metadata-compatible page SEO helpers
|
|
132
|
+
theme-settings-store.ts # Studio-compatible theme settings store
|
|
133
|
+
types.ts # shared public types
|
|
39
134
|
server/
|
|
40
|
-
server-client.ts
|
|
41
|
-
|
|
42
|
-
|
|
135
|
+
server-client.ts # createWeaverseNextServerClient
|
|
136
|
+
revalidate-handler.ts # createWeaverseNextRevalidateHandler
|
|
137
|
+
configs.ts # host/api/env/design-mode config resolution
|
|
138
|
+
normalize-page-url.ts # stable page URL normalization
|
|
43
139
|
```
|
|
44
140
|
|
|
45
141
|
## Root API (`@weaverse/next`)
|
|
46
142
|
|
|
47
|
-
|
|
48
|
-
I/O to injected `fetchPage` / `fetchThemeSettings`.
|
|
49
|
-
- `WeaverseNextProvider` / `WeaverseNextRenderer` — client provider + page-tree
|
|
50
|
-
renderer.
|
|
51
|
-
- `runWeaverseComponentLoaders(args)` — walk a page tree and run each registered
|
|
52
|
-
component's `loader` server-side.
|
|
53
|
-
- Hooks: `useThemeSettings`, `useWeaversePageData`, `useWeaverseRootData`,
|
|
54
|
-
`useWeaverseCommerce` (plus re-exported `@weaverse/react` hooks).
|
|
55
|
-
- Studio helpers: `WeaverseNextRuntime`, `createWeaverseNextRuntime`,
|
|
56
|
-
`bindWeaverseNextStudioRuntime`, `WeaverseNextStudioBridge`,
|
|
57
|
-
`WeaverseNextStudioConnect`, `resolveWeaverseNextStudioScriptSrc`,
|
|
58
|
-
`buildWeaverseNextRequestInfo`, `createWeaverseNextThemeSettingsStore`.
|
|
59
|
-
- `createSchema` (re-exported from `@weaverse/schema`).
|
|
143
|
+
Main exports:
|
|
60
144
|
|
|
61
|
-
|
|
145
|
+
- `createWeaverseNextClient(config)` — client/runtime state holder. It registers client render components and carries request/page/theme context.
|
|
146
|
+
- `WeaverseNextProvider` — React provider for the client runtime.
|
|
147
|
+
- `WeaverseNextRenderer` — renders a Weaverse page tree into React and binds the Studio runtime.
|
|
148
|
+
- `runWeaverseComponentLoaders(args)` — walks a page tree and runs component `loader` functions server-side.
|
|
149
|
+
- `WeaverseNextStudioConnect` — root client component that loads the Next Studio bridge script in design/preview contexts.
|
|
150
|
+
- `WeaverseNextStudio` / `WeaverseNextStudioBridge` — Next-native page-level Studio runtime binding.
|
|
151
|
+
- `createWeaverseNextRuntime` / `bindWeaverseNextStudioRuntime` — lower-level runtime helpers.
|
|
152
|
+
- `revalidateWeaverseNextItem()` — client-side per-item revalidation helper wired by `WeaverseNextStudio`.
|
|
153
|
+
- Hooks: `useThemeSettings`, `useWeaversePageData`, `useWeaverseRootData`, `useWeaverseCommerce`.
|
|
154
|
+
- Re-exported migration helpers from `@weaverse/react`: `useWeaverse`, `useParentInstance`, `useItemInstance`, `useChildInstances`.
|
|
155
|
+
- `createSchema` and schema types from `@weaverse/schema`.
|
|
62
156
|
|
|
63
|
-
|
|
64
|
-
Weaverse public API fetch — the equivalent of Hydrogen/Pilot's
|
|
65
|
-
`context.weaverse.loadPage(...)`.
|
|
157
|
+
## Server API (`@weaverse/next/server`)
|
|
66
158
|
|
|
67
|
-
|
|
159
|
+
Main exports:
|
|
68
160
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
161
|
+
- `createWeaverseNextServerClient(config)` — request-scoped server client.
|
|
162
|
+
- `createWeaverseNextRevalidateHandler(config)` — route-handler factory for Studio per-item loader revalidation.
|
|
163
|
+
- `getWeaverseNextConfigs(...)` — config/env/query resolution.
|
|
164
|
+
- `normalizeNextPageUrl(...)` / `resolveRequestUrl(...)` — stable page URL helpers.
|
|
165
|
+
- `getWeaverseNextSeoMetadata(...)` / `formatWeaverseNextSeoMetadata(...)` — convert Weaverse `page.seo` into a Next Metadata-compatible object.
|
|
73
166
|
|
|
74
|
-
|
|
75
|
-
`app/page.tsx`. A page is a Server Component whose framework-provided inputs are
|
|
76
|
-
`params` and `searchParams`; request-scoped data is read with Next functions such
|
|
77
|
-
as `headers()` / `cookies()`. Hydrogen preview follows that framework model for
|
|
78
|
-
Next examples: fetch in `app/page.tsx` or a server helper, then pass the snapshot
|
|
79
|
-
to client components.
|
|
167
|
+
### `createWeaverseNextServerClient`
|
|
80
168
|
|
|
81
|
-
|
|
169
|
+
Example helper for a Next route/server component boundary:
|
|
82
170
|
|
|
83
171
|
```ts
|
|
84
172
|
import { createWeaverseNextServerClient } from '@weaverse/next/server'
|
|
85
|
-
import {
|
|
173
|
+
import { headers } from 'next/headers'
|
|
174
|
+
import { serverComponents } from './server-components'
|
|
175
|
+
import { getStaticStorefrontClient } from './storefront'
|
|
86
176
|
|
|
87
|
-
export async function
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
177
|
+
export async function getWeaverseServerClient(
|
|
178
|
+
searchParamsPromise: Promise<Record<string, string | string[] | undefined>>,
|
|
179
|
+
pathname = '/'
|
|
180
|
+
) {
|
|
181
|
+
let [headersList, rawSearchParams] = await Promise.all([
|
|
182
|
+
headers(),
|
|
183
|
+
searchParamsPromise,
|
|
184
|
+
])
|
|
185
|
+
let requestHeaders = new Headers(Object.fromEntries(headersList.entries()))
|
|
186
|
+
let searchParams = new URLSearchParams()
|
|
187
|
+
|
|
188
|
+
for (let [key, value] of Object.entries(rawSearchParams)) {
|
|
189
|
+
if (Array.isArray(value)) {
|
|
190
|
+
for (let item of value) searchParams.append(key, item)
|
|
191
|
+
} else if (value !== undefined) {
|
|
192
|
+
searchParams.set(key, value)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
let url = new URL(
|
|
197
|
+
`${pathname}?${searchParams}`,
|
|
198
|
+
`${requestHeaders.get('x-forwarded-proto') ?? 'https'}://${
|
|
199
|
+
requestHeaders.get('x-forwarded-host') ?? requestHeaders.get('host')
|
|
200
|
+
}`
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
let storefront = getStaticStorefrontClient()
|
|
204
|
+
|
|
205
|
+
return createWeaverseNextServerClient({
|
|
206
|
+
components: serverComponents,
|
|
207
|
+
projectId: process.env.WEAVERSE_PROJECT_ID,
|
|
208
|
+
weaverseHost: process.env.WEAVERSE_HOST,
|
|
94
209
|
env: process.env,
|
|
210
|
+
commerce: storefront
|
|
211
|
+
? {
|
|
212
|
+
storefront: {
|
|
213
|
+
i18n: { country: 'US', language: 'EN' },
|
|
214
|
+
query: async (query, options) => {
|
|
215
|
+
let response = await storefront.graphql(query, options as never)
|
|
216
|
+
return response.data
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
}
|
|
220
|
+
: undefined,
|
|
95
221
|
requestContext: {
|
|
96
222
|
url,
|
|
97
|
-
headers:
|
|
98
|
-
searchParams
|
|
99
|
-
|
|
223
|
+
headers: requestHeaders,
|
|
224
|
+
searchParams,
|
|
225
|
+
pathname,
|
|
226
|
+
i18n: { country: 'US', language: 'EN', locale: 'en-US' },
|
|
100
227
|
},
|
|
101
228
|
cache: { revalidate: 60, tags: ['weaverse'] },
|
|
102
229
|
})
|
|
103
|
-
|
|
104
|
-
let [page, theme] = await Promise.all([
|
|
105
|
-
weaverse.loadPage({ type: 'INDEX' }),
|
|
106
|
-
weaverse.loadThemeSettings(),
|
|
107
|
-
])
|
|
108
|
-
return { page, theme, weaverse }
|
|
109
230
|
}
|
|
110
231
|
```
|
|
111
232
|
|
|
112
|
-
|
|
233
|
+
Then each route can stay close to the Hydrogen mental model:
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
let weaverse = await getWeaverseServerClient(props.searchParams, '/')
|
|
237
|
+
let data = await weaverse.loadPage({ type: 'INDEX' })
|
|
238
|
+
let theme = await weaverse.loadThemeSettings()
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Server client members
|
|
113
242
|
|
|
114
243
|
| Member | Description |
|
|
115
244
|
| --- | --- |
|
|
116
|
-
| `loadPage(input?)` |
|
|
117
|
-
| `loadThemeSettings(options?)` |
|
|
118
|
-
| `
|
|
119
|
-
| `
|
|
120
|
-
| `
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
245
|
+
| `loadPage(input?)` | Fetches the page from `/api/public/project`, builds `WeaverseNextLoaderData`, runs component loaders, and returns page/project/config data. In section preview mode it can synthesize a single-section preview page. |
|
|
246
|
+
| `loadThemeSettings(options?)` | Fetches `/api/public/project_configs`, merges schema defaults under merchant settings, returns theme settings/static content/schema data for design mode. |
|
|
247
|
+
| `fetchCustomPages(options?)` | Fetches published custom pages from `/api/public/v1/projects/:projectId/custom-pages` for sitemap generation, paginating automatically and returning partial results if a later page fails. |
|
|
248
|
+
| `fetchWithCache<T>(url, options?)` | Next-aware fetch helper. Uses `cache: 'no-store'` in design/revision preview and `next: { revalidate, tags }` in published mode. |
|
|
249
|
+
| `resolveProjectId()` / `projectId` | Resolves project id from Studio query, config function/string, or env. |
|
|
250
|
+
| `components` | Server component registry. Components may include `schema` and optional `loader`. |
|
|
251
|
+
| `commerce` / `storefront` | Commerce context. `storefront` is a compatibility alias for Pilot-style loaders. |
|
|
252
|
+
| `requestContext` | URL, headers, search params, i18n, and design/preview flags. |
|
|
253
|
+
|
|
254
|
+
### Component loaders
|
|
255
|
+
|
|
256
|
+
A registered component may expose a server-side `loader`:
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
export let productHero = {
|
|
260
|
+
schema: productHeroSchema,
|
|
261
|
+
loader: async ({ data, commerce, weaverse }) => {
|
|
262
|
+
let handle = data.product?.handle
|
|
263
|
+
if (!handle) return null
|
|
264
|
+
|
|
265
|
+
return commerce?.storefront?.query(PRODUCT_QUERY, {
|
|
266
|
+
variables: { handle },
|
|
267
|
+
})
|
|
268
|
+
},
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Loader args match the page-load and revalidation paths:
|
|
273
|
+
|
|
274
|
+
- `data` — schema defaults merged under item data.
|
|
275
|
+
- `commerce` — explicit commerce context.
|
|
276
|
+
- `weaverse` — server/client object with `weaverse.storefront` compatibility alias.
|
|
277
|
+
- `context` — request context.
|
|
278
|
+
|
|
279
|
+
### SEO metadata
|
|
280
|
+
|
|
281
|
+
Use the server entry in `generateMetadata()` to map Weaverse `page.seo` into a Next Metadata-compatible object:
|
|
282
|
+
|
|
283
|
+
```ts
|
|
284
|
+
// app/pages/[handle]/page.tsx
|
|
285
|
+
import { getWeaverseNextSeoMetadata } from '@weaverse/next/server'
|
|
286
|
+
|
|
287
|
+
export async function generateMetadata(props: {
|
|
288
|
+
params: Promise<{ handle: string }>
|
|
289
|
+
searchParams: Promise<Record<string, string | string[] | undefined>>
|
|
290
|
+
}) {
|
|
291
|
+
let { handle } = await props.params
|
|
292
|
+
let weaverse = await getWeaverseServerClient(props.searchParams, `/pages/${handle}`)
|
|
293
|
+
let data = await weaverse.loadPage({ type: 'PAGE', handle })
|
|
294
|
+
return getWeaverseNextSeoMetadata(data)
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
The helper is pure and does not import `next/*`; its return shape is intentionally structural so the app can type it as `Metadata` if desired.
|
|
299
|
+
|
|
300
|
+
### Custom pages for sitemap generation
|
|
301
|
+
|
|
302
|
+
`fetchCustomPages()` mirrors Hydrogen's sitemap helper and accepts Next cache knobs:
|
|
303
|
+
|
|
304
|
+
```ts
|
|
305
|
+
let weaverse = await getWeaverseServerClient(Promise.resolve({}), '/')
|
|
306
|
+
let pages = await weaverse.fetchCustomPages({
|
|
307
|
+
locale: 'en-us',
|
|
308
|
+
limit: 100,
|
|
309
|
+
revalidate: 3600,
|
|
310
|
+
tags: ['weaverse:custom-pages'],
|
|
311
|
+
})
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
It paginates until the API returns `nextCursor: null`; if a later page fails, it returns the pages already fetched instead of throwing.
|
|
315
|
+
|
|
316
|
+
## Rendering in a Next route
|
|
317
|
+
|
|
318
|
+
Typical shape:
|
|
319
|
+
|
|
320
|
+
```tsx
|
|
321
|
+
// app/page.tsx
|
|
322
|
+
export default async function Home(props: {
|
|
323
|
+
searchParams: Promise<Record<string, string | string[] | undefined>>
|
|
324
|
+
}) {
|
|
325
|
+
let data = await loadWeaverseHomePage(props.searchParams)
|
|
326
|
+
return <WeaversePage data={data} />
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
```tsx
|
|
331
|
+
// app/weaverse-page.tsx
|
|
332
|
+
'use client'
|
|
333
|
+
|
|
334
|
+
import {
|
|
335
|
+
createWeaverseNextClient,
|
|
336
|
+
WeaverseNextProvider,
|
|
337
|
+
WeaverseNextRenderer,
|
|
338
|
+
} from '@weaverse/next'
|
|
339
|
+
import type { WeaverseNextLoaderData } from '@weaverse/next'
|
|
340
|
+
import { components } from './components'
|
|
341
|
+
|
|
342
|
+
export function WeaversePage({ data }: { data: WeaverseNextLoaderData }) {
|
|
343
|
+
let requestInfo = data.configs?.requestInfo as
|
|
344
|
+
| {
|
|
345
|
+
i18n?: { country?: string; language?: string; locale?: string }
|
|
346
|
+
pathname?: string
|
|
347
|
+
search?: string
|
|
348
|
+
}
|
|
349
|
+
| undefined
|
|
350
|
+
let projectId =
|
|
351
|
+
(data.configs?.projectId as string | undefined) ?? data.project?.id
|
|
352
|
+
if (!projectId) throw new Error('Missing Weaverse project id')
|
|
353
|
+
|
|
354
|
+
let client = createWeaverseNextClient({
|
|
355
|
+
projectId,
|
|
356
|
+
components,
|
|
357
|
+
requestContext: {
|
|
358
|
+
pathname: requestInfo?.pathname ?? '/',
|
|
359
|
+
searchParams: new URLSearchParams(requestInfo?.search ?? ''),
|
|
360
|
+
isDesignMode: Boolean(data.configs?.isDesignMode),
|
|
361
|
+
isPreviewMode: Boolean(data.configs?.isPreviewMode),
|
|
362
|
+
isRevisionPreview: Boolean(data.configs?.isRevisionPreview),
|
|
363
|
+
i18n: requestInfo?.i18n,
|
|
364
|
+
},
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
return (
|
|
368
|
+
<WeaverseNextProvider client={client}>
|
|
369
|
+
<WeaverseNextRenderer data={data} />
|
|
370
|
+
</WeaverseNextProvider>
|
|
371
|
+
)
|
|
372
|
+
}
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
## Root theme provider
|
|
376
|
+
|
|
377
|
+
Global modules (`Header`, `Footer`, popups, a CSS-var bridge) render once in
|
|
378
|
+
`app/layout.tsx`, outside any route's `WeaverseNextProvider`. Mount
|
|
379
|
+
`WeaverseNextRootProvider` there so `useThemeSettings()` works for them too,
|
|
380
|
+
and so route-level providers share one theme settings store instead of each
|
|
381
|
+
creating their own — matching Hydrogen's root-owned `withWeaverse()` store.
|
|
382
|
+
|
|
383
|
+
```tsx
|
|
384
|
+
// app/layout.tsx
|
|
385
|
+
import { WeaverseNextRootProvider } from '@weaverse/next'
|
|
386
|
+
import { getWeaverseServerClient } from './weaverse-next/server'
|
|
387
|
+
|
|
388
|
+
export default async function RootLayout({
|
|
389
|
+
children,
|
|
390
|
+
}: {
|
|
391
|
+
children: React.ReactNode
|
|
392
|
+
}) {
|
|
393
|
+
let weaverse = await getWeaverseServerClient(Promise.resolve({}), '/')
|
|
394
|
+
let theme = await weaverse.loadThemeSettings()
|
|
395
|
+
|
|
396
|
+
return (
|
|
397
|
+
<html lang="en">
|
|
398
|
+
<body>
|
|
399
|
+
<WeaverseNextRootProvider
|
|
400
|
+
initialThemeSettings={theme.theme}
|
|
401
|
+
themeSchema={theme.schema}
|
|
402
|
+
>
|
|
403
|
+
<Header />
|
|
404
|
+
{children}
|
|
405
|
+
<Footer />
|
|
406
|
+
</WeaverseNextRootProvider>
|
|
407
|
+
</body>
|
|
408
|
+
</html>
|
|
409
|
+
)
|
|
410
|
+
}
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
A route's `WeaverseNextProvider` automatically adopts the root store when one
|
|
414
|
+
is mounted above it, instead of creating a second one — so Studio's live
|
|
415
|
+
edits reach both the root modules and the page renderer. The root's initial
|
|
416
|
+
theme is authoritative for SSR: routes should not load their own theme
|
|
417
|
+
settings in the final starter. If a route still carries
|
|
418
|
+
`themeSettings`/`client.themeSettings` (e.g. mid-migration), that data merges
|
|
419
|
+
into the root store in a client-only effect after mount, not during render —
|
|
420
|
+
so SSR always renders the root's value, with no render-phase mutation of the
|
|
421
|
+
shared store. Apps that don't mount `WeaverseNextRootProvider` see no
|
|
422
|
+
behavior change: `WeaverseNextProvider` falls back to creating its own store
|
|
423
|
+
and still renders `themeSettings`/`client.themeSettings` synchronously on
|
|
424
|
+
SSR, same as before.
|
|
425
|
+
|
|
426
|
+
## Studio setup
|
|
427
|
+
|
|
428
|
+
Studio has two separate pieces.
|
|
429
|
+
|
|
430
|
+
### 1. Root-level script connector
|
|
431
|
+
|
|
432
|
+
Mount `WeaverseNextStudioConnect` once near the app root. It loads the Next Studio bridge script in design/preview/revision contexts and does nothing in published mode.
|
|
433
|
+
|
|
434
|
+
```tsx
|
|
435
|
+
// app/weaverse-next/studio-connect.tsx
|
|
436
|
+
'use client'
|
|
437
|
+
|
|
438
|
+
import { WeaverseNextStudioConnect } from '@weaverse/next'
|
|
439
|
+
|
|
440
|
+
export function StudioConnect() {
|
|
441
|
+
return <WeaverseNextStudioConnect />
|
|
442
|
+
}
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
```tsx
|
|
446
|
+
// app/layout.tsx
|
|
447
|
+
import { StudioConnect } from './weaverse-next/studio-connect'
|
|
448
|
+
|
|
449
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
450
|
+
return (
|
|
451
|
+
<html lang="en">
|
|
452
|
+
<body>
|
|
453
|
+
<StudioConnect />
|
|
454
|
+
{children}
|
|
455
|
+
</body>
|
|
456
|
+
</html>
|
|
457
|
+
)
|
|
458
|
+
}
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
Mounting it in the root layout (rather than per-page) also covers App Router
|
|
462
|
+
routes that never render `WeaverseNextRenderer`, such as `not-found.tsx` and
|
|
463
|
+
`error.tsx` — Studio needs the bridge script loaded before a page tree
|
|
464
|
+
exists, and the root layout still renders around those routes. This is
|
|
465
|
+
SSR-safe: `WeaverseNextStudioConnect` only touches `document` inside a
|
|
466
|
+
`useEffect`, so it is a no-op during server rendering.
|
|
467
|
+
|
|
468
|
+
### 2. Page-level runtime binding
|
|
469
|
+
|
|
470
|
+
`WeaverseNextRenderer` creates and binds the page runtime. Internally, `WeaverseNextStudio` maps Studio internals to Next App Router:
|
|
471
|
+
|
|
472
|
+
- `internal.navigate(...)` → `router.push(...)` / `router.replace(...)`
|
|
473
|
+
- `internal.revalidate()` → `router.refresh()` fallback
|
|
474
|
+
- `internal.revalidateItem(draftItem)` → per-item loader revalidation endpoint
|
|
475
|
+
|
|
476
|
+
The root script connector and page-level renderer are both required for full Studio editing.
|
|
477
|
+
|
|
478
|
+
## Per-item Studio revalidation
|
|
479
|
+
|
|
480
|
+
Resource-picker edits should refresh only the affected item's server loader data. The happy path should not call `router.refresh()` because refreshing the whole RSC tree can remount the page and reset scroll.
|
|
481
|
+
|
|
482
|
+
Mount the route handler in the consuming app:
|
|
483
|
+
|
|
484
|
+
```ts
|
|
485
|
+
// app/api/weaverse/revalidate/route.ts
|
|
486
|
+
import { createWeaverseNextRevalidateHandler } from '@weaverse/next/server'
|
|
487
|
+
import { getWeaverseServerClient } from '../../../weaverse-next/server'
|
|
488
|
+
|
|
489
|
+
export const { POST } = createWeaverseNextRevalidateHandler({
|
|
490
|
+
getClient: () => getWeaverseServerClient(Promise.resolve({})),
|
|
491
|
+
})
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
Flow:
|
|
495
|
+
|
|
496
|
+
```text
|
|
497
|
+
Studio resource picker edit
|
|
498
|
+
-> Builder calls runtime.internal.revalidateItem(draftItem)
|
|
499
|
+
-> SDK POSTs { draftItem } to /api/weaverse/revalidate
|
|
500
|
+
-> route handler finds the registered component by draftItem.type
|
|
501
|
+
-> handler runs that component's loader with draft item data
|
|
502
|
+
-> response returns { loaderData }
|
|
503
|
+
-> SDK applies loaderData to the live item instance in place
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
If the route is missing or returns an error, Builder falls back to the older route-refresh path.
|
|
507
|
+
|
|
508
|
+
## Config resolution
|
|
509
|
+
|
|
510
|
+
Server config resolution intentionally mirrors Hydrogen where possible:
|
|
511
|
+
|
|
512
|
+
- `projectId`: `?weaverseProjectId=` → config function → config string → `WEAVERSE_PROJECT_ID`.
|
|
513
|
+
- `weaverseHost`: trusted `?weaverseHost=` over `https://*.weaverse.io` / `https://*.weaverse.dev` → `WEAVERSE_HOST` → `https://studio.weaverse.io`.
|
|
514
|
+
- API base: trusted request host → `WEAVERSE_PUBLIC_API_BASE` → non-production `WEAVERSE_HOST` → `https://api.weaverse.io`.
|
|
515
|
+
- public env: `PUBLIC_STORE_DOMAIN`, `PUBLIC_STOREFRONT_API_TOKEN`.
|
|
516
|
+
|
|
517
|
+
`WEAVERSE_API_KEY` may be read into internal base configs but is not attached to page/theme API requests and is never serialized into client-facing loader data.
|
|
518
|
+
|
|
519
|
+
## POC reference
|
|
520
|
+
|
|
521
|
+
Live POC:
|
|
522
|
+
|
|
523
|
+
- https://weaverse-hydrogen-next-poc.vercel.app
|
|
524
|
+
|
|
525
|
+
Repo:
|
|
526
|
+
|
|
527
|
+
- https://github.com/Weaverse/weaverse-hydrogen-next-poc
|
|
528
|
+
|
|
529
|
+
Important POC paths:
|
|
530
|
+
|
|
531
|
+
```text
|
|
532
|
+
app/weaverse-next/server.ts # explicit Next server context helper
|
|
533
|
+
app/weaverse-next/wrapper.tsx # client provider + renderer
|
|
534
|
+
app/weaverse-next/studio-connect.tsx # root Studio script connector
|
|
535
|
+
app/api/weaverse/revalidate/route.ts # per-item loader revalidation route
|
|
536
|
+
app/page.tsx # home page Weaverse load
|
|
537
|
+
app/products/[handle]/page.tsx # product route Weaverse load
|
|
538
|
+
app/collections/[handle]/page.tsx # collection route Weaverse load
|
|
539
|
+
```
|
|
153
540
|
|
|
154
541
|
## Current limitations
|
|
155
542
|
|
|
156
|
-
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
-
|
|
160
|
-
`next: { revalidate, tags }`; on-demand `revalidatePath`/`revalidateTag` is left
|
|
161
|
-
to the host app.
|
|
162
|
-
- `loadCustomPages` (sitemap) and SEO helpers are not ported yet.
|
|
163
|
-
- The Studio bridge currently reuses the Hydrogen bundle until a Next-specific
|
|
164
|
-
bundle is required.
|
|
165
|
-
- Package is pre-release (`alpha`); the API may still change.
|
|
543
|
+
- This package is still alpha; API names may change before stable.
|
|
544
|
+
- Next does not provide Hydrogen's global React Router loader context, so apps should keep a small `getWeaverseServerClient(...)` helper.
|
|
545
|
+
- Translation/static-text sidecar parity, multiple Weaverse runtimes on one URL, and global sections are later hardening items.
|
|
546
|
+
- Full Shopify request-handler/redirect parity is app-level follow-up work, not owned by this package yet.
|