@weaverse/next 0.1.0-alpha.2 → 0.1.0-alpha.20

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.
Files changed (107) hide show
  1. package/README.md +586 -123
  2. package/dist/chunk-LH6AAEIK.mjs +326 -0
  3. package/dist/chunk-LH6AAEIK.mjs.map +1 -0
  4. package/dist/index.js +784 -60
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +588 -58
  7. package/dist/index.mjs.map +1 -1
  8. package/dist/server.js +537 -76
  9. package/dist/server.js.map +1 -1
  10. package/dist/server.mjs +363 -27
  11. package/dist/server.mjs.map +1 -1
  12. package/dist/types/client.d.ts +16 -0
  13. package/dist/types/client.d.ts.map +1 -0
  14. package/dist/types/hooks.d.ts +20 -0
  15. package/dist/types/hooks.d.ts.map +1 -0
  16. package/dist/types/index.d.ts +34 -0
  17. package/dist/types/index.d.ts.map +1 -0
  18. package/dist/types/item.d.ts +60 -0
  19. package/dist/types/item.d.ts.map +1 -0
  20. package/dist/types/loader.d.ts +25 -0
  21. package/dist/types/loader.d.ts.map +1 -0
  22. package/dist/types/main.d.ts +11 -0
  23. package/dist/types/main.d.ts.map +1 -0
  24. package/dist/types/provider.d.ts +82 -0
  25. package/dist/types/provider.d.ts.map +1 -0
  26. package/dist/types/registry.d.ts +15 -0
  27. package/dist/types/registry.d.ts.map +1 -0
  28. package/dist/types/renderer.d.ts +21 -0
  29. package/dist/types/renderer.d.ts.map +1 -0
  30. package/dist/types/request-info.d.ts +19 -0
  31. package/dist/types/request-info.d.ts.map +1 -0
  32. package/dist/types/revalidate-item.d.ts +82 -0
  33. package/dist/types/revalidate-item.d.ts.map +1 -0
  34. package/dist/types/root-provider.d.ts +62 -0
  35. package/dist/types/root-provider.d.ts.map +1 -0
  36. package/dist/types/runtime.d.ts +113 -0
  37. package/dist/types/runtime.d.ts.map +1 -0
  38. package/dist/types/seo.d.ts +55 -0
  39. package/dist/types/seo.d.ts.map +1 -0
  40. package/dist/types/server/configs.d.ts +27 -0
  41. package/dist/types/server/configs.d.ts.map +1 -0
  42. package/dist/types/server/normalize-page-url.d.ts +19 -0
  43. package/dist/types/server/normalize-page-url.d.ts.map +1 -0
  44. package/dist/types/server/revalidate-handler.d.ts +55 -0
  45. package/dist/types/server/revalidate-handler.d.ts.map +1 -0
  46. package/dist/types/server/server-client.d.ts +21 -0
  47. package/dist/types/server/server-client.d.ts.map +1 -0
  48. package/dist/types/server.d.ts +19 -0
  49. package/dist/types/server.d.ts.map +1 -0
  50. package/dist/types/studio-bridge.d.ts +16 -0
  51. package/dist/types/studio-bridge.d.ts.map +1 -0
  52. package/dist/types/studio-connect.d.ts +27 -0
  53. package/dist/types/studio-connect.d.ts.map +1 -0
  54. package/dist/types/studio-router.d.ts +50 -0
  55. package/dist/types/studio-router.d.ts.map +1 -0
  56. package/dist/types/studio-script-src.d.ts +12 -0
  57. package/dist/types/studio-script-src.d.ts.map +1 -0
  58. package/dist/types/theme-settings-store.d.ts +18 -0
  59. package/dist/types/theme-settings-store.d.ts.map +1 -0
  60. package/dist/types/translation-context.d.ts +156 -0
  61. package/dist/types/translation-context.d.ts.map +1 -0
  62. package/dist/types/translation-store.d.ts +44 -0
  63. package/dist/types/translation-store.d.ts.map +1 -0
  64. package/dist/types/types.d.ts +665 -0
  65. package/dist/types/types.d.ts.map +1 -0
  66. package/dist/types/use-weaverse-next-studio.d.ts +34 -0
  67. package/dist/types/use-weaverse-next-studio.d.ts.map +1 -0
  68. package/dist/types/utils.d.ts +12 -0
  69. package/dist/types/utils.d.ts.map +1 -0
  70. package/package.json +12 -8
  71. package/src/client.ts +102 -0
  72. package/src/hooks.ts +50 -0
  73. package/src/index.ts +153 -0
  74. package/src/item.ts +167 -0
  75. package/src/loader.ts +135 -0
  76. package/src/main.tsx +45 -0
  77. package/src/provider.tsx +225 -0
  78. package/src/registry.ts +63 -0
  79. package/src/renderer.tsx +100 -0
  80. package/src/request-info.ts +124 -0
  81. package/src/revalidate-item.ts +211 -0
  82. package/src/root-provider.tsx +182 -0
  83. package/src/runtime.ts +555 -0
  84. package/src/seo.ts +157 -0
  85. package/src/server/configs.ts +178 -0
  86. package/src/server/normalize-page-url.ts +73 -0
  87. package/src/server/revalidate-handler.ts +357 -0
  88. package/src/server/server-client.ts +728 -0
  89. package/src/server.ts +67 -0
  90. package/src/studio-bridge.tsx +82 -0
  91. package/src/studio-connect.tsx +88 -0
  92. package/src/studio-router.ts +85 -0
  93. package/src/studio-script-src.ts +128 -0
  94. package/src/theme-settings-store.ts +47 -0
  95. package/src/translation-context.tsx +299 -0
  96. package/src/translation-store.ts +69 -0
  97. package/src/types.ts +753 -0
  98. package/src/use-weaverse-next-studio.tsx +106 -0
  99. package/src/utils.ts +37 -0
  100. package/dist/chunk-SS6QUKYY.mjs +0 -149
  101. package/dist/chunk-SS6QUKYY.mjs.map +0 -1
  102. package/dist/index.d.mts +0 -250
  103. package/dist/index.d.ts +0 -250
  104. package/dist/server.d.mts +0 -60
  105. package/dist/server.d.ts +0 -60
  106. package/dist/types-DvEeLX6L.d.mts +0 -321
  107. package/dist/types-DvEeLX6L.d.ts +0 -321
package/README.md CHANGED
@@ -1,165 +1,628 @@
1
1
  # @weaverse/next
2
2
 
3
- Next.js App Router integration for building Weaverse-powered storefronts.
3
+ Next.js App Router integration for Weaverse-powered Hydrogen storefronts.
4
4
 
5
- This package adapts the Weaverse runtime (component registry, page-tree
6
- rendering, Studio bridge) to Next.js without depending on React Router or
7
- Hydrogen. It mirrors the `@weaverse/hydrogen` contract where it makes sense, but
8
- takes plain, framework-neutral inputs (URL, headers, search params) instead of a
9
- React Router `Request`/`LoaderFunctionArgs`.
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. Where does a route load page + Weaverse data?
49
+
50
+ The route pattern is:
51
+
52
+ ```tsx
53
+ // app/page.tsx
54
+ import { loadWeaverseHomePage } from './weaverse-next/server'
55
+ import { WeaverseHome } from './weaverse-next/wrapper'
56
+
57
+ export default async function Home(props: {
58
+ searchParams: Promise<Record<string, string | string[] | undefined>>
59
+ }) {
60
+ let weaverseData = await loadWeaverseHomePage(props.searchParams)
61
+ return <WeaverseHome data={weaverseData} />
62
+ }
63
+ ```
64
+
65
+ ### 4. Is there a global app load context like Hydrogen/Pilot?
66
+
67
+ Not automatically from the framework.
68
+
69
+ 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.
70
+
71
+ Name it whatever you like — `getWeaverseServerClient(...)` in `app/weaverse-next/server.ts` below. It plays the same role as Hydrogen's global load context, but explicitly:
72
+
73
+ ```ts
74
+ let weaverse = await getWeaverseServerClient(searchParams, pathname)
75
+ let page = await weaverse.loadPage({ type: 'INDEX' })
76
+ ```
77
+
78
+ So the mental model is still `weaverse.loadPage(...)`; only the injection mechanism changes.
10
79
 
11
80
  ## Entrypoints
12
81
 
13
82
  | Import | Use in | Purpose |
14
83
  | --- | --- | --- |
15
- | `@weaverse/next` | Client & server | Registry, renderer, provider, hooks, Studio runtime/bridge. |
16
- | `@weaverse/next/server` | Server only | Server-side Weaverse client that fetches page/theme data from the Weaverse API. |
84
+ | `@weaverse/next` | Client and shared code | Component registry, provider, renderer, hooks, Studio runtime/bind components, schema re-exports. |
85
+ | `@weaverse/next/server` | Server only | Real Weaverse API client, config resolution, page URL normalization, per-item revalidation route handler. |
17
86
 
18
- > The server-side data-loading API lives under **`@weaverse/next/server`** so the
19
- > root entry stays free of server-only fetch assumptions and safe to import from
20
- > client components.
87
+ 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
88
 
22
89
  ## Package structure
23
90
 
24
91
  ```text
25
92
  packages/next/src/
26
- index.ts # root (@weaverse/next) public exports
27
- server.ts # server (@weaverse/next/server) public exports
28
- client.ts # createWeaverseNextClient (injected-fetcher client)
29
- loader.ts # runWeaverseComponentLoaders (server-side section loaders)
30
- registry.ts # register components with the Weaverse element registry
31
- renderer.tsx # WeaverseNextRenderer (page-tree → React)
32
- provider.tsx # WeaverseNextProvider (client context for hooks)
33
- hooks.ts # useThemeSettings / useWeaversePageData / ...
34
- runtime.ts # WeaverseNextRuntime + Studio binding
35
- studio-bridge.tsx # Studio script injection / connect
36
- request-info.ts # buildWeaverseNextRequestInfo (Studio-compatible)
37
- theme-settings-store.ts
38
- types.ts # shared types (root + server)
93
+ index.ts # root public exports
94
+ server.ts # server public exports
95
+ client.ts # createWeaverseNextClient for client/runtime state
96
+ loader.ts # runWeaverseComponentLoaders
97
+ registry.ts # component registration for @weaverse/react
98
+ renderer.tsx # WeaverseNextRenderer
99
+ provider.tsx # WeaverseNextProvider
100
+ hooks.ts # Next adapter hooks
101
+ runtime.ts # WeaverseNextRuntime + Studio binding
102
+ studio-connect.tsx # root Studio script loader
103
+ studio-bridge.tsx # page-level Studio runtime binder
104
+ studio-router.ts # Next router -> Studio internal callbacks
105
+ use-weaverse-next-studio.tsx # Next-native Studio binder using next/navigation
106
+ revalidate-item.ts # client per-item loader revalidation
107
+ request-info.ts # Studio-compatible request info
108
+ seo.ts # Next Metadata-compatible page SEO helpers
109
+ theme-settings-store.ts # Studio-compatible theme settings store
110
+ types.ts # shared public types
39
111
  server/
40
- server-client.ts # createWeaverseNextServerClient
41
- configs.ts # getWeaverseNextConfigs (host/api-base/flags/env)
42
- normalize-page-url.ts
112
+ server-client.ts # createWeaverseNextServerClient
113
+ revalidate-handler.ts # createWeaverseNextRevalidateHandler
114
+ configs.ts # host/api/env/design-mode config resolution
115
+ normalize-page-url.ts # stable page URL normalization
43
116
  ```
44
117
 
45
118
  ## Root API (`@weaverse/next`)
46
119
 
47
- - `createWeaverseNextClient(config)` — request-safe client that delegates network
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`).
120
+ Main exports:
121
+
122
+ - `createWeaverseNextClient(config)` — client/runtime state holder. It registers client render components and carries request/page/theme context.
123
+ - `WeaverseNextProvider` — React provider for the client runtime.
124
+ - `WeaverseNextRenderer` — renders a Weaverse page tree into React and binds the Studio runtime.
125
+ - `runWeaverseComponentLoaders(args)` — walks a page tree and runs component `loader` functions server-side.
126
+ - `WeaverseNextStudioConnect` — root client component that loads the Next Studio bridge script in design/preview contexts.
127
+ - `WeaverseNextStudio` / `WeaverseNextStudioBridge` — Next-native page-level Studio runtime binding.
128
+ - `createWeaverseNextRuntime` / `bindWeaverseNextStudioRuntime` — lower-level runtime helpers.
129
+ - `revalidateWeaverseNextItem()` — client-side per-item revalidation helper wired by `WeaverseNextStudio`.
130
+ - Hooks: `useThemeSettings`, `useWeaversePageData`, `useWeaverseRootData`, `useWeaverseCommerce`.
131
+ - Re-exported migration helpers from `@weaverse/react`: `useWeaverse`, `useParentInstance`, `useItemInstance`, `useChildInstances`.
132
+ - Full `@weaverse/schema` surface, including `createSchema`, schema types, validators, and schema helper utilities.
133
+ - `generateDataFromSchema(schema)` — framework-neutral default-setting extraction utility shared with component/theme schema flows.
134
+ - Public `WeaverseNextThemeSchema` type for root/server theme settings config.
60
135
 
61
136
  ## Server API (`@weaverse/next/server`)
62
137
 
63
- `createWeaverseNextServerClient(config)` returns a client that performs the real
64
- Weaverse public API fetch — the equivalent of Hydrogen/Pilot's
65
- `context.weaverse.loadPage(...)`.
138
+ Main exports:
66
139
 
67
- ### Next App Router server context model
140
+ - `createWeaverseNextServerClient(config)` — request-scoped server client.
141
+ - `createWeaverseNextRevalidateHandler(config)` — route-handler factory for Studio per-item loader revalidation.
142
+ - `getWeaverseNextConfigs(...)` — config/env/query resolution.
143
+ - `normalizeNextPageUrl(...)` / `resolveRequestUrl(...)` — stable page URL helpers.
144
+ - `getWeaverseNextSeoMetadata(...)` / `formatWeaverseNextSeoMetadata(...)` — convert Weaverse `page.seo` into a Next Metadata-compatible object.
68
145
 
69
- Hydrogen's classic storefront runtime is built on React Router. Its request
70
- handler accepts `getLoadContext(request)`, creates a Hydrogen context, and React
71
- Router injects that object into every route loader. That is why a Pilot route can
72
- call `context.weaverse.loadPage(...)` directly.
146
+ ### `createWeaverseNextServerClient`
73
147
 
74
- Next App Router does not inject an app-defined global loader context into
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.
148
+ Example helper for a Next route/server component boundary:
80
149
 
81
- So the Next equivalent is explicit server-client creation at the route boundary:
150
+ This helper is app-level (not exported by `@weaverse/next`); it plays the role
151
+ of Hydrogen's global load context, but the app owns route identity explicitly.
152
+ The App Router boundary supplies `pathname`, `pageType`, and `handle`, so the
153
+ same identity reaches page loads and per-item revalidation loaders. It uses the
154
+ exported `PageType` and `WeaverseNextRequestContext` types. The module defines
155
+ both helpers used by the route-handler example below:
82
156
 
83
157
  ```ts
84
158
  import { createWeaverseNextServerClient } from '@weaverse/next/server'
85
- import { components } from '@/weaverse/components'
159
+ import type { PageType, WeaverseNextRequestContext } from '@weaverse/next'
160
+ import { headers } from 'next/headers'
161
+ import { serverComponents } from './server-components'
162
+ import { getStaticStorefrontClient } from './storefront'
86
163
 
87
- export async function loadWeaverse(request: Request, storefront: Storefront) {
88
- let url = new URL(request.url)
89
- let weaverse = createWeaverseNextServerClient({
90
- projectId: process.env.WEAVERSE_PROJECT_ID, // string | (ctx) => string | Promise<string>
91
- components,
92
- themeSchema,
93
- commerce: { storefront },
164
+ export async function getWeaverseServerClient(
165
+ searchParamsPromise: Promise<Record<string, string | string[] | undefined>>,
166
+ pathname = '/',
167
+ // Route identity the App Router segment owns. Seeding it here means loaders
168
+ // resolve the same page type / handle during SSR and per-item revalidation.
169
+ identity: { pageType?: PageType; handle?: string } = {}
170
+ ) {
171
+ let [headersList, rawSearchParams] = await Promise.all([
172
+ headers(),
173
+ searchParamsPromise,
174
+ ])
175
+ let requestHeaders = new Headers(Object.fromEntries(headersList.entries()))
176
+ let searchParams = new URLSearchParams()
177
+
178
+ for (let [key, value] of Object.entries(rawSearchParams)) {
179
+ if (Array.isArray(value)) {
180
+ for (let item of value) searchParams.append(key, item)
181
+ } else if (value !== undefined) {
182
+ searchParams.set(key, value)
183
+ }
184
+ }
185
+
186
+ let url = new URL(
187
+ `${pathname}?${searchParams}`,
188
+ `${requestHeaders.get('x-forwarded-proto') ?? 'https'}://${
189
+ requestHeaders.get('x-forwarded-host') ?? requestHeaders.get('host')
190
+ }`
191
+ )
192
+
193
+ let requestContext: WeaverseNextRequestContext = {
194
+ url,
195
+ headers: requestHeaders,
196
+ searchParams,
197
+ pathname,
198
+ pageType: identity.pageType,
199
+ handle: identity.handle,
200
+ i18n: { country: 'US', language: 'EN', locale: 'en-US' },
201
+ }
202
+
203
+ return createWeaverseServerClientFromContext(requestContext)
204
+ }
205
+
206
+ // Explicit-context path used by the revalidation callback. The context has
207
+ // already been validated by the SDK handler; project/host/env remain app-owned.
208
+ export function createWeaverseServerClientFromContext(
209
+ requestContext: WeaverseNextRequestContext
210
+ ) {
211
+ // A localized app should select/cache this client from its own supported
212
+ // market allowlist using requestContext.i18n. This compact example uses the
213
+ // default market configured by the app helper.
214
+ let storefront = getStaticStorefrontClient()
215
+
216
+ return createWeaverseNextServerClient({
217
+ components: serverComponents,
218
+ projectId: process.env.WEAVERSE_PROJECT_ID,
219
+ weaverseHost: process.env.WEAVERSE_HOST,
94
220
  env: process.env,
95
- requestContext: {
96
- url,
97
- headers: request.headers,
98
- searchParams: url.searchParams,
99
- i18n: storefront.i18n,
100
- },
221
+ commerce: storefront
222
+ ? {
223
+ storefront: {
224
+ i18n: { country: 'US', language: 'EN' },
225
+ query: async (query, options) => {
226
+ let response = await storefront.graphql(query, options as never)
227
+ return response.data
228
+ },
229
+ },
230
+ }
231
+ : undefined,
232
+ requestContext,
101
233
  cache: { revalidate: 60, tags: ['weaverse'] },
102
234
  })
103
-
104
- let [page, theme] = await Promise.all([
105
- weaverse.loadPage({ type: 'INDEX' }),
106
- weaverse.loadThemeSettings(),
107
- ])
108
- return { page, theme, weaverse }
109
235
  }
110
236
  ```
111
237
 
112
- ### Returned client
238
+ Then each route stays close to the Hydrogen mental model while supplying the
239
+ route identity the segment owns — an `INDEX` home route and a handle-bearing
240
+ `PRODUCT` route:
241
+
242
+ ```ts
243
+ // app/page.tsx — the INDEX segment owns the page type.
244
+ let weaverse = await getWeaverseServerClient(props.searchParams, '/', {
245
+ pageType: 'INDEX',
246
+ })
247
+ let data = await weaverse.loadPage({ type: 'INDEX' })
248
+ let theme = await weaverse.loadThemeSettings()
249
+ ```
250
+
251
+ ```ts
252
+ // app/products/[handle]/page.tsx — the PRODUCT segment supplies the handle.
253
+ let { handle } = await props.params
254
+ let weaverse = await getWeaverseServerClient(
255
+ props.searchParams,
256
+ `/products/${handle}`,
257
+ { pageType: 'PRODUCT', handle }
258
+ )
259
+ let data = await weaverse.loadPage({ type: 'PRODUCT', handle })
260
+ ```
261
+
262
+ ### Server client members
113
263
 
114
264
  | Member | Description |
115
265
  | --- | --- |
116
- | `loadPage(input?)` | POSTs `{ projectId, url, i18n, params, isDesignMode }` to `/api/public/project`, runs component loaders, returns `WeaverseNextLoaderData` (`page`, `project`, `pageAssignment`, `configs.requestInfo`). In Studio preview mode (`?isPreviewMode=true`) it short-circuits to a synthetic single-section page built from the `sectionType` presets — no fetch, no `projectId` required. A route-level `projectId` override is propagated to `client.projectId`, loaders, and the returned `configs`. |
117
- | `loadThemeSettings(options?)` | POSTs to `/api/public/project_configs`, merges `themeSchema` defaults under `theme`, includes `staticContent`; falls back to defaults on failure. |
118
- | `fetchWithCache<T>(url, options?)` | Mode-aware fetch: `cache: 'no-store'` in design/revision, `next: { revalidate, tags }` in published mode. |
119
- | `projectId` / `resolveProjectId()` | Resolved project id (query → function → string → env). |
120
- | `components`, `commerce`, `storefront`, `requestContext`, `themeSchema`, `themeSettings`, `data`, `dataContext`, `configs` | Readonly request state. `storefront` aliases `commerce.storefront`. |
121
-
122
- ### Config resolution
123
-
124
- Close to Hydrogen's `getWeaverseConfigs`:
125
-
126
- - **projectId** — `?weaverseProjectId=` query → config function → config string →
127
- `WEAVERSE_PROJECT_ID` env.
128
- - **weaverseHost** — trusted request `?weaverseHost=` (only `weaverse.io` /
129
- `weaverse.dev` over https) → `WEAVERSE_HOST` env → `https://studio.weaverse.io`.
130
- - **API base** — trusted request host → `WEAVERSE_PUBLIC_API_BASE` env → custom
131
- non-production `WEAVERSE_HOST` → `https://api.weaverse.io`.
132
- - **publicEnv** — `PUBLIC_STORE_DOMAIN`, `PUBLIC_STOREFRONT_API_TOKEN`.
133
-
134
- `WEAVERSE_API_KEY` is read from the request query / env and kept on the internal
135
- base configs, but the current server client does **not** attach it to the
136
- page/theme API requests. It is also **never** serialized into client-facing
137
- `configs` / loader data, so it cannot leak to the browser.
138
-
139
- ## Studio helpers
140
-
141
- Design/preview detection, the Studio bridge script, and the in-page runtime are
142
- shared with the root entry: `createWeaverseNextRuntime` + `WeaverseNextProvider`
143
- hydrate Studio, and `bindWeaverseNextStudioRuntime` wires live updates. The
144
- server client emits `configs.requestInfo` and design-mode flags the bridge
145
- expects.
146
-
147
- > **Mount `WeaverseNextStudioConnect` at your root/client shell.**
148
- > `WeaverseNextRenderer` mounts the in-page runtime bridge but does **not**
149
- > inject the Studio bridge script by itself. The script (which lets Studio's
150
- > `checkWeaversePage()` handshake answer on every route, including content-less
151
- > ones) is loaded by `WeaverseNextStudioConnect`, so it must be rendered once at
152
- > the app root/client shell for the Studio editor to connect.
266
+ | `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. |
267
+ | `loadThemeSettings(options?)` | Fetches `/api/public/project_configs`, merges schema defaults under merchant settings, returns theme settings/static content/schema data for design mode. When the theme schema declares `i18n` and `requestContext.i18n` supplies both `language` and `country`, it also fetches locale static-text overrides from `/api/translation/static` in parallel and returns them as `merchantOverrides`. A failed or skipped overrides fetch never fails theme settings. |
268
+ | `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. |
269
+ | `fetchWithCache<T>(url, options?)` | Next-aware fetch helper. Uses `cache: 'no-store'` in design/revision preview and `next: { revalidate, tags }` in published mode. |
270
+ | `resolveProjectId()` / `projectId` | Resolves project id from Studio query, config function/string, or env. |
271
+ | `components` | Server component registry. Components may include `schema` and optional `loader`. |
272
+ | `commerce` / `storefront` | Commerce context. `storefront` is a compatibility alias for Pilot-style loaders. |
273
+ | `requestContext` | URL, headers, search params, i18n, and design/preview flags. |
274
+
275
+ ### Component loaders
276
+
277
+ A registered component may expose a server-side `loader`:
278
+
279
+ ```ts
280
+ export let productHero = {
281
+ schema: productHeroSchema,
282
+ loader: async ({ data, commerce, weaverse }) => {
283
+ let handle = data.product?.handle
284
+ if (!handle) return null
285
+
286
+ return commerce?.storefront?.query(PRODUCT_QUERY, {
287
+ variables: { handle },
288
+ })
289
+ },
290
+ }
291
+ ```
292
+
293
+ Loader args match the page-load and revalidation paths:
294
+
295
+ - `data` — schema defaults merged under item data.
296
+ - `commerce` — explicit commerce context.
297
+ - `weaverse` — server/client object with `weaverse.storefront` compatibility alias.
298
+ - `context` — request context.
299
+
300
+ ### SEO metadata
301
+
302
+ Use the server entry in `generateMetadata()` to map Weaverse `page.seo` into a Next Metadata-compatible object:
303
+
304
+ ```ts
305
+ // app/pages/[handle]/page.tsx
306
+ import type { Metadata } from 'next'
307
+ import { getWeaverseNextSeoMetadata } from '@weaverse/next/server'
308
+
309
+ export async function generateMetadata(props: {
310
+ params: Promise<{ handle: string }>
311
+ searchParams: Promise<Record<string, string | string[] | undefined>>
312
+ }): Promise<Metadata> {
313
+ let { handle } = await props.params
314
+ let weaverse = await getWeaverseServerClient(props.searchParams, `/pages/${handle}`)
315
+ let data = await weaverse.loadPage({ type: 'PAGE', handle })
316
+ return getWeaverseNextSeoMetadata(data)
317
+ }
318
+ ```
319
+
320
+ The helper is pure and has no runtime dependency on `next`; it only imports the
321
+ `Metadata` type. Its return value is directly assignable to Next `Metadata`.
322
+
323
+ Two Builder-only SEO values are normalized so Next can always render the result:
324
+
325
+ - Open Graph type `product` falls back to `website` (Next throws on `product`).
326
+ The other Builder types — `website`, `article`, `profile`, `video.other` — are
327
+ passed through unchanged.
328
+ - Twitter card types `app` and `player` fall back to `summary`, because Builder
329
+ does not collect the extra descriptors those cards need (Next throws without
330
+ the `app` fields, and a `player` card without a player URL renders empty).
331
+
332
+ ### Custom pages for sitemap generation
333
+
334
+ `fetchCustomPages()` mirrors Hydrogen's sitemap helper and accepts Next cache knobs:
335
+
336
+ ```ts
337
+ let weaverse = await getWeaverseServerClient(Promise.resolve({}), '/')
338
+ let pages = await weaverse.fetchCustomPages({
339
+ locale: 'en-us',
340
+ limit: 100,
341
+ revalidate: 3600,
342
+ tags: ['weaverse:custom-pages'],
343
+ })
344
+ ```
345
+
346
+ It paginates until the API returns `nextCursor: null`; if a later page fails, it returns the pages already fetched instead of throwing.
347
+
348
+ ## Rendering in a Next route
349
+
350
+ Typical shape:
351
+
352
+ ```tsx
353
+ // app/page.tsx
354
+ export default async function Home(props: {
355
+ searchParams: Promise<Record<string, string | string[] | undefined>>
356
+ }) {
357
+ let data = await loadWeaverseHomePage(props.searchParams)
358
+ return <WeaversePage data={data} />
359
+ }
360
+ ```
361
+
362
+ ```tsx
363
+ // app/weaverse-page.tsx
364
+ 'use client'
365
+
366
+ import {
367
+ createWeaverseNextClient,
368
+ WeaverseNextProvider,
369
+ WeaverseNextRenderer,
370
+ } from '@weaverse/next'
371
+ import type {
372
+ WeaverseNextLoaderData,
373
+ WeaverseNextRequestInfo,
374
+ } from '@weaverse/next'
375
+ import { components } from './components'
376
+
377
+ export function WeaversePage({ data }: { data: WeaverseNextLoaderData }) {
378
+ // The server client returns `requestInfo` in the exported shape, so restore
379
+ // the full route identity — including `pageType` and `handle` — into the
380
+ // client request context instead of a narrow inline subset.
381
+ let requestInfo = data.configs?.requestInfo as
382
+ | WeaverseNextRequestInfo
383
+ | undefined
384
+ let projectId =
385
+ (data.configs?.projectId as string | undefined) ?? data.project?.id
386
+ if (!projectId) throw new Error('Missing Weaverse project id')
387
+
388
+ let client = createWeaverseNextClient({
389
+ projectId,
390
+ components,
391
+ requestContext: {
392
+ pathname: requestInfo?.pathname ?? '/',
393
+ searchParams: new URLSearchParams(requestInfo?.search ?? ''),
394
+ isDesignMode: Boolean(data.configs?.isDesignMode),
395
+ isPreviewMode: Boolean(data.configs?.isPreviewMode),
396
+ isRevisionPreview: Boolean(data.configs?.isRevisionPreview),
397
+ i18n: requestInfo?.i18n,
398
+ pageType: requestInfo?.pageType,
399
+ handle: requestInfo?.handle,
400
+ },
401
+ })
402
+
403
+ return (
404
+ <WeaverseNextProvider client={client}>
405
+ <WeaverseNextRenderer data={data} />
406
+ </WeaverseNextProvider>
407
+ )
408
+ }
409
+ ```
410
+
411
+ ## Root theme provider
412
+
413
+ Global modules (`Header`, `Footer`, popups, a CSS-var bridge) render once in
414
+ `app/layout.tsx`, outside any route's `WeaverseNextProvider`. Mount
415
+ `WeaverseNextRootProvider` there so `useThemeSettings()` works for them too,
416
+ and so route-level providers share one theme settings store instead of each
417
+ creating their own — matching Hydrogen's root-owned `withWeaverse()` store.
418
+
419
+ ```tsx
420
+ // app/layout.tsx
421
+ import { WeaverseNextRootProvider } from '@weaverse/next'
422
+ import { getWeaverseServerClient } from './weaverse-next/server'
423
+
424
+ export default async function RootLayout({
425
+ children,
426
+ }: {
427
+ children: React.ReactNode
428
+ }) {
429
+ let weaverse = await getWeaverseServerClient(Promise.resolve({}), '/')
430
+ let theme = await weaverse.loadThemeSettings()
431
+
432
+ return (
433
+ <html lang="en">
434
+ <body>
435
+ <WeaverseNextRootProvider
436
+ initialThemeSettings={theme.theme}
437
+ themeSchema={theme.schema}
438
+ >
439
+ <Header />
440
+ {children}
441
+ <Footer />
442
+ </WeaverseNextRootProvider>
443
+ </body>
444
+ </html>
445
+ )
446
+ }
447
+ ```
448
+
449
+ A route's `WeaverseNextProvider` automatically adopts the root store when one
450
+ is mounted above it, instead of creating a second one — so Studio's live
451
+ edits reach both the root modules and the page renderer. The root's initial
452
+ theme is authoritative for SSR: routes should not load their own theme
453
+ settings in the final starter. If a route still carries
454
+ `themeSettings`/`client.themeSettings` (e.g. mid-migration), that data merges
455
+ into the root store in a client-only effect after mount, not during render —
456
+ so SSR always renders the root's value, with no render-phase mutation of the
457
+ shared store. Apps that don't mount `WeaverseNextRootProvider` see no
458
+ behavior change: `WeaverseNextProvider` falls back to creating its own store
459
+ and still renders `themeSettings`/`client.themeSettings` synchronously on
460
+ SSR, same as before.
461
+
462
+ ## Studio setup
463
+
464
+ Studio has two separate pieces.
465
+
466
+ ### 1. Root-level script connector
467
+
468
+ 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.
469
+
470
+ ```tsx
471
+ // app/weaverse-next/studio-connect.tsx
472
+ 'use client'
473
+
474
+ import { WeaverseNextStudioConnect } from '@weaverse/next'
475
+
476
+ export function StudioConnect() {
477
+ return <WeaverseNextStudioConnect />
478
+ }
479
+ ```
480
+
481
+ ```tsx
482
+ // app/layout.tsx
483
+ import { StudioConnect } from './weaverse-next/studio-connect'
484
+
485
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
486
+ return (
487
+ <html lang="en">
488
+ <body>
489
+ <StudioConnect />
490
+ {children}
491
+ </body>
492
+ </html>
493
+ )
494
+ }
495
+ ```
496
+
497
+ Mounting it in the root layout (rather than per-page) also covers App Router
498
+ routes that never render `WeaverseNextRenderer`, such as `not-found.tsx` and
499
+ `error.tsx` — Studio needs the bridge script loaded before a page tree
500
+ exists, and the root layout still renders around those routes. This is
501
+ SSR-safe: `WeaverseNextStudioConnect` only touches `document` inside a
502
+ `useEffect`, so it is a no-op during server rendering.
503
+
504
+ ### 2. Page-level runtime binding
505
+
506
+ `WeaverseNextRenderer` creates and binds the page runtime. Internally, `WeaverseNextStudio` maps Studio internals to Next App Router:
507
+
508
+ - `internal.navigate(...)` → `router.push(...)` / `router.replace(...)`
509
+ - `internal.revalidate()` → `router.refresh()` fallback
510
+ - `internal.revalidateItem(draftItem)` → per-item loader revalidation endpoint
511
+
512
+ The root script connector and page-level renderer are both required for full Studio editing.
513
+
514
+ ## Per-item Studio revalidation
515
+
516
+ 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.
517
+
518
+ Mount the route handler in the consuming app. `getClient` receives a second
519
+ argument — the validated, same-origin route context reconstructed from the
520
+ request body — so the revalidated loader runs with the same pathname, locale,
521
+ page type, handle, and ordinary search params as the active storefront route.
522
+ The two server-client helpers below are app-level consumer helpers, not exports
523
+ from `@weaverse/next`:
524
+
525
+ ```ts
526
+ // app/api/weaverse/revalidate/route.ts
527
+ import { createWeaverseNextRevalidateHandler } from '@weaverse/next/server'
528
+ import {
529
+ createWeaverseServerClientFromContext,
530
+ getWeaverseServerClient,
531
+ } from '../../../weaverse-next/server'
532
+
533
+ export const { POST } = createWeaverseNextRevalidateHandler({
534
+ // `requestContext` is validated browser input for route identity only.
535
+ // Project ID, Studio host, API base, and env must still come from
536
+ // server config — never from this context — and it is `undefined` for
537
+ // legacy request bodies that carry no route context.
538
+ getClient: (_request, requestContext) =>
539
+ requestContext
540
+ ? createWeaverseServerClientFromContext(requestContext)
541
+ : getWeaverseServerClient(Promise.resolve({})),
542
+ })
543
+ ```
544
+
545
+ The existing one-argument callback still works. A handler mounted with a
546
+ `(request) => client` callback ignores the optional second argument and keeps
547
+ compiling and running, so old wiring needs no change:
548
+
549
+ ```ts
550
+ export const { POST } = createWeaverseNextRevalidateHandler({
551
+ getClient: () => getWeaverseServerClient(Promise.resolve({})),
552
+ })
553
+ ```
554
+
555
+ Flow:
556
+
557
+ ```text
558
+ Studio resource picker edit
559
+ -> Builder calls runtime.internal.revalidateItem(draftItem) (unchanged)
560
+ -> SDK snapshots runtime.requestInfo, strips server-owned/transient controls
561
+ -> SDK POSTs { draftItem, routeContext? } to /api/weaverse/revalidate
562
+ -> handler validates + sanitizes routeContext at the trust boundary
563
+ -> handler reconstructs a same-origin WeaverseNextRequestContext
564
+ -> getClient(request, requestContext) builds the route-aware server client
565
+ -> route handler finds the registered component by draftItem.type
566
+ -> handler runs that component's loader with draft data + client.requestContext
567
+ -> response returns { loaderData } with Cache-Control: no-store
568
+ -> SDK applies loaderData to the live item instance in place
569
+ ```
570
+
571
+ If the route is missing or returns an error, Builder falls back to the older
572
+ route-refresh path.
573
+
574
+ ### Trust boundary
575
+
576
+ The revalidation endpoint is public and every JSON field is attacker-controlled.
577
+ The SDK sanitizes `routeContext` on the client for clean traffic, but the route
578
+ handler is the security boundary and re-validates independently:
579
+
580
+ - Only `pathname`, sanitized `search`, a narrow i18n subset, a
581
+ `PageTypeSchema`-valid `pageType`, and a bounded `handle` cross the boundary.
582
+ Headers, cookies, auth, env, project ID, Studio host, API base, commerce
583
+ clients, the runtime, and the client are never serialized.
584
+ - Server-owned controls (`weaverseProjectId`, `weaverseHost`, `weaverseApiBase`,
585
+ `weaversePublicApiBase`, `weaverseVersion`, `projectId`) and transient transport
586
+ controls (`weaverseDraftItem`, `__weaverseDraftItem`, `_rsc`) are stripped
587
+ case-insensitively on both sides, so a crafted body cannot influence server
588
+ config resolution.
589
+ - The origin is fixed from the endpoint request before assigning any
590
+ browser-provided pathname/search, so input cannot change protocol, host, port,
591
+ or credentials. `pathname` and `url.pathname` share one URL-canonicalized value.
592
+ - A malformed present `routeContext` returns `400 { error: 'invalid-route-context' }`
593
+ before `getClient` runs; a missing `routeContext` is valid legacy input and
594
+ passes `undefined` to the callback.
595
+ - Route context is routing input, never an authorization decision. A registered
596
+ loader must not treat pathname, handle, page type, locale, or search as proof
597
+ of access; customer/private data still requires independent server-side session
598
+ and authorization checks derived from the actual request.
599
+
600
+ ## Config resolution
601
+
602
+ Server config resolution intentionally mirrors Hydrogen where possible:
603
+
604
+ - `projectId`: `?weaverseProjectId=` → config function → config string → `WEAVERSE_PROJECT_ID`.
605
+ - `weaverseHost`: trusted `?weaverseHost=` over `https://*.weaverse.io` / `https://*.weaverse.dev` → `WEAVERSE_HOST` → `https://studio.weaverse.io`.
606
+ - API base: trusted request host → `WEAVERSE_PUBLIC_API_BASE` → non-production `WEAVERSE_HOST` → `https://api.weaverse.io`.
607
+ - public env: `PUBLIC_STORE_DOMAIN`, `PUBLIC_STOREFRONT_API_TOKEN`.
608
+
609
+ ## Alpha migration notes
610
+
611
+ ### After `0.1.0-alpha.9`
612
+
613
+ - The root `@weaverse/next` entry re-exports the full `@weaverse/schema` surface for Hydrogen migration parity. This includes `createSchema`, schema types, validators, and helper utilities.
614
+ - Theme schema fields are now structurally typed with `WeaverseNextThemeSchema` instead of `unknown` across client/root/server theme settings APIs. Most existing schema objects remain assignable, but alpha consumers with very loose local schema types may need to annotate/cast them before the next publish.
615
+ - `generateDataFromSchema` is public from the root entry and accepts component schemas or `WeaverseNextThemeSchema`.
616
+ - Version bumping still happens in the release flow; do not publish another alpha without bumping `packages/next/package.json` first.
617
+
618
+ ### Bundle note
619
+
620
+ The Next package build keeps `@weaverse/schema` as an external package re-export. It does not inline Zod/schema validators into `@weaverse/next`'s generated `dist/index.*` bundles. App-level bundle size still depends on what a consuming Next app imports and how its bundler tree-shakes `@weaverse/schema`.
153
621
 
154
622
  ## Current limitations
155
623
 
156
- - Server fetch has a timeout + single attempt; it does not yet replicate
157
- Hydrogen's subrequest cache, retry/backoff, or merchant-overrides translation
158
- fetch.
159
- - Next caching is expressed only via `cache: 'no-store'` and
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.
624
+ - This package is still alpha; API names may change before stable.
625
+ - Next does not provide Hydrogen's global React Router loader context, so apps should keep a small `getWeaverseServerClient(...)` helper.
626
+ - Apps must still wire their App Router route context explicitly for page loads and per-item revalidation; validate product/collection/custom/locale routes in the starter before treating them as production-ready.
627
+ - Global sections are Builder-owned; `@weaverse/next` should only need fixes if manual Studio QA finds a rendering/runtime boundary issue.
628
+ - Full Shopify request-handler/redirect parity is app-level follow-up work, not owned by this package yet.