@gosalesdeal/blog 0.1.0 → 0.2.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.
@@ -1,4 +1,5 @@
1
1
  import { resolveConfig } from "../config.js";
2
+ import { getLandingPageSchema, generateHtmlFromSchema } from "../render/landingPageHtml.js";
2
3
  const EMPTY_PAGINATION = {
3
4
  total: 0,
4
5
  page: 1,
@@ -31,18 +32,48 @@ async function doFetch(path, config, query, init) {
31
32
  ...init?.headers ?? {}
32
33
  };
33
34
  if (cfg.tenantId) headers["X-Tenant"] = cfg.tenantId;
34
- return fetchFn(buildUrl(cfg.apiBaseUrl, path, query), { ...init, headers });
35
+ return fetchFn(buildUrl(cfg.apiBaseUrl, path, query), {
36
+ ...init,
37
+ headers,
38
+ cache: "no-store"
39
+ });
35
40
  }
36
41
  function normalizePost(post) {
37
- if (!post) return null;
42
+ if (!post || typeof post !== "object") return null;
43
+ if (!("post_id" in post) && !("slug" in post)) return null;
38
44
  const blocks = Array.isArray(post.content_blocks) ? post.content_blocks : Array.isArray(post.blocks) ? post.blocks : [];
45
+ const landingSchema = getLandingPageSchema(post);
46
+ const landingPageContent = blocks.length === 0 && landingSchema ? generateHtmlFromSchema(landingSchema) : null;
39
47
  return {
40
48
  ...post,
41
- content_blocks: [...blocks].sort((a, b) => (a.display_order ?? 0) - (b.display_order ?? 0))
49
+ content_blocks: [...blocks].sort(
50
+ (a, b) => (a.display_order ?? 0) - (b.display_order ?? 0)
51
+ ),
52
+ ...landingPageContent?.html ? {
53
+ landing_page_html: landingPageContent.html,
54
+ landing_page_css: landingPageContent.css
55
+ } : {}
42
56
  };
43
57
  }
58
+ function normalizePagination(raw, itemsLength) {
59
+ if (!raw || typeof raw !== "object") {
60
+ return { ...EMPTY_PAGINATION, total: itemsLength, limit: itemsLength };
61
+ }
62
+ const page = Number(raw.page ?? raw.currentPage ?? 1);
63
+ const limit = Number(raw.limit ?? raw.per_page ?? (itemsLength || 1));
64
+ const total = Number(raw.total ?? raw.totalRecords ?? itemsLength);
65
+ const pages = Number(
66
+ raw.pages ?? raw.totalPages ?? (limit > 0 ? Math.ceil(total / limit) : 1)
67
+ );
68
+ const hasNext = raw.hasNext != null ? Boolean(raw.hasNext) : page < pages;
69
+ const hasPrev = raw.hasPrev != null ? Boolean(raw.hasPrev) : page > 1;
70
+ return { total, page, limit, pages, hasNext, hasPrev };
71
+ }
44
72
  function normalizeList(body) {
45
73
  const root = body;
74
+ if (root && typeof root === "object" && typeof root.message === "string" && !("list" in root) && !("items" in root) && !("data" in root)) {
75
+ return { items: [], pagination: EMPTY_PAGINATION };
76
+ }
46
77
  const data = root?.data ?? root;
47
78
  if (Array.isArray(data)) {
48
79
  return {
@@ -50,10 +81,14 @@ function normalizeList(body) {
50
81
  pagination: { ...EMPTY_PAGINATION, total: data.length, limit: data.length }
51
82
  };
52
83
  }
53
- const items = Array.isArray(data?.items) ? data.items : [];
84
+ const rawItems = data?.items ?? data?.list ?? [];
85
+ const items = Array.isArray(rawItems) ? rawItems : [];
54
86
  return {
55
87
  items: items.map((p) => normalizePost(p)).filter(Boolean),
56
- pagination: data?.pagination ?? { ...EMPTY_PAGINATION, total: items.length }
88
+ pagination: normalizePagination(
89
+ data?.pagination,
90
+ items.length
91
+ )
57
92
  };
58
93
  }
59
94
  async function getPostBySlug(slug, config) {
@@ -67,9 +102,11 @@ async function getPosts(params = {}, config) {
67
102
  const res = await doFetch("/blog-posts/list/paginated", config, {
68
103
  page: params.page,
69
104
  limit: params.limit,
105
+ per_page: params.limit,
70
106
  category: params.category,
71
107
  search: params.search,
72
- featured: params.featured
108
+ featured: params.featured,
109
+ status: "published"
73
110
  });
74
111
  if (!res.ok) throw new Error(`[@gosalesdeal/blog] GET /blog-posts/list/paginated -> ${res.status}`);
75
112
  return normalizeList(await res.json());
@@ -1 +1 @@
1
- {"version":3,"file":"blogClient.js","sources":["../../src/client/blogClient.ts"],"sourcesContent":["import { resolveConfig, type BlogConfig } from '../config';\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), { ...init, headers });\n}\n\n/** Sort blocks by display_order and guarantee an array. */\nfunction normalizePost(post: BlogPost | null | undefined): BlogPost | null {\n if (!post) return null;\n const blocks: ContentBlock[] = Array.isArray(post.content_blocks)\n ? post.content_blocks\n : Array.isArray(post.blocks)\n ? post.blocks\n : [];\n return {\n ...post,\n content_blocks: [...blocks].sort((a, b) => (a.display_order ?? 0) - (b.display_order ?? 0)),\n };\n}\n\n/** Unwrap `{data:{items,pagination}}`, `{items,pagination}`, or `{data:[...]}`. */\nfunction normalizeList(body: unknown): PaginatedResponse<BlogPost> {\n const root = body as BlogApiResponse<unknown> & PaginatedResponse<BlogPost>;\n const data = (root?.data ?? root) as\n | PaginatedResponse<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 items = Array.isArray(data?.items) ? data!.items : [];\n return {\n items: items.map((p) => normalizePost(p)!).filter(Boolean),\n pagination: data?.pagination ?? { ...EMPTY_PAGINATION, total: items.length },\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 category: params.category,\n search: params.search,\n featured: params.featured,\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/** 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,9 +1,12 @@
1
1
  import { ReactNode } from 'react';
2
2
  import { BlogConfig } from '../config';
3
3
  import { SeoOptions } from '../seo';
4
+ import { BlogPost as BlogPostData } from '../types/blog';
4
5
  export interface BlogPostProps {
5
6
  /** Slug of the post to render. */
6
7
  slug: string;
8
+ /** Pre-fetched post (skips the slug API call when provided). */
9
+ post?: BlogPostData;
7
10
  /** API base URL (overrides configured/env default). */
8
11
  apiBaseUrl?: string;
9
12
  /** Tenant id sent as `X-Tenant`. */
@@ -18,8 +21,16 @@ export interface BlogPostProps {
18
21
  seo?: SeoOptions;
19
22
  /** Locale for date formatting. Default 'en-US'. */
20
23
  locale?: string;
24
+ /** Show the post title in the header. Default true. */
25
+ showTitle?: boolean;
26
+ /** Show the featured image when available. Default true. */
27
+ showFeatured?: boolean;
28
+ /** Show author, date, and reading time. Default true. */
29
+ showMeta?: boolean;
30
+ /** Show the excerpt. Default true. */
31
+ showExcerpt?: boolean;
21
32
  /** Rendered when the post is not found (consumer typically calls notFound()). */
22
33
  fallback?: ReactNode;
23
34
  }
24
35
  /** Async server component: fetches a post by slug and renders it. */
25
- export declare function BlogPost({ slug, apiBaseUrl, tenantId, config, trackView, renderJsonLd, seo, locale, fallback, }: BlogPostProps): Promise<import("react").JSX.Element>;
36
+ export declare function BlogPost({ slug, post: initialPost, apiBaseUrl, tenantId, config, trackView, renderJsonLd, seo, locale, showTitle, showFeatured, showMeta, showExcerpt, fallback, }: BlogPostProps): Promise<import("react").JSX.Element>;
@@ -1,9 +1,11 @@
1
1
  import { jsx, Fragment, jsxs } from "react/jsx-runtime";
2
2
  import { getPostBySlug, incrementView } from "../client/blogClient.js";
3
3
  import { renderBlocks } from "../render/BlockRenderer.js";
4
- import { formatDate } from "../lib/format.js";
5
4
  import { generateBlogPostJsonLd, generateBlogBreadcrumbJsonLd } from "../seo/index.js";
6
5
  import { JsonLd } from "./JsonLd.js";
6
+ import { BlogPostHeader } from "./BlogPostHeader.js";
7
+ import { getLandingPageSchema, generateHtmlFromSchema } from "../render/landingPageHtml.js";
8
+ import { RichText } from "../render/RichText.js";
7
9
  function mergeConfig(config, apiBaseUrl, tenantId) {
8
10
  return {
9
11
  ...config,
@@ -13,6 +15,7 @@ function mergeConfig(config, apiBaseUrl, tenantId) {
13
15
  }
14
16
  async function BlogPost({
15
17
  slug,
18
+ post: initialPost,
16
19
  apiBaseUrl,
17
20
  tenantId,
18
21
  config,
@@ -20,31 +23,40 @@ async function BlogPost({
20
23
  renderJsonLd = true,
21
24
  seo,
22
25
  locale = "en-US",
26
+ showTitle = true,
27
+ showFeatured = true,
28
+ showMeta = true,
29
+ showExcerpt = true,
23
30
  fallback = null
24
31
  }) {
25
32
  const cfg = mergeConfig(config, apiBaseUrl, tenantId);
26
- const post = await getPostBySlug(slug, cfg);
33
+ const post = initialPost ?? await getPostBySlug(slug, cfg);
27
34
  if (!post) return /* @__PURE__ */ jsx(Fragment, { children: fallback });
28
35
  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;
29
40
  return /* @__PURE__ */ jsxs("article", { className: "ggblog-post", children: [
30
41
  renderJsonLd ? /* @__PURE__ */ jsxs(Fragment, { children: [
31
42
  /* @__PURE__ */ jsx(JsonLd, { id: "ggblog-post-jsonld", data: generateBlogPostJsonLd(post, seo) }),
32
43
  /* @__PURE__ */ jsx(JsonLd, { id: "ggblog-breadcrumb-jsonld", data: generateBlogBreadcrumbJsonLd(post, seo) })
33
44
  ] }) : null,
34
- /* @__PURE__ */ jsxs("header", { className: "ggblog-post-header", children: [
35
- /* @__PURE__ */ jsx("h1", { className: "ggblog-post-title", children: post.title }),
36
- /* @__PURE__ */ jsxs("div", { className: "ggblog-post-meta", children: [
37
- post.author_name ? /* @__PURE__ */ jsx("span", { className: "ggblog-post-author", children: post.author_name }) : null,
38
- post.published_at ? /* @__PURE__ */ jsx("time", { dateTime: post.published_at, children: formatDate(post.published_at, locale) }) : null,
39
- post.reading_time ? /* @__PURE__ */ jsxs("span", { className: "ggblog-post-readtime", children: [
40
- post.reading_time,
41
- " min read"
42
- ] }) : null
43
- ] }),
44
- post.excerpt ? /* @__PURE__ */ jsx("p", { className: "ggblog-post-excerpt", children: post.excerpt }) : null,
45
- post.featured_image ? /* @__PURE__ */ jsx("figure", { className: "ggblog-post-cover", children: /* @__PURE__ */ jsx("img", { src: post.featured_image, alt: post.featured_image_alt ?? post.title }) }) : null
46
- ] }),
47
- /* @__PURE__ */ jsx("div", { className: "ggblog-post-body", children: renderBlocks(post.content_blocks) })
45
+ /* @__PURE__ */ jsx(
46
+ BlogPostHeader,
47
+ {
48
+ post,
49
+ locale,
50
+ showTitle,
51
+ showFeatured,
52
+ showMeta,
53
+ showExcerpt
54
+ }
55
+ ),
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" })
59
+ ] }) : null })
48
60
  ] });
49
61
  }
50
62
  export {
@@ -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 { formatDate } from '../lib/format';\nimport { generateBlogPostJsonLd, generateBlogBreadcrumbJsonLd, type SeoOptions } from '../seo';\nimport { JsonLd } from './JsonLd';\n\nexport interface BlogPostProps {\n /** Slug of the post to render. */\n slug: string;\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 /** 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 apiBaseUrl,\n tenantId,\n config,\n trackView = true,\n renderJsonLd = true,\n seo,\n locale = 'en-US',\n fallback = null,\n}: BlogPostProps) {\n const cfg = mergeConfig(config, apiBaseUrl, tenantId);\n const post = await getPostBySlug(slug, cfg);\n if (!post) return <>{fallback}</>;\n\n if (trackView) await incrementView(post.post_id, cfg);\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 <header className=\"ggblog-post-header\">\n <h1 className=\"ggblog-post-title\">{post.title}</h1>\n <div className=\"ggblog-post-meta\">\n {post.author_name ? <span className=\"ggblog-post-author\">{post.author_name}</span> : null}\n {post.published_at ? (\n <time dateTime={post.published_at}>{formatDate(post.published_at, locale)}</time>\n ) : null}\n {post.reading_time ? (\n <span className=\"ggblog-post-readtime\">{post.reading_time} min read</span>\n ) : null}\n </div>\n {post.excerpt ? <p className=\"ggblog-post-excerpt\">{post.excerpt}</p> : null}\n {post.featured_image ? (\n <figure className=\"ggblog-post-cover\">\n {/* eslint-disable-next-line @next/next/no-img-element */}\n <img src={post.featured_image} alt={post.featured_image_alt ?? post.title} />\n </figure>\n ) : null}\n </header>\n\n <div className=\"ggblog-post-body\">{renderBlocks(post.content_blocks)}</div>\n </article>\n );\n}\n"],"names":[],"mappings":";;;;;;AA6BA,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;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ,eAAe;AAAA,EACf;AAAA,EACA,SAAS;AAAA,EACT,WAAW;AACb,GAAkB;AAChB,QAAM,MAAM,YAAY,QAAQ,YAAY,QAAQ;AACpD,QAAM,OAAO,MAAM,cAAc,MAAM,GAAG;AAC1C,MAAI,CAAC,KAAM,QAAO,oBAAA,UAAA,EAAG,UAAA,UAAS;AAE9B,MAAI,UAAW,OAAM,cAAc,KAAK,SAAS,GAAG;AAEpD,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,qBAAC,UAAA,EAAO,WAAU,sBAChB,UAAA;AAAA,MAAA,oBAAC,MAAA,EAAG,WAAU,qBAAqB,UAAA,KAAK,OAAM;AAAA,MAC9C,qBAAC,OAAA,EAAI,WAAU,oBACZ,UAAA;AAAA,QAAA,KAAK,cAAc,oBAAC,QAAA,EAAK,WAAU,sBAAsB,UAAA,KAAK,aAAY,IAAU;AAAA,QACpF,KAAK,eACJ,oBAAC,QAAA,EAAK,UAAU,KAAK,cAAe,UAAA,WAAW,KAAK,cAAc,MAAM,EAAA,CAAE,IACxE;AAAA,QACH,KAAK,eACJ,qBAAC,QAAA,EAAK,WAAU,wBAAwB,UAAA;AAAA,UAAA,KAAK;AAAA,UAAa;AAAA,QAAA,EAAA,CAAS,IACjE;AAAA,MAAA,GACN;AAAA,MACC,KAAK,UAAU,oBAAC,KAAA,EAAE,WAAU,uBAAuB,UAAA,KAAK,SAAQ,IAAO;AAAA,MACvE,KAAK,iBACJ,oBAAC,UAAA,EAAO,WAAU,qBAEhB,UAAA,oBAAC,OAAA,EAAI,KAAK,KAAK,gBAAgB,KAAK,KAAK,sBAAsB,KAAK,OAAO,GAC7E,IACE;AAAA,IAAA,GACN;AAAA,wBAEC,OAAA,EAAI,WAAU,oBAAoB,UAAA,aAAa,KAAK,cAAc,EAAA,CAAE;AAAA,EAAA,GACvE;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 { 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;"}
@@ -0,0 +1,15 @@
1
+ import { BlogPost } from '../types/blog';
2
+ export interface BlogPostHeaderProps {
3
+ post: Pick<BlogPost, 'title' | 'featured_image' | 'featured_image_alt' | 'excerpt' | 'author_name' | 'published_at' | 'reading_time'>;
4
+ /** Show the post title. Default true. */
5
+ showTitle?: boolean;
6
+ /** Show the featured image when `post.featured_image` is set. Default true. */
7
+ showFeatured?: boolean;
8
+ /** Show author, date, and reading time. Default true. */
9
+ showMeta?: boolean;
10
+ /** Show the excerpt paragraph. Default true. */
11
+ showExcerpt?: boolean;
12
+ locale?: string;
13
+ }
14
+ /** Optional post header: title, meta, excerpt, and featured image. */
15
+ export declare function BlogPostHeader({ post, showTitle, showFeatured, showMeta, showExcerpt, locale, }: BlogPostHeaderProps): import("react").JSX.Element | null;
@@ -0,0 +1,41 @@
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
+ import { formatDate } from "../lib/format.js";
3
+ function BlogPostHeader({
4
+ post,
5
+ showTitle = true,
6
+ showFeatured = true,
7
+ showMeta = true,
8
+ showExcerpt = true,
9
+ locale = "en-US"
10
+ }) {
11
+ const hasTitle = showTitle && Boolean(post.title);
12
+ const hasFeatured = showFeatured && Boolean(post.featured_image);
13
+ const hasMeta = showMeta && Boolean(post.author_name || post.published_at || post.reading_time);
14
+ const hasExcerpt = showExcerpt && Boolean(post.excerpt);
15
+ if (!hasTitle && !hasFeatured && !hasMeta && !hasExcerpt) {
16
+ return null;
17
+ }
18
+ return /* @__PURE__ */ jsxs("header", { className: "ggblog-post-header", children: [
19
+ hasTitle ? /* @__PURE__ */ jsx("h1", { className: "ggblog-post-title", children: post.title }) : null,
20
+ hasMeta ? /* @__PURE__ */ jsxs("div", { className: "ggblog-post-meta", children: [
21
+ post.author_name ? /* @__PURE__ */ jsx("span", { className: "ggblog-post-author", children: post.author_name }) : null,
22
+ post.published_at ? /* @__PURE__ */ jsx("time", { dateTime: post.published_at, children: formatDate(post.published_at, locale) }) : null,
23
+ post.reading_time ? /* @__PURE__ */ jsxs("span", { className: "ggblog-post-readtime", children: [
24
+ post.reading_time,
25
+ " min read"
26
+ ] }) : null
27
+ ] }) : null,
28
+ hasExcerpt ? /* @__PURE__ */ jsx("p", { className: "ggblog-post-excerpt", children: post.excerpt }) : null,
29
+ hasFeatured ? /* @__PURE__ */ jsx("figure", { className: "ggblog-post-cover", children: /* @__PURE__ */ jsx(
30
+ "img",
31
+ {
32
+ src: post.featured_image,
33
+ alt: post.featured_image_alt ?? post.title
34
+ }
35
+ ) }) : null
36
+ ] });
37
+ }
38
+ export {
39
+ BlogPostHeader
40
+ };
41
+ //# sourceMappingURL=BlogPostHeader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BlogPostHeader.js","sources":["../../src/components/BlogPostHeader.tsx"],"sourcesContent":["import type { BlogPost } from '../types/blog';\nimport { formatDate } from '../lib/format';\n\nexport interface BlogPostHeaderProps {\n post: Pick<\n BlogPost,\n | 'title'\n | 'featured_image'\n | 'featured_image_alt'\n | 'excerpt'\n | 'author_name'\n | 'published_at'\n | 'reading_time'\n >;\n /** Show the post title. Default true. */\n showTitle?: boolean;\n /** Show the featured image when `post.featured_image` is set. Default true. */\n showFeatured?: boolean;\n /** Show author, date, and reading time. Default true. */\n showMeta?: boolean;\n /** Show the excerpt paragraph. Default true. */\n showExcerpt?: boolean;\n locale?: string;\n}\n\n/** Optional post header: title, meta, excerpt, and featured image. */\nexport function BlogPostHeader({\n post,\n showTitle = true,\n showFeatured = true,\n showMeta = true,\n showExcerpt = true,\n locale = 'en-US',\n}: BlogPostHeaderProps) {\n const hasTitle = showTitle && Boolean(post.title);\n const hasFeatured = showFeatured && Boolean(post.featured_image);\n const hasMeta =\n showMeta &&\n Boolean(post.author_name || post.published_at || post.reading_time);\n const hasExcerpt = showExcerpt && Boolean(post.excerpt);\n\n if (!hasTitle && !hasFeatured && !hasMeta && !hasExcerpt) {\n return null;\n }\n\n return (\n <header className=\"ggblog-post-header\">\n {hasTitle ? <h1 className=\"ggblog-post-title\">{post.title}</h1> : null}\n {hasMeta ? (\n <div className=\"ggblog-post-meta\">\n {post.author_name ? (\n <span className=\"ggblog-post-author\">{post.author_name}</span>\n ) : null}\n {post.published_at ? (\n <time dateTime={post.published_at}>{formatDate(post.published_at, locale)}</time>\n ) : null}\n {post.reading_time ? (\n <span className=\"ggblog-post-readtime\">{post.reading_time} min read</span>\n ) : null}\n </div>\n ) : null}\n {hasExcerpt ? <p className=\"ggblog-post-excerpt\">{post.excerpt}</p> : null}\n {hasFeatured ? (\n <figure className=\"ggblog-post-cover\">\n {/* eslint-disable-next-line @next/next/no-img-element */}\n <img\n src={post.featured_image}\n alt={post.featured_image_alt ?? post.title}\n />\n </figure>\n ) : null}\n </header>\n );\n}\n"],"names":[],"mappings":";;AA0BO,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA,YAAY;AAAA,EACZ,eAAe;AAAA,EACf,WAAW;AAAA,EACX,cAAc;AAAA,EACd,SAAS;AACX,GAAwB;AACtB,QAAM,WAAW,aAAa,QAAQ,KAAK,KAAK;AAChD,QAAM,cAAc,gBAAgB,QAAQ,KAAK,cAAc;AAC/D,QAAM,UACJ,YACA,QAAQ,KAAK,eAAe,KAAK,gBAAgB,KAAK,YAAY;AACpE,QAAM,aAAa,eAAe,QAAQ,KAAK,OAAO;AAEtD,MAAI,CAAC,YAAY,CAAC,eAAe,CAAC,WAAW,CAAC,YAAY;AACxD,WAAO;AAAA,EACT;AAEA,SACE,qBAAC,UAAA,EAAO,WAAU,sBACf,UAAA;AAAA,IAAA,+BAAY,MAAA,EAAG,WAAU,qBAAqB,UAAA,KAAK,OAAM,IAAQ;AAAA,IACjE,UACC,qBAAC,OAAA,EAAI,WAAU,oBACZ,UAAA;AAAA,MAAA,KAAK,cACJ,oBAAC,QAAA,EAAK,WAAU,sBAAsB,UAAA,KAAK,aAAY,IACrD;AAAA,MACH,KAAK,eACJ,oBAAC,QAAA,EAAK,UAAU,KAAK,cAAe,UAAA,WAAW,KAAK,cAAc,MAAM,EAAA,CAAE,IACxE;AAAA,MACH,KAAK,eACJ,qBAAC,QAAA,EAAK,WAAU,wBAAwB,UAAA;AAAA,QAAA,KAAK;AAAA,QAAa;AAAA,MAAA,EAAA,CAAS,IACjE;AAAA,IAAA,EAAA,CACN,IACE;AAAA,IACH,aAAa,oBAAC,KAAA,EAAE,WAAU,uBAAuB,UAAA,KAAK,SAAQ,IAAO;AAAA,IACrE,cACC,oBAAC,UAAA,EAAO,WAAU,qBAEhB,UAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,KAAK,KAAK;AAAA,QACV,KAAK,KAAK,sBAAsB,KAAK;AAAA,MAAA;AAAA,IAAA,GAEzC,IACE;AAAA,EAAA,GACN;AAEJ;"}
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { BlogPost, type BlogPostProps } from './components/BlogPost';
2
+ export { BlogPostHeader, type BlogPostHeaderProps } from './components/BlogPostHeader';
2
3
  export { BlogList, type BlogListProps } from './components/BlogList';
3
4
  export { JsonLd } from './components/JsonLd';
4
5
  export { BlockRenderer, renderBlocks, type BlockRendererProps } from './render/BlockRenderer';
@@ -6,4 +7,5 @@ export { configureBlog, getDefaultConfig, resolveConfig, type BlogConfig } from
6
7
  export { createBlogClient, getPostBySlug, getPosts, getPostsByCategory, getPublishedSlugs, getCategories, incrementView, type BlogClient, type GetPostsParams, } from './client/blogClient';
7
8
  export { generateBlogPostMetadata, generateBlogListMetadata, generateBlogPostJsonLd, generateBlogBreadcrumbJsonLd, type BlogMetadata, type SeoOptions, } from './seo';
8
9
  export { formatDate } from './lib/format';
10
+ export { postHasLandingPageBody } from './lib/postUtils';
9
11
  export type * from './types/blog';
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { BlogPost } from "./components/BlogPost.js";
2
+ import { BlogPostHeader } from "./components/BlogPostHeader.js";
2
3
  import { BlogList } from "./components/BlogList.js";
3
4
  import { JsonLd } from "./components/JsonLd.js";
4
5
  import { BlockRenderer, renderBlocks } from "./render/BlockRenderer.js";
@@ -6,10 +7,12 @@ import { configureBlog, getDefaultConfig, resolveConfig } from "./config.js";
6
7
  import { createBlogClient, getCategories, getPostBySlug, getPosts, getPostsByCategory, getPublishedSlugs, incrementView } from "./client/blogClient.js";
7
8
  import { generateBlogBreadcrumbJsonLd, generateBlogListMetadata, generateBlogPostJsonLd, generateBlogPostMetadata } from "./seo/index.js";
8
9
  import { formatDate } from "./lib/format.js";
10
+ import { postHasLandingPageBody } from "./lib/postUtils.js";
9
11
  export {
10
12
  BlockRenderer,
11
13
  BlogList,
12
14
  BlogPost,
15
+ BlogPostHeader,
13
16
  JsonLd,
14
17
  configureBlog,
15
18
  createBlogClient,
@@ -25,6 +28,7 @@ export {
25
28
  getPostsByCategory,
26
29
  getPublishedSlugs,
27
30
  incrementView,
31
+ postHasLandingPageBody,
28
32
  renderBlocks,
29
33
  resolveConfig
30
34
  };
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;"}
@@ -0,0 +1,3 @@
1
+ import { BlogPost } from '../types/blog';
2
+ /** True when the post body is rendered from the page-builder landing schema. */
3
+ export declare function postHasLandingPageBody(post: Pick<BlogPost, 'content_blocks' | 'landing_page_html'>): boolean;
@@ -0,0 +1,8 @@
1
+ function postHasLandingPageBody(post) {
2
+ const blocks = post.content_blocks ?? [];
3
+ return blocks.length === 0 && Boolean(post.landing_page_html);
4
+ }
5
+ export {
6
+ postHasLandingPageBody
7
+ };
8
+ //# sourceMappingURL=postUtils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"postUtils.js","sources":["../../src/lib/postUtils.ts"],"sourcesContent":["import type { BlogPost } from '../types/blog';\n\n/** True when the post body is rendered from the page-builder landing schema. */\nexport function postHasLandingPageBody(\n post: Pick<BlogPost, 'content_blocks' | 'landing_page_html'>,\n): boolean {\n const blocks = post.content_blocks ?? [];\n return blocks.length === 0 && Boolean(post.landing_page_html);\n}\n"],"names":[],"mappings":"AAGO,SAAS,uBACd,MACS;AACT,QAAM,SAAS,KAAK,kBAAkB,CAAA;AACtC,SAAO,OAAO,WAAW,KAAK,QAAQ,KAAK,iBAAiB;AAC9D;"}
@@ -0,0 +1,26 @@
1
+ /** Page-builder schema shape stored on blog posts via landingPage.publishedVersion.schema_json. */
2
+ export interface LandingPageSchema {
3
+ components?: Array<{
4
+ id?: string;
5
+ type?: string;
6
+ props?: Record<string, unknown>;
7
+ }>;
8
+ settings?: {
9
+ customCss?: string;
10
+ maxWidth?: string;
11
+ backgroundColor?: string;
12
+ };
13
+ }
14
+ /** Convert a page-builder schema to trusted HTML (+ optional CSS) for blog post bodies. */
15
+ export declare function generateHtmlFromSchema(schema: LandingPageSchema | null | undefined): {
16
+ html: string;
17
+ css: string;
18
+ };
19
+ /** Read landing page schema from a blog post API payload. */
20
+ export declare function getLandingPageSchema(post: {
21
+ landingPage?: {
22
+ publishedVersion?: {
23
+ schema_json?: LandingPageSchema;
24
+ };
25
+ };
26
+ } | null | undefined): LandingPageSchema | null;
@@ -0,0 +1,189 @@
1
+ function escapeHtml(text) {
2
+ if (!text) return "";
3
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
4
+ }
5
+ function sanitizeSrc(src) {
6
+ if (!src) return "";
7
+ if (src.startsWith("data:") || src.startsWith("http://") || src.startsWith("https://") || src.startsWith("/")) {
8
+ return src.replace(/"/g, "&quot;");
9
+ }
10
+ return escapeHtml(src);
11
+ }
12
+ function extractYouTubeId(url) {
13
+ const match = url.match(
14
+ /(?:youtube\.com\/(?:[^/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?/\s]{11})/
15
+ );
16
+ return match ? match[1] : "";
17
+ }
18
+ function renderHeroComponent(props, id) {
19
+ const headline = String(props.headline ?? props.title ?? "");
20
+ const subheadline = String(props.subheadline ?? props.subtitle ?? "");
21
+ const topHeadline = String(props.topHeadline ?? "");
22
+ const showTopHeadline = props.showTopHeadline !== false && Boolean(topHeadline);
23
+ const backgroundImage = String(props.backgroundImage ?? "");
24
+ const backgroundColor = String(props.backgroundColor ?? "#f3f4f6");
25
+ const headlineColor = String(props.headlineColor ?? props.textColor ?? "#000000");
26
+ const subheadlineColor = String(props.subheadlineColor ?? props.textColor ?? "#4b5563");
27
+ const topHeadlineColor = String(props.topHeadlineColor ?? "#9ca3af");
28
+ const paddingY = String(props.paddingY ?? "80px");
29
+ const paddingX = String(props.paddingX ?? "20px");
30
+ const textAlign = props.isCentered === false ? String(props.alignment ?? "left") : "center";
31
+ const headlineFontSize = String(props.headlineFontSize ?? "3rem");
32
+ const subheadlineFontSize = String(props.subheadlineFontSize ?? "1.25rem");
33
+ const topHeadlineFontSize = String(props.topHeadlineFontSize ?? "14px");
34
+ const ctaText = String(props.ctaText ?? "");
35
+ const ctaLink = String(props.ctaLink ?? props.ctaUrl ?? "");
36
+ const ctaBgColor = String(props.ctaBgColor ?? "#007bff");
37
+ const ctaTextColor = String(props.ctaTextColor ?? "#ffffff");
38
+ const buttonBorderRadius = String(props.buttonBorderRadius ?? "6px");
39
+ const buttonPaddingX = String(props.buttonPaddingX ?? "36px");
40
+ const buttonPaddingY = String(props.buttonPaddingY ?? "14px");
41
+ const containerMaxWidth = String(props.containerMaxWidth ?? "1200px");
42
+ const overlayOpacity = Number(props.overlayOpacity ?? 0);
43
+ const backgroundStyle = backgroundImage ? `background-image: url('${escapeHtml(backgroundImage)}'); background-size: cover; background-position: center;` : `background-color: ${backgroundColor};`;
44
+ return `
45
+ <section class="ggblog-lp-hero" data-component-id="${escapeHtml(id)}" style="${backgroundStyle} position: relative; padding: ${paddingY} ${paddingX};">
46
+ ${overlayOpacity > 0 ? `<div style="position:absolute;inset:0;background:rgba(0,0,0,${overlayOpacity});"></div>` : ""}
47
+ <div style="position:relative;z-index:1;max-width:${containerMaxWidth};margin:0 auto;text-align:${textAlign};">
48
+ ${showTopHeadline ? `<p style="margin:0 0 1rem;font-size:${topHeadlineFontSize};color:${topHeadlineColor};letter-spacing:0.15em;text-transform:uppercase;">${escapeHtml(topHeadline)}</p>` : ""}
49
+ ${headline ? `<h2 style="margin:0 0 1rem;font-size:${headlineFontSize};line-height:1.1;color:${headlineColor};font-weight:700;">${escapeHtml(headline)}</h2>` : ""}
50
+ ${subheadline ? `<p style="margin:0 0 2rem;font-size:${subheadlineFontSize};line-height:1.6;color:${subheadlineColor};max-width:650px;${textAlign === "center" ? "margin-left:auto;margin-right:auto;" : ""}">${escapeHtml(subheadline)}</p>` : ""}
51
+ ${ctaText && ctaLink ? `<a href="${escapeHtml(ctaLink)}" style="display:inline-block;padding:${buttonPaddingY} ${buttonPaddingX};border-radius:${buttonBorderRadius};text-decoration:none;font-weight:700;background:${ctaBgColor};color:${ctaTextColor};">${escapeHtml(ctaText)}</a>` : ""}
52
+ </div>
53
+ </section>
54
+ `;
55
+ }
56
+ function renderTextComponent(props, id) {
57
+ const content = String(props.content ?? "");
58
+ const alignment = String(props.alignment ?? "left");
59
+ const fontSize = String(props.fontSize ?? "16px");
60
+ return `<div class="ggblog-lp-text" data-component-id="${escapeHtml(id)}" style="text-align:${alignment};font-size:${fontSize};">${content}</div>`;
61
+ }
62
+ function renderImageComponent(props, id) {
63
+ const src = String(props.src ?? props.url ?? "");
64
+ const alt = String(props.alt ?? "");
65
+ const alignment = String(props.alignment ?? "center");
66
+ return `
67
+ <figure class="ggblog-lp-image" data-component-id="${escapeHtml(id)}" style="text-align:${alignment};margin:20px 0;">
68
+ <img src="${escapeHtml(src)}" alt="${escapeHtml(alt)}" style="max-width:100%;height:auto;" loading="lazy" />
69
+ </figure>
70
+ `;
71
+ }
72
+ function renderButtonComponent(props, id) {
73
+ const text = String(props.text ?? "Click me");
74
+ const url = String(props.url ?? props.href ?? "#");
75
+ const alignment = String(props.alignment ?? "center");
76
+ return `
77
+ <div class="ggblog-lp-button" data-component-id="${escapeHtml(id)}" style="text-align:${alignment};margin:20px 0;">
78
+ <a href="${escapeHtml(url)}" style="display:inline-block;padding:12px 24px;border-radius:6px;text-decoration:none;font-weight:600;background:#007bff;color:#fff;">${escapeHtml(text)}</a>
79
+ </div>
80
+ `;
81
+ }
82
+ function renderColumnsComponent(props, id) {
83
+ const columns = Array.isArray(props.columns) ? props.columns : [];
84
+ const gap = String(props.gap ?? "20px");
85
+ const columnHtml = columns.map((col) => {
86
+ const components = Array.isArray(col.components) ? col.components : [];
87
+ const content = components.map((c) => renderComponentToHtml(c)).join("");
88
+ return `<div class="ggblog-lp-column" style="flex:1;min-width:240px;">${content}</div>`;
89
+ }).join("");
90
+ return `<div class="ggblog-lp-columns" data-component-id="${escapeHtml(id)}" style="display:flex;gap:${gap};flex-wrap:wrap;">${columnHtml}</div>`;
91
+ }
92
+ function renderSpacerComponent(props, id) {
93
+ const height = String(props.height ?? "40px");
94
+ return `<div class="ggblog-lp-spacer" data-component-id="${escapeHtml(id)}" style="height:${height};"></div>`;
95
+ }
96
+ function renderDividerComponent(props, id) {
97
+ const color = String(props.color ?? "#e0e0e0");
98
+ const thickness = String(props.thickness ?? "1px");
99
+ const style = String(props.style ?? "solid");
100
+ const margin = String(props.margin ?? "20px 0");
101
+ return `<hr class="ggblog-lp-divider" data-component-id="${escapeHtml(id)}" style="border:none;border-top:${thickness} ${style} ${color};margin:${margin};" />`;
102
+ }
103
+ function renderVideoComponent(props, id) {
104
+ const url = String(props.url ?? "");
105
+ const poster = String(props.poster ?? "");
106
+ const controls = props.controls !== false;
107
+ const autoplay = props.autoplay === true;
108
+ const muted = props.muted === true;
109
+ const loop = props.loop === true;
110
+ if (url.includes("youtube.com") || url.includes("youtu.be")) {
111
+ const videoId = extractYouTubeId(url);
112
+ return `
113
+ <div class="ggblog-lp-video" data-component-id="${escapeHtml(id)}" style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;">
114
+ <iframe src="https://www.youtube.com/embed/${videoId}${autoplay ? "?autoplay=1" : ""}" style="position:absolute;top:0;left:0;width:100%;height:100%;" frameborder="0" allowfullscreen loading="lazy"></iframe>
115
+ </div>
116
+ `;
117
+ }
118
+ return `
119
+ <div class="ggblog-lp-video" data-component-id="${escapeHtml(id)}">
120
+ <video src="${escapeHtml(url)}" poster="${escapeHtml(poster)}" ${controls ? "controls" : ""} ${autoplay ? "autoplay" : ""} ${muted ? "muted" : ""} ${loop ? "loop" : ""} playsinline style="max-width:100%;"></video>
121
+ </div>
122
+ `;
123
+ }
124
+ function renderCtaComponent(props, id) {
125
+ return renderHeroComponent(
126
+ {
127
+ headline: props.title ?? props.headline,
128
+ subheadline: props.description ?? props.subheadline,
129
+ ctaText: props.buttonText ?? props.ctaText,
130
+ ctaLink: props.buttonUrl ?? props.ctaLink,
131
+ backgroundColor: props.backgroundColor,
132
+ textColor: props.textColor
133
+ },
134
+ id
135
+ );
136
+ }
137
+ function renderComponentToHtml(component) {
138
+ const { type, props = {}, id = "" } = component;
139
+ if (!type) return "";
140
+ if (type.startsWith("custom_navbar")) {
141
+ const logo = String(props.logo ?? "");
142
+ const companyName = String(props.companyName ?? "");
143
+ return `<nav class="ggblog-lp-navbar" data-component-id="${escapeHtml(id)}" style="padding:12px 20px;background:${String(props.backgroundColor ?? "#fff")};">${logo ? `<img src="${sanitizeSrc(logo)}" alt="${escapeHtml(companyName)}" style="height:48px;" />` : escapeHtml(companyName)}</nav>`;
144
+ }
145
+ switch (type) {
146
+ case "hero":
147
+ return renderHeroComponent(props, id);
148
+ case "text":
149
+ return renderTextComponent(props, id);
150
+ case "image":
151
+ return renderImageComponent(props, id);
152
+ case "button":
153
+ return renderButtonComponent(props, id);
154
+ case "columns":
155
+ return renderColumnsComponent(props, id);
156
+ case "spacer":
157
+ return renderSpacerComponent(props, id);
158
+ case "divider":
159
+ return renderDividerComponent(props, id);
160
+ case "video":
161
+ return renderVideoComponent(props, id);
162
+ case "cta":
163
+ return renderCtaComponent(props, id);
164
+ case "html":
165
+ return String(props.content ?? "");
166
+ default:
167
+ return `<div class="ggblog-lp-component ggblog-lp-${escapeHtml(type)}" data-component-id="${escapeHtml(id)}">${String(props.content ?? props.headline ?? props.text ?? "")}</div>`;
168
+ }
169
+ }
170
+ function generateHtmlFromSchema(schema) {
171
+ if (!schema?.components?.length) {
172
+ return { html: "", css: "" };
173
+ }
174
+ const html = schema.components.map((component) => renderComponentToHtml(component)).join("");
175
+ const css = schema.settings?.customCss ?? "";
176
+ return {
177
+ html: `<div class="ggblog-landing-page-content">${html}</div>`,
178
+ css
179
+ };
180
+ }
181
+ function getLandingPageSchema(post) {
182
+ const schema = post?.landingPage?.publishedVersion?.schema_json;
183
+ return schema?.components?.length ? schema : null;
184
+ }
185
+ export {
186
+ generateHtmlFromSchema,
187
+ getLandingPageSchema
188
+ };
189
+ //# sourceMappingURL=landingPageHtml.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"landingPageHtml.js","sources":["../../src/render/landingPageHtml.ts"],"sourcesContent":["/** Page-builder schema shape stored on blog posts via landingPage.publishedVersion.schema_json. */\nexport interface LandingPageSchema {\n components?: Array<{ id?: string; type?: string; props?: Record<string, unknown> }>;\n settings?: { customCss?: string; maxWidth?: string; backgroundColor?: string };\n}\n\nfunction escapeHtml(text: string): string {\n if (!text) return '';\n return text\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#039;');\n}\n\nfunction sanitizeSrc(src: string): string {\n if (!src) return '';\n if (\n src.startsWith('data:') ||\n src.startsWith('http://') ||\n src.startsWith('https://') ||\n src.startsWith('/')\n ) {\n return src.replace(/\"/g, '&quot;');\n }\n return escapeHtml(src);\n}\n\nfunction extractYouTubeId(url: string): string {\n const match = url.match(\n /(?:youtube\\.com\\/(?:[^/]+\\/.+\\/|(?:v|e(?:mbed)?)\\/|.*[?&]v=)|youtu\\.be\\/)([^\"&?/\\s]{11})/,\n );\n return match ? match[1] : '';\n}\n\nfunction renderHeroComponent(props: Record<string, unknown>, id: string): string {\n const headline = String(props.headline ?? props.title ?? '');\n const subheadline = String(props.subheadline ?? props.subtitle ?? '');\n const topHeadline = String(props.topHeadline ?? '');\n const showTopHeadline = props.showTopHeadline !== false && Boolean(topHeadline);\n const backgroundImage = String(props.backgroundImage ?? '');\n const backgroundColor = String(props.backgroundColor ?? '#f3f4f6');\n const headlineColor = String(props.headlineColor ?? props.textColor ?? '#000000');\n const subheadlineColor = String(props.subheadlineColor ?? props.textColor ?? '#4b5563');\n const topHeadlineColor = String(props.topHeadlineColor ?? '#9ca3af');\n const paddingY = String(props.paddingY ?? '80px');\n const paddingX = String(props.paddingX ?? '20px');\n const textAlign = props.isCentered === false ? String(props.alignment ?? 'left') : 'center';\n const headlineFontSize = String(props.headlineFontSize ?? '3rem');\n const subheadlineFontSize = String(props.subheadlineFontSize ?? '1.25rem');\n const topHeadlineFontSize = String(props.topHeadlineFontSize ?? '14px');\n const ctaText = String(props.ctaText ?? '');\n const ctaLink = String(props.ctaLink ?? props.ctaUrl ?? '');\n const ctaBgColor = String(props.ctaBgColor ?? '#007bff');\n const ctaTextColor = String(props.ctaTextColor ?? '#ffffff');\n const buttonBorderRadius = String(props.buttonBorderRadius ?? '6px');\n const buttonPaddingX = String(props.buttonPaddingX ?? '36px');\n const buttonPaddingY = String(props.buttonPaddingY ?? '14px');\n const containerMaxWidth = String(props.containerMaxWidth ?? '1200px');\n const overlayOpacity = Number(props.overlayOpacity ?? 0);\n\n const backgroundStyle = backgroundImage\n ? `background-image: url('${escapeHtml(backgroundImage)}'); background-size: cover; background-position: center;`\n : `background-color: ${backgroundColor};`;\n\n return `\n <section class=\"ggblog-lp-hero\" data-component-id=\"${escapeHtml(id)}\" style=\"${backgroundStyle} position: relative; padding: ${paddingY} ${paddingX};\">\n ${overlayOpacity > 0 ? `<div style=\"position:absolute;inset:0;background:rgba(0,0,0,${overlayOpacity});\"></div>` : ''}\n <div style=\"position:relative;z-index:1;max-width:${containerMaxWidth};margin:0 auto;text-align:${textAlign};\">\n ${showTopHeadline ? `<p style=\"margin:0 0 1rem;font-size:${topHeadlineFontSize};color:${topHeadlineColor};letter-spacing:0.15em;text-transform:uppercase;\">${escapeHtml(topHeadline)}</p>` : ''}\n ${headline ? `<h2 style=\"margin:0 0 1rem;font-size:${headlineFontSize};line-height:1.1;color:${headlineColor};font-weight:700;\">${escapeHtml(headline)}</h2>` : ''}\n ${subheadline ? `<p style=\"margin:0 0 2rem;font-size:${subheadlineFontSize};line-height:1.6;color:${subheadlineColor};max-width:650px;${textAlign === 'center' ? 'margin-left:auto;margin-right:auto;' : ''}\">${escapeHtml(subheadline)}</p>` : ''}\n ${ctaText && ctaLink ? `<a href=\"${escapeHtml(ctaLink)}\" style=\"display:inline-block;padding:${buttonPaddingY} ${buttonPaddingX};border-radius:${buttonBorderRadius};text-decoration:none;font-weight:700;background:${ctaBgColor};color:${ctaTextColor};\">${escapeHtml(ctaText)}</a>` : ''}\n </div>\n </section>\n `;\n}\n\nfunction renderTextComponent(props: Record<string, unknown>, id: string): string {\n const content = String(props.content ?? '');\n const alignment = String(props.alignment ?? 'left');\n const fontSize = String(props.fontSize ?? '16px');\n return `<div class=\"ggblog-lp-text\" data-component-id=\"${escapeHtml(id)}\" style=\"text-align:${alignment};font-size:${fontSize};\">${content}</div>`;\n}\n\nfunction renderImageComponent(props: Record<string, unknown>, id: string): string {\n const src = String(props.src ?? props.url ?? '');\n const alt = String(props.alt ?? '');\n const alignment = String(props.alignment ?? 'center');\n return `\n <figure class=\"ggblog-lp-image\" data-component-id=\"${escapeHtml(id)}\" style=\"text-align:${alignment};margin:20px 0;\">\n <img src=\"${escapeHtml(src)}\" alt=\"${escapeHtml(alt)}\" style=\"max-width:100%;height:auto;\" loading=\"lazy\" />\n </figure>\n `;\n}\n\nfunction renderButtonComponent(props: Record<string, unknown>, id: string): string {\n const text = String(props.text ?? 'Click me');\n const url = String(props.url ?? props.href ?? '#');\n const alignment = String(props.alignment ?? 'center');\n return `\n <div class=\"ggblog-lp-button\" data-component-id=\"${escapeHtml(id)}\" style=\"text-align:${alignment};margin:20px 0;\">\n <a href=\"${escapeHtml(url)}\" style=\"display:inline-block;padding:12px 24px;border-radius:6px;text-decoration:none;font-weight:600;background:#007bff;color:#fff;\">${escapeHtml(text)}</a>\n </div>\n `;\n}\n\nfunction renderColumnsComponent(props: Record<string, unknown>, id: string): string {\n const columns = Array.isArray(props.columns) ? props.columns : [];\n const gap = String(props.gap ?? '20px');\n const columnHtml = columns\n .map((col) => {\n const components = Array.isArray((col as { components?: unknown[] }).components)\n ? (col as { components: Array<{ id?: string; type?: string; props?: Record<string, unknown> }> })\n .components\n : [];\n const content = components.map((c) => renderComponentToHtml(c)).join('');\n return `<div class=\"ggblog-lp-column\" style=\"flex:1;min-width:240px;\">${content}</div>`;\n })\n .join('');\n return `<div class=\"ggblog-lp-columns\" data-component-id=\"${escapeHtml(id)}\" style=\"display:flex;gap:${gap};flex-wrap:wrap;\">${columnHtml}</div>`;\n}\n\nfunction renderSpacerComponent(props: Record<string, unknown>, id: string): string {\n const height = String(props.height ?? '40px');\n return `<div class=\"ggblog-lp-spacer\" data-component-id=\"${escapeHtml(id)}\" style=\"height:${height};\"></div>`;\n}\n\nfunction renderDividerComponent(props: Record<string, unknown>, id: string): string {\n const color = String(props.color ?? '#e0e0e0');\n const thickness = String(props.thickness ?? '1px');\n const style = String(props.style ?? 'solid');\n const margin = String(props.margin ?? '20px 0');\n return `<hr class=\"ggblog-lp-divider\" data-component-id=\"${escapeHtml(id)}\" style=\"border:none;border-top:${thickness} ${style} ${color};margin:${margin};\" />`;\n}\n\nfunction renderVideoComponent(props: Record<string, unknown>, id: string): string {\n const url = String(props.url ?? '');\n const poster = String(props.poster ?? '');\n const controls = props.controls !== false;\n const autoplay = props.autoplay === true;\n const muted = props.muted === true;\n const loop = props.loop === true;\n\n if (url.includes('youtube.com') || url.includes('youtu.be')) {\n const videoId = extractYouTubeId(url);\n return `\n <div class=\"ggblog-lp-video\" data-component-id=\"${escapeHtml(id)}\" style=\"position:relative;padding-bottom:56.25%;height:0;overflow:hidden;\">\n <iframe src=\"https://www.youtube.com/embed/${videoId}${autoplay ? '?autoplay=1' : ''}\" style=\"position:absolute;top:0;left:0;width:100%;height:100%;\" frameborder=\"0\" allowfullscreen loading=\"lazy\"></iframe>\n </div>\n `;\n }\n\n return `\n <div class=\"ggblog-lp-video\" data-component-id=\"${escapeHtml(id)}\">\n <video src=\"${escapeHtml(url)}\" poster=\"${escapeHtml(poster)}\" ${controls ? 'controls' : ''} ${autoplay ? 'autoplay' : ''} ${muted ? 'muted' : ''} ${loop ? 'loop' : ''} playsinline style=\"max-width:100%;\"></video>\n </div>\n `;\n}\n\nfunction renderCtaComponent(props: Record<string, unknown>, id: string): string {\n return renderHeroComponent(\n {\n headline: props.title ?? props.headline,\n subheadline: props.description ?? props.subheadline,\n ctaText: props.buttonText ?? props.ctaText,\n ctaLink: props.buttonUrl ?? props.ctaLink,\n backgroundColor: props.backgroundColor,\n textColor: props.textColor,\n },\n id,\n );\n}\n\nfunction renderComponentToHtml(component: {\n id?: string;\n type?: string;\n props?: Record<string, unknown>;\n}): string {\n const { type, props = {}, id = '' } = component;\n if (!type) return '';\n\n if (type.startsWith('custom_navbar')) {\n const logo = String(props.logo ?? '');\n const companyName = String(props.companyName ?? '');\n return `<nav class=\"ggblog-lp-navbar\" data-component-id=\"${escapeHtml(id)}\" style=\"padding:12px 20px;background:${String(props.backgroundColor ?? '#fff')};\">${logo ? `<img src=\"${sanitizeSrc(logo)}\" alt=\"${escapeHtml(companyName)}\" style=\"height:48px;\" />` : escapeHtml(companyName)}</nav>`;\n }\n\n switch (type) {\n case 'hero':\n return renderHeroComponent(props, id);\n case 'text':\n return renderTextComponent(props, id);\n case 'image':\n return renderImageComponent(props, id);\n case 'button':\n return renderButtonComponent(props, id);\n case 'columns':\n return renderColumnsComponent(props, id);\n case 'spacer':\n return renderSpacerComponent(props, id);\n case 'divider':\n return renderDividerComponent(props, id);\n case 'video':\n return renderVideoComponent(props, id);\n case 'cta':\n return renderCtaComponent(props, id);\n case 'html':\n return String(props.content ?? '');\n default:\n return `<div class=\"ggblog-lp-component ggblog-lp-${escapeHtml(type)}\" data-component-id=\"${escapeHtml(id)}\">${String(props.content ?? props.headline ?? props.text ?? '')}</div>`;\n }\n}\n\n/** Convert a page-builder schema to trusted HTML (+ optional CSS) for blog post bodies. */\nexport function generateHtmlFromSchema(schema: LandingPageSchema | null | undefined): {\n html: string;\n css: string;\n} {\n if (!schema?.components?.length) {\n return { html: '', css: '' };\n }\n\n const html = schema.components.map((component) => renderComponentToHtml(component)).join('');\n const css = schema.settings?.customCss ?? '';\n\n return {\n html: `<div class=\"ggblog-landing-page-content\">${html}</div>`,\n css,\n };\n}\n\n/** Read landing page schema from a blog post API payload. */\nexport function getLandingPageSchema(\n post: { landingPage?: { publishedVersion?: { schema_json?: LandingPageSchema } } } | null | undefined,\n): LandingPageSchema | null {\n const schema = post?.landingPage?.publishedVersion?.schema_json;\n return schema?.components?.length ? schema : null;\n}\n"],"names":[],"mappings":"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"}
package/dist/styles.css CHANGED
@@ -172,3 +172,17 @@
172
172
  .ggblog-animate-slideUp { animation: ggblog-slide-up 0.5s ease both; }
173
173
  @keyframes ggblog-fade { from { opacity: 0; } to { opacity: 1; } }
174
174
  @keyframes ggblog-slide-up { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: none; } }
175
+
176
+ /* ---------- Page-builder landing content ---------- */
177
+ .ggblog-landing-page {
178
+ margin-top: 1.5rem;
179
+ }
180
+
181
+ .ggblog-landing-page-content {
182
+ width: 100%;
183
+ }
184
+
185
+ .ggblog-lp-hero {
186
+ border-radius: var(--ggblog-radius);
187
+ overflow: hidden;
188
+ }
@@ -232,6 +232,9 @@ export interface BlogPost {
232
232
  content_summary?: string;
233
233
  content_blocks: ContentBlock[];
234
234
  blocks?: ContentBlock[];
235
+ /** Pre-rendered page-builder HTML when content_blocks is empty. */
236
+ landing_page_html?: string;
237
+ landing_page_css?: string;
235
238
  categories?: BlogPostCategory[];
236
239
  tags?: BlogPostTag[];
237
240
  override_seo_noindex?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gosalesdeal/blog",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Render GoEnterprise blog content (list + post) straight from the BlogPosts API. Server-component friendly, zero UI dependencies.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -17,7 +17,13 @@
17
17
  "exports": {
18
18
  ".": {
19
19
  "types": "./dist/index.d.ts",
20
- "import": "./dist/index.js"
20
+ "import": "./dist/index.js",
21
+ "default": "./dist/index.js"
22
+ },
23
+ "./BlogPostHeader": {
24
+ "types": "./dist/components/BlogPostHeader.d.ts",
25
+ "import": "./dist/components/BlogPostHeader.js",
26
+ "default": "./dist/components/BlogPostHeader.js"
21
27
  },
22
28
  "./styles.css": "./dist/styles.css",
23
29
  "./package.json": "./package.json"