@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.
- package/dist/client/blogClient.js +43 -6
- package/dist/client/blogClient.js.map +1 -1
- package/dist/components/BlogPost.d.ts +12 -1
- package/dist/components/BlogPost.js +28 -16
- package/dist/components/BlogPost.js.map +1 -1
- package/dist/components/BlogPostHeader.d.ts +15 -0
- package/dist/components/BlogPostHeader.js +41 -0
- package/dist/components/BlogPostHeader.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/postUtils.d.ts +3 -0
- package/dist/lib/postUtils.js +8 -0
- package/dist/lib/postUtils.js.map +1 -0
- package/dist/render/landingPageHtml.d.ts +26 -0
- package/dist/render/landingPageHtml.js +189 -0
- package/dist/render/landingPageHtml.js.map +1 -0
- package/dist/styles.css +14 -0
- package/dist/types/blog.d.ts +3 -0
- package/package.json +8 -2
|
@@ -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), {
|
|
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(
|
|
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
|
|
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:
|
|
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__ */
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
post
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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 {
|
|
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 @@
|
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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, """);
|
|
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, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\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, '"');\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
|
+
}
|
package/dist/types/blog.d.ts
CHANGED
|
@@ -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.
|
|
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"
|