@gosalesdeal/blog 0.2.0 → 0.3.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/README.md +265 -34
- package/dist/client/blogClient.js +36 -7
- package/dist/client/blogClient.js.map +1 -1
- package/dist/components/BlogPost.js +7 -8
- package/dist/components/BlogPost.js.map +1 -1
- package/dist/components/landing/BlogLayoutComponents.d.ts +14 -0
- package/dist/components/landing/BlogLayoutComponents.js +198 -0
- package/dist/components/landing/BlogLayoutComponents.js.map +1 -0
- package/dist/components/landing/FlexibleLayoutComponent.d.ts +40 -0
- package/dist/components/landing/FlexibleLayoutComponent.js +352 -0
- package/dist/components/landing/FlexibleLayoutComponent.js.map +1 -0
- package/dist/components/landing/HeroFeatureBanner.d.ts +36 -0
- package/dist/components/landing/HeroFeatureBanner.js +88 -0
- package/dist/components/landing/HeroFeatureBanner.js.map +1 -0
- package/dist/components/landing/HeroFeatureBannerComponent.d.ts +4 -0
- package/dist/components/landing/HeroFeatureBannerComponent.js +100 -0
- package/dist/components/landing/HeroFeatureBannerComponent.js.map +1 -0
- package/dist/components/landing/HeroFeatureBannerVideo.d.ts +34 -0
- package/dist/components/landing/HeroFeatureBannerVideo.js +261 -0
- package/dist/components/landing/HeroFeatureBannerVideo.js.map +1 -0
- package/dist/components/landing/HeroFeatureBannerVideoComponent.d.ts +5 -0
- package/dist/components/landing/HeroFeatureBannerVideoComponent.js +69 -0
- package/dist/components/landing/HeroFeatureBannerVideoComponent.js.map +1 -0
- package/dist/components/landing/VarietyReviewslistComponent.d.ts +4 -0
- package/dist/components/landing/VarietyReviewslistComponent.js +172 -0
- package/dist/components/landing/VarietyReviewslistComponent.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +21 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/resolveMediaSrc.d.ts +2 -0
- package/dist/lib/resolveMediaSrc.js +21 -0
- package/dist/lib/resolveMediaSrc.js.map +1 -0
- package/dist/render/BlockRenderer.js +5 -0
- package/dist/render/BlockRenderer.js.map +1 -1
- package/dist/render/LandingPageRenderer.d.ts +5 -0
- package/dist/render/LandingPageRenderer.js +50 -0
- package/dist/render/LandingPageRenderer.js.map +1 -0
- package/dist/render/landingPageHtml.d.ts +3 -1
- package/dist/render/landingPageHtml.js +18 -2
- package/dist/render/landingPageHtml.js.map +1 -1
- package/dist/styles.css +13 -0
- package/dist/types/blog.d.ts +5 -2
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# @gosalesdeal/blog
|
|
2
2
|
|
|
3
|
-
Render
|
|
3
|
+
Render GoEnterprise blog content (list + post detail) in any React 18+ / Next.js App Router app, straight from the BlogPosts API. Server-component friendly, typed data client, optional baseline styles, zero UI framework dependencies.
|
|
4
|
+
|
|
5
|
+
**Current version:** `0.2.0`
|
|
4
6
|
|
|
5
7
|
## Install
|
|
6
8
|
|
|
@@ -8,89 +10,318 @@ Render the GoEnterprise blog (list + post detail) in any React 18+ / Next.js App
|
|
|
8
10
|
npm install @gosalesdeal/blog
|
|
9
11
|
```
|
|
10
12
|
|
|
11
|
-
`react`
|
|
13
|
+
`react` is a required peer dependency. `react-dom` is optional (only needed if you render interactive block types in the browser).
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
### 1. Environment variables
|
|
18
|
+
|
|
19
|
+
```env
|
|
20
|
+
BLOG_API_URL=https://api.gosalesdeal.com
|
|
21
|
+
BLOG_TENANT_ID=your-tenant-id
|
|
22
|
+
```
|
|
12
23
|
|
|
13
|
-
|
|
24
|
+
Supported fallbacks: `NEXT_PUBLIC_BLOG_API_URL`, `NEXT_PUBLIC_BLOG_TENANT_ID`.
|
|
14
25
|
|
|
15
|
-
|
|
26
|
+
### 2. Optional global config
|
|
16
27
|
|
|
17
28
|
```ts
|
|
18
|
-
// e.g. instrumentation.ts
|
|
29
|
+
// e.g. instrumentation.ts
|
|
19
30
|
import { configureBlog } from '@gosalesdeal/blog';
|
|
20
31
|
|
|
21
32
|
configureBlog({
|
|
22
|
-
apiBaseUrl: process.env.BLOG_API_URL!,
|
|
23
|
-
tenantId: process.env.BLOG_TENANT_ID!,
|
|
33
|
+
apiBaseUrl: process.env.BLOG_API_URL!,
|
|
34
|
+
tenantId: process.env.BLOG_TENANT_ID!,
|
|
35
|
+
headers: {
|
|
36
|
+
// Avoid bot-middleware blocks on server-side fetch (Next.js SSR/SSG)
|
|
37
|
+
'User-Agent': 'Mozilla/5.0 (compatible; MyApp/1.0)',
|
|
38
|
+
},
|
|
24
39
|
});
|
|
25
40
|
```
|
|
26
41
|
|
|
27
|
-
|
|
42
|
+
Components also accept a `config` prop per call, which overrides these defaults.
|
|
43
|
+
|
|
44
|
+
### 3. Next.js setup
|
|
45
|
+
|
|
46
|
+
Add the package to `transpilePackages` in `next.config`:
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
/** @type {import('next').NextConfig} */
|
|
50
|
+
const nextConfig = {
|
|
51
|
+
transpilePackages: ['@gosalesdeal/blog'],
|
|
52
|
+
};
|
|
53
|
+
export default nextConfig;
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Import styles once in your blog layout:
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
import '@gosalesdeal/blog/styles.css';
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Next.js examples
|
|
63
|
+
|
|
64
|
+
### Blog list
|
|
28
65
|
|
|
29
66
|
```tsx
|
|
30
67
|
// app/blog/page.tsx
|
|
68
|
+
import type { Metadata } from 'next';
|
|
31
69
|
import { BlogList, generateBlogListMetadata } from '@gosalesdeal/blog';
|
|
32
|
-
import '@gosalesdeal/blog/styles.css';
|
|
33
70
|
|
|
34
|
-
|
|
71
|
+
const seo = { siteUrl: 'https://acme.com', siteName: 'Acme', basePath: '/blog' };
|
|
72
|
+
|
|
73
|
+
export async function generateMetadata(): Promise<Metadata> {
|
|
74
|
+
return generateBlogListMetadata({
|
|
75
|
+
...seo,
|
|
76
|
+
title: 'Blog – Acme',
|
|
77
|
+
description: 'Latest news and insights from Acme.',
|
|
78
|
+
});
|
|
79
|
+
}
|
|
35
80
|
|
|
36
|
-
export default async function
|
|
81
|
+
export default async function BlogPage({
|
|
82
|
+
searchParams,
|
|
83
|
+
}: {
|
|
84
|
+
searchParams: Promise<{ page?: string }>;
|
|
85
|
+
}) {
|
|
37
86
|
const { page } = await searchParams;
|
|
38
|
-
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<BlogList
|
|
90
|
+
page={Number(page) || 1}
|
|
91
|
+
basePath="/blog"
|
|
92
|
+
config={{
|
|
93
|
+
apiBaseUrl: process.env.BLOG_API_URL!,
|
|
94
|
+
tenantId: process.env.BLOG_TENANT_ID!,
|
|
95
|
+
}}
|
|
96
|
+
emptyState={<p>No articles published yet.</p>}
|
|
97
|
+
/>
|
|
98
|
+
);
|
|
39
99
|
}
|
|
40
100
|
```
|
|
41
101
|
|
|
102
|
+
### Blog post detail
|
|
103
|
+
|
|
104
|
+
Fetch the post first, then call `notFound()` — do **not** pass `notFound()` as a `fallback` prop (that runs during render).
|
|
105
|
+
|
|
42
106
|
```tsx
|
|
43
107
|
// app/blog/[slug]/page.tsx
|
|
108
|
+
import type { Metadata } from 'next';
|
|
44
109
|
import { notFound } from 'next/navigation';
|
|
45
|
-
import {
|
|
46
|
-
|
|
110
|
+
import {
|
|
111
|
+
BlogPost,
|
|
112
|
+
getPostBySlug,
|
|
113
|
+
getPublishedSlugs,
|
|
114
|
+
generateBlogPostMetadata,
|
|
115
|
+
} from '@gosalesdeal/blog';
|
|
116
|
+
|
|
117
|
+
const seo = { siteUrl: 'https://acme.com', siteName: 'Acme', basePath: '/blog' };
|
|
47
118
|
|
|
48
119
|
export async function generateStaticParams() {
|
|
49
|
-
const slugs = await getPublishedSlugs();
|
|
120
|
+
const slugs = await getPublishedSlugs({ apiBaseUrl: process.env.BLOG_API_URL! });
|
|
50
121
|
return slugs.map((slug) => ({ slug }));
|
|
51
122
|
}
|
|
52
123
|
|
|
53
|
-
export async function generateMetadata({
|
|
124
|
+
export async function generateMetadata({
|
|
125
|
+
params,
|
|
126
|
+
}: {
|
|
127
|
+
params: Promise<{ slug: string }>;
|
|
128
|
+
}): Promise<Metadata> {
|
|
54
129
|
const { slug } = await params;
|
|
55
|
-
const post = await getPostBySlug(slug);
|
|
130
|
+
const post = await getPostBySlug(slug, { apiBaseUrl: process.env.BLOG_API_URL! });
|
|
56
131
|
if (!post) return {};
|
|
57
|
-
return generateBlogPostMetadata(post,
|
|
132
|
+
return generateBlogPostMetadata(post, seo);
|
|
58
133
|
}
|
|
59
134
|
|
|
60
|
-
export default async function
|
|
135
|
+
export default async function BlogPostPage({
|
|
136
|
+
params,
|
|
137
|
+
}: {
|
|
138
|
+
params: Promise<{ slug: string }>;
|
|
139
|
+
}) {
|
|
61
140
|
const { slug } = await params;
|
|
141
|
+
const config = {
|
|
142
|
+
apiBaseUrl: process.env.BLOG_API_URL!,
|
|
143
|
+
tenantId: process.env.BLOG_TENANT_ID!,
|
|
144
|
+
};
|
|
145
|
+
const post = await getPostBySlug(slug, config);
|
|
146
|
+
|
|
147
|
+
if (!post) notFound();
|
|
148
|
+
|
|
62
149
|
return (
|
|
63
150
|
<BlogPost
|
|
64
151
|
slug={slug}
|
|
65
|
-
|
|
66
|
-
|
|
152
|
+
post={post}
|
|
153
|
+
config={config}
|
|
154
|
+
seo={seo}
|
|
155
|
+
trackView
|
|
156
|
+
renderJsonLd
|
|
157
|
+
showTitle
|
|
158
|
+
showFeatured={Boolean(post.featured_image)}
|
|
159
|
+
showMeta
|
|
160
|
+
showExcerpt={Boolean(post.excerpt)}
|
|
67
161
|
/>
|
|
68
162
|
);
|
|
69
163
|
}
|
|
70
164
|
```
|
|
71
165
|
|
|
72
|
-
##
|
|
166
|
+
## Components
|
|
167
|
+
|
|
168
|
+
### `<BlogList />`
|
|
169
|
+
|
|
170
|
+
Async server component. Fetches and renders a paginated grid of post cards.
|
|
171
|
+
|
|
172
|
+
| Prop | Default | Description |
|
|
173
|
+
|------|---------|-------------|
|
|
174
|
+
| `page` | `1` | 1-based page number |
|
|
175
|
+
| `limit` | `12` | Posts per page |
|
|
176
|
+
| `category` | — | Filter by category slug |
|
|
177
|
+
| `basePath` | `'/blog'` | URL prefix for post/pagination links |
|
|
178
|
+
| `config` | — | `{ apiBaseUrl, tenantId, headers, fetch }` |
|
|
179
|
+
| `locale` | `'en-US'` | Date formatting locale |
|
|
180
|
+
| `emptyState` | — | React node when no posts are found |
|
|
181
|
+
|
|
182
|
+
### `<BlogPost />`
|
|
73
183
|
|
|
74
|
-
|
|
75
|
-
- `<BlogList page limit category basePath apiBaseUrl tenantId locale emptyState />` — paginated listing of cards.
|
|
76
|
-
- `<BlogPost slug seo trackView renderJsonLd locale fallback />` — full post with header, block body, and JSON-LD.
|
|
77
|
-
- `<BlockRenderer block />` / `renderBlocks(blocks)` — render raw `ContentBlock`s yourself.
|
|
184
|
+
Async server component. Renders post header, body, and optional JSON-LD.
|
|
78
185
|
|
|
79
|
-
|
|
80
|
-
|
|
186
|
+
| Prop | Default | Description |
|
|
187
|
+
|------|---------|-------------|
|
|
188
|
+
| `slug` | — | Post slug (used to fetch when `post` is omitted) |
|
|
189
|
+
| `post` | — | Pre-fetched post (skips API call) |
|
|
190
|
+
| `config` | — | API config override |
|
|
191
|
+
| `seo` | — | `{ siteUrl, siteName, logoUrl?, basePath? }` for JSON-LD |
|
|
192
|
+
| `trackView` | `true` | Best-effort view increment |
|
|
193
|
+
| `renderJsonLd` | `true` | Emit BlogPosting + breadcrumb JSON-LD |
|
|
194
|
+
| `showTitle` | `true` | Show `<h1>` post title |
|
|
195
|
+
| `showFeatured` | `true` | Show featured image when set |
|
|
196
|
+
| `showMeta` | `true` | Show author, date, reading time |
|
|
197
|
+
| `showExcerpt` | `true` | Show excerpt paragraph |
|
|
198
|
+
| `locale` | `'en-US'` | Date formatting locale |
|
|
199
|
+
| `fallback` | `null` | Rendered when post is not found |
|
|
81
200
|
|
|
82
|
-
|
|
83
|
-
|
|
201
|
+
**Post body rendering order:**
|
|
202
|
+
|
|
203
|
+
1. `content_blocks` — block-based CMS content (17 block types)
|
|
204
|
+
2. `landing_page_html` / page-builder `schema_json` — hero, text, image, columns, etc.
|
|
205
|
+
3. Empty state when neither is available
|
|
206
|
+
|
|
207
|
+
### `<BlogPostHeader />`
|
|
208
|
+
|
|
209
|
+
Composable post header (title, meta, excerpt, featured image). Used internally by `<BlogPost />` but can be imported separately for custom layouts.
|
|
210
|
+
|
|
211
|
+
```tsx
|
|
212
|
+
import { BlogPostHeader } from '@gosalesdeal/blog';
|
|
213
|
+
// or direct subpath import:
|
|
214
|
+
import { BlogPostHeader } from '@gosalesdeal/blog/BlogPostHeader';
|
|
215
|
+
|
|
216
|
+
<BlogPostHeader
|
|
217
|
+
post={post}
|
|
218
|
+
showTitle
|
|
219
|
+
showFeatured={false}
|
|
220
|
+
showMeta
|
|
221
|
+
showExcerpt={Boolean(post.excerpt)}
|
|
222
|
+
/>
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
| Prop | Default | Description |
|
|
226
|
+
|------|---------|-------------|
|
|
227
|
+
| `post` | — | Post fields: `title`, `featured_image`, `excerpt`, `author_name`, `published_at`, `reading_time` |
|
|
228
|
+
| `showTitle` | `true` | Show title |
|
|
229
|
+
| `showFeatured` | `true` | Show featured image |
|
|
230
|
+
| `showMeta` | `true` | Show author / date / reading time |
|
|
231
|
+
| `showExcerpt` | `true` | Show excerpt |
|
|
232
|
+
| `locale` | `'en-US'` | Date formatting locale |
|
|
233
|
+
|
|
234
|
+
### `<BlockRenderer />` / `renderBlocks()`
|
|
235
|
+
|
|
236
|
+
Low-level block rendering if you build your own post layout.
|
|
237
|
+
|
|
238
|
+
## Data client
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
import {
|
|
242
|
+
createBlogClient,
|
|
243
|
+
getPostBySlug,
|
|
244
|
+
getPosts,
|
|
245
|
+
getPostsByCategory,
|
|
246
|
+
getPublishedSlugs,
|
|
247
|
+
getCategories,
|
|
248
|
+
incrementView,
|
|
249
|
+
} from '@gosalesdeal/blog';
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Each standalone function accepts an optional trailing `config` argument.
|
|
253
|
+
|
|
254
|
+
`createBlogClient(config)` returns `{ getPostBySlug, getPosts, getPostsByCategory, getPublishedSlugs, getCategories, incrementView }`.
|
|
255
|
+
|
|
256
|
+
List requests automatically filter to `status=published` and normalize the API `{ list, pagination }` response shape.
|
|
257
|
+
|
|
258
|
+
## Utilities
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
import { formatDate, postHasLandingPageBody } from '@gosalesdeal/blog';
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
- `formatDate(iso, locale?)` — locale-aware date formatting
|
|
265
|
+
- `postHasLandingPageBody(post)` — `true` when the body is rendered from page-builder landing HTML (no `content_blocks`)
|
|
266
|
+
|
|
267
|
+
Useful when you want to adjust layout for landing-page posts, e.g. hide a duplicate hero title:
|
|
268
|
+
|
|
269
|
+
```tsx
|
|
270
|
+
import { postHasLandingPageBody } from '@gosalesdeal/blog';
|
|
271
|
+
|
|
272
|
+
const isLanding = postHasLandingPageBody(post);
|
|
273
|
+
// showTitle={!isLanding} — hide package title if hero already has a headline
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## SEO helpers
|
|
277
|
+
|
|
278
|
+
Framework-neutral plain objects assignable to Next.js `Metadata`:
|
|
279
|
+
|
|
280
|
+
- `generateBlogPostMetadata(post, seo?)`
|
|
281
|
+
- `generateBlogListMetadata(options)`
|
|
282
|
+
- `generateBlogPostJsonLd(post, seo?)`
|
|
283
|
+
- `generateBlogBreadcrumbJsonLd(post, seo?)`
|
|
284
|
+
|
|
285
|
+
`SeoOptions`: `{ siteUrl, siteName, logoUrl?, basePath? }`
|
|
84
286
|
|
|
85
287
|
## Styling
|
|
86
288
|
|
|
87
|
-
|
|
289
|
+
Import the optional baseline stylesheet:
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
import '@gosalesdeal/blog/styles.css';
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
All classes are namespaced under `.ggblog-*`. Theme via CSS variables on `.ggblog-post` / `.ggblog-list`:
|
|
296
|
+
|
|
297
|
+
| Variable | Purpose |
|
|
298
|
+
|----------|---------|
|
|
299
|
+
| `--ggblog-fg` | Primary text |
|
|
300
|
+
| `--ggblog-muted` | Secondary text |
|
|
301
|
+
| `--ggblog-accent` | Links, accents |
|
|
302
|
+
| `--ggblog-border` | Borders |
|
|
303
|
+
| `--ggblog-surface` | Card background |
|
|
304
|
+
| `--ggblog-surface-alt` | Alt surface |
|
|
305
|
+
| `--ggblog-radius` | Border radius |
|
|
306
|
+
| `--ggblog-gap` | Block spacing |
|
|
307
|
+
|
|
308
|
+
You can skip the bundled CSS entirely and style `.ggblog-*` classes yourself.
|
|
309
|
+
|
|
310
|
+
## Package exports
|
|
311
|
+
|
|
312
|
+
| Import | Description |
|
|
313
|
+
|--------|-------------|
|
|
314
|
+
| `@gosalesdeal/blog` | Main entry — components, client, SEO, types |
|
|
315
|
+
| `@gosalesdeal/blog/BlogPostHeader` | Direct subpath for `BlogPostHeader` |
|
|
316
|
+
| `@gosalesdeal/blog/styles.css` | Optional baseline styles |
|
|
88
317
|
|
|
89
318
|
## Scope & notes
|
|
90
319
|
|
|
91
|
-
-
|
|
92
|
-
-
|
|
93
|
-
-
|
|
320
|
+
- **Block posts:** all 17 `content_blocks` types are supported. `accordion` and `tabs` are interactive (`"use client"`); everything else is a server component.
|
|
321
|
+
- **Page-builder posts:** when `content_blocks` is empty, the package renders `landing_page_html` (pre-computed from `schema_json.components`) or generates HTML from the landing schema at render time. Supports hero, text, image, button, columns, spacer, divider, video, and more.
|
|
322
|
+
- **Trusted HTML:** block HTML and landing-page output are treated as trusted CMS content. Sanitize upstream if authors are untrusted.
|
|
323
|
+
- **Bot middleware:** if your API blocks server `fetch` without a browser-like `User-Agent`, pass `headers` in config (see configure example above).
|
|
324
|
+
- **Pre-fetched posts:** pass `post={...}` to `<BlogPost />` to avoid a duplicate API call when you already fetched for metadata or `generateStaticParams`.
|
|
94
325
|
|
|
95
326
|
## License
|
|
96
327
|
|
|
@@ -38,14 +38,40 @@ async function doFetch(path, config, query, init) {
|
|
|
38
38
|
cache: "no-store"
|
|
39
39
|
});
|
|
40
40
|
}
|
|
41
|
-
function
|
|
41
|
+
function resolveFeaturedImageUrl(url, apiBaseUrl) {
|
|
42
|
+
const trimmed = String(url || "").trim();
|
|
43
|
+
if (!trimmed) return void 0;
|
|
44
|
+
if (trimmed.startsWith("data:")) return trimmed;
|
|
45
|
+
const base = apiBaseUrl.replace(/\/$/, "");
|
|
46
|
+
const assetsBases = [
|
|
47
|
+
"https://assets.gosalesdeal.com/images/product",
|
|
48
|
+
"https://assets.gosalesdeal.tech/images/product"
|
|
49
|
+
];
|
|
50
|
+
for (const assetsBase of assetsBases) {
|
|
51
|
+
if (trimmed.startsWith(`${assetsBase}/`)) {
|
|
52
|
+
const relative = trimmed.slice(assetsBase.length + 1);
|
|
53
|
+
return base ? `${base}/uploads/${relative}` : `/uploads/${relative}`;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (/^https?:\/\//i.test(trimmed)) return trimmed;
|
|
57
|
+
if (!base) return trimmed;
|
|
58
|
+
if (trimmed.startsWith("/")) return `${base}${trimmed}`;
|
|
59
|
+
return trimmed;
|
|
60
|
+
}
|
|
61
|
+
function normalizePost(post, apiBaseUrl = "") {
|
|
42
62
|
if (!post || typeof post !== "object") return null;
|
|
43
63
|
if (!("post_id" in post) && !("slug" in post)) return null;
|
|
44
64
|
const blocks = Array.isArray(post.content_blocks) ? post.content_blocks : Array.isArray(post.blocks) ? post.blocks : [];
|
|
45
65
|
const landingSchema = getLandingPageSchema(post);
|
|
46
66
|
const landingPageContent = blocks.length === 0 && landingSchema ? generateHtmlFromSchema(landingSchema) : null;
|
|
67
|
+
const relationUrl = post?.featureImage?.url;
|
|
68
|
+
const featured_image = resolveFeaturedImageUrl(
|
|
69
|
+
relationUrl || post.featured_image,
|
|
70
|
+
apiBaseUrl
|
|
71
|
+
);
|
|
47
72
|
return {
|
|
48
73
|
...post,
|
|
74
|
+
...featured_image ? { featured_image } : {},
|
|
49
75
|
content_blocks: [...blocks].sort(
|
|
50
76
|
(a, b) => (a.display_order ?? 0) - (b.display_order ?? 0)
|
|
51
77
|
),
|
|
@@ -69,7 +95,7 @@ function normalizePagination(raw, itemsLength) {
|
|
|
69
95
|
const hasPrev = raw.hasPrev != null ? Boolean(raw.hasPrev) : page > 1;
|
|
70
96
|
return { total, page, limit, pages, hasNext, hasPrev };
|
|
71
97
|
}
|
|
72
|
-
function normalizeList(body) {
|
|
98
|
+
function normalizeList(body, apiBaseUrl = "") {
|
|
73
99
|
const root = body;
|
|
74
100
|
if (root && typeof root === "object" && typeof root.message === "string" && !("list" in root) && !("items" in root) && !("data" in root)) {
|
|
75
101
|
return { items: [], pagination: EMPTY_PAGINATION };
|
|
@@ -77,14 +103,14 @@ function normalizeList(body) {
|
|
|
77
103
|
const data = root?.data ?? root;
|
|
78
104
|
if (Array.isArray(data)) {
|
|
79
105
|
return {
|
|
80
|
-
items: data.map((p) => normalizePost(p)).filter(Boolean),
|
|
106
|
+
items: data.map((p) => normalizePost(p, apiBaseUrl)).filter(Boolean),
|
|
81
107
|
pagination: { ...EMPTY_PAGINATION, total: data.length, limit: data.length }
|
|
82
108
|
};
|
|
83
109
|
}
|
|
84
110
|
const rawItems = data?.items ?? data?.list ?? [];
|
|
85
111
|
const items = Array.isArray(rawItems) ? rawItems : [];
|
|
86
112
|
return {
|
|
87
|
-
items: items.map((p) => normalizePost(p)).filter(Boolean),
|
|
113
|
+
items: items.map((p) => normalizePost(p, apiBaseUrl)).filter(Boolean),
|
|
88
114
|
pagination: normalizePagination(
|
|
89
115
|
data?.pagination,
|
|
90
116
|
items.length
|
|
@@ -92,13 +118,15 @@ function normalizeList(body) {
|
|
|
92
118
|
};
|
|
93
119
|
}
|
|
94
120
|
async function getPostBySlug(slug, config) {
|
|
121
|
+
const cfg = resolveConfig(config);
|
|
95
122
|
const res = await doFetch(`/blog-posts/slug/${encodeURIComponent(slug)}`, config);
|
|
96
123
|
if (res.status === 404) return null;
|
|
97
124
|
if (!res.ok) throw new Error(`[@gosalesdeal/blog] GET /blog-posts/slug/${slug} -> ${res.status}`);
|
|
98
125
|
const body = await res.json();
|
|
99
|
-
return normalizePost(body?.data ?? body);
|
|
126
|
+
return normalizePost(body?.data ?? body, cfg.apiBaseUrl);
|
|
100
127
|
}
|
|
101
128
|
async function getPosts(params = {}, config) {
|
|
129
|
+
const cfg = resolveConfig(config);
|
|
102
130
|
const res = await doFetch("/blog-posts/list/paginated", config, {
|
|
103
131
|
page: params.page,
|
|
104
132
|
limit: params.limit,
|
|
@@ -109,9 +137,10 @@ async function getPosts(params = {}, config) {
|
|
|
109
137
|
status: "published"
|
|
110
138
|
});
|
|
111
139
|
if (!res.ok) throw new Error(`[@gosalesdeal/blog] GET /blog-posts/list/paginated -> ${res.status}`);
|
|
112
|
-
return normalizeList(await res.json());
|
|
140
|
+
return normalizeList(await res.json(), cfg.apiBaseUrl);
|
|
113
141
|
}
|
|
114
142
|
async function getPostsByCategory(categorySlug, params = {}, config) {
|
|
143
|
+
const cfg = resolveConfig(config);
|
|
115
144
|
const res = await doFetch(`/blog-posts/category/${encodeURIComponent(categorySlug)}`, config, {
|
|
116
145
|
page: params.page,
|
|
117
146
|
limit: params.limit
|
|
@@ -119,7 +148,7 @@ async function getPostsByCategory(categorySlug, params = {}, config) {
|
|
|
119
148
|
if (!res.ok) {
|
|
120
149
|
throw new Error(`[@gosalesdeal/blog] GET /blog-posts/category/${categorySlug} -> ${res.status}`);
|
|
121
150
|
}
|
|
122
|
-
return normalizeList(await res.json());
|
|
151
|
+
return normalizeList(await res.json(), cfg.apiBaseUrl);
|
|
123
152
|
}
|
|
124
153
|
async function getPublishedSlugs(config) {
|
|
125
154
|
const res = await doFetch("/blog-posts/published/slugs", config);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"blogClient.js","sources":["../../src/client/blogClient.ts"],"sourcesContent":["import { resolveConfig, type BlogConfig } from '../config';\nimport { generateHtmlFromSchema, getLandingPageSchema } from '../render/landingPageHtml';\nimport type {\n BlogPost,\n BlogCategory,\n ContentBlock,\n PaginatedResponse,\n PaginationMeta,\n BlogApiResponse,\n} from '../types/blog';\n\nexport interface GetPostsParams {\n page?: number;\n limit?: number;\n category?: string;\n search?: string;\n featured?: boolean;\n}\n\nconst EMPTY_PAGINATION: PaginationMeta = {\n total: 0,\n page: 1,\n limit: 0,\n pages: 0,\n hasNext: false,\n hasPrev: false,\n};\n\nfunction buildUrl(base: string, path: string, query?: Record<string, unknown>): string {\n const url = `${base.replace(/\\/$/, '')}${path}`;\n if (!query) return url;\n const search = new URLSearchParams();\n for (const [key, value] of Object.entries(query)) {\n if (value !== undefined && value !== null && value !== '') {\n search.set(key, String(value));\n }\n }\n const qs = search.toString();\n return qs ? `${url}?${qs}` : url;\n}\n\nasync function doFetch(\n path: string,\n config: Partial<BlogConfig> | undefined,\n query?: Record<string, unknown>,\n init?: RequestInit,\n): Promise<Response> {\n const cfg = resolveConfig(config);\n const fetchFn = cfg.fetch ?? globalThis.fetch;\n if (typeof fetchFn !== 'function') {\n throw new Error('[@gosalesdeal/blog] No fetch implementation available. Provide config.fetch.');\n }\n const headers: Record<string, string> = {\n Accept: 'application/json',\n ...(cfg.headers ?? {}),\n ...((init?.headers as Record<string, string>) ?? {}),\n };\n if (cfg.tenantId) headers['X-Tenant'] = cfg.tenantId;\n return fetchFn(buildUrl(cfg.apiBaseUrl, path, query), {\n ...init,\n headers,\n cache: 'no-store',\n });\n}\n\n/** Sort blocks by display_order and guarantee an array. */\nfunction normalizePost(post: BlogPost | null | undefined): BlogPost | null {\n if (!post || typeof post !== 'object') return null;\n if (!('post_id' in post) && !('slug' in post)) return null;\n\n const blocks: ContentBlock[] = Array.isArray(post.content_blocks)\n ? post.content_blocks\n : Array.isArray(post.blocks)\n ? post.blocks\n : [];\n\n const landingSchema = getLandingPageSchema(post as Parameters<typeof getLandingPageSchema>[0]);\n const landingPageContent =\n blocks.length === 0 && landingSchema ? generateHtmlFromSchema(landingSchema) : null;\n\n return {\n ...post,\n content_blocks: [...blocks].sort(\n (a, b) => (a.display_order ?? 0) - (b.display_order ?? 0),\n ),\n ...(landingPageContent?.html\n ? {\n landing_page_html: landingPageContent.html,\n landing_page_css: landingPageContent.css,\n }\n : {}),\n };\n}\n\n/** Unwrap `{data:{items,pagination}}`, `{items,pagination}`, `{list,pagination}`, or `{data:[...]}`. */\nfunction normalizePagination(\n raw: Record<string, unknown> | undefined,\n itemsLength: number,\n): PaginationMeta {\n if (!raw || typeof raw !== 'object') {\n return { ...EMPTY_PAGINATION, total: itemsLength, limit: itemsLength };\n }\n\n const page = Number(raw.page ?? raw.currentPage ?? 1);\n const limit = Number(raw.limit ?? raw.per_page ?? (itemsLength || 1));\n const total = Number(raw.total ?? raw.totalRecords ?? itemsLength);\n const pages = Number(\n raw.pages ?? raw.totalPages ?? (limit > 0 ? Math.ceil(total / limit) : 1),\n );\n const hasNext = raw.hasNext != null ? Boolean(raw.hasNext) : page < pages;\n const hasPrev = raw.hasPrev != null ? Boolean(raw.hasPrev) : page > 1;\n\n return { total, page, limit, pages, hasNext, hasPrev };\n}\n\nfunction normalizeList(body: unknown): PaginatedResponse<BlogPost> {\n const root = body as BlogApiResponse<unknown> &\n PaginatedResponse<BlogPost> & { list?: BlogPost[] };\n\n // Bot middleware returns 200 with a message and no posts.\n if (\n root &&\n typeof root === 'object' &&\n typeof root.message === 'string' &&\n !('list' in root) &&\n !('items' in root) &&\n !('data' in root)\n ) {\n return { items: [], pagination: EMPTY_PAGINATION };\n }\n\n const data = (root?.data ?? root) as\n | (PaginatedResponse<BlogPost> & { list?: BlogPost[] })\n | BlogPost[]\n | undefined;\n if (Array.isArray(data)) {\n return {\n items: data.map((p) => normalizePost(p)!).filter(Boolean),\n pagination: { ...EMPTY_PAGINATION, total: data.length, limit: data.length },\n };\n }\n const rawItems = data?.items ?? data?.list ?? [];\n const items = Array.isArray(rawItems) ? rawItems : [];\n return {\n items: items.map((p) => normalizePost(p)!).filter(Boolean),\n pagination: normalizePagination(\n data?.pagination as Record<string, unknown> | undefined,\n items.length,\n ),\n };\n}\n\n/** Fetch a single published post by slug. Returns null when not found. */\nexport async function getPostBySlug(\n slug: string,\n config?: Partial<BlogConfig>,\n): Promise<BlogPost | null> {\n const res = await doFetch(`/blog-posts/slug/${encodeURIComponent(slug)}`, config);\n if (res.status === 404) return null;\n if (!res.ok) throw new Error(`[@gosalesdeal/blog] GET /blog-posts/slug/${slug} -> ${res.status}`);\n const body = (await res.json()) as BlogApiResponse<BlogPost>;\n return normalizePost(body?.data ?? (body as unknown as BlogPost));\n}\n\n/** Fetch a paginated list of posts. */\nexport async function getPosts(\n params: GetPostsParams = {},\n config?: Partial<BlogConfig>,\n): Promise<PaginatedResponse<BlogPost>> {\n const res = await doFetch('/blog-posts/list/paginated', config, {\n page: params.page,\n limit: params.limit,\n per_page: params.limit,\n category: params.category,\n search: params.search,\n featured: params.featured,\n status: 'published',\n });\n if (!res.ok) throw new Error(`[@gosalesdeal/blog] GET /blog-posts/list/paginated -> ${res.status}`);\n return normalizeList(await res.json());\n}\n\n/** Fetch posts within a category by its slug. */\nexport async function getPostsByCategory(\n categorySlug: string,\n params: GetPostsParams = {},\n config?: Partial<BlogConfig>,\n): Promise<PaginatedResponse<BlogPost>> {\n const res = await doFetch(`/blog-posts/category/${encodeURIComponent(categorySlug)}`, config, {\n page: params.page,\n limit: params.limit,\n });\n if (!res.ok) {\n throw new Error(`[@gosalesdeal/blog] GET /blog-posts/category/${categorySlug} -> ${res.status}`);\n }\n return normalizeList(await res.json());\n}\n\n/** Slugs of all published posts (for static generation / sitemaps). */\nexport async function getPublishedSlugs(config?: Partial<BlogConfig>): Promise<string[]> {\n const res = await doFetch('/blog-posts/published/slugs', config);\n if (!res.ok) throw new Error(`[@gosalesdeal/blog] GET /blog-posts/published/slugs -> ${res.status}`);\n const body = (await res.json()) as BlogApiResponse<Array<{ slug: string } | string>>;\n const rows = body?.data ?? [];\n return rows.map((r) => (typeof r === 'string' ? r : r.slug)).filter(Boolean);\n}\n\n/** All blog categories. */\nexport async function getCategories(config?: Partial<BlogConfig>): Promise<BlogCategory[]> {\n const res = await doFetch('/blog-posts/categories', config);\n if (!res.ok) throw new Error(`[@gosalesdeal/blog] GET /blog-posts/categories -> ${res.status}`);\n const body = (await res.json()) as BlogApiResponse<BlogCategory[]>;\n return body?.data ?? [];\n}\n\n/** Best-effort view-count increment. Never throws. */\nexport async function incrementView(\n postId: string,\n config?: Partial<BlogConfig>,\n): Promise<void> {\n try {\n await doFetch(`/blog-posts/${encodeURIComponent(postId)}/view`, config, undefined, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ increment: 1 }),\n });\n } catch {\n // view tracking is non-critical\n }\n}\n\nexport interface BlogClient {\n getPostBySlug: (slug: string) => Promise<BlogPost | null>;\n getPosts: (params?: GetPostsParams) => Promise<PaginatedResponse<BlogPost>>;\n getPostsByCategory: (\n categorySlug: string,\n params?: GetPostsParams,\n ) => Promise<PaginatedResponse<BlogPost>>;\n getPublishedSlugs: () => Promise<string[]>;\n getCategories: () => Promise<BlogCategory[]>;\n incrementView: (postId: string) => Promise<void>;\n}\n\n/** Bind all fetchers to a single config. */\nexport function createBlogClient(config: Partial<BlogConfig>): BlogClient {\n return {\n getPostBySlug: (slug) => getPostBySlug(slug, config),\n getPosts: (params) => getPosts(params, config),\n getPostsByCategory: (categorySlug, params) => getPostsByCategory(categorySlug, params, config),\n getPublishedSlugs: () => getPublishedSlugs(config),\n getCategories: () => getCategories(config),\n incrementView: (postId) => incrementView(postId, config),\n };\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"blogClient.js","sources":["../../src/client/blogClient.ts"],"sourcesContent":["import { resolveConfig, type BlogConfig } from '../config';\nimport { generateHtmlFromSchema, getLandingPageSchema } from '../render/landingPageHtml';\nimport type {\n BlogPost,\n BlogCategory,\n ContentBlock,\n PaginatedResponse,\n PaginationMeta,\n BlogApiResponse,\n} from '../types/blog';\n\nexport interface GetPostsParams {\n page?: number;\n limit?: number;\n category?: string;\n search?: string;\n featured?: boolean;\n}\n\nconst EMPTY_PAGINATION: PaginationMeta = {\n total: 0,\n page: 1,\n limit: 0,\n pages: 0,\n hasNext: false,\n hasPrev: false,\n};\n\nfunction buildUrl(base: string, path: string, query?: Record<string, unknown>): string {\n const url = `${base.replace(/\\/$/, '')}${path}`;\n if (!query) return url;\n const search = new URLSearchParams();\n for (const [key, value] of Object.entries(query)) {\n if (value !== undefined && value !== null && value !== '') {\n search.set(key, String(value));\n }\n }\n const qs = search.toString();\n return qs ? `${url}?${qs}` : url;\n}\n\nasync function doFetch(\n path: string,\n config: Partial<BlogConfig> | undefined,\n query?: Record<string, unknown>,\n init?: RequestInit,\n): Promise<Response> {\n const cfg = resolveConfig(config);\n const fetchFn = cfg.fetch ?? globalThis.fetch;\n if (typeof fetchFn !== 'function') {\n throw new Error('[@gosalesdeal/blog] No fetch implementation available. Provide config.fetch.');\n }\n const headers: Record<string, string> = {\n Accept: 'application/json',\n ...(cfg.headers ?? {}),\n ...((init?.headers as Record<string, string>) ?? {}),\n };\n if (cfg.tenantId) headers['X-Tenant'] = cfg.tenantId;\n return fetchFn(buildUrl(cfg.apiBaseUrl, path, query), {\n ...init,\n headers,\n cache: 'no-store',\n });\n}\n\n/** Make API-relative / assets CDN media paths loadable against the blog API origin. */\nfunction resolveFeaturedImageUrl(\n url: string | undefined,\n apiBaseUrl: string,\n): string | undefined {\n const trimmed = String(url || '').trim();\n if (!trimmed) return undefined;\n if (trimmed.startsWith('data:')) return trimmed;\n\n const base = apiBaseUrl.replace(/\\/$/, '');\n const assetsBases = [\n 'https://assets.gosalesdeal.com/images/product',\n 'https://assets.gosalesdeal.tech/images/product',\n ];\n for (const assetsBase of assetsBases) {\n if (trimmed.startsWith(`${assetsBase}/`)) {\n const relative = trimmed.slice(assetsBase.length + 1);\n return base ? `${base}/uploads/${relative}` : `/uploads/${relative}`;\n }\n }\n\n if (/^https?:\\/\\//i.test(trimmed)) return trimmed;\n if (!base) return trimmed;\n if (trimmed.startsWith('/')) return `${base}${trimmed}`;\n return trimmed;\n}\n\n/** Sort blocks by display_order and guarantee an array. */\nfunction normalizePost(\n post: BlogPost | null | undefined,\n apiBaseUrl = '',\n): BlogPost | null {\n if (!post || typeof post !== 'object') return null;\n if (!('post_id' in post) && !('slug' in post)) return null;\n\n const blocks: ContentBlock[] = Array.isArray(post.content_blocks)\n ? post.content_blocks\n : Array.isArray(post.blocks)\n ? post.blocks\n : [];\n\n const landingSchema = getLandingPageSchema(post as Parameters<typeof getLandingPageSchema>[0]);\n const landingPageContent =\n blocks.length === 0 && landingSchema ? generateHtmlFromSchema(landingSchema) : null;\n\n const relationUrl = (post as any)?.featureImage?.url as string | undefined;\n const featured_image = resolveFeaturedImageUrl(\n relationUrl || post.featured_image,\n apiBaseUrl,\n );\n\n return {\n ...post,\n ...(featured_image ? { featured_image } : {}),\n content_blocks: [...blocks].sort(\n (a, b) => (a.display_order ?? 0) - (b.display_order ?? 0),\n ),\n ...(landingPageContent?.html\n ? {\n landing_page_html: landingPageContent.html,\n landing_page_css: landingPageContent.css,\n }\n : {}),\n };\n}\n\n/** Unwrap `{data:{items,pagination}}`, `{items,pagination}`, `{list,pagination}`, or `{data:[...]}`. */\nfunction normalizePagination(\n raw: Record<string, unknown> | undefined,\n itemsLength: number,\n): PaginationMeta {\n if (!raw || typeof raw !== 'object') {\n return { ...EMPTY_PAGINATION, total: itemsLength, limit: itemsLength };\n }\n\n const page = Number(raw.page ?? raw.currentPage ?? 1);\n const limit = Number(raw.limit ?? raw.per_page ?? (itemsLength || 1));\n const total = Number(raw.total ?? raw.totalRecords ?? itemsLength);\n const pages = Number(\n raw.pages ?? raw.totalPages ?? (limit > 0 ? Math.ceil(total / limit) : 1),\n );\n const hasNext = raw.hasNext != null ? Boolean(raw.hasNext) : page < pages;\n const hasPrev = raw.hasPrev != null ? Boolean(raw.hasPrev) : page > 1;\n\n return { total, page, limit, pages, hasNext, hasPrev };\n}\n\nfunction normalizeList(\n body: unknown,\n apiBaseUrl = '',\n): PaginatedResponse<BlogPost> {\n const root = body as BlogApiResponse<unknown> &\n PaginatedResponse<BlogPost> & { list?: BlogPost[] };\n\n // Bot middleware returns 200 with a message and no posts.\n if (\n root &&\n typeof root === 'object' &&\n typeof root.message === 'string' &&\n !('list' in root) &&\n !('items' in root) &&\n !('data' in root)\n ) {\n return { items: [], pagination: EMPTY_PAGINATION };\n }\n\n const data = (root?.data ?? root) as\n | (PaginatedResponse<BlogPost> & { list?: BlogPost[] })\n | BlogPost[]\n | undefined;\n if (Array.isArray(data)) {\n return {\n items: data.map((p) => normalizePost(p, apiBaseUrl)!).filter(Boolean),\n pagination: { ...EMPTY_PAGINATION, total: data.length, limit: data.length },\n };\n }\n const rawItems = data?.items ?? data?.list ?? [];\n const items = Array.isArray(rawItems) ? rawItems : [];\n return {\n items: items.map((p) => normalizePost(p, apiBaseUrl)!).filter(Boolean),\n pagination: normalizePagination(\n data?.pagination as Record<string, unknown> | undefined,\n items.length,\n ),\n };\n}\n\n/** Fetch a single published post by slug. Returns null when not found. */\nexport async function getPostBySlug(\n slug: string,\n config?: Partial<BlogConfig>,\n): Promise<BlogPost | null> {\n const cfg = resolveConfig(config);\n const res = await doFetch(`/blog-posts/slug/${encodeURIComponent(slug)}`, config);\n if (res.status === 404) return null;\n if (!res.ok) throw new Error(`[@gosalesdeal/blog] GET /blog-posts/slug/${slug} -> ${res.status}`);\n const body = (await res.json()) as BlogApiResponse<BlogPost>;\n return normalizePost(body?.data ?? (body as unknown as BlogPost), cfg.apiBaseUrl);\n}\n\n/** Fetch a paginated list of posts. */\nexport async function getPosts(\n params: GetPostsParams = {},\n config?: Partial<BlogConfig>,\n): Promise<PaginatedResponse<BlogPost>> {\n const cfg = resolveConfig(config);\n const res = await doFetch('/blog-posts/list/paginated', config, {\n page: params.page,\n limit: params.limit,\n per_page: params.limit,\n category: params.category,\n search: params.search,\n featured: params.featured,\n status: 'published',\n });\n if (!res.ok) throw new Error(`[@gosalesdeal/blog] GET /blog-posts/list/paginated -> ${res.status}`);\n return normalizeList(await res.json(), cfg.apiBaseUrl);\n}\n\n/** Fetch posts within a category by its slug. */\nexport async function getPostsByCategory(\n categorySlug: string,\n params: GetPostsParams = {},\n config?: Partial<BlogConfig>,\n): Promise<PaginatedResponse<BlogPost>> {\n const cfg = resolveConfig(config);\n const res = await doFetch(`/blog-posts/category/${encodeURIComponent(categorySlug)}`, config, {\n page: params.page,\n limit: params.limit,\n });\n if (!res.ok) {\n throw new Error(`[@gosalesdeal/blog] GET /blog-posts/category/${categorySlug} -> ${res.status}`);\n }\n return normalizeList(await res.json(), cfg.apiBaseUrl);\n}\n\n/** Slugs of all published posts (for static generation / sitemaps). */\nexport async function getPublishedSlugs(config?: Partial<BlogConfig>): Promise<string[]> {\n const res = await doFetch('/blog-posts/published/slugs', config);\n if (!res.ok) throw new Error(`[@gosalesdeal/blog] GET /blog-posts/published/slugs -> ${res.status}`);\n const body = (await res.json()) as BlogApiResponse<Array<{ slug: string } | string>>;\n const rows = body?.data ?? [];\n return rows.map((r) => (typeof r === 'string' ? r : r.slug)).filter(Boolean);\n}\n\n/** All blog categories. */\nexport async function getCategories(config?: Partial<BlogConfig>): Promise<BlogCategory[]> {\n const res = await doFetch('/blog-posts/categories', config);\n if (!res.ok) throw new Error(`[@gosalesdeal/blog] GET /blog-posts/categories -> ${res.status}`);\n const body = (await res.json()) as BlogApiResponse<BlogCategory[]>;\n return body?.data ?? [];\n}\n\n/** Best-effort view-count increment. Never throws. */\nexport async function incrementView(\n postId: string,\n config?: Partial<BlogConfig>,\n): Promise<void> {\n try {\n await doFetch(`/blog-posts/${encodeURIComponent(postId)}/view`, config, undefined, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ increment: 1 }),\n });\n } catch {\n // view tracking is non-critical\n }\n}\n\nexport interface BlogClient {\n getPostBySlug: (slug: string) => Promise<BlogPost | null>;\n getPosts: (params?: GetPostsParams) => Promise<PaginatedResponse<BlogPost>>;\n getPostsByCategory: (\n categorySlug: string,\n params?: GetPostsParams,\n ) => Promise<PaginatedResponse<BlogPost>>;\n getPublishedSlugs: () => Promise<string[]>;\n getCategories: () => Promise<BlogCategory[]>;\n incrementView: (postId: string) => Promise<void>;\n}\n\n/** Bind all fetchers to a single config. */\nexport function createBlogClient(config: Partial<BlogConfig>): BlogClient {\n return {\n getPostBySlug: (slug) => getPostBySlug(slug, config),\n getPosts: (params) => getPosts(params, config),\n getPostsByCategory: (categorySlug, params) => getPostsByCategory(categorySlug, params, config),\n getPublishedSlugs: () => getPublishedSlugs(config),\n getCategories: () => getCategories(config),\n incrementView: (postId) => incrementView(postId, config),\n };\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -4,7 +4,8 @@ import { renderBlocks } from "../render/BlockRenderer.js";
|
|
|
4
4
|
import { generateBlogPostJsonLd, generateBlogBreadcrumbJsonLd } from "../seo/index.js";
|
|
5
5
|
import { JsonLd } from "./JsonLd.js";
|
|
6
6
|
import { BlogPostHeader } from "./BlogPostHeader.js";
|
|
7
|
-
import { getLandingPageSchema
|
|
7
|
+
import { getLandingPageSchema } from "../render/landingPageHtml.js";
|
|
8
|
+
import { LandingPageRenderer } from "../render/LandingPageRenderer.js";
|
|
8
9
|
import { RichText } from "../render/RichText.js";
|
|
9
10
|
function mergeConfig(config, apiBaseUrl, tenantId) {
|
|
10
11
|
return {
|
|
@@ -33,10 +34,8 @@ async function BlogPost({
|
|
|
33
34
|
const post = initialPost ?? await getPostBySlug(slug, cfg);
|
|
34
35
|
if (!post) return /* @__PURE__ */ jsx(Fragment, { children: fallback });
|
|
35
36
|
if (trackView) await incrementView(post.post_id, cfg);
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
return landingSchema ? generateHtmlFromSchema(landingSchema) : null;
|
|
39
|
-
})() : null;
|
|
37
|
+
const landingSchema = post.content_blocks.length === 0 ? getLandingPageSchema(post) : null;
|
|
38
|
+
const landingPageHtml = post.content_blocks.length === 0 && !landingSchema && post.landing_page_html ? { html: post.landing_page_html, css: post.landing_page_css ?? "" } : null;
|
|
40
39
|
return /* @__PURE__ */ jsxs("article", { className: "ggblog-post", children: [
|
|
41
40
|
renderJsonLd ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
42
41
|
/* @__PURE__ */ jsx(JsonLd, { id: "ggblog-post-jsonld", data: generateBlogPostJsonLd(post, seo) }),
|
|
@@ -53,9 +52,9 @@ async function BlogPost({
|
|
|
53
52
|
showExcerpt
|
|
54
53
|
}
|
|
55
54
|
),
|
|
56
|
-
/* @__PURE__ */ jsx("div", { className: "ggblog-post-body", children: post.content_blocks.length > 0 ? renderBlocks(post.content_blocks) :
|
|
57
|
-
|
|
58
|
-
/* @__PURE__ */ jsx(RichText, { html:
|
|
55
|
+
/* @__PURE__ */ jsx("div", { className: "ggblog-post-body", children: post.content_blocks.length > 0 ? renderBlocks(post.content_blocks) : landingSchema ? /* @__PURE__ */ jsx(LandingPageRenderer, { schema: landingSchema }) : landingPageHtml?.html ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
56
|
+
landingPageHtml.css ? /* @__PURE__ */ jsx("style", { dangerouslySetInnerHTML: { __html: landingPageHtml.css } }) : null,
|
|
57
|
+
/* @__PURE__ */ jsx(RichText, { html: landingPageHtml.html, className: "ggblog-landing-page" })
|
|
59
58
|
] }) : null })
|
|
60
59
|
] });
|
|
61
60
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BlogPost.js","sources":["../../src/components/BlogPost.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport type { BlogConfig } from '../config';\nimport { getPostBySlug, incrementView } from '../client/blogClient';\nimport { renderBlocks } from '../render/BlockRenderer';\nimport { generateBlogPostJsonLd, generateBlogBreadcrumbJsonLd, type SeoOptions } from '../seo';\nimport { JsonLd } from './JsonLd';\nimport { BlogPostHeader } from './BlogPostHeader';\nimport type { BlogPost as BlogPostData } from '../types/blog';\nimport {
|
|
1
|
+
{"version":3,"file":"BlogPost.js","sources":["../../src/components/BlogPost.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport type { BlogConfig } from '../config';\nimport { getPostBySlug, incrementView } from '../client/blogClient';\nimport { renderBlocks } from '../render/BlockRenderer';\nimport { generateBlogPostJsonLd, generateBlogBreadcrumbJsonLd, type SeoOptions } from '../seo';\nimport { JsonLd } from './JsonLd';\nimport { BlogPostHeader } from './BlogPostHeader';\nimport type { BlogPost as BlogPostData } from '../types/blog';\nimport { getLandingPageSchema, type LandingPageSchema } from '../render/landingPageHtml';\nimport { LandingPageRenderer } from '../render/LandingPageRenderer';\nimport { RichText } from '../render/RichText';\n\ntype BlogPostWithLanding = BlogPostData & {\n landingPage?: {\n publishedVersion?: {\n schema_json?: LandingPageSchema;\n };\n };\n};\n\nexport interface BlogPostProps {\n /** Slug of the post to render. */\n slug: string;\n /** Pre-fetched post (skips the slug API call when provided). */\n post?: BlogPostData;\n /** API base URL (overrides configured/env default). */\n apiBaseUrl?: string;\n /** Tenant id sent as `X-Tenant`. */\n tenantId?: string;\n /** Full config override (takes precedence over apiBaseUrl/tenantId props). */\n config?: Partial<BlogConfig>;\n /** Fire a best-effort view increment. Default true. */\n trackView?: boolean;\n /** Emit BlogPosting + breadcrumb JSON-LD. Default true. */\n renderJsonLd?: boolean;\n /** SEO context (site origin/name) used for JSON-LD canonical URLs. */\n seo?: SeoOptions;\n /** Locale for date formatting. Default 'en-US'. */\n locale?: string;\n /** Show the post title in the header. Default true. */\n showTitle?: boolean;\n /** Show the featured image when available. Default true. */\n showFeatured?: boolean;\n /** Show author, date, and reading time. Default true. */\n showMeta?: boolean;\n /** Show the excerpt. Default true. */\n showExcerpt?: boolean;\n /** Rendered when the post is not found (consumer typically calls notFound()). */\n fallback?: ReactNode;\n}\n\nfunction mergeConfig(\n config?: Partial<BlogConfig>,\n apiBaseUrl?: string,\n tenantId?: string,\n): Partial<BlogConfig> {\n return {\n ...config,\n ...(apiBaseUrl ? { apiBaseUrl } : {}),\n ...(tenantId ? { tenantId } : {}),\n };\n}\n\n/** Async server component: fetches a post by slug and renders it. */\nexport async function BlogPost({\n slug,\n post: initialPost,\n apiBaseUrl,\n tenantId,\n config,\n trackView = true,\n renderJsonLd = true,\n seo,\n locale = 'en-US',\n showTitle = true,\n showFeatured = true,\n showMeta = true,\n showExcerpt = true,\n fallback = null,\n}: BlogPostProps) {\n const cfg = mergeConfig(config, apiBaseUrl, tenantId);\n const post = initialPost ?? (await getPostBySlug(slug, cfg));\n if (!post) return <>{fallback}</>;\n\n if (trackView) await incrementView(post.post_id, cfg);\n\n const landingSchema =\n post.content_blocks.length === 0\n ? getLandingPageSchema(post as BlogPostWithLanding)\n : null;\n\n const landingPageHtml =\n post.content_blocks.length === 0 && !landingSchema && post.landing_page_html\n ? { html: post.landing_page_html, css: post.landing_page_css ?? '' }\n : null;\n\n return (\n <article className=\"ggblog-post\">\n {renderJsonLd ? (\n <>\n <JsonLd id=\"ggblog-post-jsonld\" data={generateBlogPostJsonLd(post, seo)} />\n <JsonLd id=\"ggblog-breadcrumb-jsonld\" data={generateBlogBreadcrumbJsonLd(post, seo)} />\n </>\n ) : null}\n\n <BlogPostHeader\n post={post}\n locale={locale}\n showTitle={showTitle}\n showFeatured={showFeatured}\n showMeta={showMeta}\n showExcerpt={showExcerpt}\n />\n\n <div className=\"ggblog-post-body\">\n {post.content_blocks.length > 0 ? (\n renderBlocks(post.content_blocks)\n ) : landingSchema ? (\n <LandingPageRenderer schema={landingSchema} />\n ) : landingPageHtml?.html ? (\n <>\n {landingPageHtml.css ? (\n <style dangerouslySetInnerHTML={{ __html: landingPageHtml.css }} />\n ) : null}\n <RichText html={landingPageHtml.html} className=\"ggblog-landing-page\" />\n </>\n ) : null}\n </div>\n </article>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;AAmDA,SAAS,YACP,QACA,YACA,UACqB;AACrB,SAAO;AAAA,IACL,GAAG;AAAA,IACH,GAAI,aAAa,EAAE,WAAA,IAAe,CAAA;AAAA,IAClC,GAAI,WAAW,EAAE,aAAa,CAAA;AAAA,EAAC;AAEnC;AAGA,eAAsB,SAAS;AAAA,EAC7B;AAAA,EACA,MAAM;AAAA,EACN;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ,eAAe;AAAA,EACf;AAAA,EACA,SAAS;AAAA,EACT,YAAY;AAAA,EACZ,eAAe;AAAA,EACf,WAAW;AAAA,EACX,cAAc;AAAA,EACd,WAAW;AACb,GAAkB;AAChB,QAAM,MAAM,YAAY,QAAQ,YAAY,QAAQ;AACpD,QAAM,OAAO,eAAgB,MAAM,cAAc,MAAM,GAAG;AAC1D,MAAI,CAAC,KAAM,QAAO,oBAAA,UAAA,EAAG,UAAA,UAAS;AAE9B,MAAI,UAAW,OAAM,cAAc,KAAK,SAAS,GAAG;AAEpD,QAAM,gBACJ,KAAK,eAAe,WAAW,IAC3B,qBAAqB,IAA2B,IAChD;AAEN,QAAM,kBACJ,KAAK,eAAe,WAAW,KAAK,CAAC,iBAAiB,KAAK,oBACvD,EAAE,MAAM,KAAK,mBAAmB,KAAK,KAAK,oBAAoB,OAC9D;AAEN,SACE,qBAAC,WAAA,EAAQ,WAAU,eAChB,UAAA;AAAA,IAAA,eACC,qBAAA,UAAA,EACE,UAAA;AAAA,MAAA,oBAAC,UAAO,IAAG,sBAAqB,MAAM,uBAAuB,MAAM,GAAG,GAAG;AAAA,MACzE,oBAAC,UAAO,IAAG,4BAA2B,MAAM,6BAA6B,MAAM,GAAG,EAAA,CAAG;AAAA,IAAA,EAAA,CACvF,IACE;AAAA,IAEJ;AAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MAAA;AAAA,IAAA;AAAA,IAGF,oBAAC,SAAI,WAAU,oBACZ,eAAK,eAAe,SAAS,IAC5B,aAAa,KAAK,cAAc,IAC9B,oCACD,qBAAA,EAAoB,QAAQ,eAAe,IAC1C,iBAAiB,OACnB,qBAAA,UAAA,EACG,UAAA;AAAA,MAAA,gBAAgB,0BACd,SAAA,EAAM,yBAAyB,EAAE,QAAQ,gBAAgB,IAAA,EAAI,CAAG,IAC/D;AAAA,0BACH,UAAA,EAAS,MAAM,gBAAgB,MAAM,WAAU,sBAAA,CAAsB;AAAA,IAAA,EAAA,CACxE,IACE,KAAA,CACN;AAAA,EAAA,GACF;AAEJ;"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Blog Layout Components
|
|
4
|
+
* Flexible layout components for arranging blog content in columns
|
|
5
|
+
*/
|
|
6
|
+
export declare const BlogTwoColumnComponent: React.FC<{
|
|
7
|
+
props: Record<string, any>;
|
|
8
|
+
}>;
|
|
9
|
+
export declare const BlogThreeColumnComponent: React.FC<{
|
|
10
|
+
props: Record<string, any>;
|
|
11
|
+
}>;
|
|
12
|
+
export declare const BlogContainerComponent: React.FC<{
|
|
13
|
+
props: Record<string, any>;
|
|
14
|
+
}>;
|