@contentgrowth/content-widget 1.1.0
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/LICENSE +21 -0
- package/README.md +442 -0
- package/dist/astro/ContentList.astro +177 -0
- package/dist/astro/ContentViewer.astro +252 -0
- package/dist/astro/index.d.ts +9 -0
- package/dist/astro/index.d.ts.map +1 -0
- package/dist/astro/index.js +8 -0
- package/dist/core/client.d.ts +67 -0
- package/dist/core/client.d.ts.map +1 -0
- package/dist/core/client.js +217 -0
- package/dist/core/index.d.ts +8 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +7 -0
- package/dist/core/utils.d.ts +32 -0
- package/dist/core/utils.d.ts.map +1 -0
- package/dist/core/utils.js +70 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/react/ContentList.d.ts +12 -0
- package/dist/react/ContentList.d.ts.map +1 -0
- package/dist/react/ContentList.js +106 -0
- package/dist/react/ContentViewer.d.ts +12 -0
- package/dist/react/ContentViewer.d.ts.map +1 -0
- package/dist/react/ContentViewer.js +97 -0
- package/dist/react/hooks.d.ts +63 -0
- package/dist/react/hooks.d.ts.map +1 -0
- package/dist/react/hooks.js +140 -0
- package/dist/react/index.d.ts +9 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +6 -0
- package/dist/styles.css +970 -0
- package/dist/types/index.d.ts +271 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +16 -0
- package/dist/vue/ContentList.vue +166 -0
- package/dist/vue/ContentViewer.vue +137 -0
- package/dist/vue/composables.d.ts +64 -0
- package/dist/vue/composables.d.ts.map +1 -0
- package/dist/vue/composables.js +165 -0
- package/dist/vue/index.d.ts +10 -0
- package/dist/vue/index.d.ts.map +1 -0
- package/dist/vue/index.js +8 -0
- package/dist/widget/content-card.js +190 -0
- package/dist/widget/content-list.js +289 -0
- package/dist/widget/content-viewer.js +230 -0
- package/dist/widget/index.js +40 -0
- package/dist/widget/utils/api-client.js +154 -0
- package/dist/widget/utils/helpers.js +71 -0
- package/dist/widget/widget-js/content-card.js +190 -0
- package/dist/widget/widget-js/content-list.js +289 -0
- package/dist/widget/widget-js/content-viewer.js +230 -0
- package/dist/widget/widget-js/index.js +40 -0
- package/dist/widget/widget-js/utils/api-client.js +154 -0
- package/dist/widget/widget-js/utils/helpers.js +71 -0
- package/dist/widget/widget-js/widget.d.ts +24 -0
- package/dist/widget/widget-js/widget.js +240 -0
- package/dist/widget/widget.d.ts +24 -0
- package/dist/widget/widget.js +240 -0
- package/package.json +99 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* ContentViewer Component
|
|
4
|
+
* Displays a single article with full content
|
|
5
|
+
*/
|
|
6
|
+
import type { ContentViewerProps } from '../types/index.js';
|
|
7
|
+
import { ContentGrowthClient } from '../core/client.js';
|
|
8
|
+
import { formatDate, calculateReadingTime } from '../core/utils.js';
|
|
9
|
+
import { marked } from 'marked';
|
|
10
|
+
|
|
11
|
+
interface Props extends ContentViewerProps {}
|
|
12
|
+
|
|
13
|
+
const {
|
|
14
|
+
apiKey,
|
|
15
|
+
uuid,
|
|
16
|
+
slug,
|
|
17
|
+
baseUrl,
|
|
18
|
+
theme = 'light',
|
|
19
|
+
showBackButton = false,
|
|
20
|
+
backUrl = '/articles',
|
|
21
|
+
showAiSummary = true,
|
|
22
|
+
class: className = ''
|
|
23
|
+
} = Astro.props;
|
|
24
|
+
|
|
25
|
+
// Validate that either uuid or slug is provided
|
|
26
|
+
if (!uuid && !slug) {
|
|
27
|
+
throw new Error('Either uuid or slug must be provided to ContentViewer');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Fetch article
|
|
31
|
+
const client = new ContentGrowthClient({ apiKey, baseUrl });
|
|
32
|
+
const article = slug
|
|
33
|
+
? await client.getArticleBySlug(slug)
|
|
34
|
+
: await client.getArticle(uuid!);
|
|
35
|
+
|
|
36
|
+
// Process markdown content to handle custom image syntax
|
|
37
|
+
// Convert:  to {width="width" height="height"}
|
|
38
|
+
function processImageSyntax(markdown: string): string {
|
|
39
|
+
// Match: 
|
|
40
|
+
return markdown.replace(
|
|
41
|
+
/!\[([^\]]*)\]\(([^\s)]+)\s+=(\d+)x(\d+)\)/g,
|
|
42
|
+
(match, alt, url, width, height) => {
|
|
43
|
+
return `{width="${width}" height="${height}"}`;
|
|
44
|
+
}
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const processedContent = processImageSyntax(article.content);
|
|
49
|
+
|
|
50
|
+
// Configure marked to handle image attributes
|
|
51
|
+
marked.use({
|
|
52
|
+
renderer: {
|
|
53
|
+
image(href, title, text) {
|
|
54
|
+
// Extract width/height from {width="x" height="y"} syntax
|
|
55
|
+
const attrMatch = text.match(/\{width="(\d+)"\s+height="(\d+)"\}/);
|
|
56
|
+
if (attrMatch) {
|
|
57
|
+
const cleanText = text.replace(/\{[^}]+\}/, '').trim();
|
|
58
|
+
return `<img src="${href}" alt="${cleanText}" width="${attrMatch[1]}" height="${attrMatch[2]}" ${title ? `title="${title}"` : ''} />`;
|
|
59
|
+
}
|
|
60
|
+
return `<img src="${href}" alt="${text}" ${title ? `title="${title}"` : ''} />`;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Render markdown to HTML
|
|
66
|
+
const contentHtml = marked(processedContent);
|
|
67
|
+
|
|
68
|
+
// Format metadata
|
|
69
|
+
const publishedDate = formatDate(article.publishedAt);
|
|
70
|
+
const readingTime = calculateReadingTime(article.wordCount);
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
<article
|
|
74
|
+
class={`cg-content-viewer cg-theme-${theme} ${className}`}
|
|
75
|
+
data-cg-widget="post"
|
|
76
|
+
>
|
|
77
|
+
{showBackButton && (
|
|
78
|
+
<div class="cg-post-header">
|
|
79
|
+
<a href={backUrl} class="cg-back-btn">
|
|
80
|
+
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
|
81
|
+
<path d="M12 16L6 10L12 4" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
82
|
+
</svg>
|
|
83
|
+
Back to articles
|
|
84
|
+
</a>
|
|
85
|
+
</div>
|
|
86
|
+
)}
|
|
87
|
+
|
|
88
|
+
<header class="cg-post-meta">
|
|
89
|
+
{article.category && (
|
|
90
|
+
<div class="cg-post-category">
|
|
91
|
+
<span class="cg-category-badge">{article.category}</span>
|
|
92
|
+
</div>
|
|
93
|
+
)}
|
|
94
|
+
|
|
95
|
+
<h1 class="cg-post-title">{article.title}</h1>
|
|
96
|
+
|
|
97
|
+
{showAiSummary && article.summary && (
|
|
98
|
+
<div class="cg-ai-summary">
|
|
99
|
+
<div class="cg-ai-summary-header">
|
|
100
|
+
<svg class="cg-ai-summary-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
101
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
102
|
+
</svg>
|
|
103
|
+
<span class="cg-ai-summary-label">AI Generated Summary</span>
|
|
104
|
+
</div>
|
|
105
|
+
<p class="cg-ai-summary-text">{article.summary}</p>
|
|
106
|
+
</div>
|
|
107
|
+
)}
|
|
108
|
+
|
|
109
|
+
<div class="cg-post-info">
|
|
110
|
+
<span class="cg-info-author">{article.authorName}</span>
|
|
111
|
+
<span class="cg-info-separator">•</span>
|
|
112
|
+
<time class="cg-info-date" datetime={new Date(article.publishedAt * 1000).toISOString()}>
|
|
113
|
+
{publishedDate}
|
|
114
|
+
</time>
|
|
115
|
+
<span class="cg-info-separator">•</span>
|
|
116
|
+
<span class="cg-info-reading-time">{readingTime}</span>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
{article.tags.length > 0 && (
|
|
120
|
+
<div class="cg-post-tags">
|
|
121
|
+
{article.tags.map((tag) => (
|
|
122
|
+
<span class="cg-tag">{tag}</span>
|
|
123
|
+
))}
|
|
124
|
+
</div>
|
|
125
|
+
)}
|
|
126
|
+
</header>
|
|
127
|
+
|
|
128
|
+
<div class="cg-post-content" set:html={contentHtml} />
|
|
129
|
+
</article>
|
|
130
|
+
|
|
131
|
+
<style is:global>
|
|
132
|
+
/* Markdown content styling */
|
|
133
|
+
.cg-post-content {
|
|
134
|
+
line-height: 1.7;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.cg-post-content h1,
|
|
138
|
+
.cg-post-content h2,
|
|
139
|
+
.cg-post-content h3,
|
|
140
|
+
.cg-post-content h4,
|
|
141
|
+
.cg-post-content h5,
|
|
142
|
+
.cg-post-content h6 {
|
|
143
|
+
margin-top: 2em;
|
|
144
|
+
margin-bottom: 0.5em;
|
|
145
|
+
font-weight: 600;
|
|
146
|
+
line-height: 1.3;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.cg-post-content h1 { font-size: 2em; }
|
|
150
|
+
.cg-post-content h2 { font-size: 1.5em; }
|
|
151
|
+
.cg-post-content h3 { font-size: 1.25em; }
|
|
152
|
+
.cg-post-content h4 { font-size: 1.1em; }
|
|
153
|
+
|
|
154
|
+
.cg-post-content p {
|
|
155
|
+
margin-bottom: 1.5em;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.cg-post-content a {
|
|
159
|
+
color: #2563eb;
|
|
160
|
+
text-decoration: underline;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.cg-post-content a:hover {
|
|
164
|
+
color: #1d4ed8;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.cg-post-content ul,
|
|
168
|
+
.cg-post-content ol {
|
|
169
|
+
margin-bottom: 1.5em;
|
|
170
|
+
padding-left: 2em;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.cg-post-content li {
|
|
174
|
+
margin-bottom: 0.5em;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.cg-post-content code {
|
|
178
|
+
background: #f3f4f6;
|
|
179
|
+
padding: 0.2em 0.4em;
|
|
180
|
+
border-radius: 3px;
|
|
181
|
+
font-size: 0.9em;
|
|
182
|
+
font-family: 'Courier New', monospace;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.cg-post-content pre {
|
|
186
|
+
background: #1f2937;
|
|
187
|
+
color: #f9fafb;
|
|
188
|
+
padding: 1.5em;
|
|
189
|
+
border-radius: 8px;
|
|
190
|
+
overflow-x: auto;
|
|
191
|
+
margin-bottom: 1.5em;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.cg-post-content pre code {
|
|
195
|
+
background: none;
|
|
196
|
+
padding: 0;
|
|
197
|
+
color: inherit;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.cg-post-content blockquote {
|
|
201
|
+
border-left: 4px solid #e5e7eb;
|
|
202
|
+
padding-left: 1.5em;
|
|
203
|
+
margin: 1.5em 0;
|
|
204
|
+
font-style: italic;
|
|
205
|
+
color: #6b7280;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.cg-post-content img {
|
|
209
|
+
max-width: 100%;
|
|
210
|
+
height: auto;
|
|
211
|
+
border-radius: 8px;
|
|
212
|
+
margin: 1.5em 0;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.cg-post-content table {
|
|
216
|
+
width: 100%;
|
|
217
|
+
border-collapse: collapse;
|
|
218
|
+
margin-bottom: 1.5em;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.cg-post-content th,
|
|
222
|
+
.cg-post-content td {
|
|
223
|
+
border: 1px solid #e5e7eb;
|
|
224
|
+
padding: 0.75em;
|
|
225
|
+
text-align: left;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.cg-post-content th {
|
|
229
|
+
background: #f9fafb;
|
|
230
|
+
font-weight: 600;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/* Dark theme overrides */
|
|
234
|
+
.cg-theme-dark .cg-post-content code {
|
|
235
|
+
background: #374151;
|
|
236
|
+
color: #f9fafb;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.cg-theme-dark .cg-post-content blockquote {
|
|
240
|
+
border-left-color: #4b5563;
|
|
241
|
+
color: #9ca3af;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.cg-theme-dark .cg-post-content th {
|
|
245
|
+
background: #1f2937;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.cg-theme-dark .cg-post-content th,
|
|
249
|
+
.cg-theme-dark .cg-post-content td {
|
|
250
|
+
border-color: #374151;
|
|
251
|
+
}
|
|
252
|
+
</style>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content Growth Content Widget - Astro Components
|
|
3
|
+
*
|
|
4
|
+
* Note: Astro components are not compiled by TypeScript.
|
|
5
|
+
* They are copied directly to dist/ during the build process.
|
|
6
|
+
* Import them directly from the .astro files in your Astro project.
|
|
7
|
+
*/
|
|
8
|
+
export type { ContentListProps, ContentViewerProps } from '../types/index.js';
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/astro/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,YAAY,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content Growth Content Widget - Astro Components
|
|
3
|
+
*
|
|
4
|
+
* Note: Astro components are not compiled by TypeScript.
|
|
5
|
+
* They are copied directly to dist/ during the build process.
|
|
6
|
+
* Import them directly from the .astro files in your Astro project.
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content Growth API Client
|
|
3
|
+
* Framework-agnostic API client for fetching articles
|
|
4
|
+
*/
|
|
5
|
+
import type { ClientConfig, ListArticlesOptions, ArticlesResponse, ArticleWithContent, CategoriesResponse, TagsResponse } from '../types/index.js';
|
|
6
|
+
/**
|
|
7
|
+
* Content Growth API Client
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* const client = new ContentGrowthClient({
|
|
12
|
+
* apiKey: 'pk_your_key_here'
|
|
13
|
+
* });
|
|
14
|
+
*
|
|
15
|
+
* const { articles, pagination } = await client.listArticles({
|
|
16
|
+
* page: 1,
|
|
17
|
+
* limit: 12,
|
|
18
|
+
* tags: ['tutorial']
|
|
19
|
+
* });
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export declare class ContentGrowthClient {
|
|
23
|
+
private config;
|
|
24
|
+
private cache;
|
|
25
|
+
constructor(config: ClientConfig);
|
|
26
|
+
/**
|
|
27
|
+
* List articles with pagination and filtering
|
|
28
|
+
*/
|
|
29
|
+
listArticles(options?: ListArticlesOptions): Promise<ArticlesResponse>;
|
|
30
|
+
/**
|
|
31
|
+
* Get a single article by UUID
|
|
32
|
+
*/
|
|
33
|
+
getArticle(uuid: string): Promise<ArticleWithContent>;
|
|
34
|
+
/**
|
|
35
|
+
* Get a single article by slug
|
|
36
|
+
*/
|
|
37
|
+
getArticleBySlug(slug: string): Promise<ArticleWithContent>;
|
|
38
|
+
/**
|
|
39
|
+
* Get all categories with article counts
|
|
40
|
+
*/
|
|
41
|
+
getCategories(): Promise<CategoriesResponse>;
|
|
42
|
+
/**
|
|
43
|
+
* Get all tags with article counts
|
|
44
|
+
*/
|
|
45
|
+
getTags(): Promise<TagsResponse>;
|
|
46
|
+
/**
|
|
47
|
+
* Clear the cache
|
|
48
|
+
*/
|
|
49
|
+
clearCache(): void;
|
|
50
|
+
/**
|
|
51
|
+
* Internal fetch wrapper with error handling
|
|
52
|
+
*/
|
|
53
|
+
private fetch;
|
|
54
|
+
/**
|
|
55
|
+
* Get from cache if not expired
|
|
56
|
+
*/
|
|
57
|
+
private getFromCache;
|
|
58
|
+
/**
|
|
59
|
+
* Set cache entry
|
|
60
|
+
*/
|
|
61
|
+
private setCache;
|
|
62
|
+
/**
|
|
63
|
+
* Debug logging
|
|
64
|
+
*/
|
|
65
|
+
private log;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/core/client.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EACV,YAAY,EACZ,mBAAmB,EACnB,gBAAgB,EAChB,kBAAkB,EAClB,kBAAkB,EAClB,YAAY,EAEb,MAAM,mBAAmB,CAAC;AAG3B;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,MAAM,CAAyB;IACvC,OAAO,CAAC,KAAK,CAA+B;gBAEhC,MAAM,EAAE,YAAY;IAmBhC;;OAEG;IACG,YAAY,CAAC,OAAO,GAAE,mBAAwB,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAwChF;;OAEG;IACG,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAmB3D;;OAEG;IACG,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAmBjE;;OAEG;IACG,aAAa,IAAI,OAAO,CAAC,kBAAkB,CAAC;IAelD;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC,YAAY,CAAC;IAetC;;OAEG;IACH,UAAU,IAAI,IAAI;IAKlB;;OAEG;YACW,KAAK;IAoDnB;;OAEG;IACH,OAAO,CAAC,YAAY;IAcpB;;OAEG;IACH,OAAO,CAAC,QAAQ;IAQhB;;OAEG;IACH,OAAO,CAAC,GAAG;CAKZ"}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content Growth API Client
|
|
3
|
+
* Framework-agnostic API client for fetching articles
|
|
4
|
+
*/
|
|
5
|
+
import { ContentGrowthError } from '../types/index.js';
|
|
6
|
+
/**
|
|
7
|
+
* Content Growth API Client
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* const client = new ContentGrowthClient({
|
|
12
|
+
* apiKey: 'pk_your_key_here'
|
|
13
|
+
* });
|
|
14
|
+
*
|
|
15
|
+
* const { articles, pagination } = await client.listArticles({
|
|
16
|
+
* page: 1,
|
|
17
|
+
* limit: 12,
|
|
18
|
+
* tags: ['tutorial']
|
|
19
|
+
* });
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export class ContentGrowthClient {
|
|
23
|
+
config;
|
|
24
|
+
cache;
|
|
25
|
+
constructor(config) {
|
|
26
|
+
this.config = {
|
|
27
|
+
apiKey: config.apiKey,
|
|
28
|
+
baseUrl: config.baseUrl || 'https://api.content-growth.com',
|
|
29
|
+
cacheTTL: config.cacheTTL ?? 300000, // 5 minutes default
|
|
30
|
+
debug: config.debug ?? false
|
|
31
|
+
};
|
|
32
|
+
this.cache = new Map();
|
|
33
|
+
if (!this.config.apiKey) {
|
|
34
|
+
throw new ContentGrowthError('API key is required');
|
|
35
|
+
}
|
|
36
|
+
if (!this.config.apiKey.startsWith('pk_')) {
|
|
37
|
+
console.warn('[ContentGrowth] API key should start with "pk_" for public use');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* List articles with pagination and filtering
|
|
42
|
+
*/
|
|
43
|
+
async listArticles(options = {}) {
|
|
44
|
+
const { page = 1, limit = 12, tags = [], category } = options;
|
|
45
|
+
this.log('[ContentGrowthClient] listArticles called with options:', options);
|
|
46
|
+
// Build query params
|
|
47
|
+
const params = new URLSearchParams({
|
|
48
|
+
page: page.toString(),
|
|
49
|
+
limit: limit.toString()
|
|
50
|
+
});
|
|
51
|
+
if (tags.length > 0) {
|
|
52
|
+
params.set('tags', tags.join(','));
|
|
53
|
+
}
|
|
54
|
+
if (category) {
|
|
55
|
+
params.set('category', category);
|
|
56
|
+
}
|
|
57
|
+
const cacheKey = `articles:${params.toString()}`;
|
|
58
|
+
const cached = this.getFromCache(cacheKey);
|
|
59
|
+
if (cached) {
|
|
60
|
+
this.log('[ContentGrowthClient] Cache hit:', cacheKey);
|
|
61
|
+
return cached;
|
|
62
|
+
}
|
|
63
|
+
const url = `${this.config.baseUrl}/widget/articles?${params}`;
|
|
64
|
+
this.log('[ContentGrowthClient] Fetching from URL:', url);
|
|
65
|
+
const data = await this.fetch(url);
|
|
66
|
+
this.log('[ContentGrowthClient] Response received:', data);
|
|
67
|
+
this.setCache(cacheKey, data);
|
|
68
|
+
return data;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Get a single article by UUID
|
|
72
|
+
*/
|
|
73
|
+
async getArticle(uuid) {
|
|
74
|
+
if (!uuid) {
|
|
75
|
+
throw new ContentGrowthError('Article UUID is required');
|
|
76
|
+
}
|
|
77
|
+
const cacheKey = `article:${uuid}`;
|
|
78
|
+
const cached = this.getFromCache(cacheKey);
|
|
79
|
+
if (cached) {
|
|
80
|
+
this.log('Cache hit:', cacheKey);
|
|
81
|
+
return cached;
|
|
82
|
+
}
|
|
83
|
+
const url = `${this.config.baseUrl}/widget/articles/${uuid}`;
|
|
84
|
+
const data = await this.fetch(url);
|
|
85
|
+
this.setCache(cacheKey, data);
|
|
86
|
+
return data;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Get a single article by slug
|
|
90
|
+
*/
|
|
91
|
+
async getArticleBySlug(slug) {
|
|
92
|
+
if (!slug) {
|
|
93
|
+
throw new ContentGrowthError('Article slug is required');
|
|
94
|
+
}
|
|
95
|
+
const cacheKey = `article:slug:${slug}`;
|
|
96
|
+
const cached = this.getFromCache(cacheKey);
|
|
97
|
+
if (cached) {
|
|
98
|
+
this.log('Cache hit:', cacheKey);
|
|
99
|
+
return cached;
|
|
100
|
+
}
|
|
101
|
+
const url = `${this.config.baseUrl}/widget/articles/slug/${slug}`;
|
|
102
|
+
const data = await this.fetch(url);
|
|
103
|
+
this.setCache(cacheKey, data);
|
|
104
|
+
return data;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Get all categories with article counts
|
|
108
|
+
*/
|
|
109
|
+
async getCategories() {
|
|
110
|
+
const cacheKey = 'categories';
|
|
111
|
+
const cached = this.getFromCache(cacheKey);
|
|
112
|
+
if (cached) {
|
|
113
|
+
this.log('Cache hit:', cacheKey);
|
|
114
|
+
return cached;
|
|
115
|
+
}
|
|
116
|
+
const url = `${this.config.baseUrl}/widget/categories`;
|
|
117
|
+
const data = await this.fetch(url);
|
|
118
|
+
this.setCache(cacheKey, data);
|
|
119
|
+
return data;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Get all tags with article counts
|
|
123
|
+
*/
|
|
124
|
+
async getTags() {
|
|
125
|
+
const cacheKey = 'tags';
|
|
126
|
+
const cached = this.getFromCache(cacheKey);
|
|
127
|
+
if (cached) {
|
|
128
|
+
this.log('Cache hit:', cacheKey);
|
|
129
|
+
return cached;
|
|
130
|
+
}
|
|
131
|
+
const url = `${this.config.baseUrl}/widget/tags`;
|
|
132
|
+
const data = await this.fetch(url);
|
|
133
|
+
this.setCache(cacheKey, data);
|
|
134
|
+
return data;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Clear the cache
|
|
138
|
+
*/
|
|
139
|
+
clearCache() {
|
|
140
|
+
this.cache.clear();
|
|
141
|
+
this.log('Cache cleared');
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Internal fetch wrapper with error handling
|
|
145
|
+
*/
|
|
146
|
+
async fetch(url) {
|
|
147
|
+
console.log('[ContentGrowthClient] Fetching:', url);
|
|
148
|
+
console.log('[ContentGrowthClient] API Key:', this.config.apiKey);
|
|
149
|
+
try {
|
|
150
|
+
const response = await fetch(url, {
|
|
151
|
+
headers: {
|
|
152
|
+
'X-API-Key': this.config.apiKey,
|
|
153
|
+
'Content-Type': 'application/json'
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
console.log('[ContentGrowthClient] Response status:', response.status, response.statusText);
|
|
157
|
+
if (!response.ok) {
|
|
158
|
+
const errorText = await response.text();
|
|
159
|
+
console.error('[ContentGrowthClient] Error response:', errorText);
|
|
160
|
+
let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
|
|
161
|
+
try {
|
|
162
|
+
const errorJson = JSON.parse(errorText);
|
|
163
|
+
errorMessage = errorJson.error || errorJson.message || errorMessage;
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
// Use default error message
|
|
167
|
+
}
|
|
168
|
+
throw new ContentGrowthError(errorMessage, response.status, errorText);
|
|
169
|
+
}
|
|
170
|
+
const data = await response.json();
|
|
171
|
+
console.log('[ContentGrowthClient] Response data:', data);
|
|
172
|
+
return data;
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
if (error instanceof ContentGrowthError) {
|
|
176
|
+
console.error('[ContentGrowthClient] ContentGrowthError:', error);
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
// Network or parsing error
|
|
180
|
+
console.error('[ContentGrowthClient] Network/Parse error:', error);
|
|
181
|
+
throw new ContentGrowthError(`Failed to fetch from Content Growth API: ${error.message}`, undefined, error);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Get from cache if not expired
|
|
186
|
+
*/
|
|
187
|
+
getFromCache(key) {
|
|
188
|
+
const entry = this.cache.get(key);
|
|
189
|
+
if (!entry)
|
|
190
|
+
return null;
|
|
191
|
+
const now = Date.now();
|
|
192
|
+
if (now - entry.timestamp > this.config.cacheTTL) {
|
|
193
|
+
this.cache.delete(key);
|
|
194
|
+
this.log('Cache expired:', key);
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
return entry.data;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Set cache entry
|
|
201
|
+
*/
|
|
202
|
+
setCache(key, data) {
|
|
203
|
+
this.cache.set(key, {
|
|
204
|
+
data,
|
|
205
|
+
timestamp: Date.now()
|
|
206
|
+
});
|
|
207
|
+
this.log('Cache set:', key);
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Debug logging
|
|
211
|
+
*/
|
|
212
|
+
log(...args) {
|
|
213
|
+
if (this.config.debug) {
|
|
214
|
+
console.log('[ContentGrowth]', ...args);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content Growth Content Widget - Core API Client
|
|
3
|
+
* Framework-agnostic API client for fetching articles
|
|
4
|
+
*/
|
|
5
|
+
export { ContentGrowthClient } from './client.js';
|
|
6
|
+
export * from './utils.js';
|
|
7
|
+
export * from '../types/index.js';
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAClD,cAAc,YAAY,CAAC;AAC3B,cAAc,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for the Content Growth widget
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Format a Unix timestamp to a readable date string
|
|
6
|
+
*/
|
|
7
|
+
export declare function formatDate(timestamp: number, locale?: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* Calculate reading time based on word count
|
|
10
|
+
*/
|
|
11
|
+
export declare function calculateReadingTime(wordCount: number, wordsPerMinute?: number): string;
|
|
12
|
+
/**
|
|
13
|
+
* Truncate text to a maximum length
|
|
14
|
+
*/
|
|
15
|
+
export declare function truncate(text: string, maxLength: number): string;
|
|
16
|
+
/**
|
|
17
|
+
* Generate excerpt from content
|
|
18
|
+
*/
|
|
19
|
+
export declare function generateExcerpt(content: string, maxLength?: number): string;
|
|
20
|
+
/**
|
|
21
|
+
* Slugify a string for URLs
|
|
22
|
+
*/
|
|
23
|
+
export declare function slugify(text: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* Parse tags from comma-separated string
|
|
26
|
+
*/
|
|
27
|
+
export declare function parseTags(tags: string | string[]): string[];
|
|
28
|
+
/**
|
|
29
|
+
* Build article URL
|
|
30
|
+
*/
|
|
31
|
+
export declare function buildArticleUrl(uuid: string, pattern?: string): string;
|
|
32
|
+
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/core/utils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;GAEG;AACH,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,GAAE,MAAgB,GAAG,MAAM,CAO9E;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,cAAc,GAAE,MAAY,GAAG,MAAM,CAG5F;AAED;;GAEG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAGhE;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,GAAE,MAAY,GAAG,MAAM,CAahF;AAED;;GAEG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAO5C;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,MAAM,EAAE,CAG3D;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,MAA2B,GAAG,MAAM,CAE1F"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for the Content Growth widget
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Format a Unix timestamp to a readable date string
|
|
6
|
+
*/
|
|
7
|
+
export function formatDate(timestamp, locale = 'en-US') {
|
|
8
|
+
const date = new Date(timestamp * 1000);
|
|
9
|
+
return date.toLocaleDateString(locale, {
|
|
10
|
+
year: 'numeric',
|
|
11
|
+
month: 'long',
|
|
12
|
+
day: 'numeric'
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Calculate reading time based on word count
|
|
17
|
+
*/
|
|
18
|
+
export function calculateReadingTime(wordCount, wordsPerMinute = 200) {
|
|
19
|
+
const minutes = Math.ceil(wordCount / wordsPerMinute);
|
|
20
|
+
return `${minutes} min read`;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Truncate text to a maximum length
|
|
24
|
+
*/
|
|
25
|
+
export function truncate(text, maxLength) {
|
|
26
|
+
if (text.length <= maxLength)
|
|
27
|
+
return text;
|
|
28
|
+
return text.substring(0, maxLength).trim() + '...';
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Generate excerpt from content
|
|
32
|
+
*/
|
|
33
|
+
export function generateExcerpt(content, maxLength = 200) {
|
|
34
|
+
// Remove markdown formatting
|
|
35
|
+
const plainText = content
|
|
36
|
+
.replace(/#{1,6}\s/g, '') // Remove headers
|
|
37
|
+
.replace(/\*\*(.+?)\*\*/g, '$1') // Remove bold
|
|
38
|
+
.replace(/\*(.+?)\*/g, '$1') // Remove italic
|
|
39
|
+
.replace(/\[(.+?)\]\(.+?\)/g, '$1') // Remove links
|
|
40
|
+
.replace(/`(.+?)`/g, '$1') // Remove inline code
|
|
41
|
+
.replace(/```[\s\S]*?```/g, '') // Remove code blocks
|
|
42
|
+
.replace(/\n+/g, ' ') // Replace newlines with spaces
|
|
43
|
+
.trim();
|
|
44
|
+
return truncate(plainText, maxLength);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Slugify a string for URLs
|
|
48
|
+
*/
|
|
49
|
+
export function slugify(text) {
|
|
50
|
+
return text
|
|
51
|
+
.toLowerCase()
|
|
52
|
+
.trim()
|
|
53
|
+
.replace(/[^\w\s-]/g, '')
|
|
54
|
+
.replace(/[\s_-]+/g, '-')
|
|
55
|
+
.replace(/^-+|-+$/g, '');
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Parse tags from comma-separated string
|
|
59
|
+
*/
|
|
60
|
+
export function parseTags(tags) {
|
|
61
|
+
if (Array.isArray(tags))
|
|
62
|
+
return tags;
|
|
63
|
+
return tags.split(',').map(t => t.trim()).filter(Boolean);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Build article URL
|
|
67
|
+
*/
|
|
68
|
+
export function buildArticleUrl(uuid, pattern = '/articles/{uuid}') {
|
|
69
|
+
return pattern.replace('{uuid}', uuid).replace('{id}', uuid);
|
|
70
|
+
}
|