@contentgrowth/content-widget 1.3.4 → 1.3.6

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/README.md CHANGED
@@ -268,25 +268,83 @@ const widget = new ContentGrowthWidget(container, {
268
268
  />
269
269
  ```
270
270
 
271
+ **ContentCard:**
272
+
273
+ A standalone article card component. Use it to display individual articles outside of ContentList.
274
+
275
+ ```astro
276
+ <!-- Load by slug -->
277
+ <ContentCard
278
+ apiKey="pk_your_key_here"
279
+ slug="my-article-slug"
280
+ linkPattern="/articles/{slug}"
281
+ />
282
+
283
+ <!-- Load by UUID -->
284
+ <ContentCard
285
+ apiKey="pk_your_key_here"
286
+ uuid="article-uuid"
287
+ />
288
+
289
+ <!-- Use pre-loaded article data -->
290
+ <ContentCard
291
+ article={articleData}
292
+ linkPattern="/articles/{slug}"
293
+ showSummary={true}
294
+ showTags={true}
295
+ />
296
+ ```
297
+
298
+ | Prop | Type | Default | Description |
299
+ |------|------|---------|-------------|
300
+ | `apiKey` | string | - | Your API key (required unless `article` is provided) |
301
+ | `article` | Article | - | Pre-loaded article data (skips API fetch) |
302
+ | `slug` | string | - | Load specific article by slug |
303
+ | `uuid` | string | - | Load specific article by UUID |
304
+ | `linkPattern` | string | '/articles/{slug}' | URL pattern for link |
305
+ | `linkTarget` | string | - | Link target attribute |
306
+ | `showSummary` | boolean | true | Show article summary |
307
+ | `summaryMaxLength` | number | - | Truncate summary at length |
308
+ | `showTags` | boolean | false | Show article tags |
309
+ | `showCategory` | boolean | true | Show category badge |
310
+
271
311
  **FeaturedCard:**
272
312
 
273
313
  A compact card displaying the Featured Summary with customizable styling. Perfect for landing pages.
274
314
 
275
315
  ```astro
316
+ <!-- Find latest article in category -->
276
317
  <FeaturedCard
277
318
  apiKey="pk_your_key_here"
278
319
  category="announce"
279
320
  linkPattern="/articles/{slug}"
280
321
  layout="horizontal"
281
322
  borderStyle="dashed"
282
- itemsBackground="#f3f4f6"
323
+ borderColor="#e5e7eb"
324
+ padding="20px"
283
325
  ctaText="Read full story"
284
326
  />
327
+
328
+ <!-- Load specific article by slug -->
329
+ <FeaturedCard
330
+ apiKey="pk_your_key_here"
331
+ slug="my-article-slug"
332
+ linkPattern="/articles/{slug}"
333
+ />
334
+
335
+ <!-- Use pre-loaded article data (for lists) -->
336
+ <FeaturedCard
337
+ article={articleData}
338
+ linkPattern="/articles/{slug}"
339
+ />
285
340
  ```
286
341
 
287
342
  | Prop | Type | Default | Description |
288
343
  |------|------|---------|-------------|
289
- | `apiKey` | string | - | Your API key |
344
+ | `apiKey` | string | - | Your API key (required unless `article` is provided) |
345
+ | `article` | Article | - | Pre-loaded article data (skips API fetch) |
346
+ | `slug` | string | - | Load specific article by slug |
347
+ | `uuid` | string | - | Load specific article by UUID |
290
348
  | `category` | string | - | Filter by category |
291
349
  | `tags` | string[] | [] | Filter by tags |
292
350
  | `layout` | 'vertical' \| 'horizontal' | auto | Card layout (auto uses article setting) |
@@ -294,11 +352,41 @@ A compact card displaying the Featured Summary with customizable styling. Perfec
294
352
  | `borderColor` | string | '#e5e7eb' | Border color (CSS value) |
295
353
  | `cardBackground` | string | 'none' | Card background ('none' = transparent) |
296
354
  | `itemsBackground` | string | '#f3f4f6' | Background for list/quote section |
355
+ | `padding` | string | - | Custom padding (e.g., '10px', '2rem 3rem') |
297
356
  | `ctaText` | string | 'Read full story' | Call-to-action text |
298
357
  | `showAuthor` | boolean | false | Show author name |
299
358
  | `showReadingTime` | boolean | false | Show reading time |
300
359
  | `linkPattern` | string | '/articles/{slug}' | URL pattern for link |
301
360
 
361
+ **Featured Summary Types:**
362
+
363
+ FeaturedCard supports structured JSON summaries generated via the portal wizard:
364
+
365
+ | Type | Description |
366
+ |------|-------------|
367
+ | `list` | Intro text on left, bulleted key points on right |
368
+ | `steps` | Intro text on left, numbered action steps on right |
369
+ | `quote` | Intro text on left, styled pullquote on right |
370
+ | `classic` | Simple text summary (legacy) |
371
+
372
+ **Featured Cards List:**
373
+
374
+ Display all articles as FeaturedCards in a grid using `displayAs`:
375
+
376
+ ```astro
377
+ <ContentList
378
+ apiKey="pk_your_key_here"
379
+ displayAs="featured-cards"
380
+ linkPattern="/articles/{slug}"
381
+ pageSize={12}
382
+ />
383
+ ```
384
+
385
+ | displayAs Value | Description |
386
+ |-----------------|-------------|
387
+ | `'default'` | Standard card/row layout |
388
+ | `'featured-cards'` | Renders each article as a FeaturedCard |
389
+
302
390
  ## API Client
303
391
 
304
392
  ### ContentGrowthClient
@@ -0,0 +1,170 @@
1
+ ---
2
+ import { marked } from 'marked';
3
+ import { ContentGrowthClient } from '../core/client';
4
+ import { formatDate, calculateReadingTime } from '../core/utils';
5
+ import type { Article, ArticleWithContent } from '../types';
6
+
7
+ interface ContentCardProps {
8
+ /**
9
+ * Pre-loaded article data (bypasses API fetch)
10
+ */
11
+ article?: Article | ArticleWithContent;
12
+
13
+ /**
14
+ * Load specific article by slug
15
+ */
16
+ slug?: string;
17
+
18
+ /**
19
+ * Load specific article by UUID
20
+ */
21
+ uuid?: string;
22
+
23
+ /**
24
+ * API key (required if fetching article)
25
+ */
26
+ apiKey?: string;
27
+
28
+ /**
29
+ * API base URL
30
+ */
31
+ baseUrl?: string;
32
+
33
+ /**
34
+ * URL pattern for the article link
35
+ * Supports placeholders: {uuid}, {slug}, {category}
36
+ * @default '/articles/{slug}'
37
+ */
38
+ linkPattern?: string;
39
+
40
+ /**
41
+ * Link target attribute
42
+ */
43
+ linkTarget?: string;
44
+
45
+ /**
46
+ * Show article summary
47
+ * @default true
48
+ */
49
+ showSummary?: boolean;
50
+
51
+ /**
52
+ * Maximum length of summary text
53
+ */
54
+ summaryMaxLength?: number;
55
+
56
+ /**
57
+ * Show article tags
58
+ * @default false
59
+ */
60
+ showTags?: boolean;
61
+
62
+ /**
63
+ * Show article category
64
+ * @default true
65
+ */
66
+ showCategory?: boolean;
67
+ }
68
+
69
+ type Props = ContentCardProps & { class?: string };
70
+
71
+ const {
72
+ apiKey,
73
+ baseUrl,
74
+ article: providedArticle,
75
+ slug,
76
+ uuid,
77
+ linkPattern = '/articles/{slug}',
78
+ linkTarget,
79
+ showSummary = true,
80
+ summaryMaxLength,
81
+ showTags = false,
82
+ showCategory = true,
83
+ class: className = ''
84
+ } = Astro.props;
85
+
86
+ // Determine article source
87
+ let article: Article | ArticleWithContent | null = null;
88
+ let error: string | null = null;
89
+
90
+ if (providedArticle) {
91
+ // Mode 1: Article provided directly
92
+ article = providedArticle;
93
+ } else if (apiKey) {
94
+ // Need to fetch article from API
95
+ const client = new ContentGrowthClient({ apiKey, baseUrl });
96
+
97
+ try {
98
+ if (uuid) {
99
+ // Mode 2: Load by UUID
100
+ article = await client.getArticle(uuid);
101
+ } else if (slug) {
102
+ // Mode 3: Load by slug
103
+ article = await client.getArticleBySlug(slug);
104
+ }
105
+ } catch (e) {
106
+ console.error('Failed to fetch article:', e);
107
+ error = e instanceof Error ? e.message : 'Failed to load article';
108
+ }
109
+ }
110
+
111
+ // Generate article URL
112
+ const getArticleUrl = (article: any) => {
113
+ if (!article) return '#';
114
+ return linkPattern
115
+ .replace('{uuid}', article.uuid || '')
116
+ .replace('{slug}', article.slug || article.uuid || '')
117
+ .replace('{category}', article.category || 'uncategorized');
118
+ };
119
+
120
+ // Truncate summary text
121
+ const truncateSummary = (text: string | null, maxLength?: number): string => {
122
+ if (!text) return '';
123
+ if (!maxLength || text.length <= maxLength) return text;
124
+ return text.substring(0, maxLength).trim() + '...';
125
+ };
126
+ ---
127
+
128
+ {article ? (
129
+ <article class={`cg-article-card ${className}`}>
130
+ <a href={getArticleUrl(article)} target={linkTarget} class="cg-card-link">
131
+ <div class="cg-card-content">
132
+ {showCategory && article.category && (
133
+ <div class="cg-card-category">
134
+ <span class="cg-category-badge">{article.category}</span>
135
+ </div>
136
+ )}
137
+
138
+ <h2 class="cg-card-title">{article.title}</h2>
139
+
140
+ {showSummary && article.summary && (
141
+ <p class="cg-card-summary">{truncateSummary(article.summary, summaryMaxLength)}</p>
142
+ )}
143
+
144
+ <div class="cg-card-meta">
145
+ <span class="cg-meta-author">{article.authorName}</span>
146
+ <span class="cg-meta-separator">•</span>
147
+ <time class="cg-meta-date" datetime={new Date(article.publishedAt * 1000).toISOString()}>
148
+ {formatDate(article.publishedAt)}
149
+ </time>
150
+ <span class="cg-meta-separator">•</span>
151
+ <span class="cg-meta-reading-time">{calculateReadingTime(article.wordCount)}</span>
152
+ </div>
153
+
154
+ {showTags && article.tags && article.tags.length > 0 && (
155
+ <div class="cg-card-tags">
156
+ {article.tags.map((tag) => (
157
+ <span class="cg-tag">{tag}</span>
158
+ ))}
159
+ </div>
160
+ )}
161
+ </div>
162
+ </a>
163
+ </article>
164
+ ) : (
165
+ error ? (
166
+ <div class={`cg-widget cg-error ${className}`}>
167
+ {error}
168
+ </div>
169
+ ) : null
170
+ )}
@@ -5,7 +5,8 @@
5
5
  */
6
6
  import type { ContentListProps } from '../types/index.js';
7
7
  import { ContentGrowthClient } from '../core/client.js';
8
- import { formatDate, calculateReadingTime } from '../core/utils.js';
8
+ import FeaturedCard from './FeaturedCard.astro';
9
+ import ContentCard from './ContentCard.astro';
9
10
 
10
11
  interface Props extends ContentListProps {}
11
12
 
@@ -14,6 +15,7 @@ const {
14
15
  baseUrl,
15
16
  layout = 'cards',
16
17
  displayMode = 'comfortable',
18
+ displayAs = 'default',
17
19
  theme = 'light',
18
20
  pageSize = 12,
19
21
  tags = [],
@@ -67,10 +69,13 @@ function truncateSummary(text: string | null, maxLength?: number): string {
67
69
  if (!maxLength || text.length <= maxLength) return text;
68
70
  return text.substring(0, maxLength).trim() + '...';
69
71
  }
72
+
73
+ // Check if we're in featured-cards mode
74
+ const isFeaturedCardsMode = displayAs === 'featured-cards';
70
75
  ---
71
76
 
72
77
  <div
73
- class={`cg-content-list cg-layout-${layout} cg-display-${displayMode} cg-theme-${theme} ${className}`}
78
+ class={`cg-content-list cg-layout-${layout} cg-display-${displayMode} cg-theme-${theme} ${isFeaturedCardsMode ? 'cg-featured-cards-list' : ''} ${className}`}
74
79
  data-cg-widget="list"
75
80
  >
76
81
  {articles.length === 0 ? (
@@ -79,60 +84,38 @@ function truncateSummary(text: string | null, maxLength?: number): string {
79
84
  </div>
80
85
  ) : (
81
86
  <>
82
- <div class={`cg-articles-grid ${layout === 'cards' ? 'cg-grid' : 'cg-list'}`}>
87
+ <div class={`cg-articles-grid ${isFeaturedCardsMode ? 'cg-featured-cards-grid' : (layout === 'cards' ? 'cg-grid' : 'cg-list')}`}>
83
88
  {articles.map((article) => {
84
- // Build article URL from pattern
85
- const articleUrl = linkPattern
86
- .replace('{uuid}', article.uuid)
87
- .replace('{slug}', article.slug)
88
- .replace('{category}', article.category || '');
89
-
90
89
  // Build link target from pattern
91
90
  const articleTarget = linkTarget
92
91
  ? linkTarget
93
92
  .replace('{uuid}', article.uuid)
94
93
  .replace('{id}', article.uuid)
95
94
  : undefined;
96
-
97
- const readingTime = calculateReadingTime(article.wordCount);
98
- const publishedDate = formatDate(article.publishedAt);
99
95
 
96
+ // Featured Cards Mode - Use FeaturedCard component
97
+ if (isFeaturedCardsMode) {
98
+ return (
99
+ <FeaturedCard
100
+ article={article}
101
+ linkPattern={linkPattern}
102
+ linkTarget={articleTarget}
103
+ showCategory={true}
104
+ />
105
+ );
106
+ }
107
+
108
+ // Default Card Mode - Use ContentCard component
100
109
  return (
101
- <article class="cg-article-card">
102
- <a href={articleUrl} target={articleTarget} class="cg-card-link">
103
- <div class="cg-card-content">
104
- {article.category && (
105
- <div class="cg-card-category">
106
- <span class="cg-category-badge">{article.category}</span>
107
- </div>
108
- )}
109
-
110
- <h2 class="cg-card-title">{article.title}</h2>
111
-
112
- {showAiSummary && article.summary && (
113
- <p class="cg-card-summary">{truncateSummary(article.summary, summaryMaxLength)}</p>
114
- )}
115
-
116
- <div class="cg-card-meta">
117
- <span class="cg-meta-author">{article.authorName}</span>
118
- <span class="cg-meta-separator">•</span>
119
- <time class="cg-meta-date" datetime={new Date(article.publishedAt * 1000).toISOString()}>
120
- {publishedDate}
121
- </time>
122
- <span class="cg-meta-separator">•</span>
123
- <span class="cg-meta-reading-time">{readingTime}</span>
124
- </div>
125
-
126
- {showTags && article.tags && article.tags.length > 0 && (
127
- <div class="cg-card-tags">
128
- {article.tags.map((tag) => (
129
- <span class="cg-tag">{tag}</span>
130
- ))}
131
- </div>
132
- )}
133
- </div>
134
- </a>
135
- </article>
110
+ <ContentCard
111
+ article={article}
112
+ linkPattern={linkPattern}
113
+ linkTarget={articleTarget}
114
+ showSummary={showAiSummary}
115
+ summaryMaxLength={summaryMaxLength}
116
+ showTags={showTags}
117
+ showCategory={true}
118
+ />
136
119
  );
137
120
  })}
138
121
  </div>