@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.
- package/dist/index.js +83 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +83 -1
- package/dist/index.mjs.map +1 -1
- package/dist/types/WeaverseHydrogenRoot.d.ts +218 -0
- package/dist/types/WeaverseHydrogenRoot.d.ts.map +1 -0
- package/dist/types/components/index.d.ts +4 -0
- package/dist/types/components/index.d.ts.map +1 -0
- package/dist/types/components/main.d.ts +7 -0
- package/dist/types/components/main.d.ts.map +1 -0
- package/dist/types/hooks/theme-text-context.d.ts +2 -0
- package/dist/types/hooks/theme-text-context.d.ts.map +1 -0
- package/dist/types/hooks/translation-context.d.ts +138 -0
- package/dist/types/hooks/translation-context.d.ts.map +1 -0
- package/dist/types/hooks/use-weaverse-data-context.d.ts +29 -0
- package/dist/types/hooks/use-weaverse-data-context.d.ts.map +1 -0
- package/dist/types/index.d.ts +99 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/seo.d.ts +43 -0
- package/dist/types/seo.d.ts.map +1 -0
- package/dist/types/types.d.ts +826 -0
- package/dist/types/types.d.ts.map +1 -0
- package/dist/types/utils/index.d.ts +44 -0
- package/dist/types/utils/index.d.ts.map +1 -0
- package/dist/types/utils/normalize-page-url.d.ts +16 -0
- package/dist/types/utils/normalize-page-url.d.ts.map +1 -0
- package/dist/types/utils/pick-weaverse-data.d.ts +44 -0
- package/dist/types/utils/pick-weaverse-data.d.ts.map +1 -0
- package/dist/types/utils/pixel.d.ts +16 -0
- package/dist/types/utils/pixel.d.ts.map +1 -0
- package/dist/types/utils/studio-request-info.d.ts +17 -0
- package/dist/types/utils/studio-request-info.d.ts.map +1 -0
- package/dist/types/utils/studio-script-src.d.ts +78 -0
- package/dist/types/utils/studio-script-src.d.ts.map +1 -0
- package/dist/types/utils/sync-reused-instance.d.ts +21 -0
- package/dist/types/utils/sync-reused-instance.d.ts.map +1 -0
- package/dist/types/utils/theme-text-store.d.ts +2 -0
- package/dist/types/utils/theme-text-store.d.ts.map +1 -0
- package/dist/types/utils/translation-store.d.ts +40 -0
- package/dist/types/utils/translation-store.d.ts.map +1 -0
- package/dist/types/utils/use-studio.d.ts +33 -0
- package/dist/types/utils/use-studio.d.ts.map +1 -0
- package/dist/types/utils/use-theme-settings-store.d.ts +39 -0
- package/dist/types/utils/use-theme-settings-store.d.ts.map +1 -0
- package/dist/types/weaverse-client.d.ts +227 -0
- package/dist/types/weaverse-client.d.ts.map +1 -0
- package/package.json +7 -6
- package/src/WeaverseHydrogenRoot.tsx +649 -0
- package/src/components/index.ts +10 -0
- package/src/components/main.tsx +28 -0
- package/src/hooks/theme-text-context.tsx +14 -0
- package/src/hooks/translation-context.tsx +275 -0
- package/src/hooks/use-weaverse-data-context.ts +69 -0
- package/src/index.ts +179 -0
- package/src/seo.ts +125 -0
- package/src/types.ts +1020 -0
- package/src/utils/index.ts +351 -0
- package/src/utils/normalize-page-url.ts +44 -0
- package/src/utils/pick-weaverse-data.ts +89 -0
- package/src/utils/pixel.ts +71 -0
- package/src/utils/studio-request-info.ts +66 -0
- package/src/utils/studio-script-src.ts +162 -0
- package/src/utils/sync-reused-instance.ts +57 -0
- package/src/utils/theme-text-store.ts +1 -0
- package/src/utils/translation-store.ts +65 -0
- package/src/utils/use-studio.ts +168 -0
- package/src/utils/use-theme-settings-store.ts +160 -0
- package/src/weaverse-client.ts +1179 -0
- package/dist/index.d.mts +0 -1260
- 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
|
+
}
|