@weaverse/hydrogen 5.17.1 → 5.18.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 (70) hide show
  1. package/dist/index.js +83 -1
  2. package/dist/index.js.map +1 -1
  3. package/dist/index.mjs +83 -1
  4. package/dist/index.mjs.map +1 -1
  5. package/dist/types/WeaverseHydrogenRoot.d.ts +218 -0
  6. package/dist/types/WeaverseHydrogenRoot.d.ts.map +1 -0
  7. package/dist/types/components/index.d.ts +4 -0
  8. package/dist/types/components/index.d.ts.map +1 -0
  9. package/dist/types/components/main.d.ts +7 -0
  10. package/dist/types/components/main.d.ts.map +1 -0
  11. package/dist/types/hooks/theme-text-context.d.ts +2 -0
  12. package/dist/types/hooks/theme-text-context.d.ts.map +1 -0
  13. package/dist/types/hooks/translation-context.d.ts +138 -0
  14. package/dist/types/hooks/translation-context.d.ts.map +1 -0
  15. package/dist/types/hooks/use-weaverse-data-context.d.ts +29 -0
  16. package/dist/types/hooks/use-weaverse-data-context.d.ts.map +1 -0
  17. package/dist/types/index.d.ts +99 -0
  18. package/dist/types/index.d.ts.map +1 -0
  19. package/dist/types/seo.d.ts +43 -0
  20. package/dist/types/seo.d.ts.map +1 -0
  21. package/dist/types/types.d.ts +826 -0
  22. package/dist/types/types.d.ts.map +1 -0
  23. package/dist/types/utils/index.d.ts +44 -0
  24. package/dist/types/utils/index.d.ts.map +1 -0
  25. package/dist/types/utils/normalize-page-url.d.ts +16 -0
  26. package/dist/types/utils/normalize-page-url.d.ts.map +1 -0
  27. package/dist/types/utils/pick-weaverse-data.d.ts +44 -0
  28. package/dist/types/utils/pick-weaverse-data.d.ts.map +1 -0
  29. package/dist/types/utils/pixel.d.ts +16 -0
  30. package/dist/types/utils/pixel.d.ts.map +1 -0
  31. package/dist/types/utils/studio-request-info.d.ts +17 -0
  32. package/dist/types/utils/studio-request-info.d.ts.map +1 -0
  33. package/dist/types/utils/studio-script-src.d.ts +78 -0
  34. package/dist/types/utils/studio-script-src.d.ts.map +1 -0
  35. package/dist/types/utils/sync-reused-instance.d.ts +21 -0
  36. package/dist/types/utils/sync-reused-instance.d.ts.map +1 -0
  37. package/dist/types/utils/theme-text-store.d.ts +2 -0
  38. package/dist/types/utils/theme-text-store.d.ts.map +1 -0
  39. package/dist/types/utils/translation-store.d.ts +40 -0
  40. package/dist/types/utils/translation-store.d.ts.map +1 -0
  41. package/dist/types/utils/use-studio.d.ts +33 -0
  42. package/dist/types/utils/use-studio.d.ts.map +1 -0
  43. package/dist/types/utils/use-theme-settings-store.d.ts +39 -0
  44. package/dist/types/utils/use-theme-settings-store.d.ts.map +1 -0
  45. package/dist/types/weaverse-client.d.ts +227 -0
  46. package/dist/types/weaverse-client.d.ts.map +1 -0
  47. package/package.json +7 -6
  48. package/src/WeaverseHydrogenRoot.tsx +649 -0
  49. package/src/components/index.ts +10 -0
  50. package/src/components/main.tsx +28 -0
  51. package/src/hooks/theme-text-context.tsx +14 -0
  52. package/src/hooks/translation-context.tsx +275 -0
  53. package/src/hooks/use-weaverse-data-context.ts +69 -0
  54. package/src/index.ts +179 -0
  55. package/src/seo.ts +125 -0
  56. package/src/types.ts +1020 -0
  57. package/src/utils/index.ts +351 -0
  58. package/src/utils/normalize-page-url.ts +44 -0
  59. package/src/utils/pick-weaverse-data.ts +89 -0
  60. package/src/utils/pixel.ts +71 -0
  61. package/src/utils/studio-request-info.ts +66 -0
  62. package/src/utils/studio-script-src.ts +162 -0
  63. package/src/utils/sync-reused-instance.ts +57 -0
  64. package/src/utils/theme-text-store.ts +1 -0
  65. package/src/utils/translation-store.ts +65 -0
  66. package/src/utils/use-studio.ts +168 -0
  67. package/src/utils/use-theme-settings-store.ts +160 -0
  68. package/src/weaverse-client.ts +1179 -0
  69. package/dist/index.d.mts +0 -1260
  70. package/dist/index.d.ts +0 -1260
@@ -0,0 +1,275 @@
1
+ import { useSafeExternalStore } from '@weaverse/react'
2
+ import { createContext, type ReactNode, useContext, useMemo } from 'react'
3
+ import type { TranslationStore } from '../utils/translation-store'
4
+
5
+ /**
6
+ * Traverse a nested object by dot-path.
7
+ *
8
+ * @example
9
+ * getNestedKey({ cart: { title: 'Cart' } }, 'cart.title') // => 'Cart'
10
+ * getNestedKey({}, 'cart.title', 'fallback') // => 'fallback'
11
+ */
12
+ export function getNestedKey(
13
+ obj: Record<string, unknown>,
14
+ path: string,
15
+ fallback?: string
16
+ ): string | undefined {
17
+ const keys = path.split('.')
18
+ let current: unknown = obj
19
+
20
+ for (const key of keys) {
21
+ if (current == null || typeof current !== 'object') {
22
+ return fallback
23
+ }
24
+ current = (current as Record<string, unknown>)[key]
25
+ }
26
+
27
+ if (typeof current === 'string') {
28
+ return current
29
+ }
30
+
31
+ return fallback
32
+ }
33
+
34
+ /**
35
+ * Replace `{{variable}}` patterns in a string with provided values.
36
+ *
37
+ * @example
38
+ * interpolate('Hello {{name}}!', { name: 'World' }) // => 'Hello World!'
39
+ * interpolate('-{{percentage}}% Off', { percentage: 15 }) // => '-15% Off'
40
+ */
41
+ export function interpolate(
42
+ template: string,
43
+ variables?: Record<string, string | number>
44
+ ): string {
45
+ if (!variables) {
46
+ return template
47
+ }
48
+
49
+ return template.replace(/\{\{(\w+)\}\}/g, (match, key: string) => {
50
+ const value = variables[key]
51
+ return value === undefined ? match : String(value)
52
+ })
53
+ }
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // Context & Provider
57
+ // ---------------------------------------------------------------------------
58
+
59
+ /**
60
+ * Signature for a translation function.
61
+ */
62
+ export type TranslateFunction = (
63
+ key: string,
64
+ variables?: Record<string, string | number>
65
+ ) => string
66
+
67
+ /** Translation function, stores, and locale overrides exposed by the provider. */
68
+ export type TranslationValue = {
69
+ /**
70
+ * Translate a key with optional variable interpolation.
71
+ *
72
+ * Priority chain (highest wins):
73
+ * 1. External `t` (passed into `withWeaverse` / `TranslationProvider`)
74
+ * 2. `merchantOverrides` (DB overrides for the current locale + design overrides)
75
+ * 3. `staticContent` (theme default JSON)
76
+ * 4. The key itself (fallback)
77
+ *
78
+ * @example
79
+ * t('cart.title') // => "Cart"
80
+ * t('badge.sale', { percentage: 15 }) // => "-15% Off"
81
+ */
82
+ t: TranslateFunction
83
+ /** Exposed for useStudio to attach to weaverse.internal. Null in production. */
84
+ translationStore: TranslationStore | null
85
+ /**
86
+ * @deprecated Use {@link TranslationValue.translationStore} instead. Kept as a
87
+ * backward-compatible alias pointing at the same store instance.
88
+ */
89
+ themeTextStore: TranslationStore | null
90
+ /** Active-locale translated keys loaded from the storefront API. */
91
+ merchantOverrides: Record<string, unknown> | null
92
+ }
93
+
94
+ const TranslationContext = createContext<TranslationValue | null>(null)
95
+ TranslationContext.displayName = 'WeaverseTranslationContext'
96
+
97
+ /** Inputs accepted by {@link TranslationProvider}. */
98
+ export type TranslationProviderProps = {
99
+ /** Theme-provided default-locale translation content. */
100
+ staticContent: Record<string, unknown>
101
+ /** Active-locale translations published by the merchant. */
102
+ merchantOverrides?: Record<string, unknown>
103
+ /**
104
+ * Optional external translation function.
105
+ * When provided it becomes the **highest-priority** source:
106
+ * if it returns a non-empty string that differs from the raw key,
107
+ * that value is used; otherwise the internal resolution chain continues.
108
+ */
109
+ t?: TranslateFunction
110
+ /** Optional store for live design-mode text overrides from the builder. */
111
+ translationStore?: TranslationStore | null
112
+ /**
113
+ * @deprecated Use {@link TranslationProviderProps.translationStore} instead.
114
+ * Still accepted for backward compatibility; ignored when `translationStore`
115
+ * is also provided.
116
+ */
117
+ themeTextStore?: TranslationStore | null
118
+ /** React content that can consume the translation context. */
119
+ children: ReactNode
120
+ }
121
+
122
+ /**
123
+ * Build the `t()` translation function over a fixed set of sources.
124
+ *
125
+ * Resolution order, highest priority first:
126
+ * 1. `externalT` – host i18n (e.g. Shopify), unless it echoes the key
127
+ * 2. `designOverrides` – live design-mode edits, flat dot-path keys
128
+ * 3. `merchantOverrides` – locale overrides from the API, nested
129
+ * 4. `staticContent` – the theme's default locale JSON
130
+ * 5. the key itself
131
+ *
132
+ * Design overrides are looked up per-key (own properties only) rather than
133
+ * merged into `merchantOverrides`: a flat key like `cart.title` must override
134
+ * exactly that leaf without wiping sibling keys (`cart.subtitle`), and a
135
+ * per-key own-property lookup never builds nested objects from
136
+ * merchant-controlled keys, so it cannot pollute `Object.prototype`.
137
+ */
138
+ export function createTranslate(sources: {
139
+ staticContent: Record<string, unknown>
140
+ merchantOverrides?: Record<string, unknown>
141
+ designOverrides?: Record<string, string>
142
+ externalT?: TranslateFunction
143
+ }): TranslateFunction {
144
+ const { staticContent, merchantOverrides, designOverrides, externalT } =
145
+ sources
146
+ return (key, variables) => {
147
+ // Priority 1: external t (e.g. Shopify i18n, third-party library).
148
+ if (externalT) {
149
+ const external = externalT(key, variables)
150
+ // Accept the external value unless it echoed the key unchanged.
151
+ if (external && external !== key) {
152
+ return external
153
+ }
154
+ }
155
+
156
+ // Priority 2: live design-mode override (flat, keyed by exact dot-path,
157
+ // own properties only so inherited keys never resolve to functions).
158
+ const design =
159
+ designOverrides && Object.hasOwn(designOverrides, key)
160
+ ? designOverrides[key]
161
+ : undefined
162
+ // Priority 3: merchant overrides (nested). Priority 4: static content.
163
+ const override = merchantOverrides
164
+ ? getNestedKey(merchantOverrides, key)
165
+ : undefined
166
+ const staticValue = getNestedKey(staticContent, key)
167
+
168
+ // Priority 5: the key itself as the final fallback.
169
+ const raw = design ?? override ?? staticValue ?? key
170
+ return interpolate(raw, variables)
171
+ }
172
+ }
173
+
174
+ // No-op fallback for useSafeExternalStore when store is null (production).
175
+ // Module-level constants — stable references, zero allocation per render.
176
+ const EMPTY: Record<string, string> = {}
177
+ const NOOP_SUBSCRIBE = () => () => {}
178
+ const NOOP_SNAPSHOT = () => EMPTY
179
+
180
+ /**
181
+ * Provider that wraps the app to expose the `t()` function.
182
+ *
183
+ * - `staticContent` – the theme's default locale JSON (from themeSchema.i18n)
184
+ * - `merchantOverrides` – locale-specific overrides fetched from the API
185
+ * - `translationStore` – optional store for live design-mode text overrides
186
+ * (the deprecated `themeTextStore` prop is accepted as a fallback)
187
+ */
188
+ export function TranslationProvider({
189
+ staticContent,
190
+ merchantOverrides,
191
+ t: externalT,
192
+ translationStore,
193
+ themeTextStore,
194
+ children,
195
+ }: TranslationProviderProps) {
196
+ // Canonical store, falling back to the deprecated `themeTextStore` prop.
197
+ const store = translationStore ?? themeTextStore ?? null
198
+
199
+ // Subscribe to design-mode overrides. No-op when store is null (production).
200
+ const designOverrides = useSafeExternalStore(
201
+ store?.subscribe ?? NOOP_SUBSCRIBE,
202
+ store?.getSnapshot ?? NOOP_SNAPSHOT,
203
+ store?.getServerSnapshot ?? NOOP_SNAPSHOT
204
+ )
205
+
206
+ const value = useMemo<TranslationValue>(
207
+ () => ({
208
+ t: createTranslate({
209
+ staticContent,
210
+ merchantOverrides,
211
+ designOverrides,
212
+ externalT,
213
+ }),
214
+ translationStore: store,
215
+ // Deprecated alias — same instance, kept for backward compatibility.
216
+ themeTextStore: store,
217
+ merchantOverrides: merchantOverrides ?? null,
218
+ }),
219
+ [staticContent, merchantOverrides, designOverrides, externalT, store]
220
+ )
221
+
222
+ return (
223
+ <TranslationContext.Provider value={value}>
224
+ {children}
225
+ </TranslationContext.Provider>
226
+ )
227
+ }
228
+
229
+ /**
230
+ * Hook to access the translation `t()` function.
231
+ *
232
+ * Must be used inside a component wrapped by `withWeaverse` or `TranslationProvider`.
233
+ *
234
+ * @example
235
+ * ```tsx
236
+ * function CartTitle() {
237
+ * const { t } = useTranslation()
238
+ * return <h2>{t('cart.title')}</h2>
239
+ * }
240
+ * ```
241
+ */
242
+ export function useTranslation(): TranslationValue {
243
+ const ctx = useContext(TranslationContext)
244
+ if (!ctx) {
245
+ throw new Error(
246
+ 'useTranslation must be used within <TranslationProvider>. Wrap your app with withWeaverse().'
247
+ )
248
+ }
249
+ return ctx
250
+ }
251
+
252
+ // ---------------------------------------------------------------------------
253
+ // Deprecated aliases (backward compatibility — kept exported indefinitely)
254
+ // ---------------------------------------------------------------------------
255
+
256
+ /**
257
+ * Legacy name for {@link TranslationProvider}.
258
+ * @deprecated Use {@link TranslationProvider} instead.
259
+ */
260
+ export const ThemeTextProvider = TranslationProvider
261
+ /**
262
+ * Legacy name for {@link useTranslation}.
263
+ * @deprecated Use {@link useTranslation} instead.
264
+ */
265
+ export const useThemeText = useTranslation
266
+ /**
267
+ * Legacy name for {@link TranslationValue}.
268
+ * @deprecated Use {@link TranslationValue} instead.
269
+ */
270
+ export type ThemeTextValue = TranslationValue
271
+ /**
272
+ * Legacy name for {@link TranslationProviderProps}.
273
+ * @deprecated Use {@link TranslationProviderProps} instead.
274
+ */
275
+ export type ThemeTextProviderProps = TranslationProviderProps
@@ -0,0 +1,69 @@
1
+ import { type UIMatch, useMatches } from 'react-router'
2
+
3
+ /** Loader data indexed by React Router's conventional route identifiers. */
4
+ export interface RouteKeyedDataContext {
5
+ /** Root route loader data, when the root loader returned an object. */
6
+ root?: Record<string, unknown>
7
+ /** Loader data for a non-root route keyed by its route identifier. */
8
+ [routeKey: `routes/${string}`]: Record<string, unknown>
9
+ }
10
+
11
+ /**
12
+ * Flat loader-data map keyed by React Router route ID.
13
+ * Use route IDs such as `root` or `routes/products.$handle` in data bindings.
14
+ */
15
+ export type WeaverseDataContext = RouteKeyedDataContext &
16
+ Record<string, unknown>
17
+
18
+ /**
19
+ * Creates flat route-keyed structure from matches
20
+ * Target: {root: {...rootData}, "routes/product": {...productData}}
21
+ * Eliminates all duplication by using route IDs as keys
22
+ */
23
+ function createFlatDataContext(
24
+ matches: UIMatch<unknown, unknown>[]
25
+ ): WeaverseDataContext {
26
+ const context: WeaverseDataContext = {}
27
+
28
+ // Process matches from root to leaf, creating route-keyed structure
29
+ for (const match of matches) {
30
+ if (match.data && typeof match.data === 'object') {
31
+ // Use route ID as key (e.g., "root", "routes/product")
32
+ context[match.id] = match.data
33
+ }
34
+ }
35
+
36
+ return context
37
+ }
38
+
39
+ /**
40
+ * Hook to get flat route-keyed data context from matches
41
+ * Returns: {root: {...}, "routes/xxx": {...}}
42
+ * No more useLoaderData dependency - everything comes from matches
43
+ */
44
+ export function useWeaverseDataContext(): WeaverseDataContext {
45
+ const matches = useMatches()
46
+
47
+ // Create flat route-keyed structure - eliminates duplication
48
+ return createFlatDataContext(matches)
49
+ }
50
+
51
+ /**
52
+ * Helper function to create flat route-keyed data context from useMatches() result
53
+ * Returns: {root: {...}, "routes/xxx": {...}}
54
+ */
55
+ export function createWeaverseDataContext(
56
+ matches: UIMatch<unknown, unknown>[]
57
+ ): WeaverseDataContext {
58
+ // Create flat route-keyed structure - clean and efficient
59
+ return createFlatDataContext(matches)
60
+ }
61
+
62
+ /**
63
+ * Type guard to check if data is WeaverseDataContext (simplified)
64
+ */
65
+ export function isWeaverseDataContext(
66
+ data: unknown
67
+ ): data is WeaverseDataContext {
68
+ return data !== null && typeof data === 'object'
69
+ }
package/src/index.ts ADDED
@@ -0,0 +1,179 @@
1
+ /**
2
+ * React and React Router integrations for rendering Weaverse content in
3
+ * Shopify Hydrogen storefronts.
4
+ *
5
+ * @packageDocumentation
6
+ */
7
+
8
+ import { getSelectedProductOptions as hydrogen_getSelectedProductOptions } from '@shopify/hydrogen'
9
+ import type { SelectedOptionInput } from '@shopify/hydrogen/storefront-api-types'
10
+ import {
11
+ isBrowser,
12
+ isIframe,
13
+ useChildInstances,
14
+ useItemInstance,
15
+ useParentInstance,
16
+ useWeaverse,
17
+ } from '@weaverse/react'
18
+
19
+ /**
20
+ * Filter out Weaverse-specific query parameters from product options.
21
+ * Removes `isDesignMode` and any parameters starting with `weaverse` prefix.
22
+ *
23
+ * @param request - The incoming HTTP request
24
+ * @returns Filtered product options for Shopify Hydrogen
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * export async function loader({ request }: LoaderFunctionArgs) {
29
+ * const selectedOptions = getSelectedProductOptions(request)
30
+ * // Use selectedOptions for product queries without Weaverse params
31
+ * }
32
+ * ```
33
+ */
34
+ export function getSelectedProductOptions(
35
+ request: Request
36
+ ): SelectedOptionInput[] {
37
+ let options = hydrogen_getSelectedProductOptions(request)
38
+ return options.filter(
39
+ ({ name }) => name !== 'isDesignMode' && !name.startsWith('weaverse')
40
+ )
41
+ }
42
+
43
+ /** Placeholder asset URLs for component schema defaults and theme previews. */
44
+ export const IMAGES_PLACEHOLDERS = {
45
+ /** White Weaverse logo placeholder. */
46
+ logo_white:
47
+ 'https://cdn.shopify.com/s/files/1/0838/0052/3057/files/fpo-logo-w-600x200.svg',
48
+ /** Black Weaverse logo placeholder. */
49
+ logo_black:
50
+ 'https://cdn.shopify.com/s/files/1/0838/0052/3057/files/fpo-logo-k-600x200.svg',
51
+ /** Generic square image placeholder. */
52
+ image:
53
+ 'https://cdn.shopify.com/s/files/1/0838/0052/3057/files/fpo-image-1024x1024.svg',
54
+ /** First landscape banner placeholder. */
55
+ banner_1:
56
+ 'https://cdn.shopify.com/s/files/1/0838/0052/3057/files/fpo-lifestyle-1-1800x900.svg',
57
+ /** Second landscape banner placeholder. */
58
+ banner_2:
59
+ 'https://cdn.shopify.com/s/files/1/0838/0052/3057/files/fpo-lifestyle-2-1800x900.svg',
60
+ /** Hats collection placeholder. */
61
+ collection_1:
62
+ 'https://cdn.shopify.com/s/files/1/0838/0052/3057/files/fpo-collection-hats-1024x1024.svg',
63
+ /** Shoes collection placeholder. */
64
+ collection_2:
65
+ 'https://cdn.shopify.com/s/files/1/0838/0052/3057/files/fpo-collection-shoes-1024x1024.svg',
66
+ /** Glasses collection placeholder. */
67
+ collection_3:
68
+ 'https://cdn.shopify.com/s/files/1/0838/0052/3057/files/fpo-collection-glasses-1024x1024.svg',
69
+ /** Lamps collection placeholder. */
70
+ collection_4:
71
+ 'https://cdn.shopify.com/s/files/1/0838/0052/3057/files/fpo-collection-lamps-1024x1024.svg',
72
+ /** Bags collection placeholder. */
73
+ collection_5:
74
+ 'https://cdn.shopify.com/s/files/1/0838/0052/3057/files/fpo-collection-bags-1024x1024.svg',
75
+ /** Watches collection placeholder. */
76
+ collection_6:
77
+ 'https://cdn.shopify.com/s/files/1/0838/0052/3057/files/fpo-collection-watches-1024x1024.svg',
78
+ /** Third watch product placeholder. */
79
+ product_1:
80
+ 'https://cdn.shopify.com/s/files/1/0838/0052/3057/files/fpo-product-watch-3-1024x1024.svg',
81
+ /** Second bag product placeholder. */
82
+ product_2:
83
+ 'https://cdn.shopify.com/s/files/1/0838/0052/3057/files/fpo-product-bag-2-1024x1024.svg',
84
+ /** First shoe product placeholder. */
85
+ product_3:
86
+ 'https://cdn.shopify.com/s/files/1/0838/0052/3057/files/fpo-product-shoe-1-1024x1024.svg',
87
+ /** Second lamp product placeholder. */
88
+ product_4:
89
+ 'https://cdn.shopify.com/s/files/1/0838/0052/3057/files/fpo-product-lamp-2-1024x1024.svg',
90
+ /** First glasses product placeholder. */
91
+ product_5:
92
+ 'https://cdn.shopify.com/s/files/1/0838/0052/3057/files/fpo-product-glasses-1-1024x1024.svg',
93
+ /** First lamp product placeholder. */
94
+ product_6:
95
+ 'https://cdn.shopify.com/s/files/1/0838/0052/3057/files/fpo-product-lamp-1-1024x1024.svg',
96
+ /** Second glasses product placeholder. */
97
+ product_7:
98
+ 'https://cdn.shopify.com/s/files/1/0838/0052/3057/files/fpo-product-glasses-2-1024x1024.svg',
99
+ /** Third hat product placeholder. */
100
+ product_8:
101
+ 'https://cdn.shopify.com/s/files/1/0838/0052/3057/files/fpo-product-hat-3-1024x1024.svg',
102
+ /** First bag product placeholder. */
103
+ product_9:
104
+ 'https://cdn.shopify.com/s/files/1/0838/0052/3057/files/fpo-product-bag-1-1024x1024.svg',
105
+ /** Second shoe product placeholder. */
106
+ product_10:
107
+ 'https://cdn.shopify.com/s/files/1/0838/0052/3057/files/fpo-product-shoe-2-1024x1024.svg',
108
+ /** Second hat product placeholder. */
109
+ product_11:
110
+ 'https://cdn.shopify.com/s/files/1/0838/0052/3057/files/fpo-product-hat-2-1024x1024.svg',
111
+ /** Third glasses product placeholder. */
112
+ product_12:
113
+ 'https://cdn.shopify.com/s/files/1/0838/0052/3057/files/fpo-product-glasses-3-1024x1024.svg',
114
+ /** Third shoe product placeholder. */
115
+ product_13:
116
+ 'https://cdn.shopify.com/s/files/1/0838/0052/3057/files/fpo-product-shoe-3-1024x1024.svg',
117
+ /** Third bag product placeholder. */
118
+ product_14:
119
+ 'https://cdn.shopify.com/s/files/1/0838/0052/3057/files/fpo-product-bag-3-1024x1024.svg',
120
+ /** First hat product placeholder. */
121
+ product_15:
122
+ 'https://cdn.shopify.com/s/files/1/0838/0052/3057/files/fpo-product-hat-1-1024x1024.svg',
123
+ /** Third lamp product placeholder. */
124
+ product_16:
125
+ 'https://cdn.shopify.com/s/files/1/0838/0052/3057/files/fpo-product-lamp-3-1024x1024.svg',
126
+ /** First watch product placeholder. */
127
+ product_17:
128
+ 'https://cdn.shopify.com/s/files/1/0838/0052/3057/files/fpo-product-watch-1-1024x1024.svg',
129
+ /** Second watch product placeholder. */
130
+ product_18:
131
+ 'https://cdn.shopify.com/s/files/1/0838/0052/3057/files/fpo-product-watch-2-1024x1024.svg',
132
+ }
133
+
134
+ // Re-export all types and utilities from schema package
135
+ export * from '@weaverse/schema'
136
+ export * from './components'
137
+ export {
138
+ getNestedKey,
139
+ interpolate,
140
+ // Deprecated aliases — kept for backward compatibility.
141
+ ThemeTextProvider,
142
+ type ThemeTextProviderProps,
143
+ type ThemeTextValue,
144
+ type TranslateFunction,
145
+ // Canonical translation API.
146
+ TranslationProvider,
147
+ type TranslationProviderProps,
148
+ type TranslationValue,
149
+ useThemeText,
150
+ useTranslation,
151
+ } from './hooks/translation-context'
152
+ // Export Hydrogen-specific hooks
153
+ export {
154
+ createWeaverseDataContext,
155
+ type RouteKeyedDataContext,
156
+ useWeaverseDataContext,
157
+ type WeaverseDataContext,
158
+ } from './hooks/use-weaverse-data-context'
159
+ // Export hydrogen-specific modules
160
+ export * from './seo'
161
+ export * from './types'
162
+ export * from './utils'
163
+ export type { WeaverseDataValue } from './utils/pick-weaverse-data'
164
+ export { ThemeTextStore, TranslationStore } from './utils/translation-store'
165
+ export type {
166
+ ThemeSettingsStore,
167
+ WeaverseThemeData,
168
+ } from './utils/use-theme-settings-store'
169
+ export * from './WeaverseHydrogenRoot'
170
+ export * from './weaverse-client'
171
+ // Re-export utilities from @weaverse/react
172
+ export {
173
+ isBrowser,
174
+ isIframe,
175
+ useChildInstances,
176
+ useItemInstance,
177
+ useParentInstance,
178
+ useWeaverse,
179
+ }
package/src/seo.ts ADDED
@@ -0,0 +1,125 @@
1
+ import type { PageSEOData } from '@weaverse/schema'
2
+ import type { MetaDescriptor } from 'react-router'
3
+ import type { HydrogenPageData, WeaverseLoaderData } from './types'
4
+
5
+ /** Object containing an optional Weaverse page for SEO extraction. */
6
+ export interface WeaverseSeoPageContainer {
7
+ /** Page whose published SEO fields should be formatted. */
8
+ page?: HydrogenPageData | null
9
+ }
10
+
11
+ /** Input accepted by {@link getWeaverseSeoMeta}. */
12
+ export type WeaverseSeoInput =
13
+ | WeaverseLoaderData
14
+ | HydrogenPageData
15
+ | WeaverseSeoPageContainer
16
+ | null
17
+ | undefined
18
+
19
+ /**
20
+ * Convert a Weaverse page-level SEO payload into React Router
21
+ * `MetaDescriptor[]`. Pure, synchronous, tree-shakeable — does not fetch.
22
+ *
23
+ * Behavior:
24
+ * - `title` → top-level `{ title }` descriptor.
25
+ * - `description` / `keywords` → `<meta name="...">`.
26
+ * - `canonicalUrl` → `<link rel="canonical">`.
27
+ * - `openGraph.*` → `<meta property="og:*">`. `og:type` defaults to
28
+ * `'website'` when any other OG field is present.
29
+ * - `twitter.*` → `<meta name="twitter:*">`. `twitter:card` defaults to
30
+ * `'summary'` when any other Twitter field is present.
31
+ * - `robots` is always emitted (defaults to `'index,follow'`) so themes
32
+ * stacking multiple SEO sources have a deterministic last word on
33
+ * indexing for Weaverse-managed pages.
34
+ * - Empty strings / missing fields are filtered before return so empty
35
+ * descriptors don't override earlier sources (e.g. Hydrogen
36
+ * `getSeoMeta`).
37
+ */
38
+ export function formatMetaDescriptors(
39
+ seo: PageSEOData | null | undefined
40
+ ): MetaDescriptor[] {
41
+ if (!seo) {
42
+ return [{ name: 'robots', content: 'index,follow' }]
43
+ }
44
+
45
+ let descriptors: MetaDescriptor[] = []
46
+
47
+ if (seo.title) {
48
+ descriptors.push({ title: seo.title })
49
+ }
50
+ if (seo.description) {
51
+ descriptors.push({ name: 'description', content: seo.description })
52
+ }
53
+ if (seo.keywords) {
54
+ descriptors.push({ name: 'keywords', content: seo.keywords })
55
+ }
56
+ if (seo.canonicalUrl) {
57
+ descriptors.push({
58
+ tagName: 'link',
59
+ rel: 'canonical',
60
+ href: seo.canonicalUrl,
61
+ })
62
+ }
63
+
64
+ let og = seo.openGraph
65
+ if (og && (og.title || og.description || og.image || og.type)) {
66
+ if (og.title) {
67
+ descriptors.push({ property: 'og:title', content: og.title })
68
+ }
69
+ if (og.description) {
70
+ descriptors.push({ property: 'og:description', content: og.description })
71
+ }
72
+ if (og.image) {
73
+ descriptors.push({ property: 'og:image', content: og.image })
74
+ }
75
+ descriptors.push({ property: 'og:type', content: og.type || 'website' })
76
+ }
77
+
78
+ let tw = seo.twitter
79
+ if (tw && (tw.title || tw.description || tw.image || tw.cardType)) {
80
+ descriptors.push({
81
+ name: 'twitter:card',
82
+ content: tw.cardType || 'summary',
83
+ })
84
+ if (tw.title) {
85
+ descriptors.push({ name: 'twitter:title', content: tw.title })
86
+ }
87
+ if (tw.description) {
88
+ descriptors.push({ name: 'twitter:description', content: tw.description })
89
+ }
90
+ if (tw.image) {
91
+ descriptors.push({ name: 'twitter:image', content: tw.image })
92
+ }
93
+ }
94
+
95
+ let index = seo.robots?.index !== false
96
+ let follow = seo.robots?.follow !== false
97
+ descriptors.push({
98
+ name: 'robots',
99
+ content: `${index ? 'index' : 'noindex'},${follow ? 'follow' : 'nofollow'}`,
100
+ })
101
+
102
+ return descriptors
103
+ }
104
+
105
+ /**
106
+ * Convenience accessor: pull `page.seo` out of a `WeaverseLoaderData`
107
+ * (or `HydrogenPageData`) and emit `MetaDescriptor[]`. Null-safe.
108
+ *
109
+ * @example
110
+ * ```ts
111
+ * export const meta: MetaFunction<typeof loader> = ({ data }) => {
112
+ * return getWeaverseSeoMeta(data?.weaverseData)
113
+ * }
114
+ * ```
115
+ */
116
+ export function getWeaverseSeoMeta(data: WeaverseSeoInput): MetaDescriptor[] {
117
+ if (!data) {
118
+ return formatMetaDescriptors(null)
119
+ }
120
+ let page =
121
+ 'items' in data && 'id' in data
122
+ ? (data as HydrogenPageData)
123
+ : ((data as WeaverseSeoPageContainer).page ?? null)
124
+ return formatMetaDescriptors(page?.seo ?? null)
125
+ }