chaingrow-blog 0.7.0 → 0.7.2
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 +3 -1
- package/dist/{blog-cards-D1j7ZqHK.d.ts → blog-cards-CcHb2bcv.d.ts} +12 -3
- package/dist/{blog-cards-DPfcmUUD.d.cts → blog-cards-PjckhJAG.d.cts} +12 -3
- package/dist/{chunk-BXEQ6QKM.js → chunk-2FIOFNM6.js} +4 -3
- package/dist/{chunk-BXEQ6QKM.js.map → chunk-2FIOFNM6.js.map} +1 -1
- package/dist/{chunk-K3HNYPEP.js → chunk-KDUQNIJ2.js} +61 -18
- package/dist/chunk-KDUQNIJ2.js.map +1 -0
- package/dist/index.cjs +62 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/next/index.cjs +59 -18
- package/dist/next/index.cjs.map +1 -1
- package/dist/next/index.d.cts +1 -1
- package/dist/next/index.d.ts +1 -1
- package/dist/next/index.js +2 -2
- package/dist/next/webhook.d.cts +1 -1
- package/dist/next/webhook.d.ts +1 -1
- package/dist/react/index.cjs +59 -18
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +4 -4
- package/dist/react/index.d.ts +4 -4
- package/dist/react/index.js +2 -2
- package/dist/{types-DvkCm1rh.d.ts → types-DSBF7V30.d.cts} +6 -4
- package/dist/{types-DvkCm1rh.d.cts → types-DSBF7V30.d.ts} +6 -4
- package/package.json +18 -5
- package/dist/chunk-K3HNYPEP.js.map +0 -1
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/client.ts","../src/json-ld.ts","../src/media-placement.ts","../src/sitemap-builder.ts","../src/blog-cards.ts"],"sourcesContent":["export * from './types';\nexport { createClient, type ChaingrowClient, type ClientOptions } from './client';\nexport {\n buildBlogPostingSchema,\n buildFAQPageSchema,\n buildBlogJsonLd,\n SCHEMA_ORG_CONTEXT,\n type BlogJsonLdOptions,\n} from './json-ld';\nexport {\n distributeMediaPositions,\n interleaveMedia,\n type InterleavedSlot,\n} from './media-placement';\nexport {\n buildSitemapXml,\n buildNewsSitemapXml,\n filterRecentForNews,\n escapeXml,\n type SitemapBuildOptions,\n type NewsSitemapBuildOptions,\n} from './sitemap-builder';\nexport {\n toBlogCard,\n mergeBlogs,\n type BlogCard,\n type ToBlogCardOptions,\n} from './blog-cards';\n","import type { Blog } from './types';\n\nexport type ClientOptions = {\n apiUrl: string;\n apiKey: string;\n fetch?: typeof fetch;\n};\n\nexport type ListBlogsOptions = {\n status?: 'DRAFT' | 'PUBLISHED';\n language?: string;\n};\n\nexport type ChaingrowClient = {\n getBlog(slug: string, language?: string): Promise<Blog | null>;\n getBlogById(id: string): Promise<Blog | null>;\n listBlogs(opts?: ListBlogsOptions): Promise<Blog[]>;\n};\n\nexport function createClient(opts: ClientOptions): ChaingrowClient {\n const baseUrl = opts.apiUrl.replace(/\\/$/, '');\n const doFetch = opts.fetch ?? fetch;\n\n const request = async <T>(\n path: string,\n init?: RequestInit,\n ): Promise<T | null> => {\n const url = `${baseUrl}${path}`;\n const method = init?.method ?? 'GET';\n const buildInit = (): RequestInit => ({\n ...init,\n headers: {\n 'X-API-Key': opts.apiKey,\n Accept: 'application/json',\n ...(init?.headers ?? {}),\n },\n });\n\n let res: Response;\n try {\n res = await doFetch(url, buildInit());\n\n // Transparent one-shot retry on 429, honoring Retry-After (seconds).\n if (res.status === 429) {\n const waitSec = parseInt(res.headers.get('Retry-After') ?? '1', 10);\n await new Promise((resolve) =>\n setTimeout(resolve, Math.max(0, waitSec) * 1000),\n );\n res = await doFetch(url, buildInit());\n }\n } catch (err) {\n // `fetch failed` / DNS / connection errors land here. Node's default\n // message buries the host in `err.cause`, which makes integration\n // debugging miserable. Surface the URL and the override env var on\n // the top-level message so it appears in the consumer's server log.\n const cause = err instanceof Error ? err.cause : undefined;\n const code =\n cause && typeof cause === 'object' && 'code' in cause\n ? String((cause as { code?: unknown }).code)\n : undefined;\n const detail = code\n ? `${code}${err instanceof Error && err.message ? ` (${err.message})` : ''}`\n : err instanceof Error\n ? err.message\n : String(err);\n const e = new Error(\n `chaingrow-blog: could not reach ${method} ${url} — ${detail}. ` +\n `Set CHAINGROW_API_URL to override the API host (default https://api.chaingrow.de).`,\n );\n (e as Error & { cause?: unknown }).cause = err;\n throw e;\n }\n\n if (res.status === 404) return null;\n if (!res.ok) {\n throw new Error(\n `chaingrow-blog: ${method} ${url} failed (${res.status})`,\n );\n }\n return (await res.json()) as T;\n };\n\n return {\n async getBlog(slug, language) {\n const qs = language ? `?language=${encodeURIComponent(language)}` : '';\n return request<Blog>(\n `/api/v1/blogs/by-slug/${encodeURIComponent(slug)}${qs}`,\n );\n },\n\n async getBlogById(id) {\n return request<Blog>(`/api/v1/blogs/${encodeURIComponent(id)}`);\n },\n\n async listBlogs(listOpts = {}) {\n const collected: Blog[] = [];\n // Page through until the server-reported `total` is reached. Backends\n // without pagination return everything on the first request\n // (total === data.length), so this loop runs exactly once there.\n const MAX_PAGES = 100;\n for (let page = 0; page < MAX_PAGES; page++) {\n const params = new URLSearchParams();\n if (listOpts.status) params.set('status', listOpts.status);\n if (listOpts.language) params.set('language', listOpts.language);\n if (collected.length > 0)\n params.set('offset', String(collected.length));\n const qs = params.toString();\n const response = await request<{ data: Blog[]; total: number }>(\n `/api/v1/blogs${qs ? `?${qs}` : ''}`,\n );\n if (!response) break;\n const batch = response.data ?? [];\n collected.push(...batch);\n const total =\n typeof response.total === 'number' ? response.total : collected.length;\n if (batch.length === 0 || collected.length >= total) break;\n }\n // The language filter also runs server-side (API builds that predate\n // it ignore the query param) — keep this client-side pass as a\n // fallback; against a filtering server it's a no-op.\n if (listOpts.language) {\n return collected.filter((b) => b.language === listOpts.language);\n }\n return collected;\n },\n };\n}\n","import type { Blog, FaqItem } from './types';\n\n/**\n * Consumer-supplied extras for BlogPosting. Everything is optional — the\n * SDK doesn't know the customer's base URL, organisation name, or author\n * identity, so it just omits what it isn't told. Schema.org treats\n * `author`, `publisher`, and `url` as recommended but not required.\n */\nexport type BlogJsonLdOptions = {\n /** Absolute URL of the blog page (e.g. `https://example.com/blog/my-post`). */\n url?: string;\n author?: { name: string; type?: 'Person' | 'Organization'; url?: string };\n publisher?: { name: string; logo?: string };\n};\n\nconst Schema = Object.freeze({ context: 'https://schema.org' });\n\n/**\n * Build a Schema.org `BlogPosting` JSON-LD object from a chaingrow Blog.\n * Fields that aren't present on the Blog (description, og_image, …) are\n * omitted entirely so the resulting JSON has no `undefined` junk.\n */\nexport function buildBlogPostingSchema(\n blog: Blog,\n opts: BlogJsonLdOptions = {},\n): Record<string, unknown> {\n const schema: Record<string, unknown> = {\n '@type': 'BlogPosting',\n headline: blog.title,\n datePublished: blog.published_at,\n };\n if (blog.description) schema.description = blog.description;\n if (blog.og_image) schema.image = blog.og_image;\n if (opts.url) {\n schema.url = opts.url;\n schema.mainEntityOfPage = opts.url;\n }\n if (opts.author) {\n schema.author = {\n '@type': opts.author.type ?? 'Organization',\n name: opts.author.name,\n ...(opts.author.url ? { url: opts.author.url } : {}),\n };\n }\n if (opts.publisher) {\n schema.publisher = {\n '@type': 'Organization',\n name: opts.publisher.name,\n ...(opts.publisher.logo\n ? { logo: { '@type': 'ImageObject', url: opts.publisher.logo } }\n : {}),\n };\n }\n return schema;\n}\n\n/**\n * Build a Schema.org `FAQPage` JSON-LD object from chaingrow's `{question, answer}[]`\n * pairs. Returns `null` for empty input so callers can skip it with a single\n * `if (faqSchema) items.push(faqSchema)` / `filter(Boolean)`.\n */\nexport function buildFAQPageSchema(\n faq: FaqItem[] | null | undefined,\n): Record<string, unknown> | null {\n if (!faq || faq.length === 0) return null;\n return {\n '@type': 'FAQPage',\n mainEntity: faq.map(({ question, answer }) => ({\n '@type': 'Question',\n name: question,\n acceptedAnswer: { '@type': 'Answer', text: answer },\n })),\n };\n}\n\n/**\n * Combines BlogPosting + FAQPage + any LLM-generated `structured_data.jsonLd`\n * extras into a flat array ready for `@graph`. Tolerant of missing/malformed\n * `structured_data` — if the backend ever sends something unexpected we just\n * skip it rather than crashing the consumer's page.\n */\nexport function buildBlogJsonLd(\n blog: Blog,\n opts: BlogJsonLdOptions = {},\n): Record<string, unknown>[] {\n const items: Record<string, unknown>[] = [buildBlogPostingSchema(blog, opts)];\n const faq = buildFAQPageSchema(blog.faq);\n if (faq) items.push(faq);\n items.push(...extractLlmJsonLd(blog.structured_data));\n return items;\n}\n\n/** Schema-org context string — exposed so `<JsonLdGraph>` and any\n * bring-your-own renderer use the same value. */\nexport const SCHEMA_ORG_CONTEXT = Schema.context;\n\n/**\n * Pull the JSON-LD array out of `blog.structured_data`. Per\n * `structuredDataOutputSchema` in @chaingrow/contracts the expected shape is\n * `{ jsonLd: Array<Record<string, unknown>> }`, but historical rows may be\n * `null`, a plain object, or even malformed — normalise to an empty array in\n * every degenerate case so the caller gets a safe spread target.\n */\nfunction extractLlmJsonLd(\n sd: Blog['structured_data'],\n): Record<string, unknown>[] {\n if (!sd || typeof sd !== 'object') return [];\n const ld = (sd as { jsonLd?: unknown }).jsonLd;\n if (!Array.isArray(ld)) return [];\n return ld.filter(\n (entry): entry is Record<string, unknown> =>\n typeof entry === 'object' && entry !== null && !Array.isArray(entry),\n );\n}\n","import type { Block, EmbeddedMedia } from './types';\n\n/**\n * Compute the block-index insertion point for each media item when\n * distributing `mediaCount` attachments across `blockCount` blocks.\n *\n * Formula: media `i` goes at insertion index `floor(i * blockCount / mediaCount)`.\n * An insertion index of `0` means \"before block 0\" (top of body); `blockCount`\n * means \"after the last block\".\n *\n * Examples (blockCount = 10):\n * mediaCount = 1 → [0] (single media at top)\n * mediaCount = 2 → [0, 5] (top + middle)\n * mediaCount = 3 → [0, 3, 6] (top, 1/3, 2/3)\n * mediaCount = 4 → [0, 2, 5, 7] (every quarter)\n *\n * When `blockCount` is 0 every media falls at position 0 and stacks at the\n * top. When two media round to the same position they both land there and\n * render consecutively; this is deterministic but not always visually ideal\n * for very small `blockCount / mediaCount` ratios — assume content has\n * enough blocks to spread across.\n */\nexport function distributeMediaPositions(\n blockCount: number,\n mediaCount: number,\n): number[] {\n if (mediaCount <= 0) return [];\n if (blockCount <= 0) return Array.from({ length: mediaCount }, () => 0);\n return Array.from({ length: mediaCount }, (_, i) =>\n Math.floor((i * blockCount) / mediaCount),\n );\n}\n\n/** One interleaved slot in a blog's body — either a block or a media item. */\nexport type InterleavedSlot =\n | { kind: 'block'; block: Block; index: number }\n | { kind: 'media'; media: EmbeddedMedia; index: number };\n\n/**\n * Walk `blocks` once, splicing in `media` at the positions returned by\n * `distributeMediaPositions`. Returns a flat array of typed slots that\n * `<BlogRenderer>` can map over directly. Exposed so consumers who need a\n * custom layout (e.g. wrapping each section in a `<section>`) can use the\n * same placement logic without re-implementing it.\n */\nexport function interleaveMedia(\n blocks: Block[],\n media: EmbeddedMedia[] | undefined,\n): InterleavedSlot[] {\n const mediaList = media ?? [];\n const positions = distributeMediaPositions(blocks.length, mediaList.length);\n\n const slots: InterleavedSlot[] = [];\n let mediaCursor = 0;\n for (let b = 0; b <= blocks.length; b++) {\n while (mediaCursor < mediaList.length && positions[mediaCursor] === b) {\n const media = mediaList[mediaCursor]!;\n slots.push({ kind: 'media', media, index: mediaCursor });\n mediaCursor++;\n }\n if (b < blocks.length) {\n const block = blocks[b]!;\n slots.push({ kind: 'block', block, index: b });\n }\n }\n return slots;\n}\n","import type { Blog, EmbeddedMedia } from './types';\n\nconst NEWS_WINDOW_MS = 48 * 60 * 60 * 1000;\nconst NEWS_MAX_ENTRIES = 1000;\n\nexport type SitemapBuildOptions = {\n baseUrl: string;\n pathPrefix?: string;\n};\n\nexport type NewsSitemapBuildOptions = SitemapBuildOptions & {\n publicationName: string;\n defaultLanguage?: string;\n now?: Date;\n};\n\nexport function escapeXml(s: string): string {\n return s\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\nconst trimTrailingSlash = (s: string): string => s.replace(/\\/+$/, '');\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\nconst buildBlogUrl = (\n baseUrl: string,\n pathPrefix: string,\n slug: string,\n): string => `${baseUrl}${pathPrefix}/${slug}`;\n\nconst isImageMedia = (m: EmbeddedMedia): boolean => m.media_type === 'IMAGE';\n\nconst renderImageEntry = (m: EmbeddedMedia): string =>\n ` <image:image>\\n` +\n ` <image:loc>${escapeXml(m.media_url)}</image:loc>\\n` +\n ` </image:image>\\n`;\n\nconst lastmodFor = (blog: Blog): string => blog.published_at;\n\nexport function buildSitemapXml(\n blogs: Blog[],\n opts: SitemapBuildOptions,\n): string {\n const baseUrl = trimTrailingSlash(opts.baseUrl);\n const pathPrefix = normalizePathPrefix(opts.pathPrefix ?? '/blog');\n\n const entries = blogs\n .map((blog) => {\n const url = buildBlogUrl(baseUrl, pathPrefix, blog.slug);\n const images = (blog.generatedMedia ?? [])\n .filter(isImageMedia)\n .map(renderImageEntry)\n .join('');\n return (\n ` <url>\\n` +\n ` <loc>${escapeXml(url)}</loc>\\n` +\n ` <lastmod>${escapeXml(lastmodFor(blog))}</lastmod>\\n` +\n images +\n ` </url>\\n`\n );\n })\n .join('');\n\n return (\n `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n` +\n `<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\" ` +\n `xmlns:image=\"http://www.google.com/schemas/sitemap-image/1.1\">\\n` +\n entries +\n `</urlset>\\n`\n );\n}\n\n/** Generic over anything carrying `published_at` so custom blog shapes\n * (or intersections with extra fields) pass through without a cast. */\nexport function filterRecentForNews<T extends Pick<Blog, 'published_at'>>(\n blogs: T[],\n now: Date = new Date(),\n): T[] {\n const cutoff = now.getTime() - NEWS_WINDOW_MS;\n const recent = blogs.filter((b) => {\n const t = Date.parse(b.published_at);\n return Number.isFinite(t) && t >= cutoff;\n });\n recent.sort(\n (a, b) => Date.parse(b.published_at) - Date.parse(a.published_at),\n );\n return recent.slice(0, NEWS_MAX_ENTRIES);\n}\n\nexport function buildNewsSitemapXml(\n blogs: Blog[],\n opts: NewsSitemapBuildOptions,\n): string {\n const baseUrl = trimTrailingSlash(opts.baseUrl);\n const pathPrefix = normalizePathPrefix(opts.pathPrefix ?? '/blog');\n const publicationName = opts.publicationName;\n const defaultLanguage = opts.defaultLanguage ?? 'en';\n\n const recent = filterRecentForNews(blogs, opts.now);\n\n const entries = recent\n .map((blog) => {\n const url = buildBlogUrl(baseUrl, pathPrefix, blog.slug);\n const language = blog.language || defaultLanguage;\n return (\n ` <url>\\n` +\n ` <loc>${escapeXml(url)}</loc>\\n` +\n ` <news:news>\\n` +\n ` <news:publication>\\n` +\n ` <news:name>${escapeXml(publicationName)}</news:name>\\n` +\n ` <news:language>${escapeXml(language)}</news:language>\\n` +\n ` </news:publication>\\n` +\n ` <news:publication_date>${escapeXml(blog.published_at)}</news:publication_date>\\n` +\n ` <news:title>${escapeXml(blog.title)}</news:title>\\n` +\n ` </news:news>\\n` +\n ` </url>\\n`\n );\n })\n .join('');\n\n return (\n `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n` +\n `<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\" ` +\n `xmlns:news=\"http://www.google.com/schemas/sitemap-news/0.9\">\\n` +\n entries +\n `</urlset>\\n`\n );\n}\n","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":"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","names":["media","normalizePathPrefix"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/client.ts","../src/json-ld.ts","../src/media-placement.ts","../src/sitemap-builder.ts","../src/blog-cards.ts"],"sourcesContent":["export * from './types';\nexport { createClient, type ChaingrowClient, type ClientOptions } from './client';\nexport {\n buildBlogPostingSchema,\n buildFAQPageSchema,\n buildBlogJsonLd,\n SCHEMA_ORG_CONTEXT,\n type BlogJsonLdOptions,\n} from './json-ld';\nexport {\n distributeMediaPositions,\n interleaveMedia,\n placedMediaIds,\n hasTextImage,\n type InterleavedSlot,\n} from './media-placement';\nexport {\n buildSitemapXml,\n buildNewsSitemapXml,\n filterRecentForNews,\n escapeXml,\n type SitemapBuildOptions,\n type NewsSitemapBuildOptions,\n} from './sitemap-builder';\nexport {\n toBlogCard,\n mergeBlogs,\n type BlogCard,\n type ToBlogCardOptions,\n} from './blog-cards';\n","import type { Blog } from './types';\n\nexport type ClientOptions = {\n apiUrl: string;\n apiKey: string;\n fetch?: typeof fetch;\n};\n\nexport type ListBlogsOptions = {\n status?: 'DRAFT' | 'PUBLISHED';\n language?: string;\n};\n\nexport type ChaingrowClient = {\n getBlog(slug: string, language?: string): Promise<Blog | null>;\n getBlogById(id: string): Promise<Blog | null>;\n listBlogs(opts?: ListBlogsOptions): Promise<Blog[]>;\n};\n\n// Error codes that indicate a transient network hiccup rather than a broken\n// setup: flaky DNS resolution (musl containers throw EAI_AGAIN under load),\n// dropped or timed-out connections. Anything else (bad host, TLS failure)\n// fails fast so misconfiguration stays visible.\nconst TRANSIENT_NETWORK_CODES = new Set([\n 'EAI_AGAIN',\n 'ETIMEDOUT',\n 'ECONNRESET',\n 'UND_ERR_CONNECT_TIMEOUT',\n 'UND_ERR_SOCKET',\n]);\n\nconst NETWORK_ATTEMPTS = 3;\n\nfunction causeCode(err: unknown): string | undefined {\n const cause = err instanceof Error ? err.cause : undefined;\n return cause && typeof cause === 'object' && 'code' in cause\n ? String((cause as { code?: unknown }).code)\n : undefined;\n}\n\nexport function createClient(opts: ClientOptions): ChaingrowClient {\n const baseUrl = opts.apiUrl.replace(/\\/$/, '');\n const doFetch = opts.fetch ?? fetch;\n\n const request = async <T>(\n path: string,\n init?: RequestInit,\n ): Promise<T | null> => {\n const url = `${baseUrl}${path}`;\n const method = init?.method ?? 'GET';\n const buildInit = (): RequestInit => ({\n ...init,\n headers: {\n 'X-API-Key': opts.apiKey,\n Accept: 'application/json',\n ...(init?.headers ?? {}),\n },\n });\n\n let res: Response | undefined;\n for (let attempt = 1; attempt <= NETWORK_ATTEMPTS; attempt++) {\n try {\n res = await doFetch(url, buildInit());\n\n // Transparent one-shot retry on 429, honoring Retry-After (seconds).\n if (res.status === 429) {\n const waitSec = parseInt(res.headers.get('Retry-After') ?? '1', 10);\n await new Promise((resolve) =>\n setTimeout(resolve, Math.max(0, waitSec) * 1000),\n );\n res = await doFetch(url, buildInit());\n }\n break;\n } catch (err) {\n const code = causeCode(err);\n if (\n attempt < NETWORK_ATTEMPTS &&\n code !== undefined &&\n TRANSIENT_NETWORK_CODES.has(code)\n ) {\n await new Promise((resolve) => setTimeout(resolve, 250 * attempt));\n continue;\n }\n\n // `fetch failed` / DNS / connection errors land here. Node's default\n // message buries the host in `err.cause`, which makes integration\n // debugging miserable. Surface the URL and the override env var on\n // the top-level message so it appears in the consumer's server log.\n const detail = code\n ? `${code}${err instanceof Error && err.message ? ` (${err.message})` : ''}`\n : err instanceof Error\n ? err.message\n : String(err);\n const e = new Error(\n `chaingrow-blog: could not reach ${method} ${url} — ${detail}. ` +\n `Set CHAINGROW_API_URL to override the API host (default https://api.chaingrow.de).`,\n );\n (e as Error & { cause?: unknown }).cause = err;\n throw e;\n }\n }\n if (!res) {\n throw new Error(`chaingrow-blog: ${method} ${url} — no response`);\n }\n\n if (res.status === 404) return null;\n if (!res.ok) {\n throw new Error(\n `chaingrow-blog: ${method} ${url} failed (${res.status})`,\n );\n }\n return (await res.json()) as T;\n };\n\n return {\n async getBlog(slug, language) {\n const qs = language ? `?language=${encodeURIComponent(language)}` : '';\n return request<Blog>(\n `/api/v1/blogs/by-slug/${encodeURIComponent(slug)}${qs}`,\n );\n },\n\n async getBlogById(id) {\n return request<Blog>(`/api/v1/blogs/${encodeURIComponent(id)}`);\n },\n\n async listBlogs(listOpts = {}) {\n const collected: Blog[] = [];\n // Page through until the server-reported `total` is reached. Backends\n // without pagination return everything on the first request\n // (total === data.length), so this loop runs exactly once there.\n const MAX_PAGES = 100;\n for (let page = 0; page < MAX_PAGES; page++) {\n const params = new URLSearchParams();\n if (listOpts.status) params.set('status', listOpts.status);\n if (listOpts.language) params.set('language', listOpts.language);\n if (collected.length > 0)\n params.set('offset', String(collected.length));\n const qs = params.toString();\n const response = await request<{ data: Blog[]; total: number }>(\n `/api/v1/blogs${qs ? `?${qs}` : ''}`,\n );\n if (!response) break;\n const batch = response.data ?? [];\n collected.push(...batch);\n const total =\n typeof response.total === 'number' ? response.total : collected.length;\n if (batch.length === 0 || collected.length >= total) break;\n }\n // The language filter also runs server-side (API builds that predate\n // it ignore the query param) — keep this client-side pass as a\n // fallback; against a filtering server it's a no-op.\n if (listOpts.language) {\n return collected.filter((b) => b.language === listOpts.language);\n }\n return collected;\n },\n };\n}\n","import type { Blog, FaqItem } from './types';\n\n/**\n * Consumer-supplied extras for BlogPosting. Everything is optional — the\n * SDK doesn't know the customer's base URL, organisation name, or author\n * identity, so it just omits what it isn't told. Schema.org treats\n * `author`, `publisher`, and `url` as recommended but not required.\n */\nexport type BlogJsonLdOptions = {\n /** Absolute URL of the blog page (e.g. `https://example.com/blog/my-post`). */\n url?: string;\n author?: { name: string; type?: 'Person' | 'Organization'; url?: string };\n publisher?: { name: string; logo?: string };\n};\n\nconst Schema = Object.freeze({ context: 'https://schema.org' });\n\n/**\n * Build a Schema.org `BlogPosting` JSON-LD object from a chaingrow Blog.\n * Fields that aren't present on the Blog (description, og_image, …) are\n * omitted entirely so the resulting JSON has no `undefined` junk.\n */\nexport function buildBlogPostingSchema(\n blog: Blog,\n opts: BlogJsonLdOptions = {},\n): Record<string, unknown> {\n const schema: Record<string, unknown> = {\n '@type': 'BlogPosting',\n headline: blog.title,\n datePublished: blog.published_at,\n };\n if (blog.description) schema.description = blog.description;\n if (blog.og_image) schema.image = blog.og_image;\n if (opts.url) {\n schema.url = opts.url;\n schema.mainEntityOfPage = opts.url;\n }\n if (opts.author) {\n schema.author = {\n '@type': opts.author.type ?? 'Organization',\n name: opts.author.name,\n ...(opts.author.url ? { url: opts.author.url } : {}),\n };\n }\n if (opts.publisher) {\n schema.publisher = {\n '@type': 'Organization',\n name: opts.publisher.name,\n ...(opts.publisher.logo\n ? { logo: { '@type': 'ImageObject', url: opts.publisher.logo } }\n : {}),\n };\n }\n return schema;\n}\n\n/**\n * Build a Schema.org `FAQPage` JSON-LD object from chaingrow's `{question, answer}[]`\n * pairs. Returns `null` for empty input so callers can skip it with a single\n * `if (faqSchema) items.push(faqSchema)` / `filter(Boolean)`.\n */\nexport function buildFAQPageSchema(\n faq: FaqItem[] | null | undefined,\n): Record<string, unknown> | null {\n if (!faq || faq.length === 0) return null;\n return {\n '@type': 'FAQPage',\n mainEntity: faq.map(({ question, answer }) => ({\n '@type': 'Question',\n name: question,\n acceptedAnswer: { '@type': 'Answer', text: answer },\n })),\n };\n}\n\n/**\n * Combines BlogPosting + FAQPage + any LLM-generated `structured_data.jsonLd`\n * extras into a flat array ready for `@graph`. Tolerant of missing/malformed\n * `structured_data` — if the backend ever sends something unexpected we just\n * skip it rather than crashing the consumer's page.\n */\nexport function buildBlogJsonLd(\n blog: Blog,\n opts: BlogJsonLdOptions = {},\n): Record<string, unknown>[] {\n const items: Record<string, unknown>[] = [buildBlogPostingSchema(blog, opts)];\n const faq = buildFAQPageSchema(blog.faq);\n if (faq) items.push(faq);\n items.push(...extractLlmJsonLd(blog.structured_data));\n return items;\n}\n\n/** Schema-org context string — exposed so `<JsonLdGraph>` and any\n * bring-your-own renderer use the same value. */\nexport const SCHEMA_ORG_CONTEXT = Schema.context;\n\n/**\n * Pull the JSON-LD array out of `blog.structured_data`. Per\n * `structuredDataOutputSchema` in @chaingrow/contracts the expected shape is\n * `{ jsonLd: Array<Record<string, unknown>> }`, but historical rows may be\n * `null`, a plain object, or even malformed — normalise to an empty array in\n * every degenerate case so the caller gets a safe spread target.\n */\nfunction extractLlmJsonLd(\n sd: Blog['structured_data'],\n): Record<string, unknown>[] {\n if (!sd || typeof sd !== 'object') return [];\n const ld = (sd as { jsonLd?: unknown }).jsonLd;\n if (!Array.isArray(ld)) return [];\n return ld.filter(\n (entry): entry is Record<string, unknown> =>\n typeof entry === 'object' && entry !== null && !Array.isArray(entry),\n );\n}\n","import type { Block, EmbeddedMedia } from './types';\n\n/**\n * Compute the block-index insertion point for each media item when\n * distributing `mediaCount` attachments across `blockCount` blocks.\n *\n * Formula: media `i` goes at insertion index `floor(i * blockCount / mediaCount)`.\n * An insertion index of `0` means \"before block 0\" (top of body); `blockCount`\n * means \"after the last block\".\n *\n * Examples (blockCount = 10):\n * mediaCount = 1 → [0] (single media at top)\n * mediaCount = 2 → [0, 5] (top + middle)\n * mediaCount = 3 → [0, 3, 6] (top, 1/3, 2/3)\n * mediaCount = 4 → [0, 2, 5, 7] (every quarter)\n *\n * When `blockCount` is 0 every media falls at position 0 and stacks at the\n * top. When two media round to the same position they both land there and\n * render consecutively; this is deterministic but not always visually ideal\n * for very small `blockCount / mediaCount` ratios — assume content has\n * enough blocks to spread across.\n */\nexport function distributeMediaPositions(\n blockCount: number,\n mediaCount: number,\n): number[] {\n if (mediaCount <= 0) return [];\n if (blockCount <= 0) return Array.from({ length: mediaCount }, () => 0);\n return Array.from({ length: mediaCount }, (_, i) =>\n Math.floor((i * blockCount) / mediaCount),\n );\n}\n\n/**\n * Ids of the attachments already placed in the text as `image` blocks\n * (`mediaId`), list items included. `interleaveMedia` skips those, so an\n * image the author placed never shows a second time.\n */\nexport function placedMediaIds(blocks: Block[]): Set<string> {\n const ids = new Set<string>();\n const walk = (list: Block[]) => {\n for (const b of list) {\n if (b.type === 'image' && b.mediaId) ids.add(b.mediaId);\n else if (b.type === 'bulletList' || b.type === 'orderedList') {\n for (const item of b.items) walk(item.content);\n }\n }\n };\n walk(blocks);\n return ids;\n}\n\n/** Whether the text itself contains an image (list items included). */\nexport function hasTextImage(blocks: Block[]): boolean {\n const walk = (list: Block[]): boolean =>\n list.some(\n (b) =>\n b.type === 'image' ||\n ((b.type === 'bulletList' || b.type === 'orderedList') &&\n b.items.some((item) => walk(item.content))),\n );\n return walk(blocks);\n}\n\n/** One interleaved slot in a blog's body — either a block or a media item. */\nexport type InterleavedSlot =\n | { kind: 'block'; block: Block; index: number }\n | { kind: 'media'; media: EmbeddedMedia; index: number };\n\n/**\n * Walk `blocks` once, splicing in `media` at the positions returned by\n * `distributeMediaPositions`. Returns a flat array of typed slots that\n * `<BlogRenderer>` can map over directly. Exposed so consumers who need a\n * custom layout (e.g. wrapping each section in a `<section>`) can use the\n * same placement logic without re-implementing it. Media already placed in\n * the text as an `image` block is skipped (see `placedMediaIds`).\n */\nexport function interleaveMedia(\n blocks: Block[],\n media: EmbeddedMedia[] | undefined,\n): InterleavedSlot[] {\n const placed = placedMediaIds(blocks);\n const mediaList = (media ?? []).filter((m) => !placed.has(m.id));\n const positions = distributeMediaPositions(blocks.length, mediaList.length);\n\n const slots: InterleavedSlot[] = [];\n let mediaCursor = 0;\n for (let b = 0; b <= blocks.length; b++) {\n while (mediaCursor < mediaList.length && positions[mediaCursor] === b) {\n const media = mediaList[mediaCursor]!;\n slots.push({ kind: 'media', media, index: mediaCursor });\n mediaCursor++;\n }\n if (b < blocks.length) {\n const block = blocks[b]!;\n slots.push({ kind: 'block', block, index: b });\n }\n }\n return slots;\n}\n","import type { Blog, EmbeddedMedia } from './types';\n\nconst NEWS_WINDOW_MS = 48 * 60 * 60 * 1000;\nconst NEWS_MAX_ENTRIES = 1000;\n\nexport type SitemapBuildOptions = {\n baseUrl: string;\n pathPrefix?: string;\n};\n\nexport type NewsSitemapBuildOptions = SitemapBuildOptions & {\n publicationName: string;\n defaultLanguage?: string;\n now?: Date;\n};\n\nexport function escapeXml(s: string): string {\n return s\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\nconst trimTrailingSlash = (s: string): string => s.replace(/\\/+$/, '');\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\nconst buildBlogUrl = (\n baseUrl: string,\n pathPrefix: string,\n slug: string,\n): string => `${baseUrl}${pathPrefix}/${slug}`;\n\nconst isImageMedia = (m: EmbeddedMedia): boolean => m.media_type === 'IMAGE';\n\nconst renderImageEntry = (m: EmbeddedMedia): string =>\n ` <image:image>\\n` +\n ` <image:loc>${escapeXml(m.media_url)}</image:loc>\\n` +\n ` </image:image>\\n`;\n\nconst lastmodFor = (blog: Blog): string => blog.published_at;\n\nexport function buildSitemapXml(\n blogs: Blog[],\n opts: SitemapBuildOptions,\n): string {\n const baseUrl = trimTrailingSlash(opts.baseUrl);\n const pathPrefix = normalizePathPrefix(opts.pathPrefix ?? '/blog');\n\n const entries = blogs\n .map((blog) => {\n const url = buildBlogUrl(baseUrl, pathPrefix, blog.slug);\n const images = (blog.generatedMedia ?? [])\n .filter(isImageMedia)\n .map(renderImageEntry)\n .join('');\n return (\n ` <url>\\n` +\n ` <loc>${escapeXml(url)}</loc>\\n` +\n ` <lastmod>${escapeXml(lastmodFor(blog))}</lastmod>\\n` +\n images +\n ` </url>\\n`\n );\n })\n .join('');\n\n return (\n `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n` +\n `<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\" ` +\n `xmlns:image=\"http://www.google.com/schemas/sitemap-image/1.1\">\\n` +\n entries +\n `</urlset>\\n`\n );\n}\n\n/** Generic over anything carrying `published_at` so custom blog shapes\n * (or intersections with extra fields) pass through without a cast. */\nexport function filterRecentForNews<T extends Pick<Blog, 'published_at'>>(\n blogs: T[],\n now: Date = new Date(),\n): T[] {\n const cutoff = now.getTime() - NEWS_WINDOW_MS;\n const recent = blogs.filter((b) => {\n const t = Date.parse(b.published_at);\n return Number.isFinite(t) && t >= cutoff;\n });\n recent.sort(\n (a, b) => Date.parse(b.published_at) - Date.parse(a.published_at),\n );\n return recent.slice(0, NEWS_MAX_ENTRIES);\n}\n\nexport function buildNewsSitemapXml(\n blogs: Blog[],\n opts: NewsSitemapBuildOptions,\n): string {\n const baseUrl = trimTrailingSlash(opts.baseUrl);\n const pathPrefix = normalizePathPrefix(opts.pathPrefix ?? '/blog');\n const publicationName = opts.publicationName;\n const defaultLanguage = opts.defaultLanguage ?? 'en';\n\n const recent = filterRecentForNews(blogs, opts.now);\n\n const entries = recent\n .map((blog) => {\n const url = buildBlogUrl(baseUrl, pathPrefix, blog.slug);\n const language = blog.language || defaultLanguage;\n return (\n ` <url>\\n` +\n ` <loc>${escapeXml(url)}</loc>\\n` +\n ` <news:news>\\n` +\n ` <news:publication>\\n` +\n ` <news:name>${escapeXml(publicationName)}</news:name>\\n` +\n ` <news:language>${escapeXml(language)}</news:language>\\n` +\n ` </news:publication>\\n` +\n ` <news:publication_date>${escapeXml(blog.published_at)}</news:publication_date>\\n` +\n ` <news:title>${escapeXml(blog.title)}</news:title>\\n` +\n ` </news:news>\\n` +\n ` </url>\\n`\n );\n })\n .join('');\n\n return (\n `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n` +\n `<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\" ` +\n `xmlns:news=\"http://www.google.com/schemas/sitemap-news/0.9\">\\n` +\n entries +\n `</urlset>\\n`\n );\n}\n","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":"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","names":["media","normalizePathPrefix"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { B as Blog } from './types-
|
|
2
|
-
export { a as Block, b as BlockType, E as EmbeddedMedia, F as FaqItem, I as Inline, L as ListItem, M as Mark, S as SocialPost, W as WebhookEvent, c as WebhookPayload } from './types-
|
|
3
|
-
export { B as BlogCard, a as BlogJsonLdOptions, I as InterleavedSlot, S as SCHEMA_ORG_CONTEXT, T as ToBlogCardOptions, b as buildBlogJsonLd, c as buildBlogPostingSchema, d as buildFAQPageSchema, e as distributeMediaPositions, i as interleaveMedia, m as mergeBlogs, t as toBlogCard } from './blog-cards-
|
|
1
|
+
import { B as Blog } from './types-DSBF7V30.cjs';
|
|
2
|
+
export { a as Block, b as BlockType, E as EmbeddedMedia, F as FaqItem, I as Inline, L as ListItem, M as Mark, S as SocialPost, W as WebhookEvent, c as WebhookPayload } from './types-DSBF7V30.cjs';
|
|
3
|
+
export { B as BlogCard, a as BlogJsonLdOptions, I as InterleavedSlot, S as SCHEMA_ORG_CONTEXT, T as ToBlogCardOptions, b as buildBlogJsonLd, c as buildBlogPostingSchema, d as buildFAQPageSchema, e as distributeMediaPositions, h as hasTextImage, i as interleaveMedia, m as mergeBlogs, p as placedMediaIds, t as toBlogCard } from './blog-cards-PjckhJAG.cjs';
|
|
4
4
|
|
|
5
5
|
type ClientOptions = {
|
|
6
6
|
apiUrl: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { B as Blog } from './types-
|
|
2
|
-
export { a as Block, b as BlockType, E as EmbeddedMedia, F as FaqItem, I as Inline, L as ListItem, M as Mark, S as SocialPost, W as WebhookEvent, c as WebhookPayload } from './types-
|
|
3
|
-
export { B as BlogCard, a as BlogJsonLdOptions, I as InterleavedSlot, S as SCHEMA_ORG_CONTEXT, T as ToBlogCardOptions, b as buildBlogJsonLd, c as buildBlogPostingSchema, d as buildFAQPageSchema, e as distributeMediaPositions, i as interleaveMedia, m as mergeBlogs, t as toBlogCard } from './blog-cards-
|
|
1
|
+
import { B as Blog } from './types-DSBF7V30.js';
|
|
2
|
+
export { a as Block, b as BlockType, E as EmbeddedMedia, F as FaqItem, I as Inline, L as ListItem, M as Mark, S as SocialPost, W as WebhookEvent, c as WebhookPayload } from './types-DSBF7V30.js';
|
|
3
|
+
export { B as BlogCard, a as BlogJsonLdOptions, I as InterleavedSlot, S as SCHEMA_ORG_CONTEXT, T as ToBlogCardOptions, b as buildBlogJsonLd, c as buildBlogPostingSchema, d as buildFAQPageSchema, e as distributeMediaPositions, h as hasTextImage, i as interleaveMedia, m as mergeBlogs, p as placedMediaIds, t as toBlogCard } from './blog-cards-CcHb2bcv.js';
|
|
4
4
|
|
|
5
5
|
type ClientOptions = {
|
|
6
6
|
apiUrl: string;
|
package/dist/index.js
CHANGED
|
@@ -9,8 +9,10 @@ import {
|
|
|
9
9
|
buildFAQPageSchema,
|
|
10
10
|
createClient,
|
|
11
11
|
distributeMediaPositions,
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
hasTextImage,
|
|
13
|
+
interleaveMedia,
|
|
14
|
+
placedMediaIds
|
|
15
|
+
} from "./chunk-KDUQNIJ2.js";
|
|
14
16
|
|
|
15
17
|
// src/sitemap-builder.ts
|
|
16
18
|
var NEWS_WINDOW_MS = 48 * 60 * 60 * 1e3;
|
|
@@ -98,8 +100,10 @@ export {
|
|
|
98
100
|
distributeMediaPositions,
|
|
99
101
|
escapeXml,
|
|
100
102
|
filterRecentForNews,
|
|
103
|
+
hasTextImage,
|
|
101
104
|
interleaveMedia,
|
|
102
105
|
mergeBlogs,
|
|
106
|
+
placedMediaIds,
|
|
103
107
|
toBlogCard
|
|
104
108
|
};
|
|
105
109
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/sitemap-builder.ts"],"sourcesContent":["import type { Blog, EmbeddedMedia } from './types';\n\nconst NEWS_WINDOW_MS = 48 * 60 * 60 * 1000;\nconst NEWS_MAX_ENTRIES = 1000;\n\nexport type SitemapBuildOptions = {\n baseUrl: string;\n pathPrefix?: string;\n};\n\nexport type NewsSitemapBuildOptions = SitemapBuildOptions & {\n publicationName: string;\n defaultLanguage?: string;\n now?: Date;\n};\n\nexport function escapeXml(s: string): string {\n return s\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\nconst trimTrailingSlash = (s: string): string => s.replace(/\\/+$/, '');\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\nconst buildBlogUrl = (\n baseUrl: string,\n pathPrefix: string,\n slug: string,\n): string => `${baseUrl}${pathPrefix}/${slug}`;\n\nconst isImageMedia = (m: EmbeddedMedia): boolean => m.media_type === 'IMAGE';\n\nconst renderImageEntry = (m: EmbeddedMedia): string =>\n ` <image:image>\\n` +\n ` <image:loc>${escapeXml(m.media_url)}</image:loc>\\n` +\n ` </image:image>\\n`;\n\nconst lastmodFor = (blog: Blog): string => blog.published_at;\n\nexport function buildSitemapXml(\n blogs: Blog[],\n opts: SitemapBuildOptions,\n): string {\n const baseUrl = trimTrailingSlash(opts.baseUrl);\n const pathPrefix = normalizePathPrefix(opts.pathPrefix ?? '/blog');\n\n const entries = blogs\n .map((blog) => {\n const url = buildBlogUrl(baseUrl, pathPrefix, blog.slug);\n const images = (blog.generatedMedia ?? [])\n .filter(isImageMedia)\n .map(renderImageEntry)\n .join('');\n return (\n ` <url>\\n` +\n ` <loc>${escapeXml(url)}</loc>\\n` +\n ` <lastmod>${escapeXml(lastmodFor(blog))}</lastmod>\\n` +\n images +\n ` </url>\\n`\n );\n })\n .join('');\n\n return (\n `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n` +\n `<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\" ` +\n `xmlns:image=\"http://www.google.com/schemas/sitemap-image/1.1\">\\n` +\n entries +\n `</urlset>\\n`\n );\n}\n\n/** Generic over anything carrying `published_at` so custom blog shapes\n * (or intersections with extra fields) pass through without a cast. */\nexport function filterRecentForNews<T extends Pick<Blog, 'published_at'>>(\n blogs: T[],\n now: Date = new Date(),\n): T[] {\n const cutoff = now.getTime() - NEWS_WINDOW_MS;\n const recent = blogs.filter((b) => {\n const t = Date.parse(b.published_at);\n return Number.isFinite(t) && t >= cutoff;\n });\n recent.sort(\n (a, b) => Date.parse(b.published_at) - Date.parse(a.published_at),\n );\n return recent.slice(0, NEWS_MAX_ENTRIES);\n}\n\nexport function buildNewsSitemapXml(\n blogs: Blog[],\n opts: NewsSitemapBuildOptions,\n): string {\n const baseUrl = trimTrailingSlash(opts.baseUrl);\n const pathPrefix = normalizePathPrefix(opts.pathPrefix ?? '/blog');\n const publicationName = opts.publicationName;\n const defaultLanguage = opts.defaultLanguage ?? 'en';\n\n const recent = filterRecentForNews(blogs, opts.now);\n\n const entries = recent\n .map((blog) => {\n const url = buildBlogUrl(baseUrl, pathPrefix, blog.slug);\n const language = blog.language || defaultLanguage;\n return (\n ` <url>\\n` +\n ` <loc>${escapeXml(url)}</loc>\\n` +\n ` <news:news>\\n` +\n ` <news:publication>\\n` +\n ` <news:name>${escapeXml(publicationName)}</news:name>\\n` +\n ` <news:language>${escapeXml(language)}</news:language>\\n` +\n ` </news:publication>\\n` +\n ` <news:publication_date>${escapeXml(blog.published_at)}</news:publication_date>\\n` +\n ` <news:title>${escapeXml(blog.title)}</news:title>\\n` +\n ` </news:news>\\n` +\n ` </url>\\n`\n );\n })\n .join('');\n\n return (\n `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n` +\n `<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\" ` +\n `xmlns:news=\"http://www.google.com/schemas/sitemap-news/0.9\">\\n` +\n entries +\n `</urlset>\\n`\n );\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../src/sitemap-builder.ts"],"sourcesContent":["import type { Blog, EmbeddedMedia } from './types';\n\nconst NEWS_WINDOW_MS = 48 * 60 * 60 * 1000;\nconst NEWS_MAX_ENTRIES = 1000;\n\nexport type SitemapBuildOptions = {\n baseUrl: string;\n pathPrefix?: string;\n};\n\nexport type NewsSitemapBuildOptions = SitemapBuildOptions & {\n publicationName: string;\n defaultLanguage?: string;\n now?: Date;\n};\n\nexport function escapeXml(s: string): string {\n return s\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\nconst trimTrailingSlash = (s: string): string => s.replace(/\\/+$/, '');\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\nconst buildBlogUrl = (\n baseUrl: string,\n pathPrefix: string,\n slug: string,\n): string => `${baseUrl}${pathPrefix}/${slug}`;\n\nconst isImageMedia = (m: EmbeddedMedia): boolean => m.media_type === 'IMAGE';\n\nconst renderImageEntry = (m: EmbeddedMedia): string =>\n ` <image:image>\\n` +\n ` <image:loc>${escapeXml(m.media_url)}</image:loc>\\n` +\n ` </image:image>\\n`;\n\nconst lastmodFor = (blog: Blog): string => blog.published_at;\n\nexport function buildSitemapXml(\n blogs: Blog[],\n opts: SitemapBuildOptions,\n): string {\n const baseUrl = trimTrailingSlash(opts.baseUrl);\n const pathPrefix = normalizePathPrefix(opts.pathPrefix ?? '/blog');\n\n const entries = blogs\n .map((blog) => {\n const url = buildBlogUrl(baseUrl, pathPrefix, blog.slug);\n const images = (blog.generatedMedia ?? [])\n .filter(isImageMedia)\n .map(renderImageEntry)\n .join('');\n return (\n ` <url>\\n` +\n ` <loc>${escapeXml(url)}</loc>\\n` +\n ` <lastmod>${escapeXml(lastmodFor(blog))}</lastmod>\\n` +\n images +\n ` </url>\\n`\n );\n })\n .join('');\n\n return (\n `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n` +\n `<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\" ` +\n `xmlns:image=\"http://www.google.com/schemas/sitemap-image/1.1\">\\n` +\n entries +\n `</urlset>\\n`\n );\n}\n\n/** Generic over anything carrying `published_at` so custom blog shapes\n * (or intersections with extra fields) pass through without a cast. */\nexport function filterRecentForNews<T extends Pick<Blog, 'published_at'>>(\n blogs: T[],\n now: Date = new Date(),\n): T[] {\n const cutoff = now.getTime() - NEWS_WINDOW_MS;\n const recent = blogs.filter((b) => {\n const t = Date.parse(b.published_at);\n return Number.isFinite(t) && t >= cutoff;\n });\n recent.sort(\n (a, b) => Date.parse(b.published_at) - Date.parse(a.published_at),\n );\n return recent.slice(0, NEWS_MAX_ENTRIES);\n}\n\nexport function buildNewsSitemapXml(\n blogs: Blog[],\n opts: NewsSitemapBuildOptions,\n): string {\n const baseUrl = trimTrailingSlash(opts.baseUrl);\n const pathPrefix = normalizePathPrefix(opts.pathPrefix ?? '/blog');\n const publicationName = opts.publicationName;\n const defaultLanguage = opts.defaultLanguage ?? 'en';\n\n const recent = filterRecentForNews(blogs, opts.now);\n\n const entries = recent\n .map((blog) => {\n const url = buildBlogUrl(baseUrl, pathPrefix, blog.slug);\n const language = blog.language || defaultLanguage;\n return (\n ` <url>\\n` +\n ` <loc>${escapeXml(url)}</loc>\\n` +\n ` <news:news>\\n` +\n ` <news:publication>\\n` +\n ` <news:name>${escapeXml(publicationName)}</news:name>\\n` +\n ` <news:language>${escapeXml(language)}</news:language>\\n` +\n ` </news:publication>\\n` +\n ` <news:publication_date>${escapeXml(blog.published_at)}</news:publication_date>\\n` +\n ` <news:title>${escapeXml(blog.title)}</news:title>\\n` +\n ` </news:news>\\n` +\n ` </url>\\n`\n );\n })\n .join('');\n\n return (\n `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n` +\n `<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\" ` +\n `xmlns:news=\"http://www.google.com/schemas/sitemap-news/0.9\">\\n` +\n entries +\n `</urlset>\\n`\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAEA,IAAM,iBAAiB,KAAK,KAAK,KAAK;AACtC,IAAM,mBAAmB;AAalB,SAAS,UAAU,GAAmB;AAC3C,SAAO,EACJ,QAAQ,MAAM,OAAO,EACrB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,QAAQ,EACtB,QAAQ,MAAM,QAAQ;AAC3B;AAEA,IAAM,oBAAoB,CAAC,MAAsB,EAAE,QAAQ,QAAQ,EAAE;AAErE,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;AAEA,IAAM,eAAe,CACnB,SACA,YACA,SACW,GAAG,OAAO,GAAG,UAAU,IAAI,IAAI;AAE5C,IAAM,eAAe,CAAC,MAA8B,EAAE,eAAe;AAErE,IAAM,mBAAmB,CAAC,MACxB;AAAA,mBACoB,UAAU,EAAE,SAAS,CAAC;AAAA;AAAA;AAG5C,IAAM,aAAa,CAAC,SAAuB,KAAK;AAEzC,SAAS,gBACd,OACA,MACQ;AACR,QAAM,UAAU,kBAAkB,KAAK,OAAO;AAC9C,QAAM,aAAa,oBAAoB,KAAK,cAAc,OAAO;AAEjE,QAAM,UAAU,MACb,IAAI,CAAC,SAAS;AACb,UAAM,MAAM,aAAa,SAAS,YAAY,KAAK,IAAI;AACvD,UAAM,UAAU,KAAK,kBAAkB,CAAC,GACrC,OAAO,YAAY,EACnB,IAAI,gBAAgB,EACpB,KAAK,EAAE;AACV,WACE;AAAA,WACY,UAAU,GAAG,CAAC;AAAA,eACV,UAAU,WAAW,IAAI,CAAC,CAAC;AAAA,IAC3C,SACA;AAAA;AAAA,EAEJ,CAAC,EACA,KAAK,EAAE;AAEV,SACE;AAAA;AAAA,IAGA,UACA;AAAA;AAEJ;AAIO,SAAS,oBACd,OACA,MAAY,oBAAI,KAAK,GAChB;AACL,QAAM,SAAS,IAAI,QAAQ,IAAI;AAC/B,QAAM,SAAS,MAAM,OAAO,CAAC,MAAM;AACjC,UAAM,IAAI,KAAK,MAAM,EAAE,YAAY;AACnC,WAAO,OAAO,SAAS,CAAC,KAAK,KAAK;AAAA,EACpC,CAAC;AACD,SAAO;AAAA,IACL,CAAC,GAAG,MAAM,KAAK,MAAM,EAAE,YAAY,IAAI,KAAK,MAAM,EAAE,YAAY;AAAA,EAClE;AACA,SAAO,OAAO,MAAM,GAAG,gBAAgB;AACzC;AAEO,SAAS,oBACd,OACA,MACQ;AACR,QAAM,UAAU,kBAAkB,KAAK,OAAO;AAC9C,QAAM,aAAa,oBAAoB,KAAK,cAAc,OAAO;AACjE,QAAM,kBAAkB,KAAK;AAC7B,QAAM,kBAAkB,KAAK,mBAAmB;AAEhD,QAAM,SAAS,oBAAoB,OAAO,KAAK,GAAG;AAElD,QAAM,UAAU,OACb,IAAI,CAAC,SAAS;AACb,UAAM,MAAM,aAAa,SAAS,YAAY,KAAK,IAAI;AACvD,UAAM,WAAW,KAAK,YAAY;AAClC,WACE;AAAA,WACY,UAAU,GAAG,CAAC;AAAA;AAAA;AAAA,qBAGJ,UAAU,eAAe,CAAC;AAAA,yBACtB,UAAU,QAAQ,CAAC;AAAA;AAAA,+BAEb,UAAU,KAAK,YAAY,CAAC;AAAA,oBACvC,UAAU,KAAK,KAAK,CAAC;AAAA;AAAA;AAAA;AAAA,EAI9C,CAAC,EACA,KAAK,EAAE;AAEV,SACE;AAAA;AAAA,IAGA,UACA;AAAA;AAEJ;","names":[]}
|
package/dist/next/index.cjs
CHANGED
|
@@ -31,6 +31,18 @@ __export(next_exports, {
|
|
|
31
31
|
module.exports = __toCommonJS(next_exports);
|
|
32
32
|
|
|
33
33
|
// src/client.ts
|
|
34
|
+
var TRANSIENT_NETWORK_CODES = /* @__PURE__ */ new Set([
|
|
35
|
+
"EAI_AGAIN",
|
|
36
|
+
"ETIMEDOUT",
|
|
37
|
+
"ECONNRESET",
|
|
38
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
39
|
+
"UND_ERR_SOCKET"
|
|
40
|
+
]);
|
|
41
|
+
var NETWORK_ATTEMPTS = 3;
|
|
42
|
+
function causeCode(err) {
|
|
43
|
+
const cause = err instanceof Error ? err.cause : void 0;
|
|
44
|
+
return cause && typeof cause === "object" && "code" in cause ? String(cause.code) : void 0;
|
|
45
|
+
}
|
|
34
46
|
function createClient(opts) {
|
|
35
47
|
const baseUrl = opts.apiUrl.replace(/\/$/, "");
|
|
36
48
|
const doFetch = opts.fetch ?? fetch;
|
|
@@ -46,24 +58,33 @@ function createClient(opts) {
|
|
|
46
58
|
}
|
|
47
59
|
});
|
|
48
60
|
let res;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (res.status === 429) {
|
|
52
|
-
const waitSec = parseInt(res.headers.get("Retry-After") ?? "1", 10);
|
|
53
|
-
await new Promise(
|
|
54
|
-
(resolve) => setTimeout(resolve, Math.max(0, waitSec) * 1e3)
|
|
55
|
-
);
|
|
61
|
+
for (let attempt = 1; attempt <= NETWORK_ATTEMPTS; attempt++) {
|
|
62
|
+
try {
|
|
56
63
|
res = await doFetch(url, buildInit());
|
|
64
|
+
if (res.status === 429) {
|
|
65
|
+
const waitSec = parseInt(res.headers.get("Retry-After") ?? "1", 10);
|
|
66
|
+
await new Promise(
|
|
67
|
+
(resolve) => setTimeout(resolve, Math.max(0, waitSec) * 1e3)
|
|
68
|
+
);
|
|
69
|
+
res = await doFetch(url, buildInit());
|
|
70
|
+
}
|
|
71
|
+
break;
|
|
72
|
+
} catch (err) {
|
|
73
|
+
const code = causeCode(err);
|
|
74
|
+
if (attempt < NETWORK_ATTEMPTS && code !== void 0 && TRANSIENT_NETWORK_CODES.has(code)) {
|
|
75
|
+
await new Promise((resolve) => setTimeout(resolve, 250 * attempt));
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const detail = code ? `${code}${err instanceof Error && err.message ? ` (${err.message})` : ""}` : err instanceof Error ? err.message : String(err);
|
|
79
|
+
const e = new Error(
|
|
80
|
+
`chaingrow-blog: could not reach ${method} ${url} \u2014 ${detail}. Set CHAINGROW_API_URL to override the API host (default https://api.chaingrow.de).`
|
|
81
|
+
);
|
|
82
|
+
e.cause = err;
|
|
83
|
+
throw e;
|
|
57
84
|
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const detail = code ? `${code}${err instanceof Error && err.message ? ` (${err.message})` : ""}` : err instanceof Error ? err.message : String(err);
|
|
62
|
-
const e = new Error(
|
|
63
|
-
`chaingrow-blog: could not reach ${method} ${url} \u2014 ${detail}. Set CHAINGROW_API_URL to override the API host (default https://api.chaingrow.de).`
|
|
64
|
-
);
|
|
65
|
-
e.cause = err;
|
|
66
|
-
throw e;
|
|
85
|
+
}
|
|
86
|
+
if (!res) {
|
|
87
|
+
throw new Error(`chaingrow-blog: ${method} ${url} \u2014 no response`);
|
|
67
88
|
}
|
|
68
89
|
if (res.status === 404) return null;
|
|
69
90
|
if (!res.ok) {
|
|
@@ -212,8 +233,28 @@ function distributeMediaPositions(blockCount, mediaCount) {
|
|
|
212
233
|
(_, i) => Math.floor(i * blockCount / mediaCount)
|
|
213
234
|
);
|
|
214
235
|
}
|
|
236
|
+
function placedMediaIds(blocks) {
|
|
237
|
+
const ids = /* @__PURE__ */ new Set();
|
|
238
|
+
const walk = (list) => {
|
|
239
|
+
for (const b of list) {
|
|
240
|
+
if (b.type === "image" && b.mediaId) ids.add(b.mediaId);
|
|
241
|
+
else if (b.type === "bulletList" || b.type === "orderedList") {
|
|
242
|
+
for (const item of b.items) walk(item.content);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
walk(blocks);
|
|
247
|
+
return ids;
|
|
248
|
+
}
|
|
249
|
+
function hasTextImage(blocks) {
|
|
250
|
+
const walk = (list) => list.some(
|
|
251
|
+
(b) => b.type === "image" || (b.type === "bulletList" || b.type === "orderedList") && b.items.some((item) => walk(item.content))
|
|
252
|
+
);
|
|
253
|
+
return walk(blocks);
|
|
254
|
+
}
|
|
215
255
|
function interleaveMedia(blocks, media) {
|
|
216
|
-
const
|
|
256
|
+
const placed = placedMediaIds(blocks);
|
|
257
|
+
const mediaList = (media ?? []).filter((m) => !placed.has(m.id));
|
|
217
258
|
const positions = distributeMediaPositions(blocks.length, mediaList.length);
|
|
218
259
|
const slots = [];
|
|
219
260
|
let mediaCursor = 0;
|
|
@@ -452,7 +493,7 @@ function DefaultHeader({
|
|
|
452
493
|
blog,
|
|
453
494
|
formatDate
|
|
454
495
|
}) {
|
|
455
|
-
const hasInlineMedia = (blog.generatedMedia?.length ?? 0) > 0;
|
|
496
|
+
const hasInlineMedia = (blog.generatedMedia?.length ?? 0) > 0 || hasTextImage(blog.blocks);
|
|
456
497
|
const heroImage = !hasInlineMedia && blog.og_image ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("img", { src: blog.og_image, alt: blog.title }) : null;
|
|
457
498
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
458
499
|
heroImage,
|