fragment-headless-sdk 2.3.1 → 2.3.3
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/components/Announcement/AnnouncementButton.d.ts +1 -2
- package/dist/components/Announcement/AnnouncementButton.js +3 -5
- package/dist/components/Announcement/index.js +7 -9
- package/dist/components/Hero/DesktopHero.d.ts +1 -2
- package/dist/components/Hero/DesktopHero.js +3 -10
- package/dist/components/Hero/MobileHero.d.ts +1 -2
- package/dist/components/Hero/MobileHero.js +3 -10
- package/dist/components/Hero/index.js +7 -9
- package/dist/types/announcement.d.ts +0 -2
- package/dist/types/hero.d.ts +0 -2
- package/dist/utils/cache.js +5 -3
- package/dist/utils/fetch-resource.js +3 -1
- package/dist/utils/metrics.d.ts +2 -4
- package/dist/utils/metrics.js +3 -51
- package/docs/CHANGELOG.md +21 -0
- package/package.json +31 -4
- package/readme.md +15 -8
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { IAnnouncementContent } from "../../types";
|
|
3
|
-
export default function AnnouncementButton({ content, buttonHref,
|
|
3
|
+
export default function AnnouncementButton({ content, buttonHref, }: {
|
|
4
4
|
content: IAnnouncementContent;
|
|
5
5
|
buttonHref?: string;
|
|
6
|
-
clickHref?: string;
|
|
7
6
|
}): React.JSX.Element | null;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { ButtonType, SectionType } from "../../constants";
|
|
3
3
|
import { fireClickMetric, mergeSlotAttributes, mergeSlotClasses, mergeSlotStyles, resolveAnnouncementColors, } from "../../utils";
|
|
4
|
-
export default function AnnouncementButton({ content, buttonHref,
|
|
4
|
+
export default function AnnouncementButton({ content, buttonHref, }) {
|
|
5
5
|
// Don’t render if no button or explicitly None
|
|
6
6
|
if (!content?.buttonText || content.buttonType === ButtonType.None)
|
|
7
7
|
return null;
|
|
@@ -29,9 +29,7 @@ export default function AnnouncementButton({ content, buttonHref, clickHref, })
|
|
|
29
29
|
delete attributes["aria-label"];
|
|
30
30
|
}
|
|
31
31
|
const handleClick = React.useCallback(() => {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
fireClickMetric(clickHref, content.measurementId, content.sectionType || SectionType.Announcement, content.sectionId);
|
|
35
|
-
}, [clickHref, content.measurementId, content.sectionType, content.sectionId]);
|
|
32
|
+
fireClickMetric(content.measurementId, content.sectionType || SectionType.Announcement, content.sectionId);
|
|
33
|
+
}, [content.measurementId, content.sectionType, content.sectionId]);
|
|
36
34
|
return (React.createElement("a", { href: buttonHref, className: className, style: style, onClick: handleClick, ...(attributes ?? {}), "aria-label": ariaLabel }, content.buttonText));
|
|
37
35
|
}
|
|
@@ -1,21 +1,19 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import { XMarkIcon } from "@heroicons/react/24/outline";
|
|
2
3
|
import React, { useEffect, useRef } from "react";
|
|
3
4
|
import { AnnouncementType, ButtonType, SectionType } from "../../constants";
|
|
4
|
-
import {
|
|
5
|
+
import { fireImpressionWhenVisible, getThemeClasses, mergeSlotAttributes, mergeSlotClasses, mergeSlotStyles, resolveAnnouncementColors, } from "../../utils";
|
|
5
6
|
import AnnouncementButton from "./AnnouncementButton";
|
|
6
7
|
import { AnnouncementStyles } from "./AnnouncementStyles";
|
|
7
8
|
import CountdownTimer from "./CountdownTimer";
|
|
8
9
|
export default function Announcement({ content, type, handleClose, }) {
|
|
9
10
|
const ref = useRef(null);
|
|
10
11
|
useEffect(() => {
|
|
11
|
-
if (ref.current
|
|
12
|
-
fireImpressionWhenVisible(ref.current, content.
|
|
12
|
+
if (ref.current) {
|
|
13
|
+
fireImpressionWhenVisible(ref.current, content.measurementId, content.sectionType || SectionType.Announcement, content.sectionId);
|
|
13
14
|
}
|
|
14
|
-
}, [content?.
|
|
15
|
+
}, [content?.measurementId, content?.sectionType, content?.sectionId]);
|
|
15
16
|
const buttonHref = content?.buttonLink || undefined;
|
|
16
|
-
const clickTrackingHref = content?.buttonLink && content?.clickUrlBase
|
|
17
|
-
? buildClickUrl(content.clickUrlBase, content.buttonLink)
|
|
18
|
-
: undefined;
|
|
19
17
|
if (!content)
|
|
20
18
|
return null;
|
|
21
19
|
const styling = content.styling;
|
|
@@ -60,13 +58,13 @@ export default function Announcement({ content, type, handleClose, }) {
|
|
|
60
58
|
React.createElement("div", { className: marqueeContentClass, style: marqueeContentStyle, ...(marqueeContentAttributes ?? {}), dangerouslySetInnerHTML: {
|
|
61
59
|
__html: content.announcementHtml || "",
|
|
62
60
|
} }))),
|
|
63
|
-
content.buttonText && content.buttonType !== ButtonType.None && (React.createElement(AnnouncementButton, { content: content, buttonHref: buttonHref
|
|
61
|
+
content.buttonText && content.buttonType !== ButtonType.None && (React.createElement(AnnouncementButton, { content: content, buttonHref: buttonHref })))) : (React.createElement("div", { className: contentRowClass, style: contentRowStyle, ...(contentRowAttributes ?? {}) },
|
|
64
62
|
React.createElement("div", { className: announcementTextClass, style: announcementTextStyle, ...(announcementTextAttributes ?? {}) },
|
|
65
63
|
React.createElement("div", { dangerouslySetInnerHTML: {
|
|
66
64
|
__html: content.announcementHtml || "",
|
|
67
65
|
} })),
|
|
68
66
|
type === AnnouncementType.Countdown && (React.createElement(CountdownTimer, { content: content })),
|
|
69
|
-
content.buttonText && content.buttonType !== ButtonType.None && (React.createElement(AnnouncementButton, { content: content, buttonHref: buttonHref
|
|
67
|
+
content.buttonText && content.buttonType !== ButtonType.None && (React.createElement(AnnouncementButton, { content: content, buttonHref: buttonHref })))),
|
|
70
68
|
React.createElement("div", { onClick: handleClose, className: closeButtonClass, style: closeButtonStyle, ...(closeButtonAttributes ?? {}) },
|
|
71
69
|
React.createElement(XMarkIcon, { className: "w-5 h-5" })))));
|
|
72
70
|
}
|
|
@@ -3,7 +3,6 @@ import { IHeroContent } from "../../types";
|
|
|
3
3
|
import { resolveHeroColors, resolveHeroTypography } from "../../utils/hero-resolvers";
|
|
4
4
|
interface HeroViewProps {
|
|
5
5
|
buttonHref?: string;
|
|
6
|
-
clickHref?: string;
|
|
7
6
|
content: IHeroContent;
|
|
8
7
|
colors: ReturnType<typeof resolveHeroColors>;
|
|
9
8
|
contentWidthClass: string;
|
|
@@ -11,5 +10,5 @@ interface HeroViewProps {
|
|
|
11
10
|
position: "left" | "center" | "right";
|
|
12
11
|
height: string;
|
|
13
12
|
}
|
|
14
|
-
export default function DesktopHero({ buttonHref,
|
|
13
|
+
export default function DesktopHero({ buttonHref, content, colors, contentWidthClass, typography, position, height, }: HeroViewProps): React.JSX.Element;
|
|
15
14
|
export {};
|
|
@@ -2,7 +2,7 @@ import React from "react";
|
|
|
2
2
|
import { SectionType } from "../../constants";
|
|
3
3
|
import { fireClickMetric } from "../../utils";
|
|
4
4
|
import { DEFAULT_CONTENT_WIDTH_CLASS, joinClassNames, renderText, } from "../../utils/hero-resolvers";
|
|
5
|
-
export default function DesktopHero({ buttonHref,
|
|
5
|
+
export default function DesktopHero({ buttonHref, content, colors, contentWidthClass, typography, position, height, }) {
|
|
6
6
|
const getPositionClasses = () => {
|
|
7
7
|
switch (position) {
|
|
8
8
|
case "center":
|
|
@@ -15,15 +15,8 @@ export default function DesktopHero({ buttonHref, clickHref, content, colors, co
|
|
|
15
15
|
}
|
|
16
16
|
};
|
|
17
17
|
const handleClick = React.useCallback(() => {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
fireClickMetric(clickHref, content.measurementId, content.sectionType || SectionType.HeroBanner, content.sectionId);
|
|
21
|
-
}, [
|
|
22
|
-
clickHref,
|
|
23
|
-
content.measurementId,
|
|
24
|
-
content.sectionType,
|
|
25
|
-
content.sectionId,
|
|
26
|
-
]);
|
|
18
|
+
fireClickMetric(content.measurementId, content.sectionType || SectionType.HeroBanner, content.sectionId);
|
|
19
|
+
}, [content.measurementId, content.sectionType, content.sectionId]);
|
|
27
20
|
return (React.createElement("div", { className: `relative ${height} gap-4 w-full`, style: { backgroundColor: colors.background } },
|
|
28
21
|
content?.videoUrl ? (React.createElement("video", { src: content.videoUrl, autoPlay: true, muted: true, loop: true, playsInline: true, className: "absolute inset-0 z-0 object-cover w-full h-full" })) : (
|
|
29
22
|
/* Image Background */
|
|
@@ -3,10 +3,9 @@ import { IHeroContent } from "../../types";
|
|
|
3
3
|
import { resolveHeroColors, resolveHeroTypography } from "../../utils/hero-resolvers";
|
|
4
4
|
interface MobileHeroProps {
|
|
5
5
|
buttonHref?: string;
|
|
6
|
-
clickHref?: string;
|
|
7
6
|
content: IHeroContent;
|
|
8
7
|
colors: ReturnType<typeof resolveHeroColors>;
|
|
9
8
|
typography: ReturnType<typeof resolveHeroTypography>;
|
|
10
9
|
}
|
|
11
|
-
export default function MobileHero({ buttonHref,
|
|
10
|
+
export default function MobileHero({ buttonHref, content, colors, typography, }: MobileHeroProps): React.JSX.Element;
|
|
12
11
|
export {};
|
|
@@ -2,17 +2,10 @@ import React from "react";
|
|
|
2
2
|
import { SectionType } from "../../constants";
|
|
3
3
|
import { fireClickMetric } from "../../utils";
|
|
4
4
|
import { renderText, } from "../../utils/hero-resolvers";
|
|
5
|
-
export default function MobileHero({ buttonHref,
|
|
5
|
+
export default function MobileHero({ buttonHref, content, colors, typography, }) {
|
|
6
6
|
const handleClick = React.useCallback(() => {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
fireClickMetric(clickHref, content.measurementId, content.sectionType || SectionType.HeroBanner, content.sectionId);
|
|
10
|
-
}, [
|
|
11
|
-
clickHref,
|
|
12
|
-
content.measurementId,
|
|
13
|
-
content.sectionType,
|
|
14
|
-
content.sectionId,
|
|
15
|
-
]);
|
|
7
|
+
fireClickMetric(content.measurementId, content.sectionType || SectionType.HeroBanner, content.sectionId);
|
|
8
|
+
}, [content.measurementId, content.sectionType, content.sectionId]);
|
|
16
9
|
return (React.createElement("div", { className: "relative z-10 mx-auto gap-4 flex max-w-screen-md flex-col items-center justify-center py-6 text-center", style: { backgroundColor: colors.background } },
|
|
17
10
|
renderText({
|
|
18
11
|
fontSize: typography.title.fontSize,
|
|
@@ -1,20 +1,18 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import React, { useEffect, useRef } from "react";
|
|
2
3
|
import { SectionType } from "../../constants";
|
|
3
|
-
import {
|
|
4
|
+
import { fireImpressionWhenVisible, } from "../../utils";
|
|
4
5
|
import { resolveContentWidthClass, resolveHeight, resolveHeroColors, resolveHeroTypography, resolvePosition, } from "../../utils/hero-resolvers";
|
|
5
6
|
import DesktopHero from "./DesktopHero";
|
|
6
7
|
import MobileHero from "./MobileHero";
|
|
7
8
|
export default function Hero({ content }) {
|
|
8
9
|
const ref = useRef(null);
|
|
9
10
|
useEffect(() => {
|
|
10
|
-
if (ref.current
|
|
11
|
-
fireImpressionWhenVisible(ref.current, content.
|
|
11
|
+
if (ref.current) {
|
|
12
|
+
fireImpressionWhenVisible(ref.current, content.measurementId, content.sectionType || SectionType.HeroBanner, content.sectionId);
|
|
12
13
|
}
|
|
13
|
-
}, [content?.
|
|
14
|
+
}, [content?.measurementId, content?.sectionType, content?.sectionId]);
|
|
14
15
|
const buttonHref = content?.buttonLink || undefined;
|
|
15
|
-
const clickTrackingHref = content?.buttonLink && content?.clickUrlBase
|
|
16
|
-
? buildClickUrl(content.clickUrlBase, content.buttonLink)
|
|
17
|
-
: undefined;
|
|
18
16
|
if (!content)
|
|
19
17
|
return null;
|
|
20
18
|
const colors = resolveHeroColors(content);
|
|
@@ -24,7 +22,7 @@ export default function Hero({ content }) {
|
|
|
24
22
|
const height = resolveHeight(content);
|
|
25
23
|
return (React.createElement("div", { className: "bg-black", ref: ref, style: { backgroundColor: colors.background } },
|
|
26
24
|
React.createElement("div", { className: "hidden lg:block" },
|
|
27
|
-
React.createElement(DesktopHero, { content: content, buttonHref: buttonHref,
|
|
25
|
+
React.createElement(DesktopHero, { content: content, buttonHref: buttonHref, colors: colors, contentWidthClass: contentWidthClass, typography: typography, position: position, height: height })),
|
|
28
26
|
React.createElement("div", { className: "block lg:hidden" },
|
|
29
|
-
React.createElement(MobileHero, { content: content, buttonHref: buttonHref,
|
|
27
|
+
React.createElement(MobileHero, { content: content, buttonHref: buttonHref, colors: colors, typography: typography }))));
|
|
30
28
|
}
|
package/dist/types/hero.d.ts
CHANGED
package/dist/utils/cache.js
CHANGED
|
@@ -15,12 +15,14 @@ export async function revalidateFragmentCache(tags) {
|
|
|
15
15
|
try {
|
|
16
16
|
// Dynamic import to avoid issues in non-Next.js environments
|
|
17
17
|
// @ts-ignore - next/cache may not be available in all environments
|
|
18
|
-
const
|
|
18
|
+
const mod = await import("next/cache");
|
|
19
|
+
// Cast: Next.js types vary by version (1 arg vs 2 arg with profile). Pass tag + "max" for Next 16+.
|
|
20
|
+
const revalidateTag = mod.revalidateTag;
|
|
19
21
|
if (tags && tags.length > 0) {
|
|
20
22
|
// Revalidate specific tags
|
|
21
23
|
tags.forEach((tag) => {
|
|
22
24
|
try {
|
|
23
|
-
revalidateTag(tag);
|
|
25
|
+
revalidateTag(tag, "max");
|
|
24
26
|
}
|
|
25
27
|
catch (err) {
|
|
26
28
|
console.warn(`Failed to revalidate tag ${tag}:`, err);
|
|
@@ -31,7 +33,7 @@ export async function revalidateFragmentCache(tags) {
|
|
|
31
33
|
// Revalidate all Fragment resource types
|
|
32
34
|
Object.values(ResourceType).forEach((type) => {
|
|
33
35
|
try {
|
|
34
|
-
revalidateTag(`fragment-${type}
|
|
36
|
+
revalidateTag(`fragment-${type}`, "max");
|
|
35
37
|
}
|
|
36
38
|
catch (err) {
|
|
37
39
|
console.warn(`Failed to revalidate fragment-${type}:`, err);
|
|
@@ -119,6 +119,9 @@ async function performRequest({ baseUrl, apiKey, type, params = {}, fetchImpl, c
|
|
|
119
119
|
url.searchParams.set("page", params.pageFilter);
|
|
120
120
|
if (params.search)
|
|
121
121
|
url.searchParams.set("search", params.search);
|
|
122
|
+
// API key in the URL so Vercel's CDN can cache the response.
|
|
123
|
+
// Requests with an Authorization header are never cached by the CDN.
|
|
124
|
+
url.searchParams.set("apiKey", apiKey);
|
|
122
125
|
// Merge default cache config with user-provided options
|
|
123
126
|
const finalCacheOptions = {
|
|
124
127
|
...getDefaultCacheConfig(type),
|
|
@@ -128,7 +131,6 @@ async function performRequest({ baseUrl, apiKey, type, params = {}, fetchImpl, c
|
|
|
128
131
|
const fetchOptions = {
|
|
129
132
|
method: "GET",
|
|
130
133
|
headers: {
|
|
131
|
-
Authorization: `Bearer ${apiKey}`,
|
|
132
134
|
"Content-Type": "application/json",
|
|
133
135
|
},
|
|
134
136
|
cache: finalCacheOptions.cache,
|
package/dist/utils/metrics.d.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { SectionType } from "../constants";
|
|
2
|
-
export declare function
|
|
3
|
-
export declare function buildClickUrl(clickUrlBase: string, targetHref: string): string;
|
|
4
|
-
export declare function fireClickMetric(clickUrl: string, measurementId?: string, sectionType?: SectionType, sectionId?: string): void;
|
|
2
|
+
export declare function fireClickMetric(measurementId?: string, sectionType?: SectionType, sectionId?: string): void;
|
|
5
3
|
declare global {
|
|
6
4
|
interface Window {
|
|
7
5
|
gtag?: (command: string, targetId: string | Date, config?: Record<string, unknown>) => void;
|
|
@@ -12,4 +10,4 @@ declare global {
|
|
|
12
10
|
* Fire a scroll past metric when user scrolls past a section
|
|
13
11
|
*/
|
|
14
12
|
export declare function fireScrollPastMetric(measurementId: string | undefined, sectionType: SectionType, sectionId: string): void;
|
|
15
|
-
export declare function fireImpressionWhenVisible(el: HTMLElement,
|
|
13
|
+
export declare function fireImpressionWhenVisible(el: HTMLElement, measurementId?: string, sectionType?: SectionType, sectionId?: string): void;
|
package/dist/utils/metrics.js
CHANGED
|
@@ -1,59 +1,15 @@
|
|
|
1
1
|
import { SectionType } from "../constants";
|
|
2
2
|
import { setAttribution } from "./attribution";
|
|
3
|
-
|
|
4
|
-
export function toBase64Url(input) {
|
|
5
|
-
// Handles emojis & non-ASCII reliably:
|
|
6
|
-
const b64 = btoa(encodeURIComponent(input).replace(/%([0-9A-F]{2})/g, (_, p1) => String.fromCharCode(parseInt(p1, 16))));
|
|
7
|
-
return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
8
|
-
}
|
|
9
|
-
// --- Robust query appender ---
|
|
10
|
-
function appendQuery(url, key, value) {
|
|
11
|
-
const sep = url.includes("?") ? "&" : "?";
|
|
12
|
-
return `${url}${sep}${key}=${value}`;
|
|
13
|
-
}
|
|
14
|
-
// Build the tracking URL that encodes the final destination for metrics
|
|
15
|
-
export function buildClickUrl(clickUrlBase, targetHref) {
|
|
16
|
-
const u = encodeURIComponent(toBase64Url(targetHref));
|
|
17
|
-
return appendQuery(clickUrlBase, "u", u);
|
|
18
|
-
}
|
|
19
|
-
// Fire the click tracking URL without relying on a redirect
|
|
20
|
-
// Default to GET so legacy tracking endpoints continue to accept the request
|
|
21
|
-
export function fireClickMetric(clickUrl, measurementId, sectionType, sectionId) {
|
|
3
|
+
export function fireClickMetric(measurementId, sectionType, sectionId) {
|
|
22
4
|
if (typeof window === "undefined")
|
|
23
5
|
return;
|
|
24
|
-
if (!clickUrl)
|
|
25
|
-
return;
|
|
26
6
|
// Store attribution for potential purchase/add-to-cart
|
|
27
7
|
if (sectionType && sectionId) {
|
|
28
8
|
setAttribution(sectionId, sectionType);
|
|
29
9
|
}
|
|
30
|
-
// Send to GA4 first (if available)
|
|
31
10
|
if (measurementId && sectionType && sectionId) {
|
|
32
11
|
sendGA4Event("click", measurementId, sectionType, sectionId);
|
|
33
12
|
}
|
|
34
|
-
try {
|
|
35
|
-
if (typeof fetch === "function") {
|
|
36
|
-
fetch(clickUrl, {
|
|
37
|
-
method: "GET",
|
|
38
|
-
mode: "no-cors",
|
|
39
|
-
keepalive: true,
|
|
40
|
-
}).catch(() => {
|
|
41
|
-
/* no-op */
|
|
42
|
-
});
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
catch {
|
|
47
|
-
// swallow and fall back to <img>
|
|
48
|
-
}
|
|
49
|
-
try {
|
|
50
|
-
const img = new Image();
|
|
51
|
-
img.referrerPolicy = "strict-origin-when-cross-origin";
|
|
52
|
-
img.src = clickUrl;
|
|
53
|
-
}
|
|
54
|
-
catch {
|
|
55
|
-
// nothing else we can do
|
|
56
|
-
}
|
|
57
13
|
}
|
|
58
14
|
/**
|
|
59
15
|
* Generates a section-specific event name for GA4 tracking.
|
|
@@ -102,25 +58,21 @@ export function fireScrollPastMetric(measurementId, sectionType, sectionId) {
|
|
|
102
58
|
}
|
|
103
59
|
// --- View tracking (once per element) ---
|
|
104
60
|
const seenEls = typeof WeakSet !== "undefined" ? new WeakSet() : null;
|
|
105
|
-
export function fireImpressionWhenVisible(el,
|
|
61
|
+
export function fireImpressionWhenVisible(el, measurementId, sectionType, sectionId) {
|
|
106
62
|
if (typeof window === "undefined")
|
|
107
63
|
return; // SSR guard
|
|
108
|
-
if (!el
|
|
64
|
+
if (!el)
|
|
109
65
|
return;
|
|
110
66
|
if (seenEls && seenEls.has(el))
|
|
111
67
|
return; // de-dupe by element
|
|
112
68
|
let fired = false;
|
|
113
69
|
let hasScrolledPast = false;
|
|
114
|
-
const img = new Image();
|
|
115
70
|
const fire = () => {
|
|
116
71
|
if (fired)
|
|
117
72
|
return;
|
|
118
73
|
fired = true;
|
|
119
74
|
if (seenEls)
|
|
120
75
|
seenEls.add(el);
|
|
121
|
-
img.referrerPolicy = "strict-origin-when-cross-origin";
|
|
122
|
-
img.src = pixelUrl; // GET to your /api/v1/metrics/i?e=...&sig=...
|
|
123
|
-
// Also send to GA4 if available
|
|
124
76
|
if (measurementId && sectionType && sectionId) {
|
|
125
77
|
sendGA4Event("view", measurementId, sectionType, sectionId);
|
|
126
78
|
}
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,27 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
### [Unreleased]
|
|
9
|
+
|
|
10
|
+
### [2.3.3] - 2026-03-07
|
|
11
|
+
|
|
12
|
+
#### Changed
|
|
13
|
+
|
|
14
|
+
- **`fetchResource` default caching** – Environment-aware defaults for better performance and freshness:
|
|
15
|
+
- **Next.js production**: Defaults to `force-cache` with 60-second revalidation and resource-type cache tags (`fragment-hero-banners`, `fragment-announcements`) so responses are cached at the edge while staying reasonably fresh.
|
|
16
|
+
- **Next.js development / non–Next.js**: Defaults to `cache: "default"` with no revalidation.
|
|
17
|
+
- User-provided `cacheOptions` still override these defaults.
|
|
18
|
+
- **Request deduplication**: Cache keys now normalize `status` to `"enabled"` when omitted, so identical requests are deduplicated consistently.
|
|
19
|
+
|
|
20
|
+
### [2.3.2] - 2026-02-27
|
|
21
|
+
|
|
22
|
+
#### Removed
|
|
23
|
+
|
|
24
|
+
- **Legacy server-side tracking** – The previous pixel/click tracking system has been fully replaced by Google Analytics 4 (GA4):
|
|
25
|
+
- `impressionUrl` and `clickUrlBase` on content objects are no longer used; the API now provides `measurementId`, `sectionId`, and `sectionType` for GA4 events.
|
|
26
|
+
- The `makeSignedMetricUrls` helper and server-side `/api/v1/t` endpoints (impression/click) have been removed from the Fragment app; the SDK sends view/click/scroll_past via `gtag` only.
|
|
27
|
+
- See [2.2.0] for GA4 integration details. Historical entries [1.0.5] and earlier described the old pixel-based flow.
|
|
28
|
+
|
|
8
29
|
### [2.3.1] - 2026-01-25
|
|
9
30
|
|
|
10
31
|
### 🔧 Technical Improvements
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fragment-headless-sdk",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.3",
|
|
4
|
+
"description": "Official SDK for Fragment-Shopify CMS: React components, TypeScript types, and utilities for headless Shopify storefronts.",
|
|
4
5
|
"license": "MIT",
|
|
5
6
|
"main": "dist/index.js",
|
|
6
7
|
"types": "dist/index.d.ts",
|
|
@@ -15,12 +16,38 @@
|
|
|
15
16
|
"dist",
|
|
16
17
|
"docs/CHANGELOG.md"
|
|
17
18
|
],
|
|
19
|
+
"sideEffects": false,
|
|
18
20
|
"scripts": {
|
|
19
|
-
"build": "tsc"
|
|
21
|
+
"build": "tsc && mkdir -p dist/styles && cp src/styles/*.css dist/styles/",
|
|
22
|
+
"dev": "tsc --watch",
|
|
23
|
+
"type-check": "tsc --noEmit",
|
|
24
|
+
"clean": "rm -rf dist .turbo"
|
|
20
25
|
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public",
|
|
28
|
+
"registry": "https://registry.npmjs.org/"
|
|
29
|
+
},
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/sevenbrand/fragment-monorepo.git",
|
|
33
|
+
"directory": "packages/sdk"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://github.com/sevenbrand/fragment-monorepo/tree/main/packages/sdk#readme",
|
|
36
|
+
"bugs": "https://github.com/sevenbrand/fragment-monorepo/issues",
|
|
37
|
+
"keywords": [
|
|
38
|
+
"shopify",
|
|
39
|
+
"headless",
|
|
40
|
+
"fragment",
|
|
41
|
+
"sdk",
|
|
42
|
+
"storefront",
|
|
43
|
+
"cms",
|
|
44
|
+
"react"
|
|
45
|
+
],
|
|
21
46
|
"dependencies": {
|
|
22
|
-
"@heroicons/react": "^2.2.0"
|
|
23
|
-
|
|
47
|
+
"@heroicons/react": "^2.2.0"
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"react": "^18.0.0 || ^19.0.0"
|
|
24
51
|
},
|
|
25
52
|
"devDependencies": {
|
|
26
53
|
"@types/node": "^24.7.2",
|
package/readme.md
CHANGED
|
@@ -4,7 +4,7 @@ The official SDK for integrating with Fragment-Shopify CMS. Provides React compo
|
|
|
4
4
|
|
|
5
5
|
## ✨ What's New
|
|
6
6
|
|
|
7
|
-
**v2.3.
|
|
7
|
+
**v2.3.3** – `fetchResource` uses environment-aware default caching: Next.js production defaults to 60s revalidation and resource-type cache tags; development and non–Next.js use `cache: "default"`. Request deduplication normalizes `status` for consistent cache keys.
|
|
8
8
|
|
|
9
9
|
> See [CHANGELOG.md](./docs/CHANGELOG.md) for full release history
|
|
10
10
|
|
|
@@ -80,7 +80,7 @@ Fragment-Shopify App (CMS) → API Endpoint → fragment-headless-sdk (Consumer)
|
|
|
80
80
|
- 🛡️ **Graceful Fallback**: Works even if GA4 is not configured (doesn't break functionality)
|
|
81
81
|
- 📦 **Exported Types**: `SectionType` enum available for consumer use
|
|
82
82
|
|
|
83
|
-
### Attribution Tracking System (v2.
|
|
83
|
+
### Attribution Tracking System (v2.3.0+)
|
|
84
84
|
|
|
85
85
|
- 🎯 **Session-Based Attribution**: Tracks which section was clicked for purchase/add-to-cart attribution
|
|
86
86
|
- 💾 **SessionStorage Integration**: Uses sessionStorage for session-scoped attribution (last-click wins)
|
|
@@ -330,10 +330,15 @@ Fetches sections from your Fragment-Shopify app with intelligent request dedupli
|
|
|
330
330
|
|
|
331
331
|
**CacheOptions:**
|
|
332
332
|
|
|
333
|
-
- `cache?: RequestCache` - Request cache mode
|
|
334
|
-
- `revalidate?: number | false` - Next.js revalidation time in seconds
|
|
333
|
+
- `cache?: RequestCache` - Request cache mode
|
|
334
|
+
- `revalidate?: number | false` - Next.js revalidation time in seconds
|
|
335
335
|
- `tags?: string[]` - Next.js cache tags for selective invalidation
|
|
336
336
|
|
|
337
|
+
**Default cache behavior:**
|
|
338
|
+
|
|
339
|
+
- **Next.js production**: `force-cache` with `revalidate: 60` and tags `fragment-{type}`. Override with `cacheOptions` for different behavior.
|
|
340
|
+
- **Next.js development or non–Next.js**: `cache: "default"`; no revalidation or tags unless you pass `cacheOptions`.
|
|
341
|
+
|
|
337
342
|
**ResourceType Options:**
|
|
338
343
|
|
|
339
344
|
- `ResourceType.HeroBanners` - Fetch hero banners
|
|
@@ -377,8 +382,9 @@ interface IHeroContent {
|
|
|
377
382
|
imageUrl: string;
|
|
378
383
|
mobileImageUrl: string;
|
|
379
384
|
videoUrl?: string;
|
|
380
|
-
|
|
381
|
-
|
|
385
|
+
measurementId?: string; // GA4 measurement ID (from app config)
|
|
386
|
+
sectionId?: string; // Section ID for GA4 event params
|
|
387
|
+
sectionType?: SectionType;
|
|
382
388
|
styling?: IHeroStyling; // v2.0+ enhanced styling
|
|
383
389
|
}
|
|
384
390
|
```
|
|
@@ -434,8 +440,9 @@ interface IAnnouncementContent {
|
|
|
434
440
|
buttonLink: string;
|
|
435
441
|
announcementHtml: string;
|
|
436
442
|
counterEndDate?: string;
|
|
437
|
-
|
|
438
|
-
|
|
443
|
+
measurementId?: string; // GA4 measurement ID (from app config)
|
|
444
|
+
sectionId?: string; // Section ID for GA4 event params
|
|
445
|
+
sectionType?: SectionType;
|
|
439
446
|
styling?: IAnnouncementStyling; // v2.0+ enhanced styling
|
|
440
447
|
}
|
|
441
448
|
```
|