@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.
Files changed (43) hide show
  1. package/README.md +265 -34
  2. package/dist/client/blogClient.js +36 -7
  3. package/dist/client/blogClient.js.map +1 -1
  4. package/dist/components/BlogPost.js +7 -8
  5. package/dist/components/BlogPost.js.map +1 -1
  6. package/dist/components/landing/BlogLayoutComponents.d.ts +14 -0
  7. package/dist/components/landing/BlogLayoutComponents.js +198 -0
  8. package/dist/components/landing/BlogLayoutComponents.js.map +1 -0
  9. package/dist/components/landing/FlexibleLayoutComponent.d.ts +40 -0
  10. package/dist/components/landing/FlexibleLayoutComponent.js +352 -0
  11. package/dist/components/landing/FlexibleLayoutComponent.js.map +1 -0
  12. package/dist/components/landing/HeroFeatureBanner.d.ts +36 -0
  13. package/dist/components/landing/HeroFeatureBanner.js +88 -0
  14. package/dist/components/landing/HeroFeatureBanner.js.map +1 -0
  15. package/dist/components/landing/HeroFeatureBannerComponent.d.ts +4 -0
  16. package/dist/components/landing/HeroFeatureBannerComponent.js +100 -0
  17. package/dist/components/landing/HeroFeatureBannerComponent.js.map +1 -0
  18. package/dist/components/landing/HeroFeatureBannerVideo.d.ts +34 -0
  19. package/dist/components/landing/HeroFeatureBannerVideo.js +261 -0
  20. package/dist/components/landing/HeroFeatureBannerVideo.js.map +1 -0
  21. package/dist/components/landing/HeroFeatureBannerVideoComponent.d.ts +5 -0
  22. package/dist/components/landing/HeroFeatureBannerVideoComponent.js +69 -0
  23. package/dist/components/landing/HeroFeatureBannerVideoComponent.js.map +1 -0
  24. package/dist/components/landing/VarietyReviewslistComponent.d.ts +4 -0
  25. package/dist/components/landing/VarietyReviewslistComponent.js +172 -0
  26. package/dist/components/landing/VarietyReviewslistComponent.js.map +1 -0
  27. package/dist/index.d.ts +11 -0
  28. package/dist/index.js +21 -1
  29. package/dist/index.js.map +1 -1
  30. package/dist/lib/resolveMediaSrc.d.ts +2 -0
  31. package/dist/lib/resolveMediaSrc.js +21 -0
  32. package/dist/lib/resolveMediaSrc.js.map +1 -0
  33. package/dist/render/BlockRenderer.js +5 -0
  34. package/dist/render/BlockRenderer.js.map +1 -1
  35. package/dist/render/LandingPageRenderer.d.ts +5 -0
  36. package/dist/render/LandingPageRenderer.js +50 -0
  37. package/dist/render/LandingPageRenderer.js.map +1 -0
  38. package/dist/render/landingPageHtml.d.ts +3 -1
  39. package/dist/render/landingPageHtml.js +18 -2
  40. package/dist/render/landingPageHtml.js.map +1 -1
  41. package/dist/styles.css +13 -0
  42. package/dist/types/blog.d.ts +5 -2
  43. package/package.json +6 -2
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # @gosalesdeal/blog
2
2
 
3
- Render the GoEnterprise blog (list + post detail) in any React 18+ / Next.js App Router app, straight from the BlogPosts API. Server-component friendly, with a typed data client and zero UI dependencies.
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` (and `react-dom`) are peer dependencies.
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
- ## Configure
24
+ Supported fallbacks: `NEXT_PUBLIC_BLOG_API_URL`, `NEXT_PUBLIC_BLOG_TENANT_ID`.
14
25
 
15
- Components read config from props, then `configureBlog(...)` defaults, then env vars (`BLOG_API_URL` / `NEXT_PUBLIC_BLOG_API_URL`, `BLOG_TENANT_ID` / `NEXT_PUBLIC_BLOG_TENANT_ID`).
26
+ ### 2. Optional global config
16
27
 
17
28
  ```ts
18
- // e.g. instrumentation.ts or a server entry
29
+ // e.g. instrumentation.ts
19
30
  import { configureBlog } from '@gosalesdeal/blog';
20
31
 
21
32
  configureBlog({
22
- apiBaseUrl: process.env.BLOG_API_URL!, // base of the BlogPosts API
23
- tenantId: process.env.BLOG_TENANT_ID!, // sent as the X-Tenant header
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
- ## Next.js App Router usage
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
- export const metadata = generateBlogListMetadata({ siteName: 'Acme' });
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 Page({ searchParams }: { searchParams: Promise<{ page?: string }> }) {
81
+ export default async function BlogPage({
82
+ searchParams,
83
+ }: {
84
+ searchParams: Promise<{ page?: string }>;
85
+ }) {
37
86
  const { page } = await searchParams;
38
- return <BlogList page={Number(page) || 1} basePath="/blog" />;
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 { BlogPost, getPostBySlug, getPublishedSlugs, generateBlogPostMetadata } from '@gosalesdeal/blog';
46
- import '@gosalesdeal/blog/styles.css';
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({ params }: { params: Promise<{ slug: string }> }) {
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, { siteUrl: 'https://acme.com', siteName: 'Acme' });
132
+ return generateBlogPostMetadata(post, seo);
58
133
  }
59
134
 
60
- export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
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
- seo={{ siteUrl: 'https://acme.com', siteName: 'Acme' }}
66
- fallback={notFound()}
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
- ## API
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
- ### Components
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
- ### Data client
80
- `createBlogClient({ apiBaseUrl, tenantId })` → `{ getPostBySlug, getPosts, getPostsByCategory, getPublishedSlugs, getCategories, incrementView }`. The same functions are exported standalone (each accepts an optional trailing `config`).
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
- ### SEO
83
- `generateBlogPostMetadata`, `generateBlogListMetadata`, `generateBlogPostJsonLd`, `generateBlogBreadcrumbJsonLd` — framework-neutral plain objects (assignable to Next's `Metadata`).
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
- The bundled `@gosalesdeal/blog/styles.css` is optional and namespaced under `.ggblog-*`. Theme it by overriding the CSS variables on `.ggblog-post` / `.ggblog-list` (`--ggblog-accent`, `--ggblog-fg`, `--ggblog-border`, …), or skip it and bring your own.
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
- - Renders **block-based** posts (`content_blocks`). All 17 block types are supported; `accordion` and `tabs` are interactive (`"use client"`), everything else is a server component.
92
- - Block HTML (text/embed/accordion/tab content) is treated as **trusted CMS output**. If your authors are untrusted, sanitize upstream.
93
- - The page-builder (`landingPage.schema_json`) rendering path is out of scope.
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 normalizePost(post) {
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":";;AAmBA,MAAM,mBAAmC;AAAA,EACvC,OAAO;AAAA,EACP,MAAM;AAAA,EACN,OAAO;AAAA,EACP,OAAO;AAAA,EACP,SAAS;AAAA,EACT,SAAS;AACX;AAEA,SAAS,SAAS,MAAc,MAAc,OAAyC;AACrF,QAAM,MAAM,GAAG,KAAK,QAAQ,OAAO,EAAE,CAAC,GAAG,IAAI;AAC7C,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,SAAS,IAAI,gBAAA;AACnB,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,KAAK,GAAG;AAChD,QAAI,UAAU,UAAa,UAAU,QAAQ,UAAU,IAAI;AACzD,aAAO,IAAI,KAAK,OAAO,KAAK,CAAC;AAAA,IAC/B;AAAA,EACF;AACA,QAAM,KAAK,OAAO,SAAA;AAClB,SAAO,KAAK,GAAG,GAAG,IAAI,EAAE,KAAK;AAC/B;AAEA,eAAe,QACb,MACA,QACA,OACA,MACmB;AACnB,QAAM,MAAM,cAAc,MAAM;AAChC,QAAM,UAAU,IAAI,SAAS,WAAW;AACxC,MAAI,OAAO,YAAY,YAAY;AACjC,UAAM,IAAI,MAAM,8EAA8E;AAAA,EAChG;AACA,QAAM,UAAkC;AAAA,IACtC,QAAQ;AAAA,IACR,GAAI,IAAI,WAAW,CAAA;AAAA,IACnB,GAAK,MAAM,WAAsC,CAAA;AAAA,EAAC;AAEpD,MAAI,IAAI,SAAU,SAAQ,UAAU,IAAI,IAAI;AAC5C,SAAO,QAAQ,SAAS,IAAI,YAAY,MAAM,KAAK,GAAG;AAAA,IACpD,GAAG;AAAA,IACH;AAAA,IACA,OAAO;AAAA,EAAA,CACR;AACH;AAGA,SAAS,wBACP,KACA,YACoB;AACpB,QAAM,UAAU,OAAO,OAAO,EAAE,EAAE,KAAA;AAClC,MAAI,CAAC,QAAS,QAAO;AACrB,MAAI,QAAQ,WAAW,OAAO,EAAG,QAAO;AAExC,QAAM,OAAO,WAAW,QAAQ,OAAO,EAAE;AACzC,QAAM,cAAc;AAAA,IAClB;AAAA,IACA;AAAA,EAAA;AAEF,aAAW,cAAc,aAAa;AACpC,QAAI,QAAQ,WAAW,GAAG,UAAU,GAAG,GAAG;AACxC,YAAM,WAAW,QAAQ,MAAM,WAAW,SAAS,CAAC;AACpD,aAAO,OAAO,GAAG,IAAI,YAAY,QAAQ,KAAK,YAAY,QAAQ;AAAA,IACpE;AAAA,EACF;AAEA,MAAI,gBAAgB,KAAK,OAAO,EAAG,QAAO;AAC1C,MAAI,CAAC,KAAM,QAAO;AAClB,MAAI,QAAQ,WAAW,GAAG,UAAU,GAAG,IAAI,GAAG,OAAO;AACrD,SAAO;AACT;AAGA,SAAS,cACP,MACA,aAAa,IACI;AACjB,MAAI,CAAC,QAAQ,OAAO,SAAS,SAAU,QAAO;AAC9C,MAAI,EAAE,aAAa,SAAS,EAAE,UAAU,MAAO,QAAO;AAEtD,QAAM,SAAyB,MAAM,QAAQ,KAAK,cAAc,IAC5D,KAAK,iBACL,MAAM,QAAQ,KAAK,MAAM,IACvB,KAAK,SACL,CAAA;AAEN,QAAM,gBAAgB,qBAAqB,IAAkD;AAC7F,QAAM,qBACJ,OAAO,WAAW,KAAK,gBAAgB,uBAAuB,aAAa,IAAI;AAEjF,QAAM,cAAe,MAAc,cAAc;AACjD,QAAM,iBAAiB;AAAA,IACrB,eAAe,KAAK;AAAA,IACpB;AAAA,EAAA;AAGF,SAAO;AAAA,IACL,GAAG;AAAA,IACH,GAAI,iBAAiB,EAAE,eAAA,IAAmB,CAAA;AAAA,IAC1C,gBAAgB,CAAC,GAAG,MAAM,EAAE;AAAA,MAC1B,CAAC,GAAG,OAAO,EAAE,iBAAiB,MAAM,EAAE,iBAAiB;AAAA,IAAA;AAAA,IAEzD,GAAI,oBAAoB,OACpB;AAAA,MACE,mBAAmB,mBAAmB;AAAA,MACtC,kBAAkB,mBAAmB;AAAA,IAAA,IAEvC,CAAA;AAAA,EAAC;AAET;AAGA,SAAS,oBACP,KACA,aACgB;AAChB,MAAI,CAAC,OAAO,OAAO,QAAQ,UAAU;AACnC,WAAO,EAAE,GAAG,kBAAkB,OAAO,aAAa,OAAO,YAAA;AAAA,EAC3D;AAEA,QAAM,OAAO,OAAO,IAAI,QAAQ,IAAI,eAAe,CAAC;AACpD,QAAM,QAAQ,OAAO,IAAI,SAAS,IAAI,aAAa,eAAe,EAAE;AACpE,QAAM,QAAQ,OAAO,IAAI,SAAS,IAAI,gBAAgB,WAAW;AACjE,QAAM,QAAQ;AAAA,IACZ,IAAI,SAAS,IAAI,eAAe,QAAQ,IAAI,KAAK,KAAK,QAAQ,KAAK,IAAI;AAAA,EAAA;AAEzE,QAAM,UAAU,IAAI,WAAW,OAAO,QAAQ,IAAI,OAAO,IAAI,OAAO;AACpE,QAAM,UAAU,IAAI,WAAW,OAAO,QAAQ,IAAI,OAAO,IAAI,OAAO;AAEpE,SAAO,EAAE,OAAO,MAAM,OAAO,OAAO,SAAS,QAAA;AAC/C;AAEA,SAAS,cACP,MACA,aAAa,IACgB;AAC7B,QAAM,OAAO;AAIb,MACE,QACA,OAAO,SAAS,YAChB,OAAO,KAAK,YAAY,YACxB,EAAE,UAAU,SACZ,EAAE,WAAW,SACb,EAAE,UAAU,OACZ;AACA,WAAO,EAAE,OAAO,IAAI,YAAY,iBAAA;AAAA,EAClC;AAEA,QAAM,OAAQ,MAAM,QAAQ;AAI5B,MAAI,MAAM,QAAQ,IAAI,GAAG;AACvB,WAAO;AAAA,MACL,OAAO,KAAK,IAAI,CAAC,MAAM,cAAc,GAAG,UAAU,CAAE,EAAE,OAAO,OAAO;AAAA,MACpE,YAAY,EAAE,GAAG,kBAAkB,OAAO,KAAK,QAAQ,OAAO,KAAK,OAAA;AAAA,IAAO;AAAA,EAE9E;AACA,QAAM,WAAW,MAAM,SAAS,MAAM,QAAQ,CAAA;AAC9C,QAAM,QAAQ,MAAM,QAAQ,QAAQ,IAAI,WAAW,CAAA;AACnD,SAAO;AAAA,IACL,OAAO,MAAM,IAAI,CAAC,MAAM,cAAc,GAAG,UAAU,CAAE,EAAE,OAAO,OAAO;AAAA,IACrE,YAAY;AAAA,MACV,MAAM;AAAA,MACN,MAAM;AAAA,IAAA;AAAA,EACR;AAEJ;AAGA,eAAsB,cACpB,MACA,QAC0B;AAC1B,QAAM,MAAM,cAAc,MAAM;AAChC,QAAM,MAAM,MAAM,QAAQ,oBAAoB,mBAAmB,IAAI,CAAC,IAAI,MAAM;AAChF,MAAI,IAAI,WAAW,IAAK,QAAO;AAC/B,MAAI,CAAC,IAAI,GAAI,OAAM,IAAI,MAAM,4CAA4C,IAAI,OAAO,IAAI,MAAM,EAAE;AAChG,QAAM,OAAQ,MAAM,IAAI,KAAA;AACxB,SAAO,cAAc,MAAM,QAAS,MAA8B,IAAI,UAAU;AAClF;AAGA,eAAsB,SACpB,SAAyB,CAAA,GACzB,QACsC;AACtC,QAAM,MAAM,cAAc,MAAM;AAChC,QAAM,MAAM,MAAM,QAAQ,8BAA8B,QAAQ;AAAA,IAC9D,MAAM,OAAO;AAAA,IACb,OAAO,OAAO;AAAA,IACd,UAAU,OAAO;AAAA,IACjB,UAAU,OAAO;AAAA,IACjB,QAAQ,OAAO;AAAA,IACf,UAAU,OAAO;AAAA,IACjB,QAAQ;AAAA,EAAA,CACT;AACD,MAAI,CAAC,IAAI,GAAI,OAAM,IAAI,MAAM,yDAAyD,IAAI,MAAM,EAAE;AAClG,SAAO,cAAc,MAAM,IAAI,KAAA,GAAQ,IAAI,UAAU;AACvD;AAGA,eAAsB,mBACpB,cACA,SAAyB,CAAA,GACzB,QACsC;AACtC,QAAM,MAAM,cAAc,MAAM;AAChC,QAAM,MAAM,MAAM,QAAQ,wBAAwB,mBAAmB,YAAY,CAAC,IAAI,QAAQ;AAAA,IAC5F,MAAM,OAAO;AAAA,IACb,OAAO,OAAO;AAAA,EAAA,CACf;AACD,MAAI,CAAC,IAAI,IAAI;AACX,UAAM,IAAI,MAAM,gDAAgD,YAAY,OAAO,IAAI,MAAM,EAAE;AAAA,EACjG;AACA,SAAO,cAAc,MAAM,IAAI,KAAA,GAAQ,IAAI,UAAU;AACvD;AAGA,eAAsB,kBAAkB,QAAiD;AACvF,QAAM,MAAM,MAAM,QAAQ,+BAA+B,MAAM;AAC/D,MAAI,CAAC,IAAI,GAAI,OAAM,IAAI,MAAM,0DAA0D,IAAI,MAAM,EAAE;AACnG,QAAM,OAAQ,MAAM,IAAI,KAAA;AACxB,QAAM,OAAO,MAAM,QAAQ,CAAA;AAC3B,SAAO,KAAK,IAAI,CAAC,MAAO,OAAO,MAAM,WAAW,IAAI,EAAE,IAAK,EAAE,OAAO,OAAO;AAC7E;AAGA,eAAsB,cAAc,QAAuD;AACzF,QAAM,MAAM,MAAM,QAAQ,0BAA0B,MAAM;AAC1D,MAAI,CAAC,IAAI,GAAI,OAAM,IAAI,MAAM,qDAAqD,IAAI,MAAM,EAAE;AAC9F,QAAM,OAAQ,MAAM,IAAI,KAAA;AACxB,SAAO,MAAM,QAAQ,CAAA;AACvB;AAGA,eAAsB,cACpB,QACA,QACe;AACf,MAAI;AACF,UAAM,QAAQ,eAAe,mBAAmB,MAAM,CAAC,SAAS,QAAQ,QAAW;AAAA,MACjF,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,mBAAA;AAAA,MAC3B,MAAM,KAAK,UAAU,EAAE,WAAW,GAAG;AAAA,IAAA,CACtC;AAAA,EACH,QAAQ;AAAA,EAER;AACF;AAeO,SAAS,iBAAiB,QAAyC;AACxE,SAAO;AAAA,IACL,eAAe,CAAC,SAAS,cAAc,MAAM,MAAM;AAAA,IACnD,UAAU,CAAC,WAAW,SAAS,QAAQ,MAAM;AAAA,IAC7C,oBAAoB,CAAC,cAAc,WAAW,mBAAmB,cAAc,QAAQ,MAAM;AAAA,IAC7F,mBAAmB,MAAM,kBAAkB,MAAM;AAAA,IACjD,eAAe,MAAM,cAAc,MAAM;AAAA,IACzC,eAAe,CAAC,WAAW,cAAc,QAAQ,MAAM;AAAA,EAAA;AAE3D;"}
@@ -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, generateHtmlFromSchema } from "../render/landingPageHtml.js";
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 landingPage = post.content_blocks.length === 0 && post.landing_page_html ? { html: post.landing_page_html, css: post.landing_page_css ?? "" } : post.content_blocks.length === 0 ? (() => {
37
- const landingSchema = getLandingPageSchema(post);
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) : landingPage?.html ? /* @__PURE__ */ jsxs(Fragment, { children: [
57
- landingPage.css ? /* @__PURE__ */ jsx("style", { dangerouslySetInnerHTML: { __html: landingPage.css } }) : null,
58
- /* @__PURE__ */ jsx(RichText, { html: landingPage.html, className: "ggblog-landing-page" })
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 { generateHtmlFromSchema, getLandingPageSchema, type LandingPageSchema } from '../render/landingPageHtml';\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 landingPage =\n post.content_blocks.length === 0 && post.landing_page_html\n ? { html: post.landing_page_html, css: post.landing_page_css ?? '' }\n : post.content_blocks.length === 0\n ? (() => {\n const landingSchema = getLandingPageSchema(post as BlogPostWithLanding);\n return landingSchema ? generateHtmlFromSchema(landingSchema) : null;\n })()\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 ) : landingPage?.html ? (\n <>\n {landingPage.css ? (\n <style dangerouslySetInnerHTML={{ __html: landingPage.css }} />\n ) : null}\n <RichText html={landingPage.html} className=\"ggblog-landing-page\" />\n </>\n ) : null}\n </div>\n </article>\n );\n}\n"],"names":[],"mappings":";;;;;;;;AAkDA,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,cACJ,KAAK,eAAe,WAAW,KAAK,KAAK,oBACrC,EAAE,MAAM,KAAK,mBAAmB,KAAK,KAAK,oBAAoB,GAAA,IAC9D,KAAK,eAAe,WAAW,KAC5B,MAAM;AACL,UAAM,gBAAgB,qBAAqB,IAA2B;AACtE,WAAO,gBAAgB,uBAAuB,aAAa,IAAI;AAAA,EACjE,OACA;AAER,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,OAAA,EAAI,WAAU,oBACZ,eAAK,eAAe,SAAS,IAC5B,aAAa,KAAK,cAAc,IAC9B,aAAa,OACf,qBAAA,UAAA,EACG,UAAA;AAAA,MAAA,YAAY,0BACV,SAAA,EAAM,yBAAyB,EAAE,QAAQ,YAAY,IAAA,EAAI,CAAG,IAC3D;AAAA,0BACH,UAAA,EAAS,MAAM,YAAY,MAAM,WAAU,sBAAA,CAAsB;AAAA,IAAA,EAAA,CACpE,IACE,KAAA,CACN;AAAA,EAAA,GACF;AAEJ;"}
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
+ }>;