medusa-storefront-data 2.5.5 → 2.5.8

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.
@@ -166,7 +166,7 @@ Compose `options.configs` from `homepageSectionFields` or individual `*HomepageS
166
166
 
167
167
  Help page FAQs use **`helpFaqHomepageSectionField`** from `medusa-ui-home-config/sections/helpFaq` (also included in `homepageSectionFields`).
168
168
 
169
- **Split dynamic-config keys** (Medusa plugin): store FAQs under `faqs.helpFaq`; homepage sections under `homepage-sections.sections`. `homepageConfigFromDynamicResponse` assembles these automatically:
169
+ **Split dynamic-config keys** (Medusa plugin): store FAQs under `faqs.helpFaq`; homepage sections under `homepage-sections.sections`; hero slides under `hero-banners` (`homeBanners` / `appBanners` with `slide` rows). `homepageConfigFromDynamicResponse` assembles these automatically:
170
170
 
171
171
  ```json
172
172
  {
@@ -196,6 +196,28 @@ Help page FAQs use **`helpFaqHomepageSectionField`** from `medusa-ui-home-config
196
196
  }
197
197
  ```
198
198
 
199
+ **Hero banners** (`hero-banners`): each slide uses `image`, `title`, `subtitle`, `description`, `buttonText`, `buttonLink`. These map to `homepage-banner-array` / `app-banner-array` via `normalizeHomepageConfig` (same fields as `DynamicBanner`: title → eyebrow, subtitle → headline).
200
+
201
+ ```json
202
+ {
203
+ "hero-banners": {
204
+ "homeBanners": [
205
+ {
206
+ "slide": {
207
+ "image": "https://…",
208
+ "title": "BEST QUALITY",
209
+ "subtitle": "Electronics & Essentials",
210
+ "description": "…",
211
+ "buttonText": "Shop now",
212
+ "buttonLink": "/store"
213
+ }
214
+ }
215
+ ],
216
+ "appBanners": []
217
+ }
218
+ }
219
+ ```
220
+
199
221
  Legacy single `homepage-config` with `sections.helpFaq` (flat `items` or nested `category` / `faq` wrappers) still works. Legacy flat `faq-array` entries are grouped by `faq-category`.
200
222
 
201
223
  Storefront UI still imports from `medusa-ui-home/sections/<id>`.
@@ -205,8 +227,8 @@ Storefront UI still imports from `medusa-ui-home/sections/<id>`.
205
227
  | Export | Use |
206
228
  |--------|-----|
207
229
  | `loadHeroSectionData`, `loadNewArrivalsSectionData`, … | **One section** catalog + copy |
208
- | `loadHelpFaqSectionData` | Help page FAQ (`sections.helpFaq`) |
209
- | `getHelpFaqFromPageInput(pageInput)` | FAQ categories + section header copy |
230
+ | `getHelpFaqFromPageInput(pageInput)` | Help page FAQ from dynamic config (categories + section copy) — pass as `helpFaq` to `HelpTemplate` |
231
+ | `loadHelpFaqSectionData` | Thin wrapper around `getHelpFaqFromPageInput` (optional; prefer direct call) |
210
232
  | `getDynamicConfig()` | **Main project only** |
211
233
  | `get*FromPageInput(pageInput)` | Read CMS copy/banners without fetching |
212
234
  | `homepageConfigFromDynamicResponse(raw)` | Merge API → `pageInput` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "medusa-storefront-data",
3
- "version": "2.5.5",
3
+ "version": "2.5.8",
4
4
  "type": "module",
5
5
  "description": "Medusa storefront server data layer extracted from Next.js storefront",
6
6
  "license": "MIT",
@@ -17,7 +17,8 @@
17
17
  ],
18
18
  "scripts": {
19
19
  "build": "tsc",
20
- "clean": "rm -rf dist"
20
+ "clean": "rm -rf dist",
21
+ "test": "npm run build"
21
22
  },
22
23
  "peerDependencies": {
23
24
  "next": ">=14",
@@ -172,6 +173,11 @@
172
173
  "types": "./dist/server/help-sections/help-faq.d.ts",
173
174
  "import": "./src/server/help-sections/help-faq.ts",
174
175
  "default": "./src/server/help-sections/help-faq.ts"
176
+ },
177
+ "./storefront-cms": {
178
+ "types": "./src/server/storefront-cms.ts",
179
+ "import": "./src/server/storefront-cms.ts",
180
+ "default": "./src/server/storefront-cms.ts"
175
181
  }
176
182
  },
177
183
  "main": "./dist/server/index.js",
@@ -9,7 +9,10 @@ import {
9
9
  } from "./homepage-section-defaults"
10
10
  import type { DynamicConfigResponse, HomepageConfig } from "./homepage-config.types"
11
11
  import { mergeSectionBlock } from "./config-merge"
12
- import { mergePageInputWithDefaults, resolvePageInput } from "./page-input"
12
+ import {
13
+ mergePageInputWithDefaults,
14
+ resolvePageInput,
15
+ } from "./page-input"
13
16
 
14
17
  export {
15
18
  DEFAULT_PAGE_INPUT,
@@ -20,7 +23,6 @@ export {
20
23
  type StorefrontPageInput,
21
24
  } from "./page-input"
22
25
 
23
-
24
26
  export const getDynamicConfig = cache(async (): Promise<DynamicConfigResponse | null> => {
25
27
  try {
26
28
  const publishableKey = process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY
@@ -80,6 +82,7 @@ export function getLogoFromPageInput(
80
82
 
81
83
 
82
84
 
85
+ /** Normalized hero slide used by storefront UI (`getHomeBannersFromPageInput`). */
83
86
  export type HomepageBanner = {
84
87
  image?: string
85
88
  title?: string
@@ -131,8 +134,6 @@ export function getAppBannersFromPageInput(
131
134
  return null
132
135
  }
133
136
 
134
-
135
-
136
137
  // Legacy function for backward compatibility
137
138
 
138
139
  export function getWebsiteDescriptionFromPageInput(
@@ -675,11 +676,64 @@ export function getFaqsFromPageInput(
675
676
  }
676
677
 
677
678
 
679
+ export type PromoAnnouncementsConfig = {
680
+ messages: string[]
681
+ icon?: string
682
+ iconAlt?: string
683
+ ariaLabel: string
684
+ separator?: string
685
+ }
686
+
687
+ function parsePromoAnnouncementsUi(
688
+ ...sources: Array<Record<string, unknown> | null | undefined>
689
+ ): Pick<PromoAnnouncementsConfig, "icon" | "iconAlt" | "ariaLabel" | "separator"> {
690
+ const pick = (key: string, alt?: string) => {
691
+ for (const src of sources) {
692
+ if (!src) continue
693
+ const value = normalizeCmsText(src[key] ?? (alt ? src[alt] : undefined))
694
+ if (value) return value
695
+ }
696
+ return undefined
697
+ }
698
+ return {
699
+ icon: pick("icon", "ticker-icon"),
700
+ iconAlt: pick("iconAlt", "icon-alt"),
701
+ ariaLabel: pick("ariaLabel", "aria-label") ?? "Store announcements",
702
+ separator: pick("separator", "message-separator"),
703
+ }
704
+ }
705
+
706
+ /** Promo ticker messages + UI copy (icon, aria label, separator). */
707
+ export function getPromoAnnouncementsFromPageInput(
708
+ pageInput?: HomepageConfig | null
709
+ ): PromoAnnouncementsConfig {
710
+ const homepageConfig = resolvePageInput(pageInput)
711
+ const block = getMergedSectionBlock("promoAnnouncements", pageInput)
712
+ const promoUi =
713
+ (homepageConfig.promoUi as Record<string, unknown> | undefined) ??
714
+ (homepageConfig["promo-ui"] as Record<string, unknown> | undefined)
715
+
716
+ let messages = parseAnnouncementMessages(block)
717
+ if (messages.length === 0) {
718
+ const legacy =
719
+ homepageConfig["announcement-messages"] ??
720
+ homepageConfig["announcement-bar"]
721
+ if (Array.isArray(legacy)) {
722
+ messages = legacy
723
+ .map((m) => (typeof m === "string" ? m.trim() : ""))
724
+ .filter(Boolean)
725
+ }
726
+ }
727
+
728
+ const ui = parsePromoAnnouncementsUi(promoUi, block)
729
+ return { messages, ...ui }
730
+ }
731
+
732
+ /** @deprecated Use {@link getPromoAnnouncementsFromPageInput}. */
678
733
  export function getAnnouncementMessagesFromPageInput(
679
734
  pageInput?: HomepageConfig | null
680
735
  ): string[] {
681
- const block = getMergedSectionBlock("promoAnnouncements", pageInput)
682
- return parseAnnouncementMessages(block)
736
+ return getPromoAnnouncementsFromPageInput(pageInput).messages
683
737
  }
684
738
 
685
739
 
@@ -734,14 +788,11 @@ export type BrandPillarConfig = {
734
788
  href?: string
735
789
  }
736
790
 
737
- export function getBrandPillarsFromPageInput(
738
- pageInput?: HomepageConfig | null
739
- ): { sectionTitle: string; pillars: BrandPillarConfig[] } {
740
- const block = getMergedSectionBlock("brandPillars", pageInput)
741
- const copy = parseSectionCopy(block)
742
- const sectionTitle = copy.title ?? copy.name ?? ""
791
+ function parseBrandPillarsFromBlock(
792
+ block: Record<string, unknown>
793
+ ): BrandPillarConfig[] {
743
794
  const raw = block.pillars ?? block["brand-pillars"] ?? []
744
- const pillars = (Array.isArray(raw) ? raw : [])
795
+ return (Array.isArray(raw) ? raw : [])
745
796
  .map((item: unknown) => {
746
797
  if (!item || typeof item !== "object") return null
747
798
  const row = (item as Record<string, unknown>)["pillar"] ?? item
@@ -749,9 +800,26 @@ export function getBrandPillarsFromPageInput(
749
800
  const title = String(p.title ?? p.name ?? "").trim()
750
801
  const description = String(p.description ?? p.text ?? "").trim()
751
802
  const image = (p.image ?? p.icon) as string | undefined
752
- return title && description ? { title, description, image } : null
803
+ const href = (p.href ?? p.link ?? p.url) as string | undefined
804
+ return title && description
805
+ ? { title, description, image, href: href ? String(href) : undefined }
806
+ : null
753
807
  })
754
808
  .filter(Boolean) as BrandPillarConfig[]
809
+ }
810
+
811
+ export function getBrandPillarsFromPageInput(
812
+ pageInput?: HomepageConfig | null
813
+ ): { sectionTitle: string; pillars: BrandPillarConfig[] } {
814
+ const block = getMergedSectionBlock("brandPillars", pageInput)
815
+ const copy = parseSectionCopy(block)
816
+ const sectionTitle = copy.title ?? copy.name ?? ""
817
+ let pillars = parseBrandPillarsFromBlock(block)
818
+ if (pillars.length === 0) {
819
+ pillars = parseBrandPillarsFromBlock(
820
+ DEFAULT_HOMEPAGE_SECTIONS.brandPillars as Record<string, unknown>
821
+ )
822
+ }
755
823
  return { sectionTitle, pillars }
756
824
  }
757
825
 
@@ -1,17 +1,19 @@
1
1
  import "server-only"
2
2
 
3
- import { getAnnouncementMessagesFromPageInput } from "../dynamic-config"
3
+ import { getPromoAnnouncementsFromPageInput } from "../dynamic-config"
4
4
  import type { SectionLoaderOptions } from "./shared"
5
5
 
6
6
  export type PromoAnnouncementsSectionData = {
7
7
  messages: string[]
8
+ icon?: string
9
+ iconAlt?: string
10
+ ariaLabel: string
11
+ separator?: string
8
12
  }
9
13
 
10
14
  export async function loadPromoAnnouncementsSectionData(
11
15
  _countryCode: string,
12
16
  options?: SectionLoaderOptions
13
17
  ): Promise<PromoAnnouncementsSectionData> {
14
- return {
15
- messages: getAnnouncementMessagesFromPageInput(options?.pageInput ?? null),
16
- }
18
+ return getPromoAnnouncementsFromPageInput(options?.pageInput ?? null)
17
19
  }
@@ -10,7 +10,10 @@ import type {
10
10
  } from "./dynamic-config"
11
11
  import type { ShoppableLook } from "./shoppable-looks"
12
12
 
13
- /** Shape returned by the legacy `loadHomePageData` loader (removed — use per-section loaders). */
13
+ /**
14
+ * Shared homepage data shape used by `medusa-storefront-compose`.
15
+ * This is a type-only module (no runtime loaders).
16
+ */
14
17
  export type HomePageData = {
15
18
  region: HttpTypes.StoreRegion | null
16
19
  collections: Array<Record<string, unknown>>
@@ -48,3 +51,4 @@ export type HomePageData = {
48
51
  features: Array<{ name: string; icon: string }>
49
52
  }
50
53
  }
54
+
@@ -23,6 +23,18 @@ export type HomepagePromoAnnouncementsSection = HomepageSectionCopyFields & {
23
23
  messages?: string[]
24
24
  items?: Array<string | { message?: string; text?: string }>
25
25
  "announcement-messages"?: string[]
26
+ /** Optional icon shown before the ticker. */
27
+ icon?: string
28
+ iconAlt?: string
29
+ ariaLabel?: string
30
+ separator?: string
31
+ }
32
+
33
+ export type HomepagePromoUiConfig = {
34
+ icon?: string
35
+ iconAlt?: string
36
+ ariaLabel?: string
37
+ separator?: string
26
38
  }
27
39
 
28
40
  export type HomepagePromoCountdownSection = HomepageSectionCopyFields & {
@@ -277,6 +289,84 @@ export type HomepagePromoBarConfig = {
277
289
  "promo-active"?: boolean
278
290
  }
279
291
 
292
+ export type HomepageHeaderNavStaticItem = {
293
+ type?: "static"
294
+ label?: string
295
+ href?: string
296
+ icon?: string
297
+ iconSrc?: string
298
+ }
299
+
300
+ export type HomepageHeaderNavCategoriesItem = {
301
+ type: "categories"
302
+ /** Limit how many categories to include (after filtering). */
303
+ limit?: number
304
+ /** Show only these category handles (in this exact order). */
305
+ handles?: string[]
306
+ /** Only include top-level categories (no parent). Default: true */
307
+ topLevelOnly?: boolean
308
+ /** Optional prefix label like "Shop by category" (not used in desktop links). */
309
+ label?: string
310
+ }
311
+
312
+ export type HomepageHeaderNavItem =
313
+ | HomepageHeaderNavStaticItem
314
+ | HomepageHeaderNavCategoriesItem
315
+
316
+ export type HomepageHeaderNavConfig = {
317
+ items?: HomepageHeaderNavItem[]
318
+ }
319
+
320
+ export type HomepageFooterNavLink = {
321
+ label?: string
322
+ href?: string
323
+ }
324
+
325
+ export type HomepageFooterNavColumn = {
326
+ title?: string
327
+ links?: HomepageFooterNavLink[]
328
+ }
329
+
330
+ export type HomepageFooterNavConfig = {
331
+ columns?: HomepageFooterNavColumn[]
332
+ }
333
+
334
+ export type HomepageFooterUiConfig = {
335
+ shopColumnTitle?: string
336
+ contactTitle?: string
337
+ newsletterBlurb?: string
338
+ newsletterPlaceholder?: string
339
+ newsletterSubmitLabel?: string
340
+ newsletterInvalidEmail?: string
341
+ newsletterErrorMessage?: string
342
+ /** Supports `{siteName}` token. */
343
+ copyrightText?: string
344
+ icons?: {
345
+ phone?: string
346
+ email?: string
347
+ address?: string
348
+ }
349
+ social?: {
350
+ facebook?: string
351
+ linkedin?: string
352
+ instagram?: string
353
+ youtube?: string
354
+ }
355
+ }
356
+
357
+ export type HomepageHeaderUiConfig = {
358
+ icons?: {
359
+ cart?: string
360
+ account?: string
361
+ search?: string
362
+ }
363
+ search?: {
364
+ placeholder?: string
365
+ title?: string
366
+ emptyStateText?: string
367
+ }
368
+ }
369
+
280
370
  export type HomepageFaqEntry = {
281
371
  "faq-category"?: string
282
372
  "faq-question"?: string
@@ -292,6 +382,29 @@ export type HomepageConfig = {
292
382
  "website-description"?: string
293
383
  website_description?: string
294
384
 
385
+ /**
386
+ * Header navigation (desktop + mobile).
387
+ * Use `type: "static"` for hardcoded links, or `type: "categories"` to auto-populate from product categories.
388
+ */
389
+ headerNav?: HomepageHeaderNavConfig
390
+ "header-nav"?: HomepageHeaderNavConfig
391
+
392
+ /** Footer navigation columns + links. */
393
+ footerNav?: HomepageFooterNavConfig
394
+ "footer-nav"?: HomepageFooterNavConfig
395
+
396
+ /** Footer static copy + icon overrides. */
397
+ footerUi?: HomepageFooterUiConfig
398
+ "footer-ui"?: HomepageFooterUiConfig
399
+
400
+ /** Header icons + search copy. */
401
+ headerUi?: HomepageHeaderUiConfig
402
+ "header-ui"?: HomepageHeaderUiConfig
403
+
404
+ /** Promo ticker icon + accessibility copy (merged with `sections.promoAnnouncements`). */
405
+ promoUi?: HomepagePromoUiConfig
406
+ "promo-ui"?: HomepagePromoUiConfig
407
+
295
408
  /** Hero carousel — used by `hero` section (not `sections.hero`). */
296
409
  "homepage-banner-array"?: HomepageBannerEntry[]
297
410
 
@@ -5,7 +5,6 @@ export * from "./customer-registration"
5
5
  export * from "./customer"
6
6
  export * from "./dynamic-config"
7
7
  export * from "./page-input"
8
- export * from "./resolve-home-segment-data"
9
8
  export * from "./fulfillment"
10
9
  export * from "./guest"
11
10
  export * from "./locale-actions"
@@ -22,3 +21,5 @@ export * from "./swaps"
22
21
  export * from "./variants"
23
22
  export * from "./wishlist"
24
23
  export * from "./home"
24
+ export * from "./shoppable-looks"
25
+ export * from "./storefront-cms"
@@ -263,6 +263,44 @@ export function normalizeHomepageConfig(input: unknown): HomepageConfig {
263
263
  sections: sections as HomepageConfig["sections"],
264
264
  }
265
265
 
266
+ const headerNav =
267
+ asObject(site?.headerNav) ?? asObject(site?.["header-nav"]) ?? asObject(site?.nav)
268
+ if (headerNav) {
269
+ legacy.headerNav = headerNav as never
270
+ }
271
+
272
+ const footerNav =
273
+ asObject(site?.footerNav) ??
274
+ asObject(site?.["footer-nav"]) ??
275
+ asObject(site?.footer)
276
+ if (footerNav) {
277
+ legacy.footerNav = footerNav as never
278
+ }
279
+
280
+ const footerUi =
281
+ asObject(site?.footerUi) ??
282
+ asObject(site?.["footer-ui"]) ??
283
+ asObject(site?.footer)
284
+ if (footerUi) {
285
+ legacy.footerUi = footerUi as never
286
+ }
287
+
288
+ const headerUi =
289
+ asObject(site?.headerUi) ??
290
+ asObject(site?.["header-ui"]) ??
291
+ asObject(site?.header)
292
+ if (headerUi) {
293
+ legacy.headerUi = headerUi as never
294
+ }
295
+
296
+ const promoUi =
297
+ asObject(site?.promoUi) ??
298
+ asObject(site?.["promo-ui"]) ??
299
+ asObject(site?.promo)
300
+ if (promoUi) {
301
+ legacy.promoUi = promoUi as never
302
+ }
303
+
266
304
  if (desktop.length) legacy["homepage-banner-array"] = desktop as never
267
305
  if (mobile.length) legacy["app-banner-array"] = mobile as never
268
306
 
@@ -14,6 +14,87 @@ function asObject(v: unknown): Loose | null {
14
14
  return v && typeof v === "object" && !Array.isArray(v) ? (v as Loose) : null
15
15
  }
16
16
 
17
+ function asArray(v: unknown): unknown[] {
18
+ return Array.isArray(v) ? v : []
19
+ }
20
+
21
+ /** `hero-banners.homeBanners[].slide` from medusa-plugin-dynamic-config. */
22
+ function heroSlideRow(row: unknown): Loose | null {
23
+ const rowObj = asObject(row)
24
+ if (!rowObj) return null
25
+ const slide = asObject(rowObj.slide) ?? rowObj
26
+ const image = slide.image
27
+ if (image == null || image === "") return null
28
+ return {
29
+ image,
30
+ title: slide.title,
31
+ subtitle: slide.subtitle,
32
+ description: slide.description,
33
+ buttonText: slide.buttonText ?? slide.buttonName,
34
+ buttonLink: slide.buttonLink,
35
+ }
36
+ }
37
+
38
+ function simplifiedConfigFromHeroBanners(
39
+ response: DynamicConfigResponse | null | undefined
40
+ ): HomepageConfig | null {
41
+ if (!response || typeof response !== "object") return null
42
+ const root = asObject((response as Loose)["hero-banners"])
43
+ if (!root) return null
44
+
45
+ const desktop = asArray(root.homeBanners)
46
+ .map(heroSlideRow)
47
+ .filter((row): row is Loose => row !== null)
48
+ const mobile = asArray(root.appBanners)
49
+ .map(heroSlideRow)
50
+ .filter((row): row is Loose => row !== null)
51
+
52
+ if (!desktop.length && !mobile.length) return null
53
+
54
+ const banners: Loose = {}
55
+ if (desktop.length) banners.desktop = desktop
56
+ if (mobile.length) banners.mobile = mobile
57
+
58
+ return { site: { banners } } as HomepageConfig
59
+ }
60
+
61
+ function mergeHomepageConfigLayers(
62
+ ...layers: Array<HomepageConfig | null | undefined>
63
+ ): HomepageConfig | null {
64
+ const defined = layers.filter(Boolean) as HomepageConfig[]
65
+ if (!defined.length) return null
66
+ if (defined.length === 1) return defined[0]
67
+
68
+ const merged: Loose = {}
69
+ for (const layer of defined) {
70
+ const site = asObject((layer as Loose).site) ?? {}
71
+ const mergedSite = asObject(merged.site) ?? {}
72
+ const layerBanners = asObject(site.banners)
73
+ if (layerBanners) {
74
+ mergedSite.banners = {
75
+ ...(asObject(mergedSite.banners) ?? {}),
76
+ ...layerBanners,
77
+ }
78
+ }
79
+ for (const [key, value] of Object.entries(site)) {
80
+ if (key !== "banners" && value !== undefined) mergedSite[key] = value
81
+ }
82
+ if (Object.keys(mergedSite).length) merged.site = mergedSite
83
+
84
+ const sections = asObject((layer as Loose).sections)
85
+ if (sections) {
86
+ merged.sections = { ...(asObject(merged.sections) ?? {}), ...sections }
87
+ }
88
+
89
+ for (const [key, value] of Object.entries(layer as Loose)) {
90
+ if (key === "site" || key === "sections") continue
91
+ if (value !== undefined) merged[key] = value
92
+ }
93
+ }
94
+
95
+ return merged as HomepageConfig
96
+ }
97
+
17
98
  /** Full `homepage-config` blob passed from the main project into pages and layout. */
18
99
  export type StorefrontPageInput = HomepageConfig
19
100
 
@@ -86,6 +167,7 @@ export function simplifiedHomepageConfigFromDynamicResponse(
86
167
  const site: Loose = {}
87
168
  if (siteBranding) {
88
169
  if (siteBranding.logo != null) site.logo = siteBranding.logo
170
+ if (siteBranding.brandName != null) site.brandName = siteBranding.brandName
89
171
  if (siteBranding.description != null) {
90
172
  site.description = siteBranding.description
91
173
  }
@@ -105,16 +187,24 @@ export function simplifiedHomepageConfigFromDynamicResponse(
105
187
  : null
106
188
  }
107
189
 
190
+ /** CMS-only homepage config (no package JSON defaults). */
191
+ export function homepageConfigFromDynamicResponseNormalized(
192
+ response: DynamicConfigResponse | null | undefined
193
+ ): HomepageConfig {
194
+ const legacy = response?.["homepage-config"] ?? null
195
+ const fromSplit = simplifiedHomepageConfigFromDynamicResponse(response)
196
+ const fromHeroBanners = simplifiedConfigFromHeroBanners(response)
197
+ const raw = mergeHomepageConfigLayers(fromSplit, legacy, fromHeroBanners)
198
+ return normalizeHomepageConfig(raw)
199
+ }
200
+
108
201
  /**
109
202
  * Build `homepage-config` from Medusa `GET /store/dynamic-config` (main project only).
110
- * Supports legacy `homepage-config` or split plugin keys.
203
+ * Supports legacy `homepage-config`, split plugin keys, and `hero-banners`.
111
204
  */
112
205
  export function homepageConfigFromDynamicResponse(
113
206
  response: DynamicConfigResponse | null | undefined
114
207
  ): StorefrontPageInput {
115
- const legacy = response?.["homepage-config"] ?? null
116
- const fromSplit = simplifiedHomepageConfigFromDynamicResponse(response)
117
- const raw = fromSplit ?? legacy
118
- const normalized = normalizeHomepageConfig(raw)
208
+ const normalized = homepageConfigFromDynamicResponseNormalized(response)
119
209
  return mergePageInputWithDefaults(normalized)
120
210
  }
@@ -0,0 +1,46 @@
1
+ import "server-only"
2
+
3
+ import { cache } from "react"
4
+ import type { StorefrontPageInput } from "./page-input"
5
+ import {
6
+ getContactInfoFromPageInput,
7
+ getHelpFaqFromPageInput,
8
+ } from "./dynamic-config"
9
+ import { getDynamicConfig, homepageConfigFromDynamicResponse } from "./dynamic-config"
10
+
11
+ type Maybe<T> = T | null | undefined
12
+
13
+ /** Cached CMS merge for the storefront (fetch once per request). */
14
+ export const loadStorefrontPageInput = cache(
15
+ async (): Promise<StorefrontPageInput> => {
16
+ const raw = await getDynamicConfig()
17
+ return homepageConfigFromDynamicResponse(raw)
18
+ }
19
+ )
20
+
21
+ export async function resolveStorefrontPageInput(
22
+ pageInput?: Maybe<StorefrontPageInput>
23
+ ): Promise<StorefrontPageInput> {
24
+ return (pageInput ?? (await loadStorefrontPageInput())) as StorefrontPageInput
25
+ }
26
+
27
+ export async function loadContactInfoFromStorefrontInput(
28
+ pageInput?: Maybe<StorefrontPageInput>
29
+ ) {
30
+ const resolved = await resolveStorefrontPageInput(pageInput)
31
+ return getContactInfoFromPageInput(resolved)
32
+ }
33
+
34
+ export async function loadContactEmailFromStorefrontInput(
35
+ pageInput?: Maybe<StorefrontPageInput>
36
+ ): Promise<string> {
37
+ const info = await loadContactInfoFromStorefrontInput(pageInput)
38
+ return info?.email ?? ""
39
+ }
40
+
41
+ export async function loadHelpFaqFromStorefrontInput(
42
+ pageInput?: Maybe<StorefrontPageInput>
43
+ ) {
44
+ const resolved = await resolveStorefrontPageInput(pageInput)
45
+ return getHelpFaqFromPageInput(resolved)
46
+ }
@@ -1,63 +0,0 @@
1
- import "server-only"
2
-
3
- import type { HomepageConfig } from "./homepage-config.types"
4
- import {
5
- getAboutBrandFromPageInput,
6
- getAnnouncementMessagesFromPageInput,
7
- getAppBannersFromPageInput,
8
- getBlogPostsFromPageInput,
9
- getBrandPillarsFromPageInput,
10
- getFeaturesFromPageInput,
11
- getHomeBannersFromPageInput,
12
- getHomeSectionsCopyFromPageInput,
13
- getInstagramPostsFromPageInput,
14
- getPromoCountdownFromPageInput,
15
- getTestimonialsFromPageInput,
16
- getTrustFeaturesFromPageInput,
17
- getVideoStoriesFromPageInput,
18
- type AboutBrandConfig,
19
- type BlogPostConfig,
20
- type BrandPillarConfig,
21
- type HomepageBanner,
22
- type HomeSectionsCopy,
23
- type InstagramPostsData,
24
- type TrustFeaturesConfig,
25
- } from "./dynamic-config"
26
-
27
- /** Config-driven homepage fields (no Medusa catalog APIs). */
28
- export type HomeSegmentConfigData = {
29
- homeBanners: HomepageBanner[] | null
30
- appBanners: HomepageBanner[] | null
31
- whyChooseUsFeatures: ReturnType<typeof getFeaturesFromPageInput>
32
- testimonials: ReturnType<typeof getTestimonialsFromPageInput>
33
- videoStories: ReturnType<typeof getVideoStoriesFromPageInput>
34
- announcementMessages: string[]
35
- promoCountdown: ReturnType<typeof getPromoCountdownFromPageInput>
36
- aboutBrand: AboutBrandConfig
37
- brandPillars: { sectionTitle: string; pillars: BrandPillarConfig[] }
38
- blogPosts: BlogPostConfig[]
39
- instagramPosts: InstagramPostsData
40
- sectionsCopy: HomeSectionsCopy
41
- trustFeatures: TrustFeaturesConfig
42
- }
43
-
44
- /** Resolve all segment JSON from `pageInput` (package defaults when omitted). */
45
- export function resolveHomeSegmentConfigData(
46
- pageInput?: HomepageConfig | null
47
- ): HomeSegmentConfigData {
48
- return {
49
- homeBanners: getHomeBannersFromPageInput(pageInput),
50
- appBanners: getAppBannersFromPageInput(pageInput),
51
- whyChooseUsFeatures: getFeaturesFromPageInput(pageInput),
52
- testimonials: getTestimonialsFromPageInput(pageInput),
53
- videoStories: getVideoStoriesFromPageInput(pageInput),
54
- announcementMessages: getAnnouncementMessagesFromPageInput(pageInput),
55
- promoCountdown: getPromoCountdownFromPageInput(pageInput),
56
- aboutBrand: getAboutBrandFromPageInput(pageInput),
57
- brandPillars: getBrandPillarsFromPageInput(pageInput),
58
- blogPosts: getBlogPostsFromPageInput(pageInput),
59
- instagramPosts: getInstagramPostsFromPageInput(pageInput),
60
- sectionsCopy: getHomeSectionsCopyFromPageInput(pageInput),
61
- trustFeatures: getTrustFeaturesFromPageInput(pageInput),
62
- }
63
- }