@weaverse/next 0.1.0-alpha.8 → 1.0.0

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 (106) hide show
  1. package/README.md +155 -73
  2. package/dist/{chunk-6PMZ77RA.mjs → chunk-LH6AAEIK.mjs} +115 -8
  3. package/dist/chunk-LH6AAEIK.mjs.map +1 -0
  4. package/dist/index.js +260 -30
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +167 -46
  7. package/dist/index.mjs.map +1 -1
  8. package/dist/server.js +321 -91
  9. package/dist/server.js.map +1 -1
  10. package/dist/server.mjs +205 -28
  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-6PMZ77RA.mjs.map +0 -1
  101. package/dist/index.d.mts +0 -517
  102. package/dist/index.d.ts +0 -517
  103. package/dist/seo-CpN6tC36.d.mts +0 -477
  104. package/dist/seo-CpN6tC36.d.ts +0 -477
  105. package/dist/server.d.mts +0 -99
  106. package/dist/server.d.ts +0 -99
package/README.md CHANGED
@@ -45,30 +45,7 @@ Implementation lives in [`src/server/server-client.ts`](./src/server/server-clie
45
45
  - forces `no-store` for design/revision preview reads;
46
46
  - returns safe client-facing configs including `requestInfo`.
47
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
48
+ ### 3. Where does a route load page + Weaverse data?
72
49
 
73
50
  The route pattern is:
74
51
 
@@ -85,13 +62,13 @@ export default async function Home(props: {
85
62
  }
86
63
  ```
87
64
 
88
- ### 5. Is there a global app load context like Hydrogen/Pilot?
65
+ ### 4. Is there a global app load context like Hydrogen/Pilot?
89
66
 
90
67
  Not automatically from the framework.
91
68
 
92
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.
93
70
 
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:
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:
95
72
 
96
73
  ```ts
97
74
  let weaverse = await getWeaverseServerClient(searchParams, pathname)
@@ -152,7 +129,9 @@ Main exports:
152
129
  - `revalidateWeaverseNextItem()` — client-side per-item revalidation helper wired by `WeaverseNextStudio`.
153
130
  - Hooks: `useThemeSettings`, `useWeaversePageData`, `useWeaverseRootData`, `useWeaverseCommerce`.
154
131
  - Re-exported migration helpers from `@weaverse/react`: `useWeaverse`, `useParentInstance`, `useItemInstance`, `useChildInstances`.
155
- - `createSchema` and schema types from `@weaverse/schema`.
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.
156
135
 
157
136
  ## Server API (`@weaverse/next/server`)
158
137
 
@@ -168,15 +147,26 @@ Main exports:
168
147
 
169
148
  Example helper for a Next route/server component boundary:
170
149
 
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:
156
+
171
157
  ```ts
172
158
  import { createWeaverseNextServerClient } from '@weaverse/next/server'
159
+ import type { PageType, WeaverseNextRequestContext } from '@weaverse/next'
173
160
  import { headers } from 'next/headers'
174
161
  import { serverComponents } from './server-components'
175
162
  import { getStaticStorefrontClient } from './storefront'
176
163
 
177
164
  export async function getWeaverseServerClient(
178
165
  searchParamsPromise: Promise<Record<string, string | string[] | undefined>>,
179
- pathname = '/'
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 } = {}
180
170
  ) {
181
171
  let [headersList, rawSearchParams] = await Promise.all([
182
172
  headers(),
@@ -200,6 +190,27 @@ export async function getWeaverseServerClient(
200
190
  }`
201
191
  )
202
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.
203
214
  let storefront = getStaticStorefrontClient()
204
215
 
205
216
  return createWeaverseNextServerClient({
@@ -218,32 +229,42 @@ export async function getWeaverseServerClient(
218
229
  },
219
230
  }
220
231
  : undefined,
221
- requestContext: {
222
- url,
223
- headers: requestHeaders,
224
- searchParams,
225
- pathname,
226
- i18n: { country: 'US', language: 'EN', locale: 'en-US' },
227
- },
232
+ requestContext,
228
233
  cache: { revalidate: 60, tags: ['weaverse'] },
229
234
  })
230
235
  }
231
236
  ```
232
237
 
233
- Then each route can stay close to the Hydrogen mental model:
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:
234
241
 
235
242
  ```ts
236
- let weaverse = await getWeaverseServerClient(props.searchParams, '/')
243
+ // app/page.tsx — the INDEX segment owns the page type.
244
+ let weaverse = await getWeaverseServerClient(props.searchParams, '/', {
245
+ pageType: 'INDEX',
246
+ })
237
247
  let data = await weaverse.loadPage({ type: 'INDEX' })
238
248
  let theme = await weaverse.loadThemeSettings()
239
249
  ```
240
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
+
241
262
  ### Server client members
242
263
 
243
264
  | Member | Description |
244
265
  | --- | --- |
245
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. |
246
- | `loadThemeSettings(options?)` | Fetches `/api/public/project_configs`, merges schema defaults under merchant settings, returns theme settings/static content/schema data for design mode. |
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. |
247
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. |
248
269
  | `fetchWithCache<T>(url, options?)` | Next-aware fetch helper. Uses `cache: 'no-store'` in design/revision preview and `next: { revalidate, tags }` in published mode. |
249
270
  | `resolveProjectId()` / `projectId` | Resolves project id from Studio query, config function/string, or env. |
@@ -282,12 +303,13 @@ Use the server entry in `generateMetadata()` to map Weaverse `page.seo` into a N
282
303
 
283
304
  ```ts
284
305
  // app/pages/[handle]/page.tsx
306
+ import type { Metadata } from 'next'
285
307
  import { getWeaverseNextSeoMetadata } from '@weaverse/next/server'
286
308
 
287
309
  export async function generateMetadata(props: {
288
310
  params: Promise<{ handle: string }>
289
311
  searchParams: Promise<Record<string, string | string[] | undefined>>
290
- }) {
312
+ }): Promise<Metadata> {
291
313
  let { handle } = await props.params
292
314
  let weaverse = await getWeaverseServerClient(props.searchParams, `/pages/${handle}`)
293
315
  let data = await weaverse.loadPage({ type: 'PAGE', handle })
@@ -295,7 +317,17 @@ export async function generateMetadata(props: {
295
317
  }
296
318
  ```
297
319
 
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.
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).
299
331
 
300
332
  ### Custom pages for sitemap generation
301
333
 
@@ -336,16 +368,18 @@ import {
336
368
  WeaverseNextProvider,
337
369
  WeaverseNextRenderer,
338
370
  } from '@weaverse/next'
339
- import type { WeaverseNextLoaderData } from '@weaverse/next'
371
+ import type {
372
+ WeaverseNextLoaderData,
373
+ WeaverseNextRequestInfo,
374
+ } from '@weaverse/next'
340
375
  import { components } from './components'
341
376
 
342
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.
343
381
  let requestInfo = data.configs?.requestInfo as
344
- | {
345
- i18n?: { country?: string; language?: string; locale?: string }
346
- pathname?: string
347
- search?: string
348
- }
382
+ | WeaverseNextRequestInfo
349
383
  | undefined
350
384
  let projectId =
351
385
  (data.configs?.projectId as string | undefined) ?? data.project?.id
@@ -361,6 +395,8 @@ export function WeaversePage({ data }: { data: WeaverseNextLoaderData }) {
361
395
  isPreviewMode: Boolean(data.configs?.isPreviewMode),
362
396
  isRevisionPreview: Boolean(data.configs?.isRevisionPreview),
363
397
  i18n: requestInfo?.i18n,
398
+ pageType: requestInfo?.pageType,
399
+ handle: requestInfo?.handle,
364
400
  },
365
401
  })
366
402
 
@@ -479,13 +515,38 @@ The root script connector and page-level renderer are both required for full Stu
479
515
 
480
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.
481
517
 
482
- Mount the route handler in the consuming app:
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`:
483
524
 
484
525
  ```ts
485
526
  // app/api/weaverse/revalidate/route.ts
486
527
  import { createWeaverseNextRevalidateHandler } from '@weaverse/next/server'
487
- import { getWeaverseServerClient } 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:
488
548
 
549
+ ```ts
489
550
  export const { POST } = createWeaverseNextRevalidateHandler({
490
551
  getClient: () => getWeaverseServerClient(Promise.resolve({})),
491
552
  })
@@ -495,15 +556,46 @@ Flow:
495
556
 
496
557
  ```text
497
558
  Studio resource picker edit
498
- -> Builder calls runtime.internal.revalidateItem(draftItem)
499
- -> SDK POSTs { draftItem } to /api/weaverse/revalidate
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
500
565
  -> route handler finds the registered component by draftItem.type
501
- -> handler runs that component's loader with draft item data
502
- -> response returns { loaderData }
566
+ -> handler runs that component's loader with draft data + client.requestContext
567
+ -> response returns { loaderData } with Cache-Control: no-store
503
568
  -> SDK applies loaderData to the live item instance in place
504
569
  ```
505
570
 
506
- If the route is missing or returns an error, Builder falls back to the older route-refresh path.
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.
507
599
 
508
600
  ## Config resolution
509
601
 
@@ -514,33 +606,23 @@ Server config resolution intentionally mirrors Hydrogen where possible:
514
606
  - API base: trusted request host → `WEAVERSE_PUBLIC_API_BASE` → non-production `WEAVERSE_HOST` → `https://api.weaverse.io`.
515
607
  - public env: `PUBLIC_STORE_DOMAIN`, `PUBLIC_STOREFRONT_API_TOKEN`.
516
608
 
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
609
+ ## Alpha migration notes
520
610
 
521
- Live POC:
611
+ ### After `0.1.0-alpha.9`
522
612
 
523
- - https://weaverse-hydrogen-next-poc.vercel.app
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.
524
617
 
525
- Repo:
618
+ ### Bundle note
526
619
 
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
- ```
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`.
540
621
 
541
622
  ## Current limitations
542
623
 
543
624
  - This package is still alpha; API names may change before stable.
544
625
  - 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.
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.
546
628
  - Full Shopify request-handler/redirect parity is app-level follow-up work, not owned by this package yet.
@@ -1,3 +1,5 @@
1
+ import { PageTypeSchema } from '@weaverse/schema';
2
+
1
3
  // src/utils.ts
2
4
  function generateDataFromSchema(schema) {
3
5
  let data = {};
@@ -107,13 +109,22 @@ function getSearchParamsFromContext(context) {
107
109
  }
108
110
  return new URLSearchParams();
109
111
  }
112
+ function withPathPrefix(pathname, i18n) {
113
+ let raw = typeof i18n?.pathPrefix === "string" ? i18n.pathPrefix : "";
114
+ let segments = raw.split("/").filter(Boolean);
115
+ let prefix = segments.length > 0 ? `/${segments.join("/")}` : "";
116
+ if (prefix === "" || pathname === prefix || pathname.startsWith(`${prefix}/`)) {
117
+ return pathname;
118
+ }
119
+ return pathname === "/" ? prefix : `${prefix}${pathname}`;
120
+ }
110
121
  function getPathnameFromContext(context) {
111
122
  if (context?.pathname) {
112
- return context.pathname;
123
+ return withPathPrefix(context.pathname, context.i18n);
113
124
  }
114
125
  if (context?.url) {
115
126
  let url = toUrl(context.url);
116
- return url.pathname || "/";
127
+ return withPathPrefix(url.pathname || "/", context.i18n);
117
128
  }
118
129
  return "/";
119
130
  }
@@ -154,9 +165,96 @@ function buildWeaverseNextRequestInfo(context) {
154
165
  pathname: getPathnameFromContext(context),
155
166
  search: search ? `?${search}` : "",
156
167
  queries: toQueryRecord(searchParams),
157
- ...context?.i18n ? { i18n: context.i18n } : {}
168
+ ...context?.i18n ? { i18n: context.i18n } : {},
169
+ ...context?.pageType ? { pageType: context.pageType } : {},
170
+ ...context?.handle ? { handle: context.handle } : {}
158
171
  };
159
172
  }
173
+ var DEFAULT_REVALIDATE_ENDPOINT = "/api/weaverse/revalidate";
174
+ var MAX_ROUTE_CONTEXT_HANDLE_LENGTH = 512;
175
+ var DENIED_ROUTE_CONTEXT_PARAMS = [
176
+ "weaverseprojectid",
177
+ "weaversehost",
178
+ "weaverseapikey",
179
+ "weaverseapibase",
180
+ "weaversepublicapibase",
181
+ "weaverseversion",
182
+ "projectid",
183
+ "weaversedraftitem",
184
+ "__weaversedraftitem",
185
+ "_rsc"
186
+ ];
187
+ var ROUTE_CONTEXT_I18N_KEYS = [
188
+ "country",
189
+ "label",
190
+ "language",
191
+ "locale",
192
+ "pathPrefix"
193
+ ];
194
+ function sanitizeRouteContextSearch(search) {
195
+ let params = new URLSearchParams(search);
196
+ let denied = new Set(DENIED_ROUTE_CONTEXT_PARAMS);
197
+ for (let key of [...new Set(params.keys())]) {
198
+ if (denied.has(key.toLowerCase())) {
199
+ params.delete(key);
200
+ }
201
+ }
202
+ let serialized = params.toString();
203
+ return serialized ? `?${serialized}` : "";
204
+ }
205
+ function buildWeaverseNextRevalidateRouteContext(requestInfo) {
206
+ if (!requestInfo || typeof requestInfo.pathname !== "string" || !requestInfo.pathname) {
207
+ return;
208
+ }
209
+ let routeContext = {
210
+ pathname: requestInfo.pathname,
211
+ search: sanitizeRouteContextSearch(requestInfo.search ?? "")
212
+ };
213
+ let { i18n } = requestInfo;
214
+ if (i18n && typeof i18n === "object") {
215
+ let picked = {};
216
+ for (let key of ROUTE_CONTEXT_I18N_KEYS) {
217
+ let value = i18n[key];
218
+ if (typeof value === "string") {
219
+ picked[key] = value;
220
+ }
221
+ }
222
+ if (Object.keys(picked).length) {
223
+ routeContext.i18n = picked;
224
+ }
225
+ }
226
+ if (requestInfo.pageType && PageTypeSchema.safeParse(requestInfo.pageType).success) {
227
+ routeContext.pageType = requestInfo.pageType;
228
+ }
229
+ if (typeof requestInfo.handle === "string" && requestInfo.handle && requestInfo.handle.length <= MAX_ROUTE_CONTEXT_HANDLE_LENGTH) {
230
+ routeContext.handle = requestInfo.handle;
231
+ }
232
+ return routeContext;
233
+ }
234
+ async function revalidateWeaverseNextItem(runtime, draftItem, endpoint = DEFAULT_REVALIDATE_ENDPOINT) {
235
+ let instance = runtime.itemInstances.get(draftItem.id);
236
+ if (!instance) {
237
+ throw new Error(
238
+ `[weaverse-next] No live item instance for "${draftItem.id}".`
239
+ );
240
+ }
241
+ let routeContext = buildWeaverseNextRevalidateRouteContext(
242
+ runtime.requestInfo
243
+ );
244
+ let body = routeContext ? { draftItem, routeContext } : { draftItem };
245
+ let response = await fetch(endpoint, {
246
+ method: "POST",
247
+ headers: { "Content-Type": "application/json" },
248
+ body: JSON.stringify(body)
249
+ });
250
+ if (!response.ok) {
251
+ throw new Error(
252
+ `[weaverse-next] Revalidate endpoint responded ${response.status}.`
253
+ );
254
+ }
255
+ let { loaderData } = await response.json();
256
+ instance.setData({ loaderData: loaderData ?? null });
257
+ }
160
258
 
161
259
  // src/seo.ts
162
260
  function hasOpenGraphContent(seo) {
@@ -165,6 +263,15 @@ function hasOpenGraphContent(seo) {
165
263
  function hasTwitterContent(seo) {
166
264
  return Boolean(seo?.title || seo?.description || seo?.image || seo?.cardType);
167
265
  }
266
+ function normalizeTwitterCard(cardType) {
267
+ return cardType === "summary_large_image" ? "summary_large_image" : "summary";
268
+ }
269
+ function normalizeOpenGraphType(type) {
270
+ if (type === "article" || type === "profile" || type === "video.other") {
271
+ return type;
272
+ }
273
+ return "website";
274
+ }
168
275
  function formatWeaverseNextSeoMetadata(seo) {
169
276
  let index = seo?.robots?.index !== false;
170
277
  let follow = seo?.robots?.follow !== false;
@@ -192,13 +299,13 @@ function formatWeaverseNextSeoMetadata(seo) {
192
299
  title: openGraph?.title,
193
300
  description: openGraph?.description,
194
301
  images: openGraph?.image ? [openGraph.image] : void 0,
195
- type: openGraph?.type || "website"
302
+ type: normalizeOpenGraphType(openGraph?.type)
196
303
  };
197
304
  }
198
305
  let twitter = seo.twitter;
199
306
  if (hasTwitterContent(twitter)) {
200
307
  metadata.twitter = {
201
- card: twitter?.cardType || "summary",
308
+ card: normalizeTwitterCard(twitter?.cardType),
202
309
  title: twitter?.title,
203
310
  description: twitter?.description,
204
311
  images: twitter?.image ? [twitter.image] : void 0
@@ -214,6 +321,6 @@ function getWeaverseNextSeoMetadata(data) {
214
321
  return formatWeaverseNextSeoMetadata(page?.seo ?? null);
215
322
  }
216
323
 
217
- export { buildWeaverseNextRequestInfo, formatWeaverseNextSeoMetadata, generateDataFromSchema, getWeaverseNextSeoMetadata, runWeaverseComponentLoaders };
218
- //# sourceMappingURL=chunk-6PMZ77RA.mjs.map
219
- //# sourceMappingURL=chunk-6PMZ77RA.mjs.map
324
+ export { DEFAULT_REVALIDATE_ENDPOINT, ROUTE_CONTEXT_I18N_KEYS, buildWeaverseNextRequestInfo, buildWeaverseNextRevalidateRouteContext, formatWeaverseNextSeoMetadata, generateDataFromSchema, getWeaverseNextSeoMetadata, revalidateWeaverseNextItem, runWeaverseComponentLoaders, sanitizeRouteContextSearch, withPathPrefix };
325
+ //# sourceMappingURL=chunk-LH6AAEIK.mjs.map
326
+ //# sourceMappingURL=chunk-LH6AAEIK.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils.ts","../src/loader.ts","../src/request-info.ts","../src/revalidate-item.ts","../src/seo.ts"],"names":[],"mappings":"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","file":"chunk-LH6AAEIK.mjs","sourcesContent":["import type { SchemaType } from '@weaverse/schema'\nimport type { WeaverseNextThemeSchema } from './types'\n\n/**\n * Generate default component data from a schema's `settings` inputs.\n *\n * Mirrors Hydrogen's `generateDataFromSchema` (settings → `{ [name]:\n * defaultValue }`) but is kept local so `@weaverse/next` doesn't depend on\n * `@weaverse/hydrogen`. Falls back to the deprecated `inspector` array when a\n * schema predates `settings`.\n */\nexport function generateDataFromSchema(\n schema: SchemaType | WeaverseNextThemeSchema | undefined\n): Record<string, unknown> {\n let data: Record<string, unknown> = {}\n if (!schema) {\n return data\n }\n\n let groups =\n (schema.settings as {\n inputs?: { name?: string; defaultValue?: unknown }[]\n }[]) ??\n (schema as { inspector?: typeof groups }).inspector ??\n []\n\n for (let group of groups) {\n for (let input of group?.inputs ?? []) {\n let { name, defaultValue } = input\n if (name && defaultValue !== null && defaultValue !== undefined) {\n data[name] = defaultValue\n }\n }\n }\n\n return data\n}\n","import type {\n WeaverseNextClient,\n WeaverseNextCommerceContext,\n WeaverseNextComponent,\n WeaverseNextComponentData,\n WeaverseNextLoaderData,\n WeaverseNextRequestContext,\n} from './types'\nimport { generateDataFromSchema } from './utils'\n\n/** Inputs for running registered component loaders over a page tree. */\nexport interface RunComponentLoadersArgs {\n /** Weaverse client containing the component registry. */\n client: WeaverseNextClient\n /** Request-scoped commerce override; defaults to `client.commerce`. */\n commerce?: WeaverseNextCommerceContext\n /** Request-context override; defaults to `client.requestContext`. */\n context?: WeaverseNextRequestContext\n /** Page payload to walk; defaults to `client.data`. */\n data?: WeaverseNextLoaderData | null\n}\n\nfunction isItemNode(node: unknown): node is WeaverseNextComponentData {\n return (\n typeof node === 'object' &&\n node !== null &&\n typeof (node as WeaverseNextComponentData).type === 'string'\n )\n}\n\nfunction cloneItem(item: WeaverseNextComponentData): WeaverseNextComponentData {\n return {\n ...item,\n children: Array.isArray(item.children)\n ? item.children.map((child) =>\n isItemNode(child) ? cloneItem(child) : { ...child }\n )\n : item.children,\n }\n}\n\n/**\n * Walk a serialized page tree and run each registered component's `loader`,\n * attaching the result to the item as `loaderData`.\n *\n * Loaders receive explicit `commerce.storefront` (and `weaverse.storefront` as\n * a compatibility alias via the client). Children are walked recursively when\n * they are inline item objects; flat `items` arrays are covered by the\n * top-level walk.\n *\n * @returns a cloned `data` object with `loaderData` attached to cloned items.\n */\nexport async function runWeaverseComponentLoaders(\n args: RunComponentLoadersArgs\n): Promise<WeaverseNextLoaderData | null> {\n let { client, context, commerce } = args\n let sourceData = args.data ?? client.data\n if (!sourceData) {\n return null\n }\n if (!sourceData.page.items.length) {\n return sourceData\n }\n\n let data: WeaverseNextLoaderData = {\n ...sourceData,\n page: {\n ...sourceData.page,\n items: sourceData.page.items.map(cloneItem),\n },\n }\n let items = data.page.items\n\n let componentMap = new Map<string, WeaverseNextComponent>()\n for (let component of client.components) {\n if (component?.schema?.type) {\n componentMap.set(component.schema.type, component)\n }\n }\n\n let visited = new Set<string>()\n let resolvedContext = context ?? client.requestContext\n let resolvedCommerce = commerce ?? client.commerce\n\n // Per-call `weaverse` alias. When a request-scoped `commerce` is passed, the\n // `weaverse.storefront` compatibility alias must resolve to *that* storefront,\n // not the client's construction-time commerce. Pilot-style loaders call\n // `weaverse.storefront.query(...)`, so a stale alias would query the wrong\n // (or an undefined) storefront. When no override is given we reuse the client\n // as-is to preserve its prototype getters.\n let resolvedWeaverse: WeaverseNextClient =\n resolvedCommerce === client.commerce\n ? client\n : {\n ...client,\n commerce: resolvedCommerce,\n storefront: resolvedCommerce?.storefront,\n }\n\n async function walk(nodes: WeaverseNextComponentData[]) {\n for (let item of nodes) {\n if (!item || visited.has(item.id)) {\n continue\n }\n visited.add(item.id)\n\n let component = componentMap.get(item.type)\n if (component?.loader) {\n try {\n item.loaderData = await component.loader({\n data: { ...generateDataFromSchema(component.schema), ...item.data },\n weaverse: resolvedWeaverse,\n context: resolvedContext,\n commerce: resolvedCommerce,\n })\n } catch (error) {\n console.warn('❌ Item loader run failed.', item.type, item.id, error)\n }\n }\n\n let children = item.children\n if (Array.isArray(children)) {\n let inlineChildren = children.filter(\n isItemNode\n ) as WeaverseNextComponentData[]\n if (inlineChildren.length) {\n await walk(inlineChildren)\n }\n }\n }\n }\n\n await walk(items)\n return data\n}\n","import type {\n WeaverseNextRequestContext,\n WeaverseNextRequestInfo,\n} from './types'\n\nfunction toUrl(value: string | URL): URL {\n return typeof value === 'string' ? new URL(value, 'http://localhost') : value\n}\n\nfunction getSearchParamsFromContext(\n context?: WeaverseNextRequestContext\n): URLSearchParams {\n if (context?.searchParams) {\n return new URLSearchParams(context.searchParams)\n }\n\n if (context?.url) {\n let url = toUrl(context.url)\n return new URLSearchParams(url.searchParams)\n }\n\n return new URLSearchParams()\n}\n\n/**\n * Put the request's market prefix (`i18n.pathPrefix`, e.g. `/de-de`) on a\n * pathname that does not carry it yet.\n *\n * A Next app builds its request context from the route path, and with\n * locale-segmented routes that path is usually unprefixed. Studio's address\n * bar follows `requestInfo.pathname`, so without the prefix it snaps back to\n * the default market after every navigation. A path that already carries the\n * prefix, and the default market's empty prefix, pass through unchanged.\n */\nexport function withPathPrefix(\n pathname: string,\n i18n?: WeaverseNextRequestContext['i18n']\n): string {\n let raw = typeof i18n?.pathPrefix === 'string' ? i18n.pathPrefix : ''\n // Split rather than regex-trim: `pathPrefix` is caller input, and an anchored\n // `/\\/+$/` backtracks polynomially on long runs of slashes.\n let segments = raw.split('/').filter(Boolean)\n let prefix = segments.length > 0 ? `/${segments.join('/')}` : ''\n if (\n prefix === '' ||\n pathname === prefix ||\n pathname.startsWith(`${prefix}/`)\n ) {\n return pathname\n }\n return pathname === '/' ? prefix : `${prefix}${pathname}`\n}\n\nfunction getPathnameFromContext(context?: WeaverseNextRequestContext): string {\n if (context?.pathname) {\n return withPathPrefix(context.pathname, context.i18n)\n }\n\n if (context?.url) {\n let url = toUrl(context.url)\n return withPathPrefix(url.pathname || '/', context.i18n)\n }\n\n return '/'\n}\n\nfunction coerceQueryValue(value: string): string | boolean {\n if (value === 'true') {\n return true\n }\n if (value === 'false') {\n return false\n }\n return value\n}\n\nfunction toQueryRecord(\n searchParams: URLSearchParams\n): Record<string, string | boolean> {\n let queries: Record<string, string | boolean> = {}\n for (let [key, value] of searchParams.entries()) {\n queries[key] = coerceQueryValue(value)\n }\n return queries\n}\n\nconst TRANSIENT_STUDIO_PARAMS = new Set([\n 'weaverseDraftItem',\n // Legacy name from the original design-mode draft-item spike. Keep it out of\n // runtime requestInfo too, in case an older Builder bundle is in front of a\n // newer Next storefront.\n '__weaverseDraftItem',\n])\n\nfunction getRequestInfoSearchParams(\n context?: WeaverseNextRequestContext\n): URLSearchParams {\n let searchParams = new URLSearchParams(getSearchParamsFromContext(context))\n for (let key of TRANSIENT_STUDIO_PARAMS) {\n searchParams.delete(key)\n }\n return searchParams\n}\n\n/**\n * Build the Hydrogen-like request info shape consumed by the Studio bridge.\n * Duplicate query keys keep the last value, matching `URLSearchParams` iteration\n * assignment and Hydrogen's practical query-object behavior.\n */\nexport function buildWeaverseNextRequestInfo(\n context?: WeaverseNextRequestContext\n): WeaverseNextRequestInfo {\n let searchParams = getRequestInfoSearchParams(context)\n let search = searchParams.toString()\n\n return {\n pathname: getPathnameFromContext(context),\n search: search ? `?${search}` : '',\n queries: toQueryRecord(searchParams),\n ...(context?.i18n ? { i18n: context.i18n } : {}),\n ...(context?.pageType ? { pageType: context.pageType } : {}),\n ...(context?.handle ? { handle: context.handle } : {}),\n }\n}\n","import type { PageType } from '@weaverse/schema'\nimport { PageTypeSchema } from '@weaverse/schema'\nimport type {\n WeaverseNextComponentData,\n WeaverseNextI18n,\n WeaverseNextRequestInfo,\n} from './types'\n\n/** Default App Router endpoint used for Studio item-loader revalidation. */\nexport const DEFAULT_REVALIDATE_ENDPOINT = '/api/weaverse/revalidate'\n\nconst MAX_ROUTE_CONTEXT_HANDLE_LENGTH = 512\n\n/**\n * Server-owned and transient controls stripped from the revalidation route\n * context on both sides of the trust boundary. Compared case-insensitively, so\n * every entry is stored lowercased. Client sanitization keeps normal traffic\n * clean; the route handler repeats this deny-list as the security boundary.\n *\n * - Server-owned: a crafted `?weaverseProjectId=`/`?weaverseHost=` must never\n * influence server config resolution (see `getWeaverseNextConfigs`), and\n * credential-shaped params never ride along in route context.\n * - Transient: draft-item and RSC transport params never describe route identity.\n */\nexport const DENIED_ROUTE_CONTEXT_PARAMS = [\n 'weaverseprojectid',\n 'weaversehost',\n 'weaverseapikey',\n 'weaverseapibase',\n 'weaversepublicapibase',\n 'weaverseversion',\n 'projectid',\n 'weaversedraftitem',\n '__weaversedraftitem',\n '_rsc',\n]\n\n/** i18n fields allowed to cross the browser boundary. */\nexport const ROUTE_CONTEXT_I18N_KEYS = [\n 'country',\n 'label',\n 'language',\n 'locale',\n 'pathPrefix',\n] as const\n\n/**\n * JSON-safe route subset that may cross the browser boundary alongside the\n * draft item. Never carries headers, cookies, auth, env, project ID, Studio\n * host, API keys/bases, commerce clients, the request context, runtime, or\n * client. `search` is canonical (preserves duplicate keys); the derived\n * `queries` record is intentionally omitted.\n */\nexport interface WeaverseNextRevalidateRouteContext {\n /** Route handle used for page assignment. */\n handle?: string\n /** Public locale and market identity allowed across the browser boundary. */\n i18n?: Pick<\n WeaverseNextI18n,\n 'country' | 'label' | 'language' | 'locale' | 'pathPrefix'\n >\n /** Weaverse page type resolved by the active route. */\n pageType?: PageType\n /** Canonical same-origin route pathname. */\n pathname: string\n /** Sanitized serialized query string, including the leading `?` when set. */\n search: string\n}\n\n/**\n * Minimal runtime surface the in-place apply needs. Structural (not the\n * concrete `WeaverseNextRuntime` class) so unit tests can drive it with a\n * plain mock. `requestInfo` stays optional: a real `WeaverseNextRuntime`\n * always supplies it, but when absent the client omits `routeContext` and the\n * legacy `{ draftItem }` request is preserved.\n */\nexport interface RevalidateItemRuntimeLike {\n /** Live item stores keyed by Weaverse item ID. */\n itemInstances: Map<\n string,\n {\n /** Merge refreshed loader data into the live item store. */\n setData: (update: Record<string, unknown>) => unknown\n }\n >\n /** Active route identity used to construct the optional revalidation payload. */\n requestInfo?: WeaverseNextRequestInfo\n}\n\n/**\n * Strip denied (server-owned/transient) controls from a `search` string and\n * re-serialize it. Preserves ordinary application queries, duplicate keys,\n * encoded Unicode, and mode controls (`isDesignMode`, `isPreviewMode`,\n * `__revisionId`, `sectionType`). Returns `''` or a `?`-prefixed string.\n */\nexport function sanitizeRouteContextSearch(search: string): string {\n let params = new URLSearchParams(search)\n let denied = new Set(DENIED_ROUTE_CONTEXT_PARAMS)\n for (let key of [...new Set(params.keys())]) {\n if (denied.has(key.toLowerCase())) {\n params.delete(key)\n }\n }\n let serialized = params.toString()\n return serialized ? `?${serialized}` : ''\n}\n\n/**\n * Build the sanitized {@link WeaverseNextRevalidateRouteContext} from the\n * active runtime's request info. Returns `undefined` without a usable pathname\n * so the caller omits `routeContext` and keeps the legacy request. This is not\n * the security boundary — the route handler re-validates and re-sanitizes.\n */\nexport function buildWeaverseNextRevalidateRouteContext(\n requestInfo?: WeaverseNextRequestInfo\n): WeaverseNextRevalidateRouteContext | undefined {\n if (\n !requestInfo ||\n typeof requestInfo.pathname !== 'string' ||\n !requestInfo.pathname\n ) {\n return\n }\n\n let routeContext: WeaverseNextRevalidateRouteContext = {\n pathname: requestInfo.pathname,\n search: sanitizeRouteContextSearch(requestInfo.search ?? ''),\n }\n\n let { i18n } = requestInfo\n if (i18n && typeof i18n === 'object') {\n let picked: Record<string, string> = {}\n for (let key of ROUTE_CONTEXT_I18N_KEYS) {\n let value = i18n[key]\n if (typeof value === 'string') {\n picked[key] = value\n }\n }\n if (Object.keys(picked).length) {\n routeContext.i18n = picked\n }\n }\n\n if (\n requestInfo.pageType &&\n PageTypeSchema.safeParse(requestInfo.pageType).success\n ) {\n routeContext.pageType = requestInfo.pageType\n }\n\n if (\n typeof requestInfo.handle === 'string' &&\n requestInfo.handle &&\n requestInfo.handle.length <= MAX_ROUTE_CONTEXT_HANDLE_LENGTH\n ) {\n routeContext.handle = requestInfo.handle\n }\n\n return routeContext\n}\n\n/**\n * Studio per-item loader revalidation: POST the unsaved draft item (and, when\n * the runtime exposes route context, a sanitized `routeContext`) to the app's\n * revalidate route (see `createWeaverseNextRevalidateHandler`) and apply the\n * returned `loaderData` to the live item instance in place.\n *\n * No `router.refresh()` is involved, so the RSC tree is never re-applied — no\n * page remount, no scroll reset, and the draft payload never touches the URL.\n *\n * Throws on any failure (missing route, non-OK response, unknown item) so the\n * Builder Studio bridge can detect the miss and fall back to its legacy\n * refresh-based revalidation for this window.\n */\nexport async function revalidateWeaverseNextItem(\n runtime: RevalidateItemRuntimeLike,\n draftItem: WeaverseNextComponentData,\n endpoint: string = DEFAULT_REVALIDATE_ENDPOINT\n): Promise<void> {\n let instance = runtime.itemInstances.get(draftItem.id)\n if (!instance) {\n throw new Error(\n `[weaverse-next] No live item instance for \"${draftItem.id}\".`\n )\n }\n\n // Snapshot route identity at revalidation start so a later navigation cannot\n // retarget this in-flight request. Omit `routeContext` for legacy runtimes.\n let routeContext = buildWeaverseNextRevalidateRouteContext(\n runtime.requestInfo\n )\n let body = routeContext ? { draftItem, routeContext } : { draftItem }\n\n let response = await fetch(endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n })\n if (!response.ok) {\n throw new Error(\n `[weaverse-next] Revalidate endpoint responded ${response.status}.`\n )\n }\n\n let { loaderData } = (await response.json()) as { loaderData?: unknown }\n // The item's settings were already mutated by the Studio edit; only the\n // loader payload needs applying. `setData` merges and notifies subscribers,\n // and the new `loaderData` identity is what Builder's revalidation poll\n // observes for completion.\n instance.setData({ loaderData: loaderData ?? null })\n}\n","import type { PageSEOData } from '@weaverse/schema'\nimport type { Metadata } from 'next'\nimport type { WeaverseNextLoaderData, WeaverseNextPageData } from './types'\n\n/** Object containing an optional Weaverse page for SEO extraction. */\nexport interface WeaverseNextSeoPageContainer {\n /** Page whose published SEO fields should be formatted. */\n page?: WeaverseNextPageData | null\n}\n\n/** Input accepted by {@link getWeaverseNextSeoMetadata}. */\nexport type WeaverseNextSeoInput =\n | WeaverseNextLoaderData\n | WeaverseNextPageData\n | WeaverseNextSeoPageContainer\n | null\n | undefined\n\n/** Open Graph metadata accepted by Next.js's `Metadata` API. */\nexport type NextOpenGraph = NonNullable<Metadata['openGraph']>\n\n/** Twitter card metadata accepted by Next.js's `Metadata` API. */\nexport type NextTwitter = NonNullable<Metadata['twitter']>\n\n/**\n * Weaverse page SEO expressed as a subset of Next's `Metadata`. Assignable to\n * `Metadata` as-is: `const metadata: Metadata = getWeaverseNextSeoMetadata(data)`.\n */\nexport interface WeaverseNextSeoMetadata {\n /** Canonical URL settings for Next's metadata API. */\n alternates?: {\n /** Preferred URL for search indexing. */\n canonical?: string\n }\n /** Page meta description. */\n description?: string\n /** Page meta keywords. */\n keywords?: string\n /** Open Graph metadata normalized to Next's supported union. */\n openGraph?: NextOpenGraph\n /** Search crawler indexing and link-following directives. */\n robots: {\n /** Whether crawlers may follow links on the page. */\n follow: boolean\n /** Whether crawlers may index the page. */\n index: boolean\n }\n /** Page title. */\n title?: string\n /** Twitter card metadata normalized for Next's metadata API. */\n twitter?: NextTwitter\n}\n\nfunction hasOpenGraphContent(seo: PageSEOData['openGraph']): boolean {\n return Boolean(seo?.title || seo?.description || seo?.image || seo?.type)\n}\n\nfunction hasTwitterContent(seo: PageSEOData['twitter']): boolean {\n return Boolean(seo?.title || seo?.description || seo?.image || seo?.cardType)\n}\n\nfunction normalizeTwitterCard(\n cardType: NonNullable<PageSEOData['twitter']>['cardType']\n): 'summary' | 'summary_large_image' {\n return cardType === 'summary_large_image' ? 'summary_large_image' : 'summary'\n}\n\n/**\n * Map a Builder Open Graph type onto Next's `OpenGraphType` union. Every\n * Builder value except `product` exists in Next's union and is preserved\n * verbatim; `product` is not modeled by Next and makes `generateMetadata()`\n * throw, so it degrades to `website` — the closest renderable equivalent.\n */\nfunction normalizeOpenGraphType(\n type: NonNullable<PageSEOData['openGraph']>['type']\n): 'article' | 'profile' | 'video.other' | 'website' {\n if (type === 'article' || type === 'profile' || type === 'video.other') {\n return type\n }\n return 'website'\n}\n\n/**\n * Convert Builder SEO fields into metadata accepted by Next.js.\n *\n * Unsupported Open Graph product types and incomplete Twitter app/player\n * cards are safely downgraded to renderable Next.js variants.\n */\nexport function formatWeaverseNextSeoMetadata(\n seo: PageSEOData | null | undefined\n): WeaverseNextSeoMetadata {\n let index = seo?.robots?.index !== false\n let follow = seo?.robots?.follow !== false\n let metadata: WeaverseNextSeoMetadata = {\n robots: { index, follow },\n }\n\n if (!seo) {\n return metadata\n }\n\n if (seo.title) {\n metadata.title = seo.title\n }\n if (seo.description) {\n metadata.description = seo.description\n }\n if (seo.keywords) {\n metadata.keywords = seo.keywords\n }\n if (seo.canonicalUrl) {\n metadata.alternates = { canonical: seo.canonicalUrl }\n }\n\n let openGraph = seo.openGraph\n if (hasOpenGraphContent(openGraph)) {\n metadata.openGraph = {\n title: openGraph?.title,\n description: openGraph?.description,\n images: openGraph?.image ? [openGraph.image] : undefined,\n type: normalizeOpenGraphType(openGraph?.type),\n }\n }\n\n let twitter = seo.twitter\n if (hasTwitterContent(twitter)) {\n // Builder does not collect the descriptors Next requires for `app` cards\n // (Next throws when they are missing) or `player` cards (which render an\n // empty, useless card without a player URL and dimensions). Downgrade both\n // to `summary` rather than emitting metadata Next cannot render.\n metadata.twitter = {\n card: normalizeTwitterCard(twitter?.cardType),\n title: twitter?.title,\n description: twitter?.description,\n images: twitter?.image ? [twitter.image] : undefined,\n }\n }\n\n return metadata\n}\n\n/**\n * Read page SEO from loader data or a page payload and format it for Next.js\n * `generateMetadata`. Missing data returns indexable/followable robot defaults.\n */\nexport function getWeaverseNextSeoMetadata(\n data: WeaverseNextSeoInput\n): WeaverseNextSeoMetadata {\n if (!data) {\n return formatWeaverseNextSeoMetadata(null)\n }\n let page =\n 'items' in data && 'id' in data\n ? (data as WeaverseNextPageData)\n : ((data as WeaverseNextSeoPageContainer).page ?? null)\n return formatWeaverseNextSeoMetadata(page?.seo ?? null)\n}\n"]}