@gosalesdeal/blog 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +97 -0
- package/dist/client/blogClient.d.ts +31 -0
- package/dist/client/blogClient.js +129 -0
- package/dist/client/blogClient.js.map +1 -0
- package/dist/components/BlogList.d.ts +24 -0
- package/dist/components/BlogList.js +60 -0
- package/dist/components/BlogList.js.map +1 -0
- package/dist/components/BlogPost.d.ts +25 -0
- package/dist/components/BlogPost.js +53 -0
- package/dist/components/BlogPost.js.map +1 -0
- package/dist/components/JsonLd.d.ts +6 -0
- package/dist/components/JsonLd.js +15 -0
- package/dist/components/JsonLd.js.map +1 -0
- package/dist/config.d.ts +16 -0
- package/dist/config.js +32 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/format.d.ts +2 -0
- package/dist/lib/format.js +10 -0
- package/dist/lib/format.js.map +1 -0
- package/dist/render/BlockRenderer.d.ts +9 -0
- package/dist/render/BlockRenderer.js +80 -0
- package/dist/render/BlockRenderer.js.map +1 -0
- package/dist/render/RichText.d.ts +5 -0
- package/dist/render/RichText.js +12 -0
- package/dist/render/RichText.js.map +1 -0
- package/dist/render/blockStyle.d.ts +8 -0
- package/dist/render/blockStyle.js +33 -0
- package/dist/render/blockStyle.js.map +1 -0
- package/dist/render/blocks/AccordionBlock.d.ts +4 -0
- package/dist/render/blocks/AccordionBlock.js +39 -0
- package/dist/render/blocks/AccordionBlock.js.map +1 -0
- package/dist/render/blocks/AlertBlock.d.ts +4 -0
- package/dist/render/blocks/AlertBlock.js +11 -0
- package/dist/render/blocks/AlertBlock.js.map +1 -0
- package/dist/render/blocks/ButtonBlock.d.ts +4 -0
- package/dist/render/blocks/ButtonBlock.js +17 -0
- package/dist/render/blocks/ButtonBlock.js.map +1 -0
- package/dist/render/blocks/CodeBlock.d.ts +4 -0
- package/dist/render/blocks/CodeBlock.js +16 -0
- package/dist/render/blocks/CodeBlock.js.map +1 -0
- package/dist/render/blocks/ColumnsBlock.d.ts +4 -0
- package/dist/render/blocks/ColumnsBlock.js +19 -0
- package/dist/render/blocks/ColumnsBlock.js.map +1 -0
- package/dist/render/blocks/DividerBlock.d.ts +4 -0
- package/dist/render/blocks/DividerBlock.js +19 -0
- package/dist/render/blocks/DividerBlock.js.map +1 -0
- package/dist/render/blocks/EmbedBlock.d.ts +4 -0
- package/dist/render/blocks/EmbedBlock.js +26 -0
- package/dist/render/blocks/EmbedBlock.js.map +1 -0
- package/dist/render/blocks/GalleryBlock.d.ts +4 -0
- package/dist/render/blocks/GalleryBlock.js +15 -0
- package/dist/render/blocks/GalleryBlock.js.map +1 -0
- package/dist/render/blocks/HeadingBlock.d.ts +5 -0
- package/dist/render/blocks/HeadingBlock.js +10 -0
- package/dist/render/blocks/HeadingBlock.js.map +1 -0
- package/dist/render/blocks/ImageBlock.d.ts +4 -0
- package/dist/render/blocks/ImageBlock.js +33 -0
- package/dist/render/blocks/ImageBlock.js.map +1 -0
- package/dist/render/blocks/ListBlock.d.ts +5 -0
- package/dist/render/blocks/ListBlock.js +16 -0
- package/dist/render/blocks/ListBlock.js.map +1 -0
- package/dist/render/blocks/QuoteBlock.d.ts +4 -0
- package/dist/render/blocks/QuoteBlock.js +15 -0
- package/dist/render/blocks/QuoteBlock.js.map +1 -0
- package/dist/render/blocks/SpacerBlock.d.ts +4 -0
- package/dist/render/blocks/SpacerBlock.js +8 -0
- package/dist/render/blocks/SpacerBlock.js.map +1 -0
- package/dist/render/blocks/TableBlock.d.ts +4 -0
- package/dist/render/blocks/TableBlock.js +18 -0
- package/dist/render/blocks/TableBlock.js.map +1 -0
- package/dist/render/blocks/TabsBlock.d.ts +4 -0
- package/dist/render/blocks/TabsBlock.js +29 -0
- package/dist/render/blocks/TabsBlock.js.map +1 -0
- package/dist/render/blocks/TextBlock.d.ts +4 -0
- package/dist/render/blocks/TextBlock.js +12 -0
- package/dist/render/blocks/TextBlock.js.map +1 -0
- package/dist/render/blocks/VideoBlock.d.ts +4 -0
- package/dist/render/blocks/VideoBlock.js +63 -0
- package/dist/render/blocks/VideoBlock.js.map +1 -0
- package/dist/seo/index.d.ts +46 -0
- package/dist/seo/index.js +87 -0
- package/dist/seo/index.js.map +1 -0
- package/dist/styles.css +174 -0
- package/dist/types/blog.d.ts +260 -0
- package/package.json +68 -0
package/README.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# @gosalesdeal/blog
|
|
2
|
+
|
|
3
|
+
Render the GoEnterprise blog (list + post detail) in any React 18+ / Next.js App Router app, straight from the BlogPosts API. Server-component friendly, with a typed data client and zero UI dependencies.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @gosalesdeal/blog
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`react` (and `react-dom`) are peer dependencies.
|
|
12
|
+
|
|
13
|
+
## Configure
|
|
14
|
+
|
|
15
|
+
Components read config from props, then `configureBlog(...)` defaults, then env vars (`BLOG_API_URL` / `NEXT_PUBLIC_BLOG_API_URL`, `BLOG_TENANT_ID` / `NEXT_PUBLIC_BLOG_TENANT_ID`).
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
// e.g. instrumentation.ts or a server entry
|
|
19
|
+
import { configureBlog } from '@gosalesdeal/blog';
|
|
20
|
+
|
|
21
|
+
configureBlog({
|
|
22
|
+
apiBaseUrl: process.env.BLOG_API_URL!, // base of the BlogPosts API
|
|
23
|
+
tenantId: process.env.BLOG_TENANT_ID!, // sent as the X-Tenant header
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Next.js App Router usage
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
// app/blog/page.tsx
|
|
31
|
+
import { BlogList, generateBlogListMetadata } from '@gosalesdeal/blog';
|
|
32
|
+
import '@gosalesdeal/blog/styles.css';
|
|
33
|
+
|
|
34
|
+
export const metadata = generateBlogListMetadata({ siteName: 'Acme' });
|
|
35
|
+
|
|
36
|
+
export default async function Page({ searchParams }: { searchParams: Promise<{ page?: string }> }) {
|
|
37
|
+
const { page } = await searchParams;
|
|
38
|
+
return <BlogList page={Number(page) || 1} basePath="/blog" />;
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
// app/blog/[slug]/page.tsx
|
|
44
|
+
import { notFound } from 'next/navigation';
|
|
45
|
+
import { BlogPost, getPostBySlug, getPublishedSlugs, generateBlogPostMetadata } from '@gosalesdeal/blog';
|
|
46
|
+
import '@gosalesdeal/blog/styles.css';
|
|
47
|
+
|
|
48
|
+
export async function generateStaticParams() {
|
|
49
|
+
const slugs = await getPublishedSlugs();
|
|
50
|
+
return slugs.map((slug) => ({ slug }));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) {
|
|
54
|
+
const { slug } = await params;
|
|
55
|
+
const post = await getPostBySlug(slug);
|
|
56
|
+
if (!post) return {};
|
|
57
|
+
return generateBlogPostMetadata(post, { siteUrl: 'https://acme.com', siteName: 'Acme' });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
|
|
61
|
+
const { slug } = await params;
|
|
62
|
+
return (
|
|
63
|
+
<BlogPost
|
|
64
|
+
slug={slug}
|
|
65
|
+
seo={{ siteUrl: 'https://acme.com', siteName: 'Acme' }}
|
|
66
|
+
fallback={notFound()}
|
|
67
|
+
/>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## API
|
|
73
|
+
|
|
74
|
+
### Components
|
|
75
|
+
- `<BlogList page limit category basePath apiBaseUrl tenantId locale emptyState />` — paginated listing of cards.
|
|
76
|
+
- `<BlogPost slug seo trackView renderJsonLd locale fallback />` — full post with header, block body, and JSON-LD.
|
|
77
|
+
- `<BlockRenderer block />` / `renderBlocks(blocks)` — render raw `ContentBlock`s yourself.
|
|
78
|
+
|
|
79
|
+
### Data client
|
|
80
|
+
`createBlogClient({ apiBaseUrl, tenantId })` → `{ getPostBySlug, getPosts, getPostsByCategory, getPublishedSlugs, getCategories, incrementView }`. The same functions are exported standalone (each accepts an optional trailing `config`).
|
|
81
|
+
|
|
82
|
+
### SEO
|
|
83
|
+
`generateBlogPostMetadata`, `generateBlogListMetadata`, `generateBlogPostJsonLd`, `generateBlogBreadcrumbJsonLd` — framework-neutral plain objects (assignable to Next's `Metadata`).
|
|
84
|
+
|
|
85
|
+
## Styling
|
|
86
|
+
|
|
87
|
+
The bundled `@gosalesdeal/blog/styles.css` is optional and namespaced under `.ggblog-*`. Theme it by overriding the CSS variables on `.ggblog-post` / `.ggblog-list` (`--ggblog-accent`, `--ggblog-fg`, `--ggblog-border`, …), or skip it and bring your own.
|
|
88
|
+
|
|
89
|
+
## Scope & notes
|
|
90
|
+
|
|
91
|
+
- Renders **block-based** posts (`content_blocks`). All 17 block types are supported; `accordion` and `tabs` are interactive (`"use client"`), everything else is a server component.
|
|
92
|
+
- Block HTML (text/embed/accordion/tab content) is treated as **trusted CMS output**. If your authors are untrusted, sanitize upstream.
|
|
93
|
+
- The page-builder (`landingPage.schema_json`) rendering path is out of scope.
|
|
94
|
+
|
|
95
|
+
## License
|
|
96
|
+
|
|
97
|
+
MIT
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { BlogConfig } from '../config';
|
|
2
|
+
import { BlogPost, BlogCategory, PaginatedResponse } from '../types/blog';
|
|
3
|
+
export interface GetPostsParams {
|
|
4
|
+
page?: number;
|
|
5
|
+
limit?: number;
|
|
6
|
+
category?: string;
|
|
7
|
+
search?: string;
|
|
8
|
+
featured?: boolean;
|
|
9
|
+
}
|
|
10
|
+
/** Fetch a single published post by slug. Returns null when not found. */
|
|
11
|
+
export declare function getPostBySlug(slug: string, config?: Partial<BlogConfig>): Promise<BlogPost | null>;
|
|
12
|
+
/** Fetch a paginated list of posts. */
|
|
13
|
+
export declare function getPosts(params?: GetPostsParams, config?: Partial<BlogConfig>): Promise<PaginatedResponse<BlogPost>>;
|
|
14
|
+
/** Fetch posts within a category by its slug. */
|
|
15
|
+
export declare function getPostsByCategory(categorySlug: string, params?: GetPostsParams, config?: Partial<BlogConfig>): Promise<PaginatedResponse<BlogPost>>;
|
|
16
|
+
/** Slugs of all published posts (for static generation / sitemaps). */
|
|
17
|
+
export declare function getPublishedSlugs(config?: Partial<BlogConfig>): Promise<string[]>;
|
|
18
|
+
/** All blog categories. */
|
|
19
|
+
export declare function getCategories(config?: Partial<BlogConfig>): Promise<BlogCategory[]>;
|
|
20
|
+
/** Best-effort view-count increment. Never throws. */
|
|
21
|
+
export declare function incrementView(postId: string, config?: Partial<BlogConfig>): Promise<void>;
|
|
22
|
+
export interface BlogClient {
|
|
23
|
+
getPostBySlug: (slug: string) => Promise<BlogPost | null>;
|
|
24
|
+
getPosts: (params?: GetPostsParams) => Promise<PaginatedResponse<BlogPost>>;
|
|
25
|
+
getPostsByCategory: (categorySlug: string, params?: GetPostsParams) => Promise<PaginatedResponse<BlogPost>>;
|
|
26
|
+
getPublishedSlugs: () => Promise<string[]>;
|
|
27
|
+
getCategories: () => Promise<BlogCategory[]>;
|
|
28
|
+
incrementView: (postId: string) => Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
/** Bind all fetchers to a single config. */
|
|
31
|
+
export declare function createBlogClient(config: Partial<BlogConfig>): BlogClient;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { resolveConfig } from "../config.js";
|
|
2
|
+
const EMPTY_PAGINATION = {
|
|
3
|
+
total: 0,
|
|
4
|
+
page: 1,
|
|
5
|
+
limit: 0,
|
|
6
|
+
pages: 0,
|
|
7
|
+
hasNext: false,
|
|
8
|
+
hasPrev: false
|
|
9
|
+
};
|
|
10
|
+
function buildUrl(base, path, query) {
|
|
11
|
+
const url = `${base.replace(/\/$/, "")}${path}`;
|
|
12
|
+
if (!query) return url;
|
|
13
|
+
const search = new URLSearchParams();
|
|
14
|
+
for (const [key, value] of Object.entries(query)) {
|
|
15
|
+
if (value !== void 0 && value !== null && value !== "") {
|
|
16
|
+
search.set(key, String(value));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const qs = search.toString();
|
|
20
|
+
return qs ? `${url}?${qs}` : url;
|
|
21
|
+
}
|
|
22
|
+
async function doFetch(path, config, query, init) {
|
|
23
|
+
const cfg = resolveConfig(config);
|
|
24
|
+
const fetchFn = cfg.fetch ?? globalThis.fetch;
|
|
25
|
+
if (typeof fetchFn !== "function") {
|
|
26
|
+
throw new Error("[@gosalesdeal/blog] No fetch implementation available. Provide config.fetch.");
|
|
27
|
+
}
|
|
28
|
+
const headers = {
|
|
29
|
+
Accept: "application/json",
|
|
30
|
+
...cfg.headers ?? {},
|
|
31
|
+
...init?.headers ?? {}
|
|
32
|
+
};
|
|
33
|
+
if (cfg.tenantId) headers["X-Tenant"] = cfg.tenantId;
|
|
34
|
+
return fetchFn(buildUrl(cfg.apiBaseUrl, path, query), { ...init, headers });
|
|
35
|
+
}
|
|
36
|
+
function normalizePost(post) {
|
|
37
|
+
if (!post) return null;
|
|
38
|
+
const blocks = Array.isArray(post.content_blocks) ? post.content_blocks : Array.isArray(post.blocks) ? post.blocks : [];
|
|
39
|
+
return {
|
|
40
|
+
...post,
|
|
41
|
+
content_blocks: [...blocks].sort((a, b) => (a.display_order ?? 0) - (b.display_order ?? 0))
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function normalizeList(body) {
|
|
45
|
+
const root = body;
|
|
46
|
+
const data = root?.data ?? root;
|
|
47
|
+
if (Array.isArray(data)) {
|
|
48
|
+
return {
|
|
49
|
+
items: data.map((p) => normalizePost(p)).filter(Boolean),
|
|
50
|
+
pagination: { ...EMPTY_PAGINATION, total: data.length, limit: data.length }
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const items = Array.isArray(data?.items) ? data.items : [];
|
|
54
|
+
return {
|
|
55
|
+
items: items.map((p) => normalizePost(p)).filter(Boolean),
|
|
56
|
+
pagination: data?.pagination ?? { ...EMPTY_PAGINATION, total: items.length }
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
async function getPostBySlug(slug, config) {
|
|
60
|
+
const res = await doFetch(`/blog-posts/slug/${encodeURIComponent(slug)}`, config);
|
|
61
|
+
if (res.status === 404) return null;
|
|
62
|
+
if (!res.ok) throw new Error(`[@gosalesdeal/blog] GET /blog-posts/slug/${slug} -> ${res.status}`);
|
|
63
|
+
const body = await res.json();
|
|
64
|
+
return normalizePost(body?.data ?? body);
|
|
65
|
+
}
|
|
66
|
+
async function getPosts(params = {}, config) {
|
|
67
|
+
const res = await doFetch("/blog-posts/list/paginated", config, {
|
|
68
|
+
page: params.page,
|
|
69
|
+
limit: params.limit,
|
|
70
|
+
category: params.category,
|
|
71
|
+
search: params.search,
|
|
72
|
+
featured: params.featured
|
|
73
|
+
});
|
|
74
|
+
if (!res.ok) throw new Error(`[@gosalesdeal/blog] GET /blog-posts/list/paginated -> ${res.status}`);
|
|
75
|
+
return normalizeList(await res.json());
|
|
76
|
+
}
|
|
77
|
+
async function getPostsByCategory(categorySlug, params = {}, config) {
|
|
78
|
+
const res = await doFetch(`/blog-posts/category/${encodeURIComponent(categorySlug)}`, config, {
|
|
79
|
+
page: params.page,
|
|
80
|
+
limit: params.limit
|
|
81
|
+
});
|
|
82
|
+
if (!res.ok) {
|
|
83
|
+
throw new Error(`[@gosalesdeal/blog] GET /blog-posts/category/${categorySlug} -> ${res.status}`);
|
|
84
|
+
}
|
|
85
|
+
return normalizeList(await res.json());
|
|
86
|
+
}
|
|
87
|
+
async function getPublishedSlugs(config) {
|
|
88
|
+
const res = await doFetch("/blog-posts/published/slugs", config);
|
|
89
|
+
if (!res.ok) throw new Error(`[@gosalesdeal/blog] GET /blog-posts/published/slugs -> ${res.status}`);
|
|
90
|
+
const body = await res.json();
|
|
91
|
+
const rows = body?.data ?? [];
|
|
92
|
+
return rows.map((r) => typeof r === "string" ? r : r.slug).filter(Boolean);
|
|
93
|
+
}
|
|
94
|
+
async function getCategories(config) {
|
|
95
|
+
const res = await doFetch("/blog-posts/categories", config);
|
|
96
|
+
if (!res.ok) throw new Error(`[@gosalesdeal/blog] GET /blog-posts/categories -> ${res.status}`);
|
|
97
|
+
const body = await res.json();
|
|
98
|
+
return body?.data ?? [];
|
|
99
|
+
}
|
|
100
|
+
async function incrementView(postId, config) {
|
|
101
|
+
try {
|
|
102
|
+
await doFetch(`/blog-posts/${encodeURIComponent(postId)}/view`, config, void 0, {
|
|
103
|
+
method: "POST",
|
|
104
|
+
headers: { "Content-Type": "application/json" },
|
|
105
|
+
body: JSON.stringify({ increment: 1 })
|
|
106
|
+
});
|
|
107
|
+
} catch {
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function createBlogClient(config) {
|
|
111
|
+
return {
|
|
112
|
+
getPostBySlug: (slug) => getPostBySlug(slug, config),
|
|
113
|
+
getPosts: (params) => getPosts(params, config),
|
|
114
|
+
getPostsByCategory: (categorySlug, params) => getPostsByCategory(categorySlug, params, config),
|
|
115
|
+
getPublishedSlugs: () => getPublishedSlugs(config),
|
|
116
|
+
getCategories: () => getCategories(config),
|
|
117
|
+
incrementView: (postId) => incrementView(postId, config)
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
export {
|
|
121
|
+
createBlogClient,
|
|
122
|
+
getCategories,
|
|
123
|
+
getPostBySlug,
|
|
124
|
+
getPosts,
|
|
125
|
+
getPostsByCategory,
|
|
126
|
+
getPublishedSlugs,
|
|
127
|
+
incrementView
|
|
128
|
+
};
|
|
129
|
+
//# sourceMappingURL=blogClient.js.map
|
|
@@ -0,0 +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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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { BlogConfig } from '../config';
|
|
3
|
+
export interface BlogListProps {
|
|
4
|
+
/** 1-based page number. Default 1. */
|
|
5
|
+
page?: number;
|
|
6
|
+
/** Posts per page. Default 12. */
|
|
7
|
+
limit?: number;
|
|
8
|
+
/** Filter by category slug. */
|
|
9
|
+
category?: string;
|
|
10
|
+
/** Link prefix for post/pagination URLs. Default '/blog'. */
|
|
11
|
+
basePath?: string;
|
|
12
|
+
/** API base URL (overrides configured/env default). */
|
|
13
|
+
apiBaseUrl?: string;
|
|
14
|
+
/** Tenant id sent as `X-Tenant`. */
|
|
15
|
+
tenantId?: string;
|
|
16
|
+
/** Full config override (takes precedence over apiBaseUrl/tenantId props). */
|
|
17
|
+
config?: Partial<BlogConfig>;
|
|
18
|
+
/** Locale for date formatting. Default 'en-US'. */
|
|
19
|
+
locale?: string;
|
|
20
|
+
/** Rendered when there are no posts. */
|
|
21
|
+
emptyState?: ReactNode;
|
|
22
|
+
}
|
|
23
|
+
/** Async server component: fetches and renders a paginated list of posts. */
|
|
24
|
+
export declare function BlogList({ page, limit, category, basePath, apiBaseUrl, tenantId, config, locale, emptyState, }: BlogListProps): Promise<import("react").JSX.Element>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { jsx, Fragment, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { getPosts } from "../client/blogClient.js";
|
|
3
|
+
import { formatDate } from "../lib/format.js";
|
|
4
|
+
async function BlogList({
|
|
5
|
+
page = 1,
|
|
6
|
+
limit = 12,
|
|
7
|
+
category,
|
|
8
|
+
basePath = "/blog",
|
|
9
|
+
apiBaseUrl,
|
|
10
|
+
tenantId,
|
|
11
|
+
config,
|
|
12
|
+
locale = "en-US",
|
|
13
|
+
emptyState = null
|
|
14
|
+
}) {
|
|
15
|
+
const cfg = {
|
|
16
|
+
...config,
|
|
17
|
+
...apiBaseUrl ? { apiBaseUrl } : {},
|
|
18
|
+
...tenantId ? { tenantId } : {}
|
|
19
|
+
};
|
|
20
|
+
const { items, pagination } = await getPosts({ page, limit, category }, cfg);
|
|
21
|
+
if (items.length === 0) return /* @__PURE__ */ jsx(Fragment, { children: emptyState });
|
|
22
|
+
const prefix = basePath.replace(/\/$/, "");
|
|
23
|
+
const postHref = (slug) => `${prefix}/${slug}`;
|
|
24
|
+
const pageHref = (n) => `${prefix}?page=${n}`;
|
|
25
|
+
return /* @__PURE__ */ jsxs("div", { className: "ggblog-list", children: [
|
|
26
|
+
/* @__PURE__ */ jsx("div", { className: "ggblog-list-grid", children: items.map((post) => /* @__PURE__ */ jsx("article", { className: "ggblog-card", children: /* @__PURE__ */ jsxs("a", { className: "ggblog-card-link", href: postHref(post.slug), children: [
|
|
27
|
+
post.featured_image ? /* @__PURE__ */ jsx("div", { className: "ggblog-card-image", children: /* @__PURE__ */ jsx(
|
|
28
|
+
"img",
|
|
29
|
+
{
|
|
30
|
+
src: post.featured_image,
|
|
31
|
+
alt: post.featured_image_alt ?? post.title,
|
|
32
|
+
loading: "lazy"
|
|
33
|
+
}
|
|
34
|
+
) }) : null,
|
|
35
|
+
/* @__PURE__ */ jsxs("div", { className: "ggblog-card-body", children: [
|
|
36
|
+
post.categories?.[0]?.category?.name ? /* @__PURE__ */ jsx("span", { className: "ggblog-card-category", children: post.categories[0].category.name }) : null,
|
|
37
|
+
/* @__PURE__ */ jsx("h3", { className: "ggblog-card-title", children: post.title }),
|
|
38
|
+
post.excerpt ? /* @__PURE__ */ jsx("p", { className: "ggblog-card-excerpt", children: post.excerpt }) : null,
|
|
39
|
+
/* @__PURE__ */ jsxs("div", { className: "ggblog-card-meta", children: [
|
|
40
|
+
post.author_name ? /* @__PURE__ */ jsx("span", { children: post.author_name }) : null,
|
|
41
|
+
post.published_at ? /* @__PURE__ */ jsx("time", { dateTime: post.published_at, children: formatDate(post.published_at, locale) }) : null
|
|
42
|
+
] })
|
|
43
|
+
] })
|
|
44
|
+
] }) }, post.post_id)) }),
|
|
45
|
+
pagination.pages > 1 ? /* @__PURE__ */ jsxs("nav", { className: "ggblog-pagination", "aria-label": "Blog pagination", children: [
|
|
46
|
+
pagination.hasPrev ? /* @__PURE__ */ jsx("a", { className: "ggblog-page-prev", href: pageHref(pagination.page - 1), children: "Previous" }) : /* @__PURE__ */ jsx("span", {}),
|
|
47
|
+
/* @__PURE__ */ jsxs("span", { className: "ggblog-page-status", children: [
|
|
48
|
+
"Page ",
|
|
49
|
+
pagination.page,
|
|
50
|
+
" of ",
|
|
51
|
+
pagination.pages
|
|
52
|
+
] }),
|
|
53
|
+
pagination.hasNext ? /* @__PURE__ */ jsx("a", { className: "ggblog-page-next", href: pageHref(pagination.page + 1), children: "Next" }) : /* @__PURE__ */ jsx("span", {})
|
|
54
|
+
] }) : null
|
|
55
|
+
] });
|
|
56
|
+
}
|
|
57
|
+
export {
|
|
58
|
+
BlogList
|
|
59
|
+
};
|
|
60
|
+
//# sourceMappingURL=BlogList.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BlogList.js","sources":["../../src/components/BlogList.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport type { BlogConfig } from '../config';\nimport { getPosts } from '../client/blogClient';\nimport { formatDate } from '../lib/format';\n\nexport interface BlogListProps {\n /** 1-based page number. Default 1. */\n page?: number;\n /** Posts per page. Default 12. */\n limit?: number;\n /** Filter by category slug. */\n category?: string;\n /** Link prefix for post/pagination URLs. Default '/blog'. */\n basePath?: 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 /** Locale for date formatting. Default 'en-US'. */\n locale?: string;\n /** Rendered when there are no posts. */\n emptyState?: ReactNode;\n}\n\n/** Async server component: fetches and renders a paginated list of posts. */\nexport async function BlogList({\n page = 1,\n limit = 12,\n category,\n basePath = '/blog',\n apiBaseUrl,\n tenantId,\n config,\n locale = 'en-US',\n emptyState = null,\n}: BlogListProps) {\n const cfg: Partial<BlogConfig> = {\n ...config,\n ...(apiBaseUrl ? { apiBaseUrl } : {}),\n ...(tenantId ? { tenantId } : {}),\n };\n const { items, pagination } = await getPosts({ page, limit, category }, cfg);\n\n if (items.length === 0) return <>{emptyState}</>;\n\n const prefix = basePath.replace(/\\/$/, '');\n const postHref = (slug: string) => `${prefix}/${slug}`;\n const pageHref = (n: number) => `${prefix}?page=${n}`;\n\n return (\n <div className=\"ggblog-list\">\n <div className=\"ggblog-list-grid\">\n {items.map((post) => (\n <article key={post.post_id} className=\"ggblog-card\">\n <a className=\"ggblog-card-link\" href={postHref(post.slug)}>\n {post.featured_image ? (\n <div className=\"ggblog-card-image\">\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 loading=\"lazy\"\n />\n </div>\n ) : null}\n <div className=\"ggblog-card-body\">\n {post.categories?.[0]?.category?.name ? (\n <span className=\"ggblog-card-category\">{post.categories[0].category!.name}</span>\n ) : null}\n <h3 className=\"ggblog-card-title\">{post.title}</h3>\n {post.excerpt ? <p className=\"ggblog-card-excerpt\">{post.excerpt}</p> : null}\n <div className=\"ggblog-card-meta\">\n {post.author_name ? <span>{post.author_name}</span> : null}\n {post.published_at ? (\n <time dateTime={post.published_at}>{formatDate(post.published_at, locale)}</time>\n ) : null}\n </div>\n </div>\n </a>\n </article>\n ))}\n </div>\n\n {pagination.pages > 1 ? (\n <nav className=\"ggblog-pagination\" aria-label=\"Blog pagination\">\n {pagination.hasPrev ? (\n <a className=\"ggblog-page-prev\" href={pageHref(pagination.page - 1)}>\n Previous\n </a>\n ) : (\n <span />\n )}\n <span className=\"ggblog-page-status\">\n Page {pagination.page} of {pagination.pages}\n </span>\n {pagination.hasNext ? (\n <a className=\"ggblog-page-next\" href={pageHref(pagination.page + 1)}>\n Next\n </a>\n ) : (\n <span />\n )}\n </nav>\n ) : null}\n </div>\n );\n}\n"],"names":[],"mappings":";;;AA2BA,eAAsB,SAAS;AAAA,EAC7B,OAAO;AAAA,EACP,QAAQ;AAAA,EACR;AAAA,EACA,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA;AAAA,EACA,SAAS;AAAA,EACT,aAAa;AACf,GAAkB;AAChB,QAAM,MAA2B;AAAA,IAC/B,GAAG;AAAA,IACH,GAAI,aAAa,EAAE,WAAA,IAAe,CAAA;AAAA,IAClC,GAAI,WAAW,EAAE,aAAa,CAAA;AAAA,EAAC;AAEjC,QAAM,EAAE,OAAO,WAAA,IAAe,MAAM,SAAS,EAAE,MAAM,OAAO,SAAA,GAAY,GAAG;AAE3E,MAAI,MAAM,WAAW,EAAG,wCAAU,UAAA,YAAW;AAE7C,QAAM,SAAS,SAAS,QAAQ,OAAO,EAAE;AACzC,QAAM,WAAW,CAAC,SAAiB,GAAG,MAAM,IAAI,IAAI;AACpD,QAAM,WAAW,CAAC,MAAc,GAAG,MAAM,SAAS,CAAC;AAEnD,SACE,qBAAC,OAAA,EAAI,WAAU,eACb,UAAA;AAAA,IAAA,oBAAC,SAAI,WAAU,oBACZ,gBAAM,IAAI,CAAC,SACV,oBAAC,WAAA,EAA2B,WAAU,eACpC,UAAA,qBAAC,OAAE,WAAU,oBAAmB,MAAM,SAAS,KAAK,IAAI,GACrD,UAAA;AAAA,MAAA,KAAK,iBACJ,oBAAC,OAAA,EAAI,WAAU,qBAEb,UAAA;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,KAAK,KAAK;AAAA,UACV,KAAK,KAAK,sBAAsB,KAAK;AAAA,UACrC,SAAQ;AAAA,QAAA;AAAA,MAAA,GAEZ,IACE;AAAA,MACJ,qBAAC,OAAA,EAAI,WAAU,oBACZ,UAAA;AAAA,QAAA,KAAK,aAAa,CAAC,GAAG,UAAU,OAC/B,oBAAC,QAAA,EAAK,WAAU,wBAAwB,eAAK,WAAW,CAAC,EAAE,SAAU,MAAK,IACxE;AAAA,QACJ,oBAAC,MAAA,EAAG,WAAU,qBAAqB,eAAK,OAAM;AAAA,QAC7C,KAAK,UAAU,oBAAC,KAAA,EAAE,WAAU,uBAAuB,UAAA,KAAK,SAAQ,IAAO;AAAA,QACxE,qBAAC,OAAA,EAAI,WAAU,oBACZ,UAAA;AAAA,UAAA,KAAK,cAAc,oBAAC,QAAA,EAAM,UAAA,KAAK,aAAY,IAAU;AAAA,UACrD,KAAK,eACJ,oBAAC,QAAA,EAAK,UAAU,KAAK,cAAe,UAAA,WAAW,KAAK,cAAc,MAAM,EAAA,CAAE,IACxE;AAAA,QAAA,EAAA,CACN;AAAA,MAAA,EAAA,CACF;AAAA,IAAA,EAAA,CACF,EAAA,GAzBY,KAAK,OA0BnB,CACD,EAAA,CACH;AAAA,IAEC,WAAW,QAAQ,IAClB,qBAAC,SAAI,WAAU,qBAAoB,cAAW,mBAC3C,UAAA;AAAA,MAAA,WAAW,UACV,oBAAC,KAAA,EAAE,WAAU,oBAAmB,MAAM,SAAS,WAAW,OAAO,CAAC,GAAG,UAAA,WAAA,CAErE,wBAEC,QAAA,EAAK;AAAA,MAER,qBAAC,QAAA,EAAK,WAAU,sBAAqB,UAAA;AAAA,QAAA;AAAA,QAC7B,WAAW;AAAA,QAAK;AAAA,QAAK,WAAW;AAAA,MAAA,GACxC;AAAA,MACC,WAAW,UACV,oBAAC,KAAA,EAAE,WAAU,oBAAmB,MAAM,SAAS,WAAW,OAAO,CAAC,GAAG,UAAA,OAAA,CAErE,wBAEC,QAAA,CAAA,CAAK;AAAA,IAAA,EAAA,CAEV,IACE;AAAA,EAAA,GACN;AAEJ;"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { BlogConfig } from '../config';
|
|
3
|
+
import { SeoOptions } from '../seo';
|
|
4
|
+
export interface BlogPostProps {
|
|
5
|
+
/** Slug of the post to render. */
|
|
6
|
+
slug: string;
|
|
7
|
+
/** API base URL (overrides configured/env default). */
|
|
8
|
+
apiBaseUrl?: string;
|
|
9
|
+
/** Tenant id sent as `X-Tenant`. */
|
|
10
|
+
tenantId?: string;
|
|
11
|
+
/** Full config override (takes precedence over apiBaseUrl/tenantId props). */
|
|
12
|
+
config?: Partial<BlogConfig>;
|
|
13
|
+
/** Fire a best-effort view increment. Default true. */
|
|
14
|
+
trackView?: boolean;
|
|
15
|
+
/** Emit BlogPosting + breadcrumb JSON-LD. Default true. */
|
|
16
|
+
renderJsonLd?: boolean;
|
|
17
|
+
/** SEO context (site origin/name) used for JSON-LD canonical URLs. */
|
|
18
|
+
seo?: SeoOptions;
|
|
19
|
+
/** Locale for date formatting. Default 'en-US'. */
|
|
20
|
+
locale?: string;
|
|
21
|
+
/** Rendered when the post is not found (consumer typically calls notFound()). */
|
|
22
|
+
fallback?: ReactNode;
|
|
23
|
+
}
|
|
24
|
+
/** 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>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { jsx, Fragment, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { getPostBySlug, incrementView } from "../client/blogClient.js";
|
|
3
|
+
import { renderBlocks } from "../render/BlockRenderer.js";
|
|
4
|
+
import { formatDate } from "../lib/format.js";
|
|
5
|
+
import { generateBlogPostJsonLd, generateBlogBreadcrumbJsonLd } from "../seo/index.js";
|
|
6
|
+
import { JsonLd } from "./JsonLd.js";
|
|
7
|
+
function mergeConfig(config, apiBaseUrl, tenantId) {
|
|
8
|
+
return {
|
|
9
|
+
...config,
|
|
10
|
+
...apiBaseUrl ? { apiBaseUrl } : {},
|
|
11
|
+
...tenantId ? { tenantId } : {}
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
async function BlogPost({
|
|
15
|
+
slug,
|
|
16
|
+
apiBaseUrl,
|
|
17
|
+
tenantId,
|
|
18
|
+
config,
|
|
19
|
+
trackView = true,
|
|
20
|
+
renderJsonLd = true,
|
|
21
|
+
seo,
|
|
22
|
+
locale = "en-US",
|
|
23
|
+
fallback = null
|
|
24
|
+
}) {
|
|
25
|
+
const cfg = mergeConfig(config, apiBaseUrl, tenantId);
|
|
26
|
+
const post = await getPostBySlug(slug, cfg);
|
|
27
|
+
if (!post) return /* @__PURE__ */ jsx(Fragment, { children: fallback });
|
|
28
|
+
if (trackView) await incrementView(post.post_id, cfg);
|
|
29
|
+
return /* @__PURE__ */ jsxs("article", { className: "ggblog-post", children: [
|
|
30
|
+
renderJsonLd ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
31
|
+
/* @__PURE__ */ jsx(JsonLd, { id: "ggblog-post-jsonld", data: generateBlogPostJsonLd(post, seo) }),
|
|
32
|
+
/* @__PURE__ */ jsx(JsonLd, { id: "ggblog-breadcrumb-jsonld", data: generateBlogBreadcrumbJsonLd(post, seo) })
|
|
33
|
+
] }) : 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) })
|
|
48
|
+
] });
|
|
49
|
+
}
|
|
50
|
+
export {
|
|
51
|
+
BlogPost
|
|
52
|
+
};
|
|
53
|
+
//# sourceMappingURL=BlogPost.js.map
|
|
@@ -0,0 +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;"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
function JsonLd({ data, id }) {
|
|
3
|
+
return /* @__PURE__ */ jsx(
|
|
4
|
+
"script",
|
|
5
|
+
{
|
|
6
|
+
type: "application/ld+json",
|
|
7
|
+
id,
|
|
8
|
+
dangerouslySetInnerHTML: { __html: JSON.stringify(data).replace(/</g, "\\u003c") }
|
|
9
|
+
}
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
export {
|
|
13
|
+
JsonLd
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=JsonLd.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"JsonLd.js","sources":["../../src/components/JsonLd.tsx"],"sourcesContent":["/** Renders a Schema.org JSON-LD block in the server HTML. `<` is escaped so the\n * payload can never break out of the <script> tag. */\nexport function JsonLd({ data, id }: { data: Record<string, unknown>; id?: string }) {\n return (\n <script\n type=\"application/ld+json\"\n id={id}\n dangerouslySetInnerHTML={{ __html: JSON.stringify(data).replace(/</g, '\\\\u003c') }}\n />\n );\n}\n"],"names":[],"mappings":";AAEO,SAAS,OAAO,EAAE,MAAM,MAAsD;AACnF,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,MAAK;AAAA,MACL;AAAA,MACA,yBAAyB,EAAE,QAAQ,KAAK,UAAU,IAAI,EAAE,QAAQ,MAAM,SAAS,EAAA;AAAA,IAAE;AAAA,EAAA;AAGvF;"}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface BlogConfig {
|
|
2
|
+
/** Base URL of the BlogPosts API, e.g. https://api.example.com */
|
|
3
|
+
apiBaseUrl: string;
|
|
4
|
+
/** Tenant/account id sent as the `X-Tenant` header. */
|
|
5
|
+
tenantId?: string;
|
|
6
|
+
/** Extra headers merged into every request. */
|
|
7
|
+
headers?: Record<string, string>;
|
|
8
|
+
/** Custom fetch implementation (defaults to global fetch). */
|
|
9
|
+
fetch?: typeof fetch;
|
|
10
|
+
}
|
|
11
|
+
/** Set process-wide defaults used when a component/fetcher omits config. */
|
|
12
|
+
export declare function configureBlog(config: Partial<BlogConfig>): void;
|
|
13
|
+
/** Defaults from `configureBlog` then environment variables. */
|
|
14
|
+
export declare function getDefaultConfig(): Partial<BlogConfig>;
|
|
15
|
+
/** Merge an optional override over the defaults and validate. */
|
|
16
|
+
export declare function resolveConfig(override?: Partial<BlogConfig>): BlogConfig;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
let moduleDefaults = {};
|
|
2
|
+
function configureBlog(config) {
|
|
3
|
+
moduleDefaults = { ...moduleDefaults, ...config };
|
|
4
|
+
}
|
|
5
|
+
function readEnv(name) {
|
|
6
|
+
if (typeof process === "undefined" || !process.env) return void 0;
|
|
7
|
+
const value = process.env[name];
|
|
8
|
+
return value && value.length > 0 ? value : void 0;
|
|
9
|
+
}
|
|
10
|
+
function getDefaultConfig() {
|
|
11
|
+
return {
|
|
12
|
+
apiBaseUrl: moduleDefaults.apiBaseUrl ?? readEnv("BLOG_API_URL") ?? readEnv("NEXT_PUBLIC_BLOG_API_URL"),
|
|
13
|
+
tenantId: moduleDefaults.tenantId ?? readEnv("BLOG_TENANT_ID") ?? readEnv("NEXT_PUBLIC_BLOG_TENANT_ID"),
|
|
14
|
+
headers: moduleDefaults.headers,
|
|
15
|
+
fetch: moduleDefaults.fetch
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function resolveConfig(override) {
|
|
19
|
+
const merged = { ...getDefaultConfig(), ...override };
|
|
20
|
+
if (!merged.apiBaseUrl) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
"[@gosalesdeal/blog] Missing apiBaseUrl. Pass it as a prop, call configureBlog({ apiBaseUrl }), or set BLOG_API_URL."
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
return merged;
|
|
26
|
+
}
|
|
27
|
+
export {
|
|
28
|
+
configureBlog,
|
|
29
|
+
getDefaultConfig,
|
|
30
|
+
resolveConfig
|
|
31
|
+
};
|
|
32
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sources":["../src/config.ts"],"sourcesContent":["// Runtime configuration for talking to the BlogPosts API. Components accept an\n// inline config via props; `configureBlog` lets an app set process-wide\n// defaults once (e.g. in a server entry) so components can be called with just\n// a slug. Environment variables are the final fallback.\n\nexport interface BlogConfig {\n /** Base URL of the BlogPosts API, e.g. https://api.example.com */\n apiBaseUrl: string;\n /** Tenant/account id sent as the `X-Tenant` header. */\n tenantId?: string;\n /** Extra headers merged into every request. */\n headers?: Record<string, string>;\n /** Custom fetch implementation (defaults to global fetch). */\n fetch?: typeof fetch;\n}\n\nlet moduleDefaults: Partial<BlogConfig> = {};\n\n/** Set process-wide defaults used when a component/fetcher omits config. */\nexport function configureBlog(config: Partial<BlogConfig>): void {\n moduleDefaults = { ...moduleDefaults, ...config };\n}\n\nfunction readEnv(name: string): string | undefined {\n if (typeof process === 'undefined' || !process.env) return undefined;\n const value = process.env[name];\n return value && value.length > 0 ? value : undefined;\n}\n\n/** Defaults from `configureBlog` then environment variables. */\nexport function getDefaultConfig(): Partial<BlogConfig> {\n return {\n apiBaseUrl:\n moduleDefaults.apiBaseUrl ??\n readEnv('BLOG_API_URL') ??\n readEnv('NEXT_PUBLIC_BLOG_API_URL'),\n tenantId:\n moduleDefaults.tenantId ??\n readEnv('BLOG_TENANT_ID') ??\n readEnv('NEXT_PUBLIC_BLOG_TENANT_ID'),\n headers: moduleDefaults.headers,\n fetch: moduleDefaults.fetch,\n };\n}\n\n/** Merge an optional override over the defaults and validate. */\nexport function resolveConfig(override?: Partial<BlogConfig>): BlogConfig {\n const merged = { ...getDefaultConfig(), ...override };\n if (!merged.apiBaseUrl) {\n throw new Error(\n '[@gosalesdeal/blog] Missing apiBaseUrl. Pass it as a prop, call configureBlog({ apiBaseUrl }), or set BLOG_API_URL.',\n );\n }\n return merged as BlogConfig;\n}\n"],"names":[],"mappings":"AAgBA,IAAI,iBAAsC,CAAA;AAGnC,SAAS,cAAc,QAAmC;AAC/D,mBAAiB,EAAE,GAAG,gBAAgB,GAAG,OAAA;AAC3C;AAEA,SAAS,QAAQ,MAAkC;AACjD,MAAI,OAAO,YAAY,eAAe,CAAC,QAAQ,IAAK,QAAO;AAC3D,QAAM,QAAQ,QAAQ,IAAI,IAAI;AAC9B,SAAO,SAAS,MAAM,SAAS,IAAI,QAAQ;AAC7C;AAGO,SAAS,mBAAwC;AACtD,SAAO;AAAA,IACL,YACE,eAAe,cACf,QAAQ,cAAc,KACtB,QAAQ,0BAA0B;AAAA,IACpC,UACE,eAAe,YACf,QAAQ,gBAAgB,KACxB,QAAQ,4BAA4B;AAAA,IACtC,SAAS,eAAe;AAAA,IACxB,OAAO,eAAe;AAAA,EAAA;AAE1B;AAGO,SAAS,cAAc,UAA4C;AACxE,QAAM,SAAS,EAAE,GAAG,iBAAA,GAAoB,GAAG,SAAA;AAC3C,MAAI,CAAC,OAAO,YAAY;AACtB,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAAA,EAEJ;AACA,SAAO;AACT;"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { BlogPost, type BlogPostProps } from './components/BlogPost';
|
|
2
|
+
export { BlogList, type BlogListProps } from './components/BlogList';
|
|
3
|
+
export { JsonLd } from './components/JsonLd';
|
|
4
|
+
export { BlockRenderer, renderBlocks, type BlockRendererProps } from './render/BlockRenderer';
|
|
5
|
+
export { configureBlog, getDefaultConfig, resolveConfig, type BlogConfig } from './config';
|
|
6
|
+
export { createBlogClient, getPostBySlug, getPosts, getPostsByCategory, getPublishedSlugs, getCategories, incrementView, type BlogClient, type GetPostsParams, } from './client/blogClient';
|
|
7
|
+
export { generateBlogPostMetadata, generateBlogListMetadata, generateBlogPostJsonLd, generateBlogBreadcrumbJsonLd, type BlogMetadata, type SeoOptions, } from './seo';
|
|
8
|
+
export { formatDate } from './lib/format';
|
|
9
|
+
export type * from './types/blog';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { BlogPost } from "./components/BlogPost.js";
|
|
2
|
+
import { BlogList } from "./components/BlogList.js";
|
|
3
|
+
import { JsonLd } from "./components/JsonLd.js";
|
|
4
|
+
import { BlockRenderer, renderBlocks } from "./render/BlockRenderer.js";
|
|
5
|
+
import { configureBlog, getDefaultConfig, resolveConfig } from "./config.js";
|
|
6
|
+
import { createBlogClient, getCategories, getPostBySlug, getPosts, getPostsByCategory, getPublishedSlugs, incrementView } from "./client/blogClient.js";
|
|
7
|
+
import { generateBlogBreadcrumbJsonLd, generateBlogListMetadata, generateBlogPostJsonLd, generateBlogPostMetadata } from "./seo/index.js";
|
|
8
|
+
import { formatDate } from "./lib/format.js";
|
|
9
|
+
export {
|
|
10
|
+
BlockRenderer,
|
|
11
|
+
BlogList,
|
|
12
|
+
BlogPost,
|
|
13
|
+
JsonLd,
|
|
14
|
+
configureBlog,
|
|
15
|
+
createBlogClient,
|
|
16
|
+
formatDate,
|
|
17
|
+
generateBlogBreadcrumbJsonLd,
|
|
18
|
+
generateBlogListMetadata,
|
|
19
|
+
generateBlogPostJsonLd,
|
|
20
|
+
generateBlogPostMetadata,
|
|
21
|
+
getCategories,
|
|
22
|
+
getDefaultConfig,
|
|
23
|
+
getPostBySlug,
|
|
24
|
+
getPosts,
|
|
25
|
+
getPostsByCategory,
|
|
26
|
+
getPublishedSlugs,
|
|
27
|
+
incrementView,
|
|
28
|
+
renderBlocks,
|
|
29
|
+
resolveConfig
|
|
30
|
+
};
|
|
31
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
function formatDate(iso, locale = "en-US") {
|
|
2
|
+
if (!iso) return "";
|
|
3
|
+
const date = new Date(iso);
|
|
4
|
+
if (Number.isNaN(date.getTime())) return "";
|
|
5
|
+
return date.toLocaleDateString(locale, { year: "numeric", month: "long", day: "numeric" });
|
|
6
|
+
}
|
|
7
|
+
export {
|
|
8
|
+
formatDate
|
|
9
|
+
};
|
|
10
|
+
//# sourceMappingURL=format.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format.js","sources":["../../src/lib/format.ts"],"sourcesContent":["/** Format an ISO date string for display. Returns '' for missing/invalid input. */\nexport function formatDate(iso?: string, locale = 'en-US'): string {\n if (!iso) return '';\n const date = new Date(iso);\n if (Number.isNaN(date.getTime())) return '';\n return date.toLocaleDateString(locale, { year: 'numeric', month: 'long', day: 'numeric' });\n}\n"],"names":[],"mappings":"AACO,SAAS,WAAW,KAAc,SAAS,SAAiB;AACjE,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,OAAO,IAAI,KAAK,GAAG;AACzB,MAAI,OAAO,MAAM,KAAK,QAAA,CAAS,EAAG,QAAO;AACzC,SAAO,KAAK,mBAAmB,QAAQ,EAAE,MAAM,WAAW,OAAO,QAAQ,KAAK,WAAW;AAC3F;"}
|