fragment-headless-sdk 2.1.0 → 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 +436 -0
- package/dist/components/Announcement/AnnouncementButton.d.ts +2 -1
- package/dist/components/Announcement/AnnouncementButton.js +8 -9
- package/dist/components/Announcement/index.js +4 -3
- package/dist/components/Hero/DesktopHero.d.ts +2 -1
- package/dist/components/Hero/DesktopHero.js +8 -2
- package/dist/components/Hero/MobileHero.d.ts +2 -1
- package/dist/components/Hero/MobileHero.js +8 -2
- package/dist/components/Hero/index.js +5 -4
- package/dist/utils/metrics.d.ts +1 -0
- package/dist/utils/metrics.js +32 -1
- package/package.json +1 -1
- package/readme.md +24 -1
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
}
|
|
@@ -11,7 +11,8 @@ export default function Announcement({ content, type, handleClose, }) {
|
|
|
11
11
|
fireImpressionWhenVisible(ref.current, content.impressionUrl);
|
|
12
12
|
}
|
|
13
13
|
}, [content?.impressionUrl]);
|
|
14
|
-
const
|
|
14
|
+
const buttonHref = content?.buttonLink || undefined;
|
|
15
|
+
const clickTrackingHref = content?.buttonLink && content?.clickUrlBase
|
|
15
16
|
? buildClickUrl(content.clickUrlBase, content.buttonLink)
|
|
16
17
|
: undefined;
|
|
17
18
|
if (!content)
|
|
@@ -63,12 +64,12 @@ export default function Announcement({ content, type, handleClose, }) {
|
|
|
63
64
|
React.createElement("div", { className: marqueeContentClass, style: marqueeContentStyle, ...(marqueeContentAttributes ?? {}), dangerouslySetInnerHTML: {
|
|
64
65
|
__html: content.announcementHtml || "",
|
|
65
66
|
} }))),
|
|
66
|
-
content.buttonText && content.buttonType !== ButtonType.None && (React.createElement(AnnouncementButton, { content: content, buttonHref:
|
|
67
|
+
content.buttonText && content.buttonType !== ButtonType.None && (React.createElement(AnnouncementButton, { content: content, buttonHref: buttonHref, clickHref: clickTrackingHref })))) : (React.createElement("div", { className: contentRowClass, style: contentRowStyle, ...(contentRowAttributes ?? {}) },
|
|
67
68
|
React.createElement("div", { className: announcementTextClass, style: announcementTextStyle, ...(announcementTextAttributes ?? {}) },
|
|
68
69
|
React.createElement("div", { dangerouslySetInnerHTML: {
|
|
69
70
|
__html: content.announcementHtml || "",
|
|
70
71
|
} })),
|
|
71
72
|
type === AnnouncementType.Countdown ? (React.createElement(CountdownTimer, { content: content })) : (content.buttonText &&
|
|
72
|
-
content.buttonType !== ButtonType.None && (React.createElement(AnnouncementButton, { content: content, buttonHref:
|
|
73
|
+
content.buttonType !== ButtonType.None && (React.createElement(AnnouncementButton, { content: content, buttonHref: buttonHref, clickHref: clickTrackingHref }))))),
|
|
73
74
|
React.createElement("div", { onClick: handleClose, className: closeButtonClass, style: closeButtonStyle, ...(closeButtonAttributes ?? {}) }, "\u00D7"))));
|
|
74
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 */
|
|
@@ -34,7 +40,7 @@ export default function DesktopHero({ buttonHref, content, colors, contentWidthC
|
|
|
34
40
|
color: colors.text,
|
|
35
41
|
font: typography.description.font,
|
|
36
42
|
}),
|
|
37
|
-
content?.buttonLink && content?.buttonText && (React.createElement("a", { href: buttonHref,
|
|
43
|
+
content?.buttonLink && content?.buttonText && (React.createElement("a", { href: buttonHref, onClick: handleClick, className: "no-underline" },
|
|
38
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: {
|
|
39
45
|
color: colors.buttonText,
|
|
40
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,6 +1,12 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
import { fireClickMetric } from "../../utils";
|
|
2
3
|
import { renderText, } from "../../utils/hero-resolvers";
|
|
3
|
-
export default function MobileHero({ buttonHref, content, colors, typography, }) {
|
|
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]);
|
|
4
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 } },
|
|
5
11
|
renderText({
|
|
6
12
|
fontSize: typography.title.fontSize,
|
|
@@ -22,7 +28,7 @@ export default function MobileHero({ buttonHref, content, colors, typography, })
|
|
|
22
28
|
color: colors.text,
|
|
23
29
|
font: typography.description.font,
|
|
24
30
|
}),
|
|
25
|
-
content?.buttonLink && content?.buttonText && (React.createElement("a", { href: buttonHref,
|
|
31
|
+
content?.buttonLink && content?.buttonText && (React.createElement("a", { href: buttonHref, onClick: handleClick, className: "no-underline" },
|
|
26
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: {
|
|
27
33
|
color: colors.buttonText,
|
|
28
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
|
|
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:
|
|
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:
|
|
28
|
+
React.createElement(MobileHero, { content: content, buttonHref: buttonHref, clickHref: clickTrackingHref, colors: colors, typography: typography }))));
|
|
28
29
|
}
|
package/dist/utils/metrics.d.ts
CHANGED
|
@@ -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;
|
package/dist/utils/metrics.js
CHANGED
|
@@ -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
|
|
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
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.
|
|
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
|