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.
@@ -1,7 +1,6 @@
1
1
  import React from "react";
2
2
  import { IAnnouncementContent } from "../../types";
3
- export default function AnnouncementButton({ content, buttonHref, clickHref, }: {
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, clickHref, }) {
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
- if (!clickHref)
33
- return;
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 { buildClickUrl, fireImpressionWhenVisible, getThemeClasses, mergeSlotAttributes, mergeSlotClasses, mergeSlotStyles, resolveAnnouncementColors, } from "../../utils";
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 && content.impressionUrl) {
12
- fireImpressionWhenVisible(ref.current, content.impressionUrl, content.measurementId, content.sectionType || SectionType.Announcement, content.sectionId);
12
+ if (ref.current) {
13
+ fireImpressionWhenVisible(ref.current, content.measurementId, content.sectionType || SectionType.Announcement, content.sectionId);
13
14
  }
14
- }, [content?.impressionUrl, content?.measurementId, content?.sectionType, content?.sectionId]);
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, clickHref: clickTrackingHref })))) : (React.createElement("div", { className: contentRowClass, style: contentRowStyle, ...(contentRowAttributes ?? {}) },
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, clickHref: clickTrackingHref })))),
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, clickHref, content, colors, contentWidthClass, typography, position, height, }: HeroViewProps): React.JSX.Element;
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, clickHref, content, colors, contentWidthClass, typography, position, height, }) {
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
- if (!clickHref)
19
- return;
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, clickHref, content, colors, typography, }: MobileHeroProps): React.JSX.Element;
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, clickHref, content, colors, typography, }) {
5
+ export default function MobileHero({ buttonHref, content, colors, typography, }) {
6
6
  const handleClick = React.useCallback(() => {
7
- if (!clickHref)
8
- return;
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 { buildClickUrl, fireImpressionWhenVisible, } from "../../utils";
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 && content.impressionUrl) {
11
- fireImpressionWhenVisible(ref.current, content.impressionUrl, content.measurementId, content.sectionType || SectionType.HeroBanner, content.sectionId);
11
+ if (ref.current) {
12
+ fireImpressionWhenVisible(ref.current, content.measurementId, content.sectionType || SectionType.HeroBanner, content.sectionId);
12
13
  }
13
- }, [content?.impressionUrl, content?.measurementId, content?.sectionType, content?.sectionId]);
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, clickHref: clickTrackingHref, colors: colors, contentWidthClass: contentWidthClass, typography: typography, position: position, height: height })),
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, clickHref: clickTrackingHref, colors: colors, typography: typography }))));
27
+ React.createElement(MobileHero, { content: content, buttonHref: buttonHref, colors: colors, typography: typography }))));
30
28
  }
@@ -10,8 +10,6 @@ export interface IAnnouncementContent {
10
10
  buttonLink: string;
11
11
  announcementHtml: string;
12
12
  counterEndDate?: string;
13
- impressionUrl: string;
14
- clickUrlBase: string;
15
13
  measurementId?: string;
16
14
  sectionId?: string;
17
15
  sectionType?: SectionType;
@@ -13,8 +13,6 @@ export interface IHeroContent {
13
13
  imageUrl: string;
14
14
  mobileImageUrl: string;
15
15
  videoUrl?: string;
16
- impressionUrl: string;
17
- clickUrlBase: string;
18
16
  measurementId?: string;
19
17
  sectionId?: string;
20
18
  sectionType?: SectionType;
@@ -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 { revalidateTag } = await import("next/cache");
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,
@@ -1,7 +1,5 @@
1
1
  import { SectionType } from "../constants";
2
- export declare function toBase64Url(input: string): string;
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, pixelUrl: string, measurementId?: string, sectionType?: SectionType, sectionId?: string): void;
13
+ export declare function fireImpressionWhenVisible(el: HTMLElement, measurementId?: string, sectionType?: SectionType, sectionId?: string): void;
@@ -1,59 +1,15 @@
1
1
  import { SectionType } from "../constants";
2
2
  import { setAttribution } from "./attribution";
3
- // --- Unicode-safe Base64URL ---
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, pixelUrl, measurementId, sectionType, sectionId) {
61
+ export function fireImpressionWhenVisible(el, measurementId, sectionType, sectionId) {
106
62
  if (typeof window === "undefined")
107
63
  return; // SSR guard
108
- if (!el || !pixelUrl)
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.1",
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
- "react": "^19.1.0"
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.1** - Enhanced attribution tracking with session storage and global API
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.4.0+)
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 (defaults to "force-cache")
334
- - `revalidate?: number | false` - Next.js revalidation time in seconds (defaults to 60)
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
- impressionUrl: string;
381
- clickUrlBase: string;
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
- impressionUrl: string;
438
- clickUrlBase: string;
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
  ```