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.
- package/HOMEPAGE_CONFIG.md +25 -3
- package/package.json +8 -2
- package/src/server/dynamic-config.ts +82 -14
- package/src/server/home-sections/promo-announcements.ts +6 -4
- package/src/server/home.ts +5 -1
- package/src/server/homepage-config.types.ts +113 -0
- package/src/server/index.ts +2 -1
- package/src/server/normalize-homepage-config.ts +38 -0
- package/src/server/page-input.ts +95 -5
- package/src/server/storefront-cms.ts +46 -0
- package/src/server/resolve-home-segment-data.ts +0 -63
package/HOMEPAGE_CONFIG.md
CHANGED
|
@@ -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
|
|
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
|
-
| `
|
|
209
|
-
| `
|
|
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.
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
738
|
-
|
|
739
|
-
):
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
}
|
package/src/server/home.ts
CHANGED
|
@@ -10,7 +10,10 @@ import type {
|
|
|
10
10
|
} from "./dynamic-config"
|
|
11
11
|
import type { ShoppableLook } from "./shoppable-looks"
|
|
12
12
|
|
|
13
|
-
/**
|
|
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
|
|
package/src/server/index.ts
CHANGED
|
@@ -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
|
|
package/src/server/page-input.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
}
|