fragment-headless-sdk 2.1.1 → 2.1.2

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/CHANGELOG.md ADDED
@@ -0,0 +1,436 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [2.1.2] - 2025-10-30
9
+
10
+ ### 📚 Documentation Updates
11
+
12
+ - **Enhanced README** – Updated documentation to highlight new click tracking features
13
+ - **Feature Highlights** – Added comprehensive documentation for the enhanced click tracking system
14
+ - **Usage Examples** – Improved examples showing the separation of button destinations and tracking
15
+
16
+ ## [2.1.1] - 2025-10-30
17
+
18
+ ### 🎯 Enhanced Click Tracking System
19
+
20
+ - **Improved Click Tracking Architecture** – Separated button destinations from click tracking for better user experience
21
+ - `buttonHref` now contains the actual destination URL (no redirect)
22
+ - `clickHref` contains the tracking URL for metrics collection
23
+ - Users go directly to intended destinations instead of through redirects
24
+ - **New `fireClickMetric()` Function** – Advanced click tracking without relying on redirects
25
+ - Uses `fetch()` with `no-cors` mode and `keepalive` for reliable tracking
26
+ - Falls back to Image pixel tracking for maximum compatibility
27
+ - Handles server-side rendering gracefully
28
+ - **Removed Automatic New Tab Behavior** – Links no longer force `target="_blank"`
29
+ - Provides more natural user experience
30
+ - Allows developers to control link behavior explicitly
31
+ - Maintains accessibility with proper ARIA labels
32
+
33
+ ### 🛠 Technical Improvements
34
+
35
+ - **Enhanced Component Props** – Both Hero and Announcement components now accept separate tracking parameters
36
+ - `buttonHref` for the actual destination
37
+ - `clickHref` for tracking metrics
38
+ - **Better Error Handling** – Click tracking fails gracefully without affecting user experience
39
+ - **Performance Optimized** – Non-blocking click tracking that doesn't delay navigation
40
+ - **Cross-Browser Compatible** – Works across all modern browsers with appropriate fallbacks
41
+
42
+ ### 📝 Usage Examples
43
+
44
+ ```typescript
45
+ // The SDK automatically handles the separation of concerns
46
+ const heroContent = {
47
+ title: "Shop Now",
48
+ buttonText: "Get Started",
49
+ buttonLink: "https://example.com/products", // Direct destination
50
+ clickUrlBase: "https://tracking.example.com/click", // Tracking base
51
+ // SDK automatically creates:
52
+ // - buttonHref: "https://example.com/products" (direct link)
53
+ // - clickHref: "https://tracking.example.com/click?u=..." (tracking)
54
+ };
55
+
56
+ <Hero content={heroContent} />;
57
+ ```
58
+
59
+ ### ⚡ Performance Benefits
60
+
61
+ - **Faster Navigation** – Users go directly to destinations without redirect delays
62
+ - **Reliable Tracking** – Click metrics are captured even if users navigate away quickly
63
+ - **Better SEO** – Direct links improve search engine crawling and indexing
64
+ - **Enhanced UX** – More predictable link behavior for better user experience
65
+
66
+ ### 🔄 Backward Compatibility
67
+
68
+ - **No Breaking Changes** – All existing implementations continue to work
69
+ - **Automatic Upgrade** – New tracking system activates automatically when `clickUrlBase` is present
70
+ - **Legacy Support** – Existing tracking URLs continue to function as before
71
+
72
+ ## [2.1.0] - 2025-10-27
73
+
74
+ ### 🎨 Enhanced Hero Styling System
75
+
76
+ - **New Hero Resolvers Utility** – Comprehensive utility system for advanced Hero component customization
77
+ - `resolveHeroColors()` – Intelligent color resolution with fallback handling
78
+ - `resolveHeroTypography()` – Typography settings with font family, size, and line height control
79
+ - `resolveContentWidthClass()` – Dynamic content width management
80
+ - `resolvePosition()` – Content positioning (left, center, right alignment)
81
+ - `resolveHeight()` – Flexible height configuration
82
+ - `renderText()` – Unified text rendering with typography and styling support
83
+
84
+ ### ✨ Advanced Typography Features
85
+
86
+ - **Font Family Support** – Built-in support for popular font families:
87
+ - Roboto, Open Sans, Lato, Montserrat, Poppins, Inter, Nunito Sans, Source Sans Pro
88
+ - Custom font family support through `FontKey` type system
89
+ - **Responsive Typography** – Granular control over font sizes and line heights
90
+ - Separate title and description typography settings
91
+ - Tailwind CSS class integration for responsive design
92
+ - **Typography Tokens** – New styling tokens for enhanced typography control:
93
+ - `titleFontSize`, `titleLineHeight`, `titleFont`
94
+ - `descriptionFontSize`, `descriptionLineHeight`, `descriptionFont`
95
+
96
+ ### 🏗️ Layout & Positioning Enhancements
97
+
98
+ - **Content Positioning** – New positioning system for Hero content alignment
99
+ - Left, center, and right alignment options
100
+ - Responsive positioning with proper text alignment
101
+ - **Content Width Control** – Dynamic content width management
102
+ - Configurable content container widths
103
+ - Responsive design integration
104
+ - **Height Management** – Flexible height configuration system
105
+ - Custom height classes support
106
+ - Default height fallbacks
107
+
108
+ ### 🎯 Developer Experience Improvements
109
+
110
+ - **Type Safety** – Enhanced TypeScript interfaces for all new features
111
+ - `HeroResolvedColors` interface for color resolution
112
+ - `HeroTypographySettings` interface for typography configuration
113
+ - `FontKey` type for font family validation
114
+ - **Utility Functions** – New helper functions for common operations
115
+ - `joinClassNames()` – Safe CSS class concatenation
116
+ - `fallbackColor()` – Color value validation with fallbacks
117
+ - **Better Defaults** – Comprehensive default values for all styling options
118
+ - `DEFAULT_COLORS` for color fallbacks
119
+ - `DEFAULT_TYPOGRAPHY` for typography defaults
120
+ - `FONT_FAMILY_MAP` for font family mappings
121
+
122
+ ### 🔄 Backward Compatibility
123
+
124
+ - **Seamless Migration** – All existing Hero components continue to work without changes
125
+ - **Progressive Enhancement** – New features are opt-in and don't affect existing implementations
126
+ - **Legacy Support** – Existing styling approaches remain fully supported
127
+
128
+ ### 📝 Usage Examples
129
+
130
+ ```typescript
131
+ // Enhanced Hero with new typography and positioning
132
+ const heroContent = {
133
+ title: "Welcome to Our Store",
134
+ description: "Discover amazing products",
135
+ buttonText: "Shop Now",
136
+ buttonLink: "/products",
137
+ imageUrl: "https://example.com/hero.jpg",
138
+
139
+ styling: {
140
+ tokens: {
141
+ colors: {
142
+ title: "#ffffff",
143
+ text: "#f0f0f0",
144
+ button: "#007bff",
145
+ buttonText: "#ffffff",
146
+ background: "#1a1a1a",
147
+ },
148
+ typography: {
149
+ titleFont: "montserrat",
150
+ titleFontSize: "text-6xl",
151
+ titleLineHeight: "leading-tight",
152
+ descriptionFont: "inter",
153
+ descriptionFontSize: "text-xl",
154
+ descriptionLineHeight: "leading-relaxed",
155
+ },
156
+ layout: {
157
+ contentWidth: "max-w-4xl",
158
+ position: "center",
159
+ height: "min-h-screen",
160
+ },
161
+ },
162
+ },
163
+ };
164
+ ```
165
+
166
+ ### 🛠 Technical Improvements
167
+
168
+ - **Performance Optimized** – Efficient color and typography resolution
169
+ - **Memory Efficient** – Optimized utility functions with minimal overhead
170
+ - **Tree Shakeable** – Individual utility functions can be imported separately
171
+ - **CSS-in-JS Ready** – Full compatibility with styled-components and emotion
172
+
173
+ ## [1.0.6] - 2025-10-16
174
+
175
+ ### 🚀 Next.js Caching Fix
176
+
177
+ - **Fixed Vercel/Next.js Caching Issues** – Resolved aggressive caching that prevented fresh data from appearing in production deployments
178
+ - Added `cache: 'no-store'` by default for all `fetchResource()` calls
179
+ - Added Next.js-specific `revalidate: 0` configuration
180
+ - Added cache-busting headers (`Cache-Control`, `Pragma`) to prevent CDN caching
181
+ - Smart environment detection for Next.js vs other frameworks
182
+
183
+ ### ✨ New Cache Management Features
184
+
185
+ - **Cache Configuration Options** – Added optional `cacheOptions` parameter to `fetchResource()`
186
+ - `cache`: Control request cache mode (default: 'no-store' for fresh data)
187
+ - `revalidate`: Next.js revalidation time in seconds (default: 0)
188
+ - `tags`: Next.js cache tags for selective invalidation
189
+ - **Cache Invalidation Utilities** – New helper functions for cache management
190
+ - `revalidateFragmentCache()` – Invalidate all or specific Fragment caches
191
+ - `revalidateResourceType()` – Invalidate cache for specific resource type
192
+ - `revalidateAllFragmentCaches()` – Clear all Fragment-related caches
193
+ - `createCacheTag()` / `createCacheTags()` – Generate cache tags
194
+
195
+ ### 🛠 Technical Improvements
196
+
197
+ - **Environment Detection** – Automatic Next.js environment detection for optimal cache settings
198
+ - **Backward Compatibility** – All existing code continues to work without changes
199
+ - **TypeScript Support** – Full type definitions for new cache options
200
+
201
+ ### 📝 Usage Examples
202
+
203
+ ```typescript
204
+ // Default behavior - always fresh data (recommended)
205
+ const announcements = await fetchResource({
206
+ baseUrl: process.env.FRAGMENT_BASE_URL,
207
+ apiKey: process.env.FRAGMENT_API_KEY,
208
+ type: ResourceType.Announcements,
209
+ });
210
+
211
+ // Optional: Enable caching for performance
212
+ const cachedHeroes = await fetchResource({
213
+ baseUrl: process.env.FRAGMENT_BASE_URL,
214
+ apiKey: process.env.FRAGMENT_API_KEY,
215
+ type: ResourceType.HeroBanners,
216
+ cacheOptions: {
217
+ cache: "default",
218
+ revalidate: 300, // 5 minutes
219
+ },
220
+ });
221
+
222
+ // Cache invalidation (server-side only)
223
+ import { revalidateResourceType } from "fragment-headless-sdk";
224
+ await revalidateResourceType(ResourceType.Announcements);
225
+ ```
226
+
227
+ ### ⚠️ Migration Notes
228
+
229
+ - **No breaking changes** – Existing code works without modification
230
+ - **Fresh data by default** – Your database updates will now appear immediately in production
231
+ - **Opt-in caching** – Use `cacheOptions` if you want to enable caching for performance
232
+
233
+ ## [1.0.5] - 2025-09-28
234
+
235
+ ### ✨ New Features
236
+
237
+ - **Metrics Tracking** – Added built-in view and click tracking for both **Hero** and **Announcement** sections.
238
+ - Each resource’s `content` object now includes two server-generated fields:
239
+ - `impressionUrl` – 1×1 pixel URL automatically fired when the component enters the viewport.
240
+ - `clickUrlBase` – base redirect URL used to record button clicks before sending the user to the final destination.
241
+ - The SDK’s `<Hero>` and `<Announcement>` components now automatically:
242
+ - trigger a view pixel when visible, and
243
+ - wrap their CTA buttons with a signed click-tracking redirect.
244
+
245
+ ### 🛠 Technical Notes
246
+
247
+ - The `makeSignedMetricUrls` helper was refactored to attach `impressionUrl` and `clickUrlBase` **inside the `content` object** for each item returned by the API.
248
+ - New client-side utilities exported from `utils`:
249
+ - `buildClickUrl()` – safely appends the final destination (`&u=...`) to a signed `clickUrlBase`.
250
+ - `fireImpressionWhenVisible()` – fires a pixel only once when an element is at least 30 % visible.
251
+
252
+ ### ⚠️ Migration Notes
253
+
254
+ - **No breaking changes.**
255
+ Existing components continue to work; the new tracking is automatic when you upgrade to v1.0.5.
256
+ - If you build custom CTAs outside the provided components, use the new helpers to track clicks and views manually.
257
+
258
+ ## [1.0.4] - 2025-09-27
259
+
260
+ - **Types:** `IHero` now includes `views_count: number` and `clicks_count: number`.
261
+
262
+ ### 📝 Notes
263
+
264
+ - No breaking changes..
265
+
266
+ ## [1.0.3] - 2025-09-21
267
+
268
+ ### 🎨 UI/UX Improvements
269
+
270
+ - **Announcement Type Rename** - Changed `AnnouncementType.Announcement` to `AnnouncementType.Static` for better clarity
271
+ - **Countdown Timer Styling** - Removed white background from countdown timer for cleaner appearance
272
+ - **Layout Optimization** - Improved announcement banner layout with:
273
+ - Removed top/bottom padding (`py-3`) for more compact design
274
+ - Added 50px minimum height for consistent banner sizing
275
+ - Enhanced vertical centering of all content elements
276
+ - **Timer Digit Sizing** - Made countdown timer digits smaller and more compact:
277
+ - Reduced digit size from 24×28px to 20×24px
278
+ - Changed font size from `text-xl` to `text-base`
279
+
280
+ ### 🔧 Technical Changes
281
+
282
+ - Updated `announcementTypes` array to reflect new "Static" label
283
+ - Improved flexbox layout for better vertical alignment
284
+ - Maintained responsive design across all screen sizes
285
+
286
+ ## [1.0.2] - 2025-09-20
287
+
288
+ ### 🔄 Breaking Changes
289
+
290
+ - **Component Naming** - Renamed `Banner` component and all related types to `Announcement`
291
+
292
+ - `Banner` → `Announcement`
293
+ - `IBannerContent` → `IAnnouncementContent`
294
+ - `IBanner` → `IAnnouncement`
295
+ - `BannerType` → `AnnouncementType`
296
+ - `BannerStatus` → `AnnouncementStatus`
297
+ - `BannerButton` → `AnnouncementButton`
298
+ - `BannerStyles` → `AnnouncementStyles`
299
+ - `bannerHtml` property → `announcementHtml`
300
+
301
+ - **Resource Type Updates** - Updated resource type enums for consistency
302
+ - `ResourceType.HeroSections` → `ResourceType.HeroBanners`
303
+ - `ResourceType.Banners` → `ResourceType.Announcements`
304
+
305
+ ### 🔗 API Endpoint Changes
306
+
307
+ - Updated API endpoints to match new naming:
308
+ - `/api/v1/hero-sections` → `/api/v1/hero-banners`
309
+ - `/api/v1/banners` → `/api/v1/announcements`
310
+
311
+ ### 📚 Documentation
312
+
313
+ - Updated all documentation to reflect new component and type names
314
+ - Updated README.md examples with new ResourceType values
315
+ - Updated code examples throughout
316
+
317
+ ### 🛠️ Migration Guide
318
+
319
+ To update your existing code:
320
+
321
+ ```typescript
322
+ // Before (v1.0.1)
323
+ import {
324
+ Banner,
325
+ BannerType,
326
+ IBannerContent,
327
+ ResourceType,
328
+ } from "fragment-headless-sdk";
329
+
330
+ const banners = await fetchResource({
331
+ type: ResourceType.Banners,
332
+ });
333
+
334
+ <Banner content={bannerContent} type={BannerType.Standard} />;
335
+
336
+ // After (v1.0.2)
337
+ import {
338
+ Announcement,
339
+ AnnouncementType,
340
+ IAnnouncementContent,
341
+ ResourceType,
342
+ } from "fragment-headless-sdk";
343
+
344
+ const announcements = await fetchResource({
345
+ type: ResourceType.Announcements,
346
+ });
347
+
348
+ <Announcement
349
+ content={announcementContent}
350
+ type={AnnouncementType.Announcement}
351
+ />;
352
+ ```
353
+
354
+ ## [1.0.1] - 2025-09-07
355
+
356
+ ### 🎉 Initial Release
357
+
358
+ The official SDK for integrating with fragment-shopify CMS. Production-ready with full API key authentication support.
359
+
360
+ ### Features
361
+
362
+ - **Complete TypeScript Support** - Full type definitions for all components and API responses
363
+ - **React Components** - Pre-built Hero and Announcement components with responsive design
364
+ - **API Integration** - Built-in utilities for fetching sections from fragment-shopify app
365
+ - **Production Ready** - Full API key authentication with v1 endpoints
366
+ - **Tailwind CSS** - Styled components with customizable design system
367
+
368
+ ### Components
369
+
370
+ - **Hero Component** - Responsive hero sections with desktop/mobile variants
371
+ - Support for images, videos, and call-to-action buttons
372
+ - Customizable content and styling
373
+ - **Announcement Component** - Flexible announcement bars with multiple display types
374
+ - Standard, marquee, and countdown announcement variants
375
+ - `AnnouncementButton` and `CountdownTimer` sub-components
376
+
377
+ ### API Integration
378
+
379
+ - **`fetchResource()` Function** - Simple API for fetching sections
380
+ - **API Key Authentication** - Secure authentication using `keyId:secret` format
381
+ - **v1 Endpoints** - Production endpoints (`/api/v1/announcements`, `/api/v1/hero-banners`)
382
+ - **Error Handling** - Comprehensive error handling and logging
383
+ - **Type Safety** - Full TypeScript support for all API responses
384
+
385
+ ### Usage
386
+
387
+ ```tsx
388
+ import {
389
+ fetchResource,
390
+ ResourceType,
391
+ Hero,
392
+ Announcement,
393
+ } from "fragment-headless-sdk";
394
+
395
+ // Fetch data
396
+ const heroes = await fetchResource({
397
+ baseUrl: process.env.EXTERNAL_API_URL,
398
+ apiKey: process.env.FRAGMENT_API_KEY,
399
+ type: ResourceType.HeroBanners,
400
+ });
401
+
402
+ // Render components
403
+ <Hero content={heroes[0]?.content} />;
404
+ ```
405
+
406
+ ### Environment Variables
407
+
408
+ ```bash
409
+ EXTERNAL_API_URL=https://your-fragment-app.vercel.app
410
+ FRAGMENT_API_KEY=bh_a1b2c3d4e5f6:your-64-char-secret
411
+ ```
412
+
413
+ ---
414
+
415
+ ## Upcoming Changes
416
+
417
+ ### v1.1.0 (Planned)
418
+
419
+ - Enhanced error handling with specific error types
420
+ - Support for additional section types
421
+ - Caching and performance optimizations
422
+
423
+ ### v1.2.0 (Planned)
424
+
425
+ - Real-time updates via webhooks
426
+ - Advanced filtering and sorting options
427
+ - Batch operations support
428
+ - TypeScript strict mode compatibility
429
+
430
+ ---
431
+
432
+ ## Support
433
+
434
+ - **Documentation**: [README.md](./README.md)
435
+ - **NPM Package**: https://www.npmjs.com/package/fragment-shopify-sdk
436
+ - **Issues**: Please report issues in the GitHub repository
@@ -1,6 +1,7 @@
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, clickHref, }: {
4
4
  content: IAnnouncementContent;
5
5
  buttonHref?: string;
6
+ clickHref?: string;
6
7
  }): React.JSX.Element | null;
@@ -1,17 +1,13 @@
1
1
  import React from "react";
2
2
  import { ButtonType } from "../../constants";
3
- import { mergeSlotAttributes, mergeSlotClasses, mergeSlotStyles, resolveToken, resolveTokenByCategory, } from "../../utils";
4
- export default function AnnouncementButton({ content, buttonHref, }) {
3
+ import { fireClickMetric, mergeSlotAttributes, mergeSlotClasses, mergeSlotStyles, resolveToken, resolveTokenByCategory, } from "../../utils";
4
+ export default function AnnouncementButton({ content, buttonHref, clickHref, }) {
5
5
  // Don’t render if no button or explicitly None
6
6
  if (!content?.buttonText || content.buttonType === ButtonType.None)
7
7
  return null;
8
8
  // If we weren’t given a usable href, don’t render a broken link
9
9
  if (!buttonHref)
10
10
  return null;
11
- // Decide if link should open in a new tab.
12
- // If you already have a boolean like `content.buttonLink` meaning "open in new tab",
13
- // keep using it; otherwise you can add one later.
14
- const openInNewTab = Boolean(content.buttonLink);
15
11
  const styling = content.styling;
16
12
  const baseTextColor = resolveTokenByCategory(styling, "colors", "text") ||
17
13
  resolveToken(styling, "textColor");
@@ -37,7 +33,10 @@ export default function AnnouncementButton({ content, buttonHref, }) {
37
33
  if (attributes && "aria-label" in attributes) {
38
34
  delete attributes["aria-label"];
39
35
  }
40
- return (React.createElement("a", { href: buttonHref, className: className, style: style, ...(openInNewTab
41
- ? { target: "_blank", rel: "noopener noreferrer" }
42
- : {}), ...(attributes ?? {}), "aria-label": ariaLabel }, content.buttonText));
36
+ const handleClick = React.useCallback(() => {
37
+ if (!clickHref)
38
+ return;
39
+ fireClickMetric(clickHref);
40
+ }, [clickHref]);
41
+ return (React.createElement("a", { href: buttonHref, className: className, style: style, onClick: handleClick, ...(attributes ?? {}), "aria-label": ariaLabel }, content.buttonText));
43
42
  }
@@ -2,6 +2,7 @@ import React, { useEffect, useRef } from "react";
2
2
  import { AnnouncementType, ButtonType } from "../../constants";
3
3
  import { buildClickUrl, fireImpressionWhenVisible, getThemeClasses, mergeSlotAttributes, mergeSlotClasses, mergeSlotStyles, resolveToken, resolveTokenByCategory, } from "../../utils";
4
4
  import AnnouncementButton from "./AnnouncementButton";
5
+ import { AnnouncementStyles } from "./AnnouncementStyles";
5
6
  import CountdownTimer from "./CountdownTimer";
6
7
  export default function Announcement({ content, type, handleClose, }) {
7
8
  const ref = useRef(null);
@@ -10,7 +11,8 @@ export default function Announcement({ content, type, handleClose, }) {
10
11
  fireImpressionWhenVisible(ref.current, content.impressionUrl);
11
12
  }
12
13
  }, [content?.impressionUrl]);
13
- const signedButtonHref = content?.buttonLink && content?.clickUrlBase
14
+ const buttonHref = content?.buttonLink || undefined;
15
+ const clickTrackingHref = content?.buttonLink && content?.clickUrlBase
14
16
  ? buildClickUrl(content.clickUrlBase, content.buttonLink)
15
17
  : undefined;
16
18
  if (!content)
@@ -54,6 +56,7 @@ export default function Announcement({ content, type, handleClose, }) {
54
56
  const closeButtonStyle = mergeSlotStyles({ color: closeButtonColor }, styling, "closeButton");
55
57
  const closeButtonAttributes = mergeSlotAttributes(styling, "closeButton");
56
58
  return (React.createElement("div", { ref: ref, className: rootClass, style: rootStyle, ...(rootAttributes ?? {}) },
59
+ React.createElement(AnnouncementStyles, null),
57
60
  React.createElement("div", { className: innerClass, style: innerStyle, ...(innerAttributes ?? {}) },
58
61
  type === AnnouncementType.Marquee ? (React.createElement("div", { className: marqueeContainerClass, style: marqueeContainerStyle, ...(marqueeContainerAttributes ?? {}) },
59
62
  React.createElement("div", { className: marqueeTextWrapperClass, style: marqueeTextWrapperStyle, ...(marqueeTextWrapperAttributes ?? {}) },
@@ -61,12 +64,12 @@ export default function Announcement({ content, type, handleClose, }) {
61
64
  React.createElement("div", { className: marqueeContentClass, style: marqueeContentStyle, ...(marqueeContentAttributes ?? {}), dangerouslySetInnerHTML: {
62
65
  __html: content.announcementHtml || "",
63
66
  } }))),
64
- content.buttonText && content.buttonType !== ButtonType.None && (React.createElement(AnnouncementButton, { content: content, buttonHref: signedButtonHref })))) : (React.createElement("div", { className: contentRowClass, style: contentRowStyle, ...(contentRowAttributes ?? {}) },
67
+ content.buttonText && content.buttonType !== ButtonType.None && (React.createElement(AnnouncementButton, { content: content, buttonHref: buttonHref, clickHref: clickTrackingHref })))) : (React.createElement("div", { className: contentRowClass, style: contentRowStyle, ...(contentRowAttributes ?? {}) },
65
68
  React.createElement("div", { className: announcementTextClass, style: announcementTextStyle, ...(announcementTextAttributes ?? {}) },
66
69
  React.createElement("div", { dangerouslySetInnerHTML: {
67
70
  __html: content.announcementHtml || "",
68
71
  } })),
69
72
  type === AnnouncementType.Countdown ? (React.createElement(CountdownTimer, { content: content })) : (content.buttonText &&
70
- content.buttonType !== ButtonType.None && (React.createElement(AnnouncementButton, { content: content, buttonHref: signedButtonHref || content.buttonLink || "#" }))))),
73
+ content.buttonType !== ButtonType.None && (React.createElement(AnnouncementButton, { content: content, buttonHref: buttonHref, clickHref: clickTrackingHref }))))),
71
74
  React.createElement("div", { onClick: handleClose, className: closeButtonClass, style: closeButtonStyle, ...(closeButtonAttributes ?? {}) }, "\u00D7"))));
72
75
  }
@@ -3,6 +3,7 @@ import { IHeroContent } from "../../types";
3
3
  import { resolveHeroColors, resolveHeroTypography } from "../../utils/hero-resolvers";
4
4
  interface HeroViewProps {
5
5
  buttonHref?: string;
6
+ clickHref?: string;
6
7
  content: IHeroContent;
7
8
  colors: ReturnType<typeof resolveHeroColors>;
8
9
  contentWidthClass: string;
@@ -10,5 +11,5 @@ interface HeroViewProps {
10
11
  position: "left" | "center" | "right";
11
12
  height: string;
12
13
  }
13
- export default function DesktopHero({ buttonHref, content, colors, contentWidthClass, typography, position, height, }: HeroViewProps): React.JSX.Element;
14
+ export default function DesktopHero({ buttonHref, clickHref, content, colors, contentWidthClass, typography, position, height, }: HeroViewProps): React.JSX.Element;
14
15
  export {};
@@ -1,6 +1,7 @@
1
1
  import React from "react";
2
+ import { fireClickMetric } from "../../utils";
2
3
  import { DEFAULT_CONTENT_WIDTH_CLASS, joinClassNames, renderText, } from "../../utils/hero-resolvers";
3
- export default function DesktopHero({ buttonHref, content, colors, contentWidthClass, typography, position, height, }) {
4
+ export default function DesktopHero({ buttonHref, clickHref, content, colors, contentWidthClass, typography, position, height, }) {
4
5
  const getPositionClasses = () => {
5
6
  switch (position) {
6
7
  case "center":
@@ -12,6 +13,11 @@ export default function DesktopHero({ buttonHref, content, colors, contentWidthC
12
13
  return "items-start text-left";
13
14
  }
14
15
  };
16
+ const handleClick = React.useCallback(() => {
17
+ if (!clickHref)
18
+ return;
19
+ fireClickMetric(clickHref);
20
+ }, [clickHref]);
15
21
  return (React.createElement("div", { className: `relative ${height} gap-4 w-full`, style: { backgroundColor: colors.background } },
16
22
  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" })) : (
17
23
  /* Image Background */
@@ -22,6 +28,7 @@ export default function DesktopHero({ buttonHref, content, colors, contentWidthC
22
28
  fontSize: typography.title.fontSize,
23
29
  lineHeight: typography.title.lineHeight,
24
30
  text: content?.title,
31
+ className: "mt-4",
25
32
  color: colors.title,
26
33
  font: typography.title.font,
27
34
  }),
@@ -33,7 +40,7 @@ export default function DesktopHero({ buttonHref, content, colors, contentWidthC
33
40
  color: colors.text,
34
41
  font: typography.description.font,
35
42
  }),
36
- content?.buttonLink && content?.buttonText && (React.createElement("a", { href: buttonHref, target: "_blank", rel: "noopener noreferrer", className: "no-underline" },
43
+ content?.buttonLink && content?.buttonText && (React.createElement("a", { href: buttonHref, onClick: handleClick, className: "no-underline" },
37
44
  React.createElement("div", { className: "mt-6 inline-block rounded-md px-8 py-2 text-2xl font-semibold drop-shadow-lg transition-all duration-200 hover:opacity-90", style: {
38
45
  color: colors.buttonText,
39
46
  backgroundColor: colors.buttonBackground,
@@ -3,9 +3,10 @@ import { IHeroContent } from "../../types";
3
3
  import { resolveHeroColors, resolveHeroTypography } from "../../utils/hero-resolvers";
4
4
  interface MobileHeroProps {
5
5
  buttonHref?: string;
6
+ clickHref?: string;
6
7
  content: IHeroContent;
7
8
  colors: ReturnType<typeof resolveHeroColors>;
8
9
  typography: ReturnType<typeof resolveHeroTypography>;
9
10
  }
10
- export default function MobileHero({ buttonHref, content, colors, typography, }: MobileHeroProps): React.JSX.Element;
11
+ export default function MobileHero({ buttonHref, clickHref, content, colors, typography, }: MobileHeroProps): React.JSX.Element;
11
12
  export {};
@@ -1,22 +1,19 @@
1
1
  import React from "react";
2
- import { ensureSafeColor } from "../../utils/color";
2
+ import { fireClickMetric } from "../../utils";
3
3
  import { renderText, } from "../../utils/hero-resolvers";
4
- export default function MobileHero({ buttonHref, content, colors, typography, }) {
5
- const safeTitleColor = ensureSafeColor(colors.title, "#ffffff");
6
- const safeTextColor = ensureSafeColor(colors.text, "#ffffff");
7
- const titleFontSize = typography.title.fontSize === "text-5xl"
8
- ? "text-3xl"
9
- : typography.title.fontSize;
10
- const descriptionFontSize = typography.description.fontSize === "text-3xl"
11
- ? "text-lg"
12
- : typography.description.fontSize;
4
+ export default function MobileHero({ buttonHref, clickHref, content, colors, typography, }) {
5
+ const handleClick = React.useCallback(() => {
6
+ if (!clickHref)
7
+ return;
8
+ fireClickMetric(clickHref);
9
+ }, [clickHref]);
13
10
  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 } },
14
11
  renderText({
15
- fontSize: titleFontSize,
12
+ fontSize: typography.title.fontSize,
16
13
  lineHeight: typography.title.lineHeight,
17
14
  text: content?.title,
18
- className: "px-4 drop-shadow-xl text-center font-bold",
19
- color: safeTitleColor,
15
+ className: "px-4 drop-shadow-xl text-center",
16
+ color: colors.title,
20
17
  font: typography.title.font,
21
18
  }),
22
19
  content?.videoUrl ? (React.createElement("div", { className: "w-full" },
@@ -24,14 +21,14 @@ export default function MobileHero({ buttonHref, content, colors, typography, })
24
21
  React.createElement("img", { src: content.mobileImageUrl, alt: content.title || "Hero", className: "h-full w-full object-cover" }))) : content?.imageUrl ? (React.createElement("div", { className: "w-full" },
25
22
  React.createElement("img", { src: content.imageUrl, alt: content.title || "Hero", className: "h-full w-full object-cover" }))) : null,
26
23
  renderText({
27
- fontSize: descriptionFontSize,
24
+ fontSize: typography.description.fontSize,
28
25
  lineHeight: typography.description.lineHeight,
29
26
  text: content?.description,
30
27
  className: "px-4 drop-shadow-lg text-center mt-4",
31
- color: safeTextColor,
28
+ color: colors.text,
32
29
  font: typography.description.font,
33
30
  }),
34
- content?.buttonLink && content?.buttonText && (React.createElement("a", { href: buttonHref, target: "_blank", rel: "noopener noreferrer", className: "no-underline" },
31
+ content?.buttonLink && content?.buttonText && (React.createElement("a", { href: buttonHref, onClick: handleClick, className: "no-underline" },
35
32
  React.createElement("div", { className: "mb-2 rounded-md px-6 py-2 text-lg font-semibold drop-shadow-lg transition-all duration-200 hover:opacity-90", style: {
36
33
  color: colors.buttonText,
37
34
  backgroundColor: colors.buttonBackground,
@@ -1,5 +1,5 @@
1
1
  import React, { useEffect, useRef } from "react";
2
- import { buildClickUrl, fireImpressionWhenVisible } from "../../utils";
2
+ import { buildClickUrl, fireImpressionWhenVisible, } from "../../utils";
3
3
  import { resolveContentWidthClass, resolveHeight, resolveHeroColors, resolveHeroTypography, resolvePosition, } from "../../utils/hero-resolvers";
4
4
  import DesktopHero from "./DesktopHero";
5
5
  import MobileHero from "./MobileHero";
@@ -10,7 +10,8 @@ export default function Hero({ content }) {
10
10
  fireImpressionWhenVisible(ref.current, content.impressionUrl);
11
11
  }
12
12
  }, [content?.impressionUrl]);
13
- const signedButtonHref = content?.buttonLink && content?.clickUrlBase
13
+ const buttonHref = content?.buttonLink || undefined;
14
+ const clickTrackingHref = content?.buttonLink && content?.clickUrlBase
14
15
  ? buildClickUrl(content.clickUrlBase, content.buttonLink)
15
16
  : undefined;
16
17
  if (!content)
@@ -22,7 +23,7 @@ export default function Hero({ content }) {
22
23
  const height = resolveHeight(content);
23
24
  return (React.createElement("div", { className: "bg-black", ref: ref, style: { backgroundColor: colors.background } },
24
25
  React.createElement("div", { className: "hidden lg:block" },
25
- React.createElement(DesktopHero, { content: content, buttonHref: signedButtonHref, colors: colors, contentWidthClass: contentWidthClass, typography: typography, position: position, height: height })),
26
+ React.createElement(DesktopHero, { content: content, buttonHref: buttonHref, clickHref: clickTrackingHref, colors: colors, contentWidthClass: contentWidthClass, typography: typography, position: position, height: height })),
26
27
  React.createElement("div", { className: "block lg:hidden" },
27
- React.createElement(MobileHero, { content: content, buttonHref: signedButtonHref, colors: colors, typography: typography }))));
28
+ React.createElement(MobileHero, { content: content, buttonHref: buttonHref, clickHref: clickTrackingHref, colors: colors, typography: typography }))));
28
29
  }
@@ -1,5 +1,4 @@
1
1
  export * from "./cache";
2
- export * from "./color";
3
2
  export * from "./fetch-resource";
4
3
  export * from "./hero-resolvers";
5
4
  export * from "./metrics";
@@ -1,5 +1,4 @@
1
1
  export * from "./cache";
2
- export * from "./color";
3
2
  export * from "./fetch-resource";
4
3
  export * from "./hero-resolvers";
5
4
  export * from "./metrics";
@@ -1,3 +1,4 @@
1
1
  export declare function toBase64Url(input: string): string;
2
2
  export declare function buildClickUrl(clickUrlBase: string, targetHref: string): string;
3
+ export declare function fireClickMetric(clickUrl: string): void;
3
4
  export declare function fireImpressionWhenVisible(el: HTMLElement, pixelUrl: string): void;
@@ -9,11 +9,42 @@ function appendQuery(url, key, value) {
9
9
  const sep = url.includes("?") ? "&" : "?";
10
10
  return `${url}${sep}${key}=${value}`;
11
11
  }
12
- // Build the final redirect URL the CTA should use
12
+ // Build the tracking URL that encodes the final destination for metrics
13
13
  export function buildClickUrl(clickUrlBase, targetHref) {
14
14
  const u = encodeURIComponent(toBase64Url(targetHref));
15
15
  return appendQuery(clickUrlBase, "u", u);
16
16
  }
17
+ // Fire the click tracking URL without relying on a redirect
18
+ // Default to GET so legacy tracking endpoints continue to accept the request
19
+ export function fireClickMetric(clickUrl) {
20
+ if (typeof window === "undefined")
21
+ return;
22
+ if (!clickUrl)
23
+ return;
24
+ try {
25
+ if (typeof fetch === "function") {
26
+ fetch(clickUrl, {
27
+ method: "GET",
28
+ mode: "no-cors",
29
+ keepalive: true,
30
+ }).catch(() => {
31
+ /* no-op */
32
+ });
33
+ return;
34
+ }
35
+ }
36
+ catch {
37
+ // swallow and fall back to <img>
38
+ }
39
+ try {
40
+ const img = new Image();
41
+ img.referrerPolicy = "strict-origin-when-cross-origin";
42
+ img.src = clickUrl;
43
+ }
44
+ catch {
45
+ // nothing else we can do
46
+ }
47
+ }
17
48
  // --- View tracking (once per element) ---
18
49
  const seenEls = typeof WeakSet !== "undefined" ? new WeakSet() : null;
19
50
  export function fireImpressionWhenVisible(el, pixelUrl) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fragment-headless-sdk",
3
- "version": "2.1.1",
3
+ "version": "2.1.2",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/readme.md CHANGED
@@ -2,7 +2,20 @@
2
2
 
3
3
  The official SDK for integrating with Fragment-Shopify CMS. Provides React components, TypeScript types, and utilities for rendering published sections in headless Shopify storefronts.
4
4
 
5
- ## ✨ What's New in v2.1.0
5
+ ## ✨ What's New in v2.1.2
6
+
7
+ 📚 **Enhanced Documentation** - Comprehensive documentation updates highlighting new click tracking features
8
+ 📖 **Better Examples** - Improved usage examples and feature descriptions
9
+ 🎯 **Feature Highlights** - Clear documentation of the enhanced click tracking system
10
+
11
+ ### Previous Release (v2.1.1)
12
+
13
+ 🎯 **Enhanced Click Tracking System** - Improved click tracking architecture with better user experience
14
+ ⚡ **Direct Navigation** - Users go directly to destinations without redirect delays
15
+ 🔗 **Separated Tracking** - Button destinations and click tracking are now handled separately
16
+ 🛠️ **Better Performance** - Non-blocking click tracking that doesn't delay navigation
17
+
18
+ ### Previous Release (v2.1.0)
6
19
 
7
20
  🎨 **Enhanced Hero Styling System** - New hero resolvers utility with advanced typography, positioning, and layout controls
8
21
  🔤 **Advanced Typography** - Built-in font family support with granular control over sizes and line heights
@@ -41,6 +54,7 @@ Fragment-Shopify App (CMS) → API Endpoint → fragment-headless-sdk (Consumer)
41
54
  - ✅ **TypeScript Support**: Full type definitions for all components and data structures
42
55
  - ✅ **Multiple Announcement Types**: Standard, marquee, and countdown announcement variants
43
56
  - ✅ **Hero Sections**: Desktop/mobile responsive hero components with video support
57
+ - ✅ **Advanced Click Tracking**: Separated tracking and navigation for optimal user experience
44
58
 
45
59
  ### Advanced Styling System (v2.0+)
46
60
 
@@ -62,6 +76,15 @@ Fragment-Shopify App (CMS) → API Endpoint → fragment-headless-sdk (Consumer)
62
76
  - 📝 **Type Safety**: Enhanced TypeScript interfaces for all styling options
63
77
  - 🔄 **Backward Compatible**: Works seamlessly with existing Hero implementations
64
78
 
79
+ ### Enhanced Click Tracking System (v2.1.1+)
80
+
81
+ - 🎯 **Separated Concerns**: Button destinations and click tracking handled independently
82
+ - ⚡ **Direct Navigation**: Users go directly to destinations without redirect delays
83
+ - 🔗 **Advanced Tracking**: `fireClickMetric()` function with fetch API and image fallback
84
+ - 🛠️ **Non-Blocking**: Click tracking doesn't delay user navigation
85
+ - 🌐 **Cross-Browser**: Works across all modern browsers with appropriate fallbacks
86
+ - 🔄 **Graceful Degradation**: Tracking fails silently without affecting user experience
87
+
65
88
  ---
66
89
 
67
90
  ## 📦 Installation
@@ -1,2 +0,0 @@
1
- export declare function isDarkColor(color: string | undefined | null): boolean;
2
- export declare function ensureSafeColor(color: string | undefined | null, fallback: string): string;
@@ -1,67 +0,0 @@
1
- const HEX_COLOR_REGEX = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
2
- const RGB_COLOR_REGEX = /^rgba?\s*\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})(?:\s*,\s*(\d*\.?\d+))?\s*\)$/i;
3
- function hexToRgb(color) {
4
- if (!HEX_COLOR_REGEX.test(color)) {
5
- return null;
6
- }
7
- let hex = color.slice(1);
8
- if (hex.length === 3) {
9
- hex = hex
10
- .split("")
11
- .map((char) => char + char)
12
- .join("");
13
- }
14
- const bigint = parseInt(hex, 16);
15
- return {
16
- r: (bigint >> 16) & 255,
17
- g: (bigint >> 8) & 255,
18
- b: bigint & 255,
19
- };
20
- }
21
- function rgbStringToRgb(color) {
22
- const match = color.match(RGB_COLOR_REGEX);
23
- if (!match) {
24
- return null;
25
- }
26
- const [, r, g, b] = match;
27
- const red = Number(r);
28
- const green = Number(g);
29
- const blue = Number(b);
30
- if ([red, green, blue].some((value) => Number.isNaN(value))) {
31
- return null;
32
- }
33
- return { r: red, g: green, b: blue };
34
- }
35
- function normalizeColor(color) {
36
- if (!color) {
37
- return null;
38
- }
39
- const trimmed = color.trim();
40
- return hexToRgb(trimmed) ?? rgbStringToRgb(trimmed);
41
- }
42
- function getRelativeLuminance(rgb) {
43
- const transform = (value) => {
44
- const channel = value / 255;
45
- if (channel <= 0.03928) {
46
- return channel / 12.92;
47
- }
48
- return Math.pow((channel + 0.055) / 1.055, 2.4);
49
- };
50
- const r = transform(rgb.r);
51
- const g = transform(rgb.g);
52
- const b = transform(rgb.b);
53
- return 0.2126 * r + 0.7152 * g + 0.0722 * b;
54
- }
55
- export function isDarkColor(color) {
56
- const rgb = normalizeColor(color);
57
- if (!rgb) {
58
- return false;
59
- }
60
- return getRelativeLuminance(rgb) < 0.5;
61
- }
62
- export function ensureSafeColor(color, fallback) {
63
- if (!color) {
64
- return fallback;
65
- }
66
- return isDarkColor(color) ? fallback : color;
67
- }