chaingrow-blog 0.2.2 → 0.7.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.
@@ -0,0 +1,123 @@
1
+ import { B as Blog, F as FaqItem, a as Block, E as EmbeddedMedia } from './types-DvkCm1rh.js';
2
+
3
+ /**
4
+ * Consumer-supplied extras for BlogPosting. Everything is optional — the
5
+ * SDK doesn't know the customer's base URL, organisation name, or author
6
+ * identity, so it just omits what it isn't told. Schema.org treats
7
+ * `author`, `publisher`, and `url` as recommended but not required.
8
+ */
9
+ type BlogJsonLdOptions = {
10
+ /** Absolute URL of the blog page (e.g. `https://example.com/blog/my-post`). */
11
+ url?: string;
12
+ author?: {
13
+ name: string;
14
+ type?: 'Person' | 'Organization';
15
+ url?: string;
16
+ };
17
+ publisher?: {
18
+ name: string;
19
+ logo?: string;
20
+ };
21
+ };
22
+ /**
23
+ * Build a Schema.org `BlogPosting` JSON-LD object from a chaingrow Blog.
24
+ * Fields that aren't present on the Blog (description, og_image, …) are
25
+ * omitted entirely so the resulting JSON has no `undefined` junk.
26
+ */
27
+ declare function buildBlogPostingSchema(blog: Blog, opts?: BlogJsonLdOptions): Record<string, unknown>;
28
+ /**
29
+ * Build a Schema.org `FAQPage` JSON-LD object from chaingrow's `{question, answer}[]`
30
+ * pairs. Returns `null` for empty input so callers can skip it with a single
31
+ * `if (faqSchema) items.push(faqSchema)` / `filter(Boolean)`.
32
+ */
33
+ declare function buildFAQPageSchema(faq: FaqItem[] | null | undefined): Record<string, unknown> | null;
34
+ /**
35
+ * Combines BlogPosting + FAQPage + any LLM-generated `structured_data.jsonLd`
36
+ * extras into a flat array ready for `@graph`. Tolerant of missing/malformed
37
+ * `structured_data` — if the backend ever sends something unexpected we just
38
+ * skip it rather than crashing the consumer's page.
39
+ */
40
+ declare function buildBlogJsonLd(blog: Blog, opts?: BlogJsonLdOptions): Record<string, unknown>[];
41
+ /** Schema-org context string — exposed so `<JsonLdGraph>` and any
42
+ * bring-your-own renderer use the same value. */
43
+ declare const SCHEMA_ORG_CONTEXT: "https://schema.org";
44
+
45
+ /**
46
+ * Compute the block-index insertion point for each media item when
47
+ * distributing `mediaCount` attachments across `blockCount` blocks.
48
+ *
49
+ * Formula: media `i` goes at insertion index `floor(i * blockCount / mediaCount)`.
50
+ * An insertion index of `0` means "before block 0" (top of body); `blockCount`
51
+ * means "after the last block".
52
+ *
53
+ * Examples (blockCount = 10):
54
+ * mediaCount = 1 → [0] (single media at top)
55
+ * mediaCount = 2 → [0, 5] (top + middle)
56
+ * mediaCount = 3 → [0, 3, 6] (top, 1/3, 2/3)
57
+ * mediaCount = 4 → [0, 2, 5, 7] (every quarter)
58
+ *
59
+ * When `blockCount` is 0 every media falls at position 0 and stacks at the
60
+ * top. When two media round to the same position they both land there and
61
+ * render consecutively; this is deterministic but not always visually ideal
62
+ * for very small `blockCount / mediaCount` ratios — assume content has
63
+ * enough blocks to spread across.
64
+ */
65
+ declare function distributeMediaPositions(blockCount: number, mediaCount: number): number[];
66
+ /** One interleaved slot in a blog's body — either a block or a media item. */
67
+ type InterleavedSlot = {
68
+ kind: 'block';
69
+ block: Block;
70
+ index: number;
71
+ } | {
72
+ kind: 'media';
73
+ media: EmbeddedMedia;
74
+ index: number;
75
+ };
76
+ /**
77
+ * Walk `blocks` once, splicing in `media` at the positions returned by
78
+ * `distributeMediaPositions`. Returns a flat array of typed slots that
79
+ * `<BlogRenderer>` can map over directly. Exposed so consumers who need a
80
+ * custom layout (e.g. wrapping each section in a `<section>`) can use the
81
+ * same placement logic without re-implementing it.
82
+ */
83
+ declare function interleaveMedia(blocks: Block[], media: EmbeddedMedia[] | undefined): InterleavedSlot[];
84
+
85
+ type BlogCard = {
86
+ id: string;
87
+ href: string;
88
+ title: string;
89
+ description: string | null;
90
+ og_image: string | null;
91
+ published_at: string | null;
92
+ language?: string;
93
+ };
94
+ type ToBlogCardOptions = {
95
+ /** URL prefix for the generated `href`. Defaults to `/blog`. */
96
+ pathPrefix?: string;
97
+ /**
98
+ * Full control over the generated `href` — takes precedence over
99
+ * `pathPrefix`. Use this when the route depends on more than a static
100
+ * prefix, e.g. locale-prefixed routes:
101
+ *
102
+ * ```ts
103
+ * hrefFor: (blog) =>
104
+ * blog.language === 'de' ? `/blog/${blog.slug}` : `/${blog.language}/blog/${blog.slug}`
105
+ * ```
106
+ */
107
+ hrefFor?: (blog: Blog) => string;
108
+ };
109
+ /**
110
+ * Project a chaingrow `Blog` into the small `BlogCard` shape used by
111
+ * `<BlogIndex>` and `mergeBlogs`. Hybrid integrators can map their own
112
+ * legacy posts into the same shape (`extra` prop on `<BlogIndex>`) and
113
+ * the two sources render through the same Card slot.
114
+ */
115
+ declare function toBlogCard(blog: Blog, opts?: ToBlogCardOptions): BlogCard;
116
+ /**
117
+ * Merge a list of chaingrow cards with extra (legacy) cards and return a
118
+ * single list sorted newest-first by `published_at`. Cards without a
119
+ * `published_at` sort to the end.
120
+ */
121
+ declare function mergeBlogs(chaingrow: BlogCard[], extra?: BlogCard[]): BlogCard[];
122
+
123
+ export { type BlogCard as B, type InterleavedSlot as I, SCHEMA_ORG_CONTEXT as S, type ToBlogCardOptions as T, type BlogJsonLdOptions as a, buildBlogJsonLd as b, buildBlogPostingSchema as c, buildFAQPageSchema as d, distributeMediaPositions as e, interleaveMedia as i, mergeBlogs as m, toBlogCard as t };
@@ -0,0 +1,123 @@
1
+ import { B as Blog, F as FaqItem, a as Block, E as EmbeddedMedia } from './types-DvkCm1rh.cjs';
2
+
3
+ /**
4
+ * Consumer-supplied extras for BlogPosting. Everything is optional — the
5
+ * SDK doesn't know the customer's base URL, organisation name, or author
6
+ * identity, so it just omits what it isn't told. Schema.org treats
7
+ * `author`, `publisher`, and `url` as recommended but not required.
8
+ */
9
+ type BlogJsonLdOptions = {
10
+ /** Absolute URL of the blog page (e.g. `https://example.com/blog/my-post`). */
11
+ url?: string;
12
+ author?: {
13
+ name: string;
14
+ type?: 'Person' | 'Organization';
15
+ url?: string;
16
+ };
17
+ publisher?: {
18
+ name: string;
19
+ logo?: string;
20
+ };
21
+ };
22
+ /**
23
+ * Build a Schema.org `BlogPosting` JSON-LD object from a chaingrow Blog.
24
+ * Fields that aren't present on the Blog (description, og_image, …) are
25
+ * omitted entirely so the resulting JSON has no `undefined` junk.
26
+ */
27
+ declare function buildBlogPostingSchema(blog: Blog, opts?: BlogJsonLdOptions): Record<string, unknown>;
28
+ /**
29
+ * Build a Schema.org `FAQPage` JSON-LD object from chaingrow's `{question, answer}[]`
30
+ * pairs. Returns `null` for empty input so callers can skip it with a single
31
+ * `if (faqSchema) items.push(faqSchema)` / `filter(Boolean)`.
32
+ */
33
+ declare function buildFAQPageSchema(faq: FaqItem[] | null | undefined): Record<string, unknown> | null;
34
+ /**
35
+ * Combines BlogPosting + FAQPage + any LLM-generated `structured_data.jsonLd`
36
+ * extras into a flat array ready for `@graph`. Tolerant of missing/malformed
37
+ * `structured_data` — if the backend ever sends something unexpected we just
38
+ * skip it rather than crashing the consumer's page.
39
+ */
40
+ declare function buildBlogJsonLd(blog: Blog, opts?: BlogJsonLdOptions): Record<string, unknown>[];
41
+ /** Schema-org context string — exposed so `<JsonLdGraph>` and any
42
+ * bring-your-own renderer use the same value. */
43
+ declare const SCHEMA_ORG_CONTEXT: "https://schema.org";
44
+
45
+ /**
46
+ * Compute the block-index insertion point for each media item when
47
+ * distributing `mediaCount` attachments across `blockCount` blocks.
48
+ *
49
+ * Formula: media `i` goes at insertion index `floor(i * blockCount / mediaCount)`.
50
+ * An insertion index of `0` means "before block 0" (top of body); `blockCount`
51
+ * means "after the last block".
52
+ *
53
+ * Examples (blockCount = 10):
54
+ * mediaCount = 1 → [0] (single media at top)
55
+ * mediaCount = 2 → [0, 5] (top + middle)
56
+ * mediaCount = 3 → [0, 3, 6] (top, 1/3, 2/3)
57
+ * mediaCount = 4 → [0, 2, 5, 7] (every quarter)
58
+ *
59
+ * When `blockCount` is 0 every media falls at position 0 and stacks at the
60
+ * top. When two media round to the same position they both land there and
61
+ * render consecutively; this is deterministic but not always visually ideal
62
+ * for very small `blockCount / mediaCount` ratios — assume content has
63
+ * enough blocks to spread across.
64
+ */
65
+ declare function distributeMediaPositions(blockCount: number, mediaCount: number): number[];
66
+ /** One interleaved slot in a blog's body — either a block or a media item. */
67
+ type InterleavedSlot = {
68
+ kind: 'block';
69
+ block: Block;
70
+ index: number;
71
+ } | {
72
+ kind: 'media';
73
+ media: EmbeddedMedia;
74
+ index: number;
75
+ };
76
+ /**
77
+ * Walk `blocks` once, splicing in `media` at the positions returned by
78
+ * `distributeMediaPositions`. Returns a flat array of typed slots that
79
+ * `<BlogRenderer>` can map over directly. Exposed so consumers who need a
80
+ * custom layout (e.g. wrapping each section in a `<section>`) can use the
81
+ * same placement logic without re-implementing it.
82
+ */
83
+ declare function interleaveMedia(blocks: Block[], media: EmbeddedMedia[] | undefined): InterleavedSlot[];
84
+
85
+ type BlogCard = {
86
+ id: string;
87
+ href: string;
88
+ title: string;
89
+ description: string | null;
90
+ og_image: string | null;
91
+ published_at: string | null;
92
+ language?: string;
93
+ };
94
+ type ToBlogCardOptions = {
95
+ /** URL prefix for the generated `href`. Defaults to `/blog`. */
96
+ pathPrefix?: string;
97
+ /**
98
+ * Full control over the generated `href` — takes precedence over
99
+ * `pathPrefix`. Use this when the route depends on more than a static
100
+ * prefix, e.g. locale-prefixed routes:
101
+ *
102
+ * ```ts
103
+ * hrefFor: (blog) =>
104
+ * blog.language === 'de' ? `/blog/${blog.slug}` : `/${blog.language}/blog/${blog.slug}`
105
+ * ```
106
+ */
107
+ hrefFor?: (blog: Blog) => string;
108
+ };
109
+ /**
110
+ * Project a chaingrow `Blog` into the small `BlogCard` shape used by
111
+ * `<BlogIndex>` and `mergeBlogs`. Hybrid integrators can map their own
112
+ * legacy posts into the same shape (`extra` prop on `<BlogIndex>`) and
113
+ * the two sources render through the same Card slot.
114
+ */
115
+ declare function toBlogCard(blog: Blog, opts?: ToBlogCardOptions): BlogCard;
116
+ /**
117
+ * Merge a list of chaingrow cards with extra (legacy) cards and return a
118
+ * single list sorted newest-first by `published_at`. Cards without a
119
+ * `published_at` sort to the end.
120
+ */
121
+ declare function mergeBlogs(chaingrow: BlogCard[], extra?: BlogCard[]): BlogCard[];
122
+
123
+ export { type BlogCard as B, type InterleavedSlot as I, SCHEMA_ORG_CONTEXT as S, type ToBlogCardOptions as T, type BlogJsonLdOptions as a, buildBlogJsonLd as b, buildBlogPostingSchema as c, buildFAQPageSchema as d, distributeMediaPositions as e, interleaveMedia as i, mergeBlogs as m, toBlogCard as t };
@@ -0,0 +1,34 @@
1
+ // src/blog-cards.ts
2
+ var normalizePathPrefix = (s) => {
3
+ let p = s.trim();
4
+ if (!p.startsWith("/")) p = "/" + p;
5
+ while (p.length > 1 && p.endsWith("/")) p = p.slice(0, -1);
6
+ return p;
7
+ };
8
+ function toBlogCard(blog, opts = {}) {
9
+ const prefix = normalizePathPrefix(opts.pathPrefix ?? "/blog");
10
+ return {
11
+ id: blog.id,
12
+ href: opts.hrefFor ? opts.hrefFor(blog) : `${prefix}/${blog.slug}`,
13
+ title: blog.title,
14
+ description: blog.description,
15
+ og_image: blog.og_image,
16
+ published_at: blog.published_at,
17
+ language: blog.language
18
+ };
19
+ }
20
+ function mergeBlogs(chaingrow, extra = []) {
21
+ const combined = [...chaingrow, ...extra];
22
+ combined.sort((a, b) => {
23
+ const ta = a.published_at ? Date.parse(a.published_at) : 0;
24
+ const tb = b.published_at ? Date.parse(b.published_at) : 0;
25
+ return tb - ta;
26
+ });
27
+ return combined;
28
+ }
29
+
30
+ export {
31
+ toBlogCard,
32
+ mergeBlogs
33
+ };
34
+ //# sourceMappingURL=chunk-4EKET2DQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/blog-cards.ts"],"sourcesContent":["import type { Blog } from './types';\n\nexport type BlogCard = {\n id: string;\n href: string;\n title: string;\n description: string | null;\n og_image: string | null;\n published_at: string | null;\n language?: string;\n};\n\nexport type ToBlogCardOptions = {\n /** URL prefix for the generated `href`. Defaults to `/blog`. */\n pathPrefix?: string;\n /**\n * Full control over the generated `href` — takes precedence over\n * `pathPrefix`. Use this when the route depends on more than a static\n * prefix, e.g. locale-prefixed routes:\n *\n * ```ts\n * hrefFor: (blog) =>\n * blog.language === 'de' ? `/blog/${blog.slug}` : `/${blog.language}/blog/${blog.slug}`\n * ```\n */\n hrefFor?: (blog: Blog) => string;\n};\n\nconst normalizePathPrefix = (s: string): string => {\n let p = s.trim();\n if (!p.startsWith('/')) p = '/' + p;\n while (p.length > 1 && p.endsWith('/')) p = p.slice(0, -1);\n return p;\n};\n\n/**\n * Project a chaingrow `Blog` into the small `BlogCard` shape used by\n * `<BlogIndex>` and `mergeBlogs`. Hybrid integrators can map their own\n * legacy posts into the same shape (`extra` prop on `<BlogIndex>`) and\n * the two sources render through the same Card slot.\n */\nexport function toBlogCard(blog: Blog, opts: ToBlogCardOptions = {}): BlogCard {\n const prefix = normalizePathPrefix(opts.pathPrefix ?? '/blog');\n return {\n id: blog.id,\n href: opts.hrefFor ? opts.hrefFor(blog) : `${prefix}/${blog.slug}`,\n title: blog.title,\n description: blog.description,\n og_image: blog.og_image,\n published_at: blog.published_at,\n language: blog.language,\n };\n}\n\n/**\n * Merge a list of chaingrow cards with extra (legacy) cards and return a\n * single list sorted newest-first by `published_at`. Cards without a\n * `published_at` sort to the end.\n */\nexport function mergeBlogs(\n chaingrow: BlogCard[],\n extra: BlogCard[] = [],\n): BlogCard[] {\n const combined = [...chaingrow, ...extra];\n combined.sort((a, b) => {\n const ta = a.published_at ? Date.parse(a.published_at) : 0;\n const tb = b.published_at ? Date.parse(b.published_at) : 0;\n return tb - ta;\n });\n return combined;\n}\n"],"mappings":";AA4BA,IAAM,sBAAsB,CAAC,MAAsB;AACjD,MAAI,IAAI,EAAE,KAAK;AACf,MAAI,CAAC,EAAE,WAAW,GAAG,EAAG,KAAI,MAAM;AAClC,SAAO,EAAE,SAAS,KAAK,EAAE,SAAS,GAAG,EAAG,KAAI,EAAE,MAAM,GAAG,EAAE;AACzD,SAAO;AACT;AAQO,SAAS,WAAW,MAAY,OAA0B,CAAC,GAAa;AAC7E,QAAM,SAAS,oBAAoB,KAAK,cAAc,OAAO;AAC7D,SAAO;AAAA,IACL,IAAI,KAAK;AAAA,IACT,MAAM,KAAK,UAAU,KAAK,QAAQ,IAAI,IAAI,GAAG,MAAM,IAAI,KAAK,IAAI;AAAA,IAChE,OAAO,KAAK;AAAA,IACZ,aAAa,KAAK;AAAA,IAClB,UAAU,KAAK;AAAA,IACf,cAAc,KAAK;AAAA,IACnB,UAAU,KAAK;AAAA,EACjB;AACF;AAOO,SAAS,WACd,WACA,QAAoB,CAAC,GACT;AACZ,QAAM,WAAW,CAAC,GAAG,WAAW,GAAG,KAAK;AACxC,WAAS,KAAK,CAAC,GAAG,MAAM;AACtB,UAAM,KAAK,EAAE,eAAe,KAAK,MAAM,EAAE,YAAY,IAAI;AACzD,UAAM,KAAK,EAAE,eAAe,KAAK,MAAM,EAAE,YAAY,IAAI;AACzD,WAAO,KAAK;AAAA,EACd,CAAC;AACD,SAAO;AACT;","names":[]}