@mailoo/blog 1.34.1 → 1.34.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/blog-article-body.tsx","../../src/markdown-link-target.ts","../../src/server/utils.ts","../../src/components/blog-linked-links-display.tsx","../../src/components/blog-printable-buttons.tsx","../../src/components/blog-article-marks.tsx","../../src/components/blog-article-share.tsx","../../src/lib/share-urls.ts"],"sourcesContent":["import ReactMarkdown from 'react-markdown'\nimport remarkGfm from 'remark-gfm'\nimport { markdownHrefOpensInNewTab } from '../markdown-link-target.js'\nimport { BLOG_ARTICLE_PROSE_CLASSNAME } from '../server/utils.js'\n\nexport interface BlogArticleBodyProps {\n htmlContent?: string | null\n markdownFallback?: string | null\n /** Override default Tailwind Typography classes */\n className?: string\n}\n\n/**\n * Renders blog article body: prefers `htmlContent` from the headless API,\n * falls back to Markdown when `htmlContent` is empty.\n */\nexport function BlogArticleBody({\n htmlContent,\n markdownFallback,\n className = BLOG_ARTICLE_PROSE_CLASSNAME,\n}: BlogArticleBodyProps) {\n const html = htmlContent?.trim()\n if (html) {\n return (\n <div\n className={className}\n dangerouslySetInnerHTML={{ __html: html }}\n />\n )\n }\n\n const md = markdownFallback?.trim()\n if (md) {\n return (\n <div className={className}>\n <ReactMarkdown\n remarkPlugins={[remarkGfm]}\n components={{\n a({ children, href, ...props }) {\n const h = href ?? ''\n const openNew = markdownHrefOpensInNewTab(h)\n return (\n <a\n href={h}\n {...(openNew\n ? { target: '_blank', rel: 'noopener noreferrer' }\n : {})}\n {...props}\n >\n {children}\n </a>\n )\n },\n }}\n >\n {md}\n </ReactMarkdown>\n </div>\n )\n }\n\n return null\n}\n","export function markdownHrefOpensInNewTab(href: string): boolean {\n const h = href.trim()\n if (h === '') return false\n if (h.startsWith('#') || h.startsWith('?')) return false\n if (h.startsWith('//')) return true\n if (h.startsWith('/')) return false\n if (h.startsWith('./') || h.startsWith('../')) return false\n const lower = h.toLowerCase()\n return lower.startsWith('http://') || lower.startsWith('https://')\n}\n","/**\n * First absolute image URL for OG/Twitter previews: prefer HTML from the API,\n * then markdown image syntax in content.\n */\nconst HTML_IMG_SRC = /<img[^>]+src=[\"']([^\"']+)[\"']/i\nconst MD_IMAGE = /!\\[[^\\]]*]\\(([^)\\s]+)\\)/\n\nfunction normalizeUrl(raw: string): string | null {\n const u = raw.trim()\n if (!u) return null\n if (\n u.startsWith('http://') ||\n u.startsWith('https://') ||\n u.startsWith('/')\n ) {\n return u\n }\n return null\n}\n\nexport function firstEmbedImageUrlFromArticle(\n htmlContent: string | null | undefined,\n markdownContent: string | null | undefined\n): string | null {\n if (htmlContent) {\n const m = htmlContent.match(HTML_IMG_SRC)\n if (m?.[1]) {\n const url = normalizeUrl(m[1])\n if (url) return url\n }\n }\n if (markdownContent) {\n const m = markdownContent.match(MD_IMAGE)\n if (m?.[1]) {\n const url = normalizeUrl(m[1])\n if (url) return url\n }\n }\n return null\n}\n\n/**\n * Default Tailwind Typography class string for article body.\n * Hosts may pass a custom `className` instead.\n */\nexport const BLOG_ARTICLE_PROSE_CLASSNAME = [\n 'prose',\n 'prose-gray',\n 'prose-lg',\n 'max-w-none',\n 'dark:prose-invert',\n 'prose-a:text-blue-600',\n 'hover:prose-a:text-blue-700',\n 'dark:prose-a:text-blue-400',\n 'dark:hover:prose-a:text-blue-300',\n 'prose-blockquote:border-blue-500',\n 'prose-img:rounded-lg',\n 'prose-img:shadow-sm',\n].join(' ')\n","import type { ReactNode } from 'react'\n\nexport type BlogLinkedLinkDisplayItem = {\n id?: string\n type: 'internal_article' | 'update_announcement' | 'external_resource'\n label: string\n url: string\n intro: string | null\n date: string | null\n sortOrder: number\n}\n\nconst TYPE_ORDER: BlogLinkedLinkDisplayItem['type'][] = [\n 'internal_article',\n 'update_announcement',\n 'external_resource',\n]\n\nfunction groupByType(\n links: BlogLinkedLinkDisplayItem[]\n): Map<BlogLinkedLinkDisplayItem['type'], BlogLinkedLinkDisplayItem[]> {\n const sorted = [...links].sort((a, b) => a.sortOrder - b.sortOrder)\n const map = new Map<\n BlogLinkedLinkDisplayItem['type'],\n BlogLinkedLinkDisplayItem[]\n >()\n for (const t of TYPE_ORDER) map.set(t, [])\n for (const link of sorted) {\n const bucket = map.get(link.type)\n if (bucket) bucket.push(link)\n }\n return map\n}\n\nexport interface BlogLinkedLinksDisplayProps {\n links: BlogLinkedLinkDisplayItem[]\n title: string\n typeLabels: Record<BlogLinkedLinkDisplayItem['type'], string>\n variant?: 'article' | 'compact'\n /**\n * When set, paths starting with `/blog/` become `/{locale}/blog/...`.\n */\n blogLocale?: string\n /** Optional link component (e.g. next/link). Defaults to `<a>`. */\n LinkComponent?: (props: {\n href: string\n className?: string\n children: ReactNode\n }) => ReactNode\n}\n\nfunction hrefForLink(url: string, blogLocale?: string): string {\n if (blogLocale && url.startsWith('/blog/')) {\n return `/${blogLocale}${url}`\n }\n return url\n}\n\nexport function BlogLinkedLinksDisplay({\n links,\n title,\n typeLabels,\n variant = 'article',\n blogLocale,\n LinkComponent,\n}: Readonly<BlogLinkedLinksDisplayProps>) {\n if (!links.length) return null\n\n const grouped = groupByType(links)\n const sectionClass =\n variant === 'article'\n ? 'mt-10 border-t border-gray-200 pt-8 dark:border-gray-700'\n : 'mt-4 rounded-lg border border-dashed border-gray-300 p-4 dark:border-gray-600'\n\n const Link =\n LinkComponent ??\n (({ href, className, children }) => (\n <a href={href} className={className}>\n {children}\n </a>\n ))\n\n return (\n <section\n className={sectionClass}\n aria-labelledby=\"mailoo-linked-links-heading\"\n >\n <h2\n id=\"mailoo-linked-links-heading\"\n className={\n variant === 'article'\n ? 'mb-6 text-xl font-semibold text-gray-900 dark:text-white'\n : 'mb-3 text-sm font-semibold text-gray-800 dark:text-gray-200'\n }\n >\n {title}\n </h2>\n <div className=\"space-y-8\">\n {TYPE_ORDER.map((type) => {\n const items = grouped.get(type) ?? []\n if (!items.length) return null\n return (\n <div key={type}>\n <h3\n className={\n variant === 'article'\n ? 'mb-3 text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400'\n : 'mb-2 text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400'\n }\n >\n {typeLabels[type]}\n </h3>\n <ul className=\"space-y-4\">\n {items.map((item) => (\n <li\n key={\n item.id ?? `${item.type}-${item.url}-${item.sortOrder}`\n }\n >\n <Link\n href={hrefForLink(item.url, blogLocale)}\n className={\n variant === 'article'\n ? 'text-lg font-medium text-blue-600 hover:underline dark:text-blue-400'\n : 'text-sm font-medium text-blue-600 hover:underline dark:text-blue-400'\n }\n >\n {item.label}\n </Link>\n {item.date && (\n <p\n className={\n variant === 'article'\n ? 'mt-1 text-sm text-gray-500 dark:text-gray-400'\n : 'mt-0.5 text-xs text-gray-500 dark:text-gray-400'\n }\n >\n <time dateTime={item.date}>\n {new Date(item.date).toLocaleDateString()}\n </time>\n </p>\n )}\n {item.intro && (\n <p\n className={\n variant === 'article'\n ? 'mt-2 text-sm text-gray-600 dark:text-gray-300'\n : 'mt-1 text-xs text-gray-600 dark:text-gray-300'\n }\n >\n {item.intro}\n </p>\n )}\n </li>\n ))}\n </ul>\n </div>\n )\n })}\n </div>\n </section>\n )\n}\n","'use client'\n\nimport { useEffect, useRef } from 'react'\n\nconst PRINT_BUTTON_CLASSNAME = [\n 'inline-flex items-center justify-center rounded-lg font-medium transition-colors',\n 'focus:outline-none focus:ring-2 focus:ring-offset-2',\n 'border border-gray-300 bg-transparent text-gray-700 hover:bg-gray-50 focus:ring-gray-500',\n 'dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-800',\n 'px-3 py-1.5 text-sm',\n].join(' ')\n\nconst ENHANCED_ATTR = 'data-mailoo-print-enhanced'\nconst SECTION_SELECTOR = `[data-mailoo-print]:not([${ENHANCED_ATTR}])`\n\nconst PRINT_WINDOW_STYLES = `\n body {\n font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;\n line-height: 1.6;\n margin: 1.5rem;\n color: #111;\n }\n h1, h2, h3, h4 {\n margin-top: 1.25em;\n margin-bottom: 0.5em;\n line-height: 1.25;\n }\n p { margin: 0.75em 0; }\n ul, ol { padding-left: 1.5em; margin: 0.75em 0; }\n li { margin: 0.25em 0; }\n img { max-width: 100%; height: auto; }\n a { color: #2563eb; }\n blockquote {\n margin: 1em 0;\n padding-left: 1em;\n border-left: 3px solid #d4d4d4;\n color: #525252;\n }\n .mailoo-print-source {\n margin-top: 2rem;\n padding: 0.75rem 1rem;\n border: 1px solid #e5e5e5;\n border-radius: 0.5rem;\n background: #fafafa;\n font-size: 0.8125rem;\n line-height: 1.45;\n color: #525252;\n }\n .mailoo-print-source-label {\n display: block;\n margin-bottom: 0.35rem;\n font-size: 0.6875rem;\n font-weight: 650;\n letter-spacing: 0.06em;\n text-transform: uppercase;\n color: #737373;\n }\n .mailoo-print-source-title {\n display: block;\n margin: 0;\n font-size: 0.875rem;\n font-weight: 600;\n color: #171717;\n text-wrap: pretty;\n }\n .mailoo-print-source-url {\n display: block;\n margin-top: 0.25rem;\n word-break: break-all;\n color: #525252;\n text-decoration: none;\n }\n`\n\nfunction escapeHtml(text: string): string {\n return text\n .replaceAll('&', '&amp;')\n .replaceAll('<', '&lt;')\n .replaceAll('>', '&gt;')\n .replaceAll('\"', '&quot;')\n .replaceAll(\"'\", '&#39;')\n}\n\nfunction buildSourceBlock(sourceLabel: string): string {\n const pageTitle = document.title\n const pageUrl = window.location.href\n return (\n `<aside class=\"mailoo-print-source\">` +\n `<span class=\"mailoo-print-source-label\">${escapeHtml(sourceLabel)}</span>` +\n `<span class=\"mailoo-print-source-title\">${escapeHtml(pageTitle)}</span>` +\n `<a class=\"mailoo-print-source-url\" href=\"${escapeHtml(pageUrl)}\">${escapeHtml(pageUrl)}</a>` +\n `</aside>`\n )\n}\n\nfunction printMailooSection(\n section: HTMLElement,\n sourceLabel: string | undefined\n): void {\n const body = section.querySelector('.mailoo-printable-body')\n if (!body) return\n\n const printTitle =\n section.getAttribute('data-print-title')?.trim() || document.title\n // Do not pass noopener: some browsers return null from window.open while\n // still opening a blank tab, so document.write never runs.\n const win = window.open('', '_blank')\n if (!win) return\n\n win.opener = null\n const lang = document.documentElement.lang\n const source = sourceLabel?.trim() ? buildSourceBlock(sourceLabel.trim()) : ''\n const html =\n `<!DOCTYPE html><html lang=\"${escapeHtml(lang)}\">` +\n `<head><meta charset=\"utf-8\"><title>${escapeHtml(printTitle)}</title>` +\n `<style>${PRINT_WINDOW_STYLES}</style></head>` +\n `<body>${body.innerHTML}${source}</body></html>`\n\n win.document.open()\n win.document.write(html)\n win.document.close()\n win.focus()\n\n const triggerPrint = () => {\n try {\n win.print()\n } catch {\n // Window may already be closed by the user.\n }\n }\n\n if (win.document.readyState === 'complete') {\n win.setTimeout(triggerPrint, 0)\n } else {\n win.addEventListener('load', triggerPrint, { once: true })\n }\n}\n\nexport type BlogPrintEvent = {\n section: HTMLElement\n printTitle: string\n}\n\nexport interface BlogPrintableButtonsProps {\n /** Button label (host provides i18n) */\n printLabel?: string\n buttonClassName?: string\n /**\n * Limit enhancement to `[data-mailoo-print]` inside this root.\n * When the root is missing, no buttons are added.\n * Omit to enhance every section in the document.\n */\n containerSelector?: string\n /** Called before the print window opens. The package does not call analytics. */\n onPrint?: (event: BlogPrintEvent) => void\n /**\n * When set, the print document appends a source block (label, document\n * title, page URL). Host provides the locale string.\n */\n sourceLabel?: string\n}\n\n/**\n * Enhances `[data-mailoo-print]` sections from API-generated article HTML with\n * a print button.\n */\nexport function BlogPrintableButtons({\n printLabel = 'Print summary',\n buttonClassName = PRINT_BUTTON_CLASSNAME,\n containerSelector,\n onPrint,\n sourceLabel,\n}: BlogPrintableButtonsProps = {}) {\n const onPrintRef = useRef(onPrint)\n const sourceLabelRef = useRef(sourceLabel)\n onPrintRef.current = onPrint\n sourceLabelRef.current = sourceLabel\n\n useEffect(() => {\n const root = containerSelector?.trim()\n ? document.querySelector(containerSelector)\n : document\n if (!root) return\n\n const sections = root.querySelectorAll<HTMLElement>(SECTION_SELECTOR)\n\n for (const section of sections) {\n section.setAttribute(ENHANCED_ATTR, 'true')\n\n const actions = document.createElement('div')\n actions.className = 'mailoo-printable-actions'\n\n const button = document.createElement('button')\n button.type = 'button'\n button.className = buttonClassName\n button.textContent = printLabel\n button.addEventListener('click', () => {\n const body = section.querySelector('.mailoo-printable-body')\n if (!body) return\n const printTitle =\n section.getAttribute('data-print-title')?.trim() || document.title\n onPrintRef.current?.({ section, printTitle })\n printMailooSection(section, sourceLabelRef.current)\n })\n\n actions.appendChild(button)\n section.insertBefore(actions, section.firstChild)\n }\n }, [printLabel, buttonClassName, containerSelector])\n\n return null\n}\n","'use client'\n\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport type {\n BlogArticleMarkCounts,\n BlogArticleMarkState,\n BlogPostReactionSentiment,\n} from '../types.js'\n\nconst STORAGE_KEY = 'mailoo_blog_visitor_key'\nconst VISITOR_STATE_PREFIX = 'mailoo_blog_mark:'\n\nconst BTN =\n 'inline-flex items-center gap-1.5 rounded-lg border px-3 py-1.5 text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-60'\nconst BTN_IDLE =\n 'border-gray-300 bg-transparent text-gray-700 hover:bg-gray-50 focus:ring-gray-500 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-800'\nconst BTN_ACTIVE =\n 'border-blue-600 bg-blue-50 text-blue-800 focus:ring-blue-500 dark:border-blue-400 dark:bg-blue-950 dark:text-blue-200'\n\nfunction createVisitorKey(): string {\n if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n return crypto.randomUUID()\n }\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0\n const v = c === 'x' ? r : (r & 0x3) | 0x8\n return v.toString(16)\n })\n}\n\nfunction readVisitorKey(): string {\n try {\n const existing = localStorage.getItem(STORAGE_KEY)\n if (existing && existing.length >= 32) return existing\n const next = createVisitorKey()\n localStorage.setItem(STORAGE_KEY, next)\n return next\n } catch {\n return createVisitorKey()\n }\n}\n\ntype LocalMark = {\n sentiment: BlogPostReactionSentiment | null\n hasFeedback: boolean\n}\n\nfunction readLocalMark(slug: string): LocalMark {\n try {\n const raw = localStorage.getItem(`${VISITOR_STATE_PREFIX}${slug}`)\n if (!raw) return { sentiment: null, hasFeedback: false }\n const parsed = JSON.parse(raw) as LocalMark\n return {\n sentiment:\n parsed.sentiment === 'LIKE' || parsed.sentiment === 'DISLIKE'\n ? parsed.sentiment\n : null,\n hasFeedback: Boolean(parsed.hasFeedback),\n }\n } catch {\n return { sentiment: null, hasFeedback: false }\n }\n}\n\nfunction writeLocalMark(slug: string, mark: LocalMark): void {\n try {\n localStorage.setItem(\n `${VISITOR_STATE_PREFIX}${slug}`,\n JSON.stringify(mark)\n )\n } catch {\n // ignore quota / private mode\n }\n}\n\nexport type BlogArticleMarksLabels = {\n like: string\n dislike: string\n feedback: string\n feedbackPlaceholder: string\n submit: string\n cancel: string\n dislikeRequiresComment: string\n error: string\n}\n\nexport type BlogArticleMarksProps = {\n slug: string\n locale?: string\n likeCount?: number\n dislikeCount?: number\n feedbackCount?: number\n /** Same-origin BFF base, e.g. `/api/v1/blog` */\n apiBasePath?: string\n labels: BlogArticleMarksLabels\n className?: string\n}\n\nasync function postMark(\n apiBasePath: string,\n slug: string,\n kind: 'reaction' | 'feedback',\n body: Record<string, unknown>\n): Promise<BlogArticleMarkState> {\n const res = await fetch(\n `${apiBasePath}/slug/${encodeURIComponent(slug)}/${kind}`,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n }\n )\n const json = (await res.json()) as {\n data?: BlogArticleMarkState\n error?: string\n message?: string\n }\n if (!res.ok || !json.data) {\n throw new Error(json.message || json.error || `HTTP ${res.status}`)\n }\n return json.data\n}\n\n/**\n * Public like / dislike / feedback controls with counts.\n * POSTs to same-origin BFF (never sends API key).\n */\nexport function BlogArticleMarks({\n slug,\n locale,\n likeCount = 0,\n dislikeCount = 0,\n feedbackCount = 0,\n apiBasePath = '/api/v1/blog',\n labels,\n className,\n}: BlogArticleMarksProps) {\n const [visitorKey, setVisitorKey] = useState<string | null>(null)\n const [counts, setCounts] = useState<BlogArticleMarkCounts>({\n likeCount,\n dislikeCount,\n feedbackCount,\n })\n const [sentiment, setSentiment] = useState<BlogPostReactionSentiment | null>(\n null\n )\n const [hasFeedback, setHasFeedback] = useState(false)\n const [pending, setPending] = useState(false)\n const [error, setError] = useState<string | null>(null)\n const [modal, setModal] = useState<'dislike' | 'feedback' | null>(null)\n const [comment, setComment] = useState('')\n\n useEffect(() => {\n const key = readVisitorKey()\n setVisitorKey(key)\n const local = readLocalMark(slug)\n setSentiment(local.sentiment)\n setHasFeedback(local.hasFeedback)\n }, [slug])\n\n useEffect(() => {\n setCounts({ likeCount, dislikeCount, feedbackCount })\n }, [likeCount, dislikeCount, feedbackCount])\n\n const applyState = useCallback(\n (state: BlogArticleMarkState) => {\n setCounts({\n likeCount: state.likeCount,\n dislikeCount: state.dislikeCount,\n feedbackCount: state.feedbackCount,\n })\n setSentiment(state.sentiment)\n setHasFeedback(state.hasFeedback)\n writeLocalMark(slug, {\n sentiment: state.sentiment,\n hasFeedback: state.hasFeedback,\n })\n },\n [slug]\n )\n\n const run = useCallback(\n async (kind: 'reaction' | 'feedback', body: Record<string, unknown>) => {\n if (!visitorKey || pending) return\n setPending(true)\n setError(null)\n try {\n const state = await postMark(apiBasePath, slug, kind, {\n ...body,\n visitorKey,\n ...(locale ? { locale } : {}),\n })\n applyState(state)\n setModal(null)\n setComment('')\n } catch {\n setError(labels.error)\n } finally {\n setPending(false)\n }\n },\n [apiBasePath, applyState, labels.error, locale, pending, slug, visitorKey]\n )\n\n const onLike = useCallback(() => {\n if (sentiment === 'LIKE') {\n void run('reaction', { action: 'clear' })\n return\n }\n void run('reaction', { action: 'set', sentiment: 'LIKE' })\n }, [run, sentiment])\n\n const onDislikeClick = useCallback(() => {\n if (sentiment === 'DISLIKE') {\n void run('reaction', { action: 'clear' })\n return\n }\n if (hasFeedback) {\n void run('reaction', { action: 'set', sentiment: 'DISLIKE' })\n return\n }\n setModal('dislike')\n }, [hasFeedback, run, sentiment])\n\n const onSubmitModal = useCallback(() => {\n const trimmed = comment.trim()\n if (!trimmed) {\n setError(labels.dislikeRequiresComment)\n return\n }\n if (modal === 'dislike') {\n void run('reaction', {\n action: 'set',\n sentiment: 'DISLIKE',\n comment: trimmed,\n })\n return\n }\n void run('feedback', { action: 'set', comment: trimmed })\n }, [comment, labels.dislikeRequiresComment, modal, run])\n\n const rootClass = useMemo(\n () =>\n [\n 'mt-8 flex flex-col gap-3 border-t border-gray-200 pt-6 dark:border-gray-700',\n className,\n ]\n .filter(Boolean)\n .join(' '),\n [className]\n )\n\n return (\n <div className={rootClass}>\n <div className=\"flex flex-wrap items-center gap-2\">\n <button\n type=\"button\"\n className={`${BTN} ${sentiment === 'LIKE' ? BTN_ACTIVE : BTN_IDLE}`}\n disabled={pending || !visitorKey}\n onClick={onLike}\n aria-pressed={sentiment === 'LIKE'}\n >\n <span>{labels.like}</span>\n <span className=\"tabular-nums\">{counts.likeCount}</span>\n </button>\n <button\n type=\"button\"\n className={`${BTN} ${sentiment === 'DISLIKE' ? BTN_ACTIVE : BTN_IDLE}`}\n disabled={pending || !visitorKey}\n onClick={onDislikeClick}\n aria-pressed={sentiment === 'DISLIKE'}\n >\n <span>{labels.dislike}</span>\n <span className=\"tabular-nums\">{counts.dislikeCount}</span>\n </button>\n <button\n type=\"button\"\n className={`${BTN} ${hasFeedback ? BTN_ACTIVE : BTN_IDLE}`}\n disabled={pending || !visitorKey}\n onClick={() => {\n setError(null)\n setModal('feedback')\n }}\n aria-pressed={hasFeedback}\n >\n <span>{labels.feedback}</span>\n <span className=\"tabular-nums\">{counts.feedbackCount}</span>\n </button>\n </div>\n\n {error ? (\n <p className=\"text-sm text-red-600 dark:text-red-400\" role=\"alert\">\n {error}\n </p>\n ) : null}\n\n {modal ? (\n <div className=\"rounded-lg border border-gray-200 p-4 dark:border-gray-700\">\n <label className=\"mb-2 block text-sm font-medium text-gray-800 dark:text-gray-200\">\n {modal === 'dislike'\n ? labels.dislikeRequiresComment\n : labels.feedback}\n </label>\n <textarea\n className=\"mb-3 w-full rounded-md border border-gray-300 bg-white p-2 text-sm text-gray-900 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-100\"\n rows={3}\n maxLength={1000}\n value={comment}\n placeholder={labels.feedbackPlaceholder}\n onChange={(e) => setComment(e.target.value)}\n />\n <div className=\"flex gap-2\">\n <button\n type=\"button\"\n className={`${BTN} ${BTN_ACTIVE}`}\n disabled={pending}\n onClick={onSubmitModal}\n >\n {labels.submit}\n </button>\n <button\n type=\"button\"\n className={`${BTN} ${BTN_IDLE}`}\n disabled={pending}\n onClick={() => {\n setModal(null)\n setComment('')\n setError(null)\n }}\n >\n {labels.cancel}\n </button>\n </div>\n </div>\n ) : null}\n </div>\n )\n}\n","'use client'\n\nimport { useCallback, useState } from 'react'\nimport {\n buildFacebookSharerUrl,\n buildLinkedInShareUrl,\n buildXIntentTweetUrl,\n getSharePageUrlFromLocation,\n requireAbsoluteHttpUrl,\n} from '../lib/share-urls.js'\n\nexport {\n buildFacebookSharerUrl,\n buildLinkedInShareUrl,\n buildXIntentTweetUrl,\n getSharePageUrlFromLocation,\n requireAbsoluteHttpUrl,\n} from '../lib/share-urls.js'\n\nconst DEFAULT_BUTTON_CLASSNAME = [\n 'inline-flex items-center justify-center rounded-lg font-medium transition-colors',\n 'focus:outline-none focus:ring-2 focus:ring-offset-2',\n 'border border-gray-300 bg-transparent text-gray-700 hover:bg-gray-50 focus:ring-gray-500',\n 'dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-800',\n 'px-3 py-1.5 text-sm',\n].join(' ')\n\nconst DEFAULT_HEADING_CLASSNAME =\n 'mb-4 text-lg font-semibold text-gray-900 dark:text-white'\n\nconst DEFAULT_SECTION_CLASSNAME =\n 'mt-12 border-t border-gray-200 pt-8 dark:border-gray-700'\n\nexport type BlogArticleShareLabels = {\n heading: string\n x: string\n facebook: string\n linkedin: string\n copyLink: string\n linkCopied: string\n}\n\nexport type BlogArticleShareProps = {\n /** Article title (host API parity with mindful-journal BlogPostShareButtons). */\n title: string\n /** Already-interpolated share text for X (e.g. \"Check out this article: …\"). */\n text: string\n labels: BlogArticleShareLabels\n /**\n * Absolute http(s) share URL. When omitted, each click uses\n * `window.location.origin + pathname` (no query/hash).\n */\n url?: string\n className?: string\n headingClassName?: string\n buttonClassName?: string\n}\n\nfunction resolveShareUrl(url: string | undefined): string {\n if (url !== undefined) {\n return requireAbsoluteHttpUrl(url)\n }\n if (typeof window === 'undefined') {\n throw new Error('Share URL requires a browser location when url is omitted')\n }\n return getSharePageUrlFromLocation(window.location)\n}\n\nfunction openShareWindow(href: string): void {\n window.open(href, '_blank', 'noopener,noreferrer')\n}\n\n/**\n * X / Facebook / LinkedIn / copy-link controls for a blog article.\n * Host provides i18n labels and share text; optional absolute `url`.\n */\nexport function BlogArticleShare({\n title: _title,\n text,\n labels,\n url,\n className = DEFAULT_SECTION_CLASSNAME,\n headingClassName = DEFAULT_HEADING_CLASSNAME,\n buttonClassName = DEFAULT_BUTTON_CLASSNAME,\n}: BlogArticleShareProps) {\n const [linkCopied, setLinkCopied] = useState(false)\n\n const shareUrl = useCallback(() => resolveShareUrl(url), [url])\n\n const onCopy = useCallback(async () => {\n try {\n await navigator.clipboard.writeText(shareUrl())\n setLinkCopied(true)\n setTimeout(() => setLinkCopied(false), 2000)\n } catch {\n // Fail-fast UI: do not claim success or invent a second copy path.\n }\n }, [shareUrl])\n\n return (\n <div className={className}>\n <h3 className={headingClassName}>{labels.heading}</h3>\n <div className=\"flex flex-wrap gap-4\">\n <button\n type=\"button\"\n className={buttonClassName}\n onClick={() => {\n openShareWindow(buildXIntentTweetUrl(shareUrl(), text))\n }}\n >\n <svg\n className=\"mr-2 h-4 w-4\"\n fill=\"currentColor\"\n viewBox=\"0 0 24 24\"\n aria-hidden=\"true\"\n >\n <path d=\"M8.29 20.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0022 5.92a8.19 8.19 0 01-2.357.646 4.118 4.118 0 001.804-2.27 8.224 8.224 0 01-2.605.996 4.107 4.107 0 00-6.993 3.743 11.65 11.65 0 01-8.457-4.287 4.106 4.106 0 001.27 5.477A4.072 4.072 0 012.8 9.713v.052a4.105 4.105 0 003.292 4.022 4.095 4.095 0 01-1.853.07 4.108 4.108 0 003.834 2.85A8.233 8.233 0 012 18.407a11.616 11.616 0 006.29 1.84\" />\n </svg>\n {labels.x}\n </button>\n <button\n type=\"button\"\n className={buttonClassName}\n onClick={() => {\n openShareWindow(buildFacebookSharerUrl(shareUrl()))\n }}\n >\n <svg\n className=\"mr-2 h-4 w-4\"\n fill=\"currentColor\"\n viewBox=\"0 0 24 24\"\n aria-hidden=\"true\"\n >\n <path d=\"M24 12.073C24 5.405 18.627 0 12 0S0 5.405 0 12.073C0 18.1 4.388 23.094 10.125 24v-8.435H7.078v-3.492h3.047V9.414c0-3.025 1.792-4.697 4.533-4.697 1.312 0 2.686.236 2.686.236v2.974h-1.513c-1.49 0-1.956.931-1.956 1.887v2.259h3.328l-.532 3.492h-2.796V24C19.612 23.094 24 18.1 24 12.073z\" />\n </svg>\n {labels.facebook}\n </button>\n <button\n type=\"button\"\n className={buttonClassName}\n onClick={() => {\n openShareWindow(buildLinkedInShareUrl(shareUrl()))\n }}\n >\n <svg\n className=\"mr-2 h-4 w-4\"\n fill=\"currentColor\"\n viewBox=\"0 0 24 24\"\n aria-hidden=\"true\"\n >\n <path d=\"M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z\" />\n </svg>\n {labels.linkedin}\n </button>\n <button\n type=\"button\"\n className={buttonClassName}\n onClick={() => void onCopy()}\n >\n <svg\n className=\"mr-2 h-4 w-4\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n aria-hidden=\"true\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z\"\n />\n </svg>\n {linkCopied ? labels.linkCopied : labels.copyLink}\n </button>\n </div>\n </div>\n )\n}\n","/** Lightweight social share URL builders (no host UI deps). */\n\nexport function requireAbsoluteHttpUrl(pageUrl: string): string {\n const trimmed = pageUrl.trim()\n if (!trimmed) {\n throw new Error('Share URL must be a non-empty absolute http(s) URL')\n }\n let parsed: URL\n try {\n parsed = new URL(trimmed)\n } catch {\n throw new Error(`Share URL is not a valid absolute URL: ${pageUrl}`)\n }\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {\n throw new Error(`Share URL must use http or https: ${pageUrl}`)\n }\n return trimmed\n}\n\nexport function buildFacebookSharerUrl(\n pageUrl: string,\n quote?: string\n): string {\n const params = new URLSearchParams({\n u: requireAbsoluteHttpUrl(pageUrl),\n })\n const trimmedQuote = quote?.trim()\n if (trimmedQuote) {\n params.set('quote', trimmedQuote)\n }\n return `https://www.facebook.com/sharer/sharer.php?${params.toString()}`\n}\n\n/** Blog article share (matches mindful-journal BlogPostShareButtons). */\nexport function buildXIntentTweetUrl(pageUrl: string, text: string): string {\n const params = new URLSearchParams({\n text,\n url: requireAbsoluteHttpUrl(pageUrl),\n })\n return `https://twitter.com/intent/tweet?${params.toString()}`\n}\n\nexport function buildLinkedInShareUrl(pageUrl: string): string {\n const params = new URLSearchParams({\n url: requireAbsoluteHttpUrl(pageUrl),\n })\n return `https://www.linkedin.com/sharing/share-offsite/?${params.toString()}`\n}\n\n/** Canonical page URL without query/hash — safer for social scrapers. */\nexport function getSharePageUrlFromLocation(\n location: Pick<Location, 'origin' | 'pathname'>\n): string {\n return `${location.origin}${location.pathname}`\n}\n"],"mappings":"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","names":["jsx","useEffect","jsx","jsxs","useCallback","useState","jsx","jsxs","useState","useCallback"]}
1
+ {"version":3,"sources":["../../src/components/blog-article-body.tsx","../../src/markdown-link-target.ts","../../src/server/utils.ts","../../src/components/blog-linked-links-display.tsx","../../src/components/blog-printable-buttons.tsx","../../src/components/blog-article-marks.tsx","../../src/components/blog-article-share.tsx","../../src/lib/share-urls.ts"],"sourcesContent":["import ReactMarkdown from 'react-markdown'\nimport remarkGfm from 'remark-gfm'\nimport { markdownHrefOpensInNewTab } from '../markdown-link-target.js'\nimport { BLOG_ARTICLE_PROSE_CLASSNAME } from '../server/utils.js'\n\nexport interface BlogArticleBodyProps {\n htmlContent?: string | null\n markdownFallback?: string | null\n /** Override default Tailwind Typography classes */\n className?: string\n}\n\n/**\n * Renders blog article body: prefers `htmlContent` from the headless API,\n * falls back to Markdown when `htmlContent` is empty.\n */\nexport function BlogArticleBody({\n htmlContent,\n markdownFallback,\n className = BLOG_ARTICLE_PROSE_CLASSNAME,\n}: BlogArticleBodyProps) {\n const html = htmlContent?.trim()\n if (html) {\n return (\n <div\n className={className}\n dangerouslySetInnerHTML={{ __html: html }}\n />\n )\n }\n\n const md = markdownFallback?.trim()\n if (md) {\n return (\n <div className={className}>\n <ReactMarkdown\n remarkPlugins={[remarkGfm]}\n components={{\n a({ children, href, ...props }) {\n const h = href ?? ''\n const openNew = markdownHrefOpensInNewTab(h)\n return (\n <a\n href={h}\n {...(openNew\n ? { target: '_blank', rel: 'noopener noreferrer' }\n : {})}\n {...props}\n >\n {children}\n </a>\n )\n },\n }}\n >\n {md}\n </ReactMarkdown>\n </div>\n )\n }\n\n return null\n}\n","export function markdownHrefOpensInNewTab(href: string): boolean {\n const h = href.trim()\n if (h === '') return false\n if (h.startsWith('#') || h.startsWith('?')) return false\n if (h.startsWith('//')) return true\n if (h.startsWith('/')) return false\n if (h.startsWith('./') || h.startsWith('../')) return false\n const lower = h.toLowerCase()\n return lower.startsWith('http://') || lower.startsWith('https://')\n}\n","/**\n * First cover-or-body image URL for OG/Twitter previews: prefer HTML from the\n * API, then markdown image syntax in content. May be absolute or a\n * site-relative path after rewrite. Resolve against the current request\n * origin before `og:image` / `twitter:image`.\n */\nconst HTML_IMG_SRC = /<img[^>]+src=[\"']([^\"']+)[\"']/i\nconst MD_IMAGE = /!\\[[^\\]]*]\\(([^)\\s]+)\\)/\n\nfunction normalizeUrl(raw: string): string | null {\n const u = raw.trim()\n if (!u) return null\n if (\n u.startsWith('http://') ||\n u.startsWith('https://') ||\n u.startsWith('/')\n ) {\n return u\n }\n return null\n}\n\nexport function firstEmbedImageUrlFromArticle(\n htmlContent: string | null | undefined,\n markdownContent: string | null | undefined\n): string | null {\n if (htmlContent) {\n const m = htmlContent.match(HTML_IMG_SRC)\n if (m?.[1]) {\n const url = normalizeUrl(m[1])\n if (url) return url\n }\n }\n if (markdownContent) {\n const m = markdownContent.match(MD_IMAGE)\n if (m?.[1]) {\n const url = normalizeUrl(m[1])\n if (url) return url\n }\n }\n return null\n}\n\n/**\n * Default Tailwind Typography class string for article body.\n * Hosts may pass a custom `className` instead.\n */\nexport const BLOG_ARTICLE_PROSE_CLASSNAME = [\n 'prose',\n 'prose-gray',\n 'prose-lg',\n 'max-w-none',\n 'dark:prose-invert',\n 'prose-a:text-blue-600',\n 'hover:prose-a:text-blue-700',\n 'dark:prose-a:text-blue-400',\n 'dark:hover:prose-a:text-blue-300',\n 'prose-blockquote:border-blue-500',\n 'prose-img:rounded-lg',\n 'prose-img:shadow-sm',\n].join(' ')\n","import type { ReactNode } from 'react'\n\nexport type BlogLinkedLinkDisplayItem = {\n id?: string\n type: 'internal_article' | 'update_announcement' | 'external_resource'\n label: string\n url: string\n intro: string | null\n date: string | null\n sortOrder: number\n}\n\nconst TYPE_ORDER: BlogLinkedLinkDisplayItem['type'][] = [\n 'internal_article',\n 'update_announcement',\n 'external_resource',\n]\n\nfunction groupByType(\n links: BlogLinkedLinkDisplayItem[]\n): Map<BlogLinkedLinkDisplayItem['type'], BlogLinkedLinkDisplayItem[]> {\n const sorted = [...links].sort((a, b) => a.sortOrder - b.sortOrder)\n const map = new Map<\n BlogLinkedLinkDisplayItem['type'],\n BlogLinkedLinkDisplayItem[]\n >()\n for (const t of TYPE_ORDER) map.set(t, [])\n for (const link of sorted) {\n const bucket = map.get(link.type)\n if (bucket) bucket.push(link)\n }\n return map\n}\n\nexport interface BlogLinkedLinksDisplayProps {\n links: BlogLinkedLinkDisplayItem[]\n title: string\n typeLabels: Record<BlogLinkedLinkDisplayItem['type'], string>\n variant?: 'article' | 'compact'\n /**\n * When set, paths starting with `/blog/` become `/{locale}/blog/...`.\n */\n blogLocale?: string\n /** Optional link component (e.g. next/link). Defaults to `<a>`. */\n LinkComponent?: (props: {\n href: string\n className?: string\n children: ReactNode\n }) => ReactNode\n}\n\nfunction hrefForLink(url: string, blogLocale?: string): string {\n if (blogLocale && url.startsWith('/blog/')) {\n return `/${blogLocale}${url}`\n }\n return url\n}\n\nexport function BlogLinkedLinksDisplay({\n links,\n title,\n typeLabels,\n variant = 'article',\n blogLocale,\n LinkComponent,\n}: Readonly<BlogLinkedLinksDisplayProps>) {\n if (!links.length) return null\n\n const grouped = groupByType(links)\n const sectionClass =\n variant === 'article'\n ? 'mt-10 border-t border-gray-200 pt-8 dark:border-gray-700'\n : 'mt-4 rounded-lg border border-dashed border-gray-300 p-4 dark:border-gray-600'\n\n const Link =\n LinkComponent ??\n (({ href, className, children }) => (\n <a href={href} className={className}>\n {children}\n </a>\n ))\n\n return (\n <section\n className={sectionClass}\n aria-labelledby=\"mailoo-linked-links-heading\"\n >\n <h2\n id=\"mailoo-linked-links-heading\"\n className={\n variant === 'article'\n ? 'mb-6 text-xl font-semibold text-gray-900 dark:text-white'\n : 'mb-3 text-sm font-semibold text-gray-800 dark:text-gray-200'\n }\n >\n {title}\n </h2>\n <div className=\"space-y-8\">\n {TYPE_ORDER.map((type) => {\n const items = grouped.get(type) ?? []\n if (!items.length) return null\n return (\n <div key={type}>\n <h3\n className={\n variant === 'article'\n ? 'mb-3 text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400'\n : 'mb-2 text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400'\n }\n >\n {typeLabels[type]}\n </h3>\n <ul className=\"space-y-4\">\n {items.map((item) => (\n <li\n key={\n item.id ?? `${item.type}-${item.url}-${item.sortOrder}`\n }\n >\n <Link\n href={hrefForLink(item.url, blogLocale)}\n className={\n variant === 'article'\n ? 'text-lg font-medium text-blue-600 hover:underline dark:text-blue-400'\n : 'text-sm font-medium text-blue-600 hover:underline dark:text-blue-400'\n }\n >\n {item.label}\n </Link>\n {item.date && (\n <p\n className={\n variant === 'article'\n ? 'mt-1 text-sm text-gray-500 dark:text-gray-400'\n : 'mt-0.5 text-xs text-gray-500 dark:text-gray-400'\n }\n >\n <time dateTime={item.date}>\n {new Date(item.date).toLocaleDateString()}\n </time>\n </p>\n )}\n {item.intro && (\n <p\n className={\n variant === 'article'\n ? 'mt-2 text-sm text-gray-600 dark:text-gray-300'\n : 'mt-1 text-xs text-gray-600 dark:text-gray-300'\n }\n >\n {item.intro}\n </p>\n )}\n </li>\n ))}\n </ul>\n </div>\n )\n })}\n </div>\n </section>\n )\n}\n","'use client'\n\nimport { useEffect, useRef } from 'react'\n\nconst PRINT_BUTTON_CLASSNAME = [\n 'inline-flex items-center justify-center rounded-lg font-medium transition-colors',\n 'focus:outline-none focus:ring-2 focus:ring-offset-2',\n 'border border-gray-300 bg-transparent text-gray-700 hover:bg-gray-50 focus:ring-gray-500',\n 'dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-800',\n 'px-3 py-1.5 text-sm',\n].join(' ')\n\nconst ENHANCED_ATTR = 'data-mailoo-print-enhanced'\nconst SECTION_SELECTOR = `[data-mailoo-print]:not([${ENHANCED_ATTR}])`\n\nconst PRINT_WINDOW_STYLES = `\n body {\n font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;\n line-height: 1.6;\n margin: 1.5rem;\n color: #111;\n }\n h1, h2, h3, h4 {\n margin-top: 1.25em;\n margin-bottom: 0.5em;\n line-height: 1.25;\n }\n p { margin: 0.75em 0; }\n ul, ol { padding-left: 1.5em; margin: 0.75em 0; }\n li { margin: 0.25em 0; }\n img { max-width: 100%; height: auto; }\n a { color: #2563eb; }\n blockquote {\n margin: 1em 0;\n padding-left: 1em;\n border-left: 3px solid #d4d4d4;\n color: #525252;\n }\n .mailoo-print-source {\n margin-top: 2rem;\n padding: 0.75rem 1rem;\n border: 1px solid #e5e5e5;\n border-radius: 0.5rem;\n background: #fafafa;\n font-size: 0.8125rem;\n line-height: 1.45;\n color: #525252;\n }\n .mailoo-print-source-label {\n display: block;\n margin-bottom: 0.35rem;\n font-size: 0.6875rem;\n font-weight: 650;\n letter-spacing: 0.06em;\n text-transform: uppercase;\n color: #737373;\n }\n .mailoo-print-source-title {\n display: block;\n margin: 0;\n font-size: 0.875rem;\n font-weight: 600;\n color: #171717;\n text-wrap: pretty;\n }\n .mailoo-print-source-url {\n display: block;\n margin-top: 0.25rem;\n word-break: break-all;\n color: #525252;\n text-decoration: none;\n }\n`\n\nfunction escapeHtml(text: string): string {\n return text\n .replaceAll('&', '&amp;')\n .replaceAll('<', '&lt;')\n .replaceAll('>', '&gt;')\n .replaceAll('\"', '&quot;')\n .replaceAll(\"'\", '&#39;')\n}\n\nfunction buildSourceBlock(sourceLabel: string): string {\n const pageTitle = document.title\n const pageUrl = window.location.href\n return (\n `<aside class=\"mailoo-print-source\">` +\n `<span class=\"mailoo-print-source-label\">${escapeHtml(sourceLabel)}</span>` +\n `<span class=\"mailoo-print-source-title\">${escapeHtml(pageTitle)}</span>` +\n `<a class=\"mailoo-print-source-url\" href=\"${escapeHtml(pageUrl)}\">${escapeHtml(pageUrl)}</a>` +\n `</aside>`\n )\n}\n\nfunction printMailooSection(\n section: HTMLElement,\n sourceLabel: string | undefined\n): void {\n const body = section.querySelector('.mailoo-printable-body')\n if (!body) return\n\n const printTitle =\n section.getAttribute('data-print-title')?.trim() || document.title\n // Do not pass noopener: some browsers return null from window.open while\n // still opening a blank tab, so document.write never runs.\n const win = window.open('', '_blank')\n if (!win) return\n\n win.opener = null\n const lang = document.documentElement.lang\n const source = sourceLabel?.trim() ? buildSourceBlock(sourceLabel.trim()) : ''\n const html =\n `<!DOCTYPE html><html lang=\"${escapeHtml(lang)}\">` +\n `<head><meta charset=\"utf-8\"><title>${escapeHtml(printTitle)}</title>` +\n `<style>${PRINT_WINDOW_STYLES}</style></head>` +\n `<body>${body.innerHTML}${source}</body></html>`\n\n win.document.open()\n win.document.write(html)\n win.document.close()\n win.focus()\n\n const triggerPrint = () => {\n try {\n win.print()\n } catch {\n // Window may already be closed by the user.\n }\n }\n\n if (win.document.readyState === 'complete') {\n win.setTimeout(triggerPrint, 0)\n } else {\n win.addEventListener('load', triggerPrint, { once: true })\n }\n}\n\nexport type BlogPrintEvent = {\n section: HTMLElement\n printTitle: string\n}\n\nexport interface BlogPrintableButtonsProps {\n /** Button label (host provides i18n) */\n printLabel?: string\n buttonClassName?: string\n /**\n * Limit enhancement to `[data-mailoo-print]` inside this root.\n * When the root is missing, no buttons are added.\n * Omit to enhance every section in the document.\n */\n containerSelector?: string\n /** Called before the print window opens. The package does not call analytics. */\n onPrint?: (event: BlogPrintEvent) => void\n /**\n * When set, the print document appends a source block (label, document\n * title, page URL). Host provides the locale string.\n */\n sourceLabel?: string\n}\n\n/**\n * Enhances `[data-mailoo-print]` sections from API-generated article HTML with\n * a print button.\n */\nexport function BlogPrintableButtons({\n printLabel = 'Print summary',\n buttonClassName = PRINT_BUTTON_CLASSNAME,\n containerSelector,\n onPrint,\n sourceLabel,\n}: BlogPrintableButtonsProps = {}) {\n const onPrintRef = useRef(onPrint)\n const sourceLabelRef = useRef(sourceLabel)\n onPrintRef.current = onPrint\n sourceLabelRef.current = sourceLabel\n\n useEffect(() => {\n const root = containerSelector?.trim()\n ? document.querySelector(containerSelector)\n : document\n if (!root) return\n\n const sections = root.querySelectorAll<HTMLElement>(SECTION_SELECTOR)\n\n for (const section of sections) {\n section.setAttribute(ENHANCED_ATTR, 'true')\n\n const actions = document.createElement('div')\n actions.className = 'mailoo-printable-actions'\n\n const button = document.createElement('button')\n button.type = 'button'\n button.className = buttonClassName\n button.textContent = printLabel\n button.addEventListener('click', () => {\n const body = section.querySelector('.mailoo-printable-body')\n if (!body) return\n const printTitle =\n section.getAttribute('data-print-title')?.trim() || document.title\n onPrintRef.current?.({ section, printTitle })\n printMailooSection(section, sourceLabelRef.current)\n })\n\n actions.appendChild(button)\n section.insertBefore(actions, section.firstChild)\n }\n }, [printLabel, buttonClassName, containerSelector])\n\n return null\n}\n","'use client'\n\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport type {\n BlogArticleMarkCounts,\n BlogArticleMarkState,\n BlogPostReactionSentiment,\n} from '../types.js'\n\nconst STORAGE_KEY = 'mailoo_blog_visitor_key'\nconst VISITOR_STATE_PREFIX = 'mailoo_blog_mark:'\n\nconst BTN =\n 'inline-flex items-center gap-1.5 rounded-lg border px-3 py-1.5 text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-60'\nconst BTN_IDLE =\n 'border-gray-300 bg-transparent text-gray-700 hover:bg-gray-50 focus:ring-gray-500 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-800'\nconst BTN_ACTIVE =\n 'border-blue-600 bg-blue-50 text-blue-800 focus:ring-blue-500 dark:border-blue-400 dark:bg-blue-950 dark:text-blue-200'\n\nfunction createVisitorKey(): string {\n if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n return crypto.randomUUID()\n }\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0\n const v = c === 'x' ? r : (r & 0x3) | 0x8\n return v.toString(16)\n })\n}\n\nfunction readVisitorKey(): string {\n try {\n const existing = localStorage.getItem(STORAGE_KEY)\n if (existing && existing.length >= 32) return existing\n const next = createVisitorKey()\n localStorage.setItem(STORAGE_KEY, next)\n return next\n } catch {\n return createVisitorKey()\n }\n}\n\ntype LocalMark = {\n sentiment: BlogPostReactionSentiment | null\n hasFeedback: boolean\n}\n\nfunction readLocalMark(slug: string): LocalMark {\n try {\n const raw = localStorage.getItem(`${VISITOR_STATE_PREFIX}${slug}`)\n if (!raw) return { sentiment: null, hasFeedback: false }\n const parsed = JSON.parse(raw) as LocalMark\n return {\n sentiment:\n parsed.sentiment === 'LIKE' || parsed.sentiment === 'DISLIKE'\n ? parsed.sentiment\n : null,\n hasFeedback: Boolean(parsed.hasFeedback),\n }\n } catch {\n return { sentiment: null, hasFeedback: false }\n }\n}\n\nfunction writeLocalMark(slug: string, mark: LocalMark): void {\n try {\n localStorage.setItem(\n `${VISITOR_STATE_PREFIX}${slug}`,\n JSON.stringify(mark)\n )\n } catch {\n // ignore quota / private mode\n }\n}\n\nexport type BlogArticleMarksLabels = {\n like: string\n dislike: string\n feedback: string\n feedbackPlaceholder: string\n submit: string\n cancel: string\n dislikeRequiresComment: string\n error: string\n}\n\nexport type BlogArticleMarksProps = {\n slug: string\n locale?: string\n likeCount?: number\n dislikeCount?: number\n feedbackCount?: number\n /** Same-origin BFF base, e.g. `/api/v1/blog` */\n apiBasePath?: string\n labels: BlogArticleMarksLabels\n className?: string\n}\n\nasync function postMark(\n apiBasePath: string,\n slug: string,\n kind: 'reaction' | 'feedback',\n body: Record<string, unknown>\n): Promise<BlogArticleMarkState> {\n const res = await fetch(\n `${apiBasePath}/slug/${encodeURIComponent(slug)}/${kind}`,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n }\n )\n const json = (await res.json()) as {\n data?: BlogArticleMarkState\n error?: string\n message?: string\n }\n if (!res.ok || !json.data) {\n throw new Error(json.message || json.error || `HTTP ${res.status}`)\n }\n return json.data\n}\n\n/**\n * Public like / dislike / feedback controls with counts.\n * POSTs to same-origin BFF (never sends API key).\n */\nexport function BlogArticleMarks({\n slug,\n locale,\n likeCount = 0,\n dislikeCount = 0,\n feedbackCount = 0,\n apiBasePath = '/api/v1/blog',\n labels,\n className,\n}: BlogArticleMarksProps) {\n const [visitorKey, setVisitorKey] = useState<string | null>(null)\n const [counts, setCounts] = useState<BlogArticleMarkCounts>({\n likeCount,\n dislikeCount,\n feedbackCount,\n })\n const [sentiment, setSentiment] = useState<BlogPostReactionSentiment | null>(\n null\n )\n const [hasFeedback, setHasFeedback] = useState(false)\n const [pending, setPending] = useState(false)\n const [error, setError] = useState<string | null>(null)\n const [modal, setModal] = useState<'dislike' | 'feedback' | null>(null)\n const [comment, setComment] = useState('')\n\n useEffect(() => {\n const key = readVisitorKey()\n setVisitorKey(key)\n const local = readLocalMark(slug)\n setSentiment(local.sentiment)\n setHasFeedback(local.hasFeedback)\n }, [slug])\n\n useEffect(() => {\n setCounts({ likeCount, dislikeCount, feedbackCount })\n }, [likeCount, dislikeCount, feedbackCount])\n\n const applyState = useCallback(\n (state: BlogArticleMarkState) => {\n setCounts({\n likeCount: state.likeCount,\n dislikeCount: state.dislikeCount,\n feedbackCount: state.feedbackCount,\n })\n setSentiment(state.sentiment)\n setHasFeedback(state.hasFeedback)\n writeLocalMark(slug, {\n sentiment: state.sentiment,\n hasFeedback: state.hasFeedback,\n })\n },\n [slug]\n )\n\n const run = useCallback(\n async (kind: 'reaction' | 'feedback', body: Record<string, unknown>) => {\n if (!visitorKey || pending) return\n setPending(true)\n setError(null)\n try {\n const state = await postMark(apiBasePath, slug, kind, {\n ...body,\n visitorKey,\n ...(locale ? { locale } : {}),\n })\n applyState(state)\n setModal(null)\n setComment('')\n } catch {\n setError(labels.error)\n } finally {\n setPending(false)\n }\n },\n [apiBasePath, applyState, labels.error, locale, pending, slug, visitorKey]\n )\n\n const onLike = useCallback(() => {\n if (sentiment === 'LIKE') {\n void run('reaction', { action: 'clear' })\n return\n }\n void run('reaction', { action: 'set', sentiment: 'LIKE' })\n }, [run, sentiment])\n\n const onDislikeClick = useCallback(() => {\n if (sentiment === 'DISLIKE') {\n void run('reaction', { action: 'clear' })\n return\n }\n if (hasFeedback) {\n void run('reaction', { action: 'set', sentiment: 'DISLIKE' })\n return\n }\n setModal('dislike')\n }, [hasFeedback, run, sentiment])\n\n const onSubmitModal = useCallback(() => {\n const trimmed = comment.trim()\n if (!trimmed) {\n setError(labels.dislikeRequiresComment)\n return\n }\n if (modal === 'dislike') {\n void run('reaction', {\n action: 'set',\n sentiment: 'DISLIKE',\n comment: trimmed,\n })\n return\n }\n void run('feedback', { action: 'set', comment: trimmed })\n }, [comment, labels.dislikeRequiresComment, modal, run])\n\n const rootClass = useMemo(\n () =>\n [\n 'mt-8 flex flex-col gap-3 border-t border-gray-200 pt-6 dark:border-gray-700',\n className,\n ]\n .filter(Boolean)\n .join(' '),\n [className]\n )\n\n return (\n <div className={rootClass}>\n <div className=\"flex flex-wrap items-center gap-2\">\n <button\n type=\"button\"\n className={`${BTN} ${sentiment === 'LIKE' ? BTN_ACTIVE : BTN_IDLE}`}\n disabled={pending || !visitorKey}\n onClick={onLike}\n aria-pressed={sentiment === 'LIKE'}\n >\n <span>{labels.like}</span>\n <span className=\"tabular-nums\">{counts.likeCount}</span>\n </button>\n <button\n type=\"button\"\n className={`${BTN} ${sentiment === 'DISLIKE' ? BTN_ACTIVE : BTN_IDLE}`}\n disabled={pending || !visitorKey}\n onClick={onDislikeClick}\n aria-pressed={sentiment === 'DISLIKE'}\n >\n <span>{labels.dislike}</span>\n <span className=\"tabular-nums\">{counts.dislikeCount}</span>\n </button>\n <button\n type=\"button\"\n className={`${BTN} ${hasFeedback ? BTN_ACTIVE : BTN_IDLE}`}\n disabled={pending || !visitorKey}\n onClick={() => {\n setError(null)\n setModal('feedback')\n }}\n aria-pressed={hasFeedback}\n >\n <span>{labels.feedback}</span>\n <span className=\"tabular-nums\">{counts.feedbackCount}</span>\n </button>\n </div>\n\n {error ? (\n <p className=\"text-sm text-red-600 dark:text-red-400\" role=\"alert\">\n {error}\n </p>\n ) : null}\n\n {modal ? (\n <div className=\"rounded-lg border border-gray-200 p-4 dark:border-gray-700\">\n <label className=\"mb-2 block text-sm font-medium text-gray-800 dark:text-gray-200\">\n {modal === 'dislike'\n ? labels.dislikeRequiresComment\n : labels.feedback}\n </label>\n <textarea\n className=\"mb-3 w-full rounded-md border border-gray-300 bg-white p-2 text-sm text-gray-900 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-100\"\n rows={3}\n maxLength={1000}\n value={comment}\n placeholder={labels.feedbackPlaceholder}\n onChange={(e) => setComment(e.target.value)}\n />\n <div className=\"flex gap-2\">\n <button\n type=\"button\"\n className={`${BTN} ${BTN_ACTIVE}`}\n disabled={pending}\n onClick={onSubmitModal}\n >\n {labels.submit}\n </button>\n <button\n type=\"button\"\n className={`${BTN} ${BTN_IDLE}`}\n disabled={pending}\n onClick={() => {\n setModal(null)\n setComment('')\n setError(null)\n }}\n >\n {labels.cancel}\n </button>\n </div>\n </div>\n ) : null}\n </div>\n )\n}\n","'use client'\n\nimport { useCallback, useState } from 'react'\nimport {\n buildFacebookSharerUrl,\n buildLinkedInShareUrl,\n buildXIntentTweetUrl,\n getSharePageUrlFromLocation,\n requireAbsoluteHttpUrl,\n} from '../lib/share-urls.js'\n\nexport {\n buildFacebookSharerUrl,\n buildLinkedInShareUrl,\n buildXIntentTweetUrl,\n getSharePageUrlFromLocation,\n requireAbsoluteHttpUrl,\n} from '../lib/share-urls.js'\n\nconst DEFAULT_BUTTON_CLASSNAME = [\n 'inline-flex items-center justify-center rounded-lg font-medium transition-colors',\n 'focus:outline-none focus:ring-2 focus:ring-offset-2',\n 'border border-gray-300 bg-transparent text-gray-700 hover:bg-gray-50 focus:ring-gray-500',\n 'dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-800',\n 'px-3 py-1.5 text-sm',\n].join(' ')\n\nconst DEFAULT_HEADING_CLASSNAME =\n 'mb-4 text-lg font-semibold text-gray-900 dark:text-white'\n\nconst DEFAULT_SECTION_CLASSNAME =\n 'mt-12 border-t border-gray-200 pt-8 dark:border-gray-700'\n\nexport type BlogArticleShareLabels = {\n heading: string\n x: string\n facebook: string\n linkedin: string\n copyLink: string\n linkCopied: string\n}\n\nexport type BlogArticleShareProps = {\n /** Article title (host API parity with mindful-journal BlogPostShareButtons). */\n title: string\n /** Already-interpolated share text for X (e.g. \"Check out this article: …\"). */\n text: string\n labels: BlogArticleShareLabels\n /**\n * Absolute http(s) share URL. When omitted, each click uses\n * `window.location.origin + pathname` (no query/hash).\n */\n url?: string\n className?: string\n headingClassName?: string\n buttonClassName?: string\n}\n\nfunction resolveShareUrl(url: string | undefined): string {\n if (url !== undefined) {\n return requireAbsoluteHttpUrl(url)\n }\n if (typeof window === 'undefined') {\n throw new Error('Share URL requires a browser location when url is omitted')\n }\n return getSharePageUrlFromLocation(window.location)\n}\n\nfunction openShareWindow(href: string): void {\n window.open(href, '_blank', 'noopener,noreferrer')\n}\n\n/**\n * X / Facebook / LinkedIn / copy-link controls for a blog article.\n * Host provides i18n labels and share text; optional absolute `url`.\n */\nexport function BlogArticleShare({\n title: _title,\n text,\n labels,\n url,\n className = DEFAULT_SECTION_CLASSNAME,\n headingClassName = DEFAULT_HEADING_CLASSNAME,\n buttonClassName = DEFAULT_BUTTON_CLASSNAME,\n}: BlogArticleShareProps) {\n const [linkCopied, setLinkCopied] = useState(false)\n\n const shareUrl = useCallback(() => resolveShareUrl(url), [url])\n\n const onCopy = useCallback(async () => {\n try {\n await navigator.clipboard.writeText(shareUrl())\n setLinkCopied(true)\n setTimeout(() => setLinkCopied(false), 2000)\n } catch {\n // Fail-fast UI: do not claim success or invent a second copy path.\n }\n }, [shareUrl])\n\n return (\n <div className={className}>\n <h3 className={headingClassName}>{labels.heading}</h3>\n <div className=\"flex flex-wrap gap-4\">\n <button\n type=\"button\"\n className={buttonClassName}\n onClick={() => {\n openShareWindow(buildXIntentTweetUrl(shareUrl(), text))\n }}\n >\n <svg\n className=\"mr-2 h-4 w-4\"\n fill=\"currentColor\"\n viewBox=\"0 0 24 24\"\n aria-hidden=\"true\"\n >\n <path d=\"M8.29 20.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0022 5.92a8.19 8.19 0 01-2.357.646 4.118 4.118 0 001.804-2.27 8.224 8.224 0 01-2.605.996 4.107 4.107 0 00-6.993 3.743 11.65 11.65 0 01-8.457-4.287 4.106 4.106 0 001.27 5.477A4.072 4.072 0 012.8 9.713v.052a4.105 4.105 0 003.292 4.022 4.095 4.095 0 01-1.853.07 4.108 4.108 0 003.834 2.85A8.233 8.233 0 012 18.407a11.616 11.616 0 006.29 1.84\" />\n </svg>\n {labels.x}\n </button>\n <button\n type=\"button\"\n className={buttonClassName}\n onClick={() => {\n openShareWindow(buildFacebookSharerUrl(shareUrl()))\n }}\n >\n <svg\n className=\"mr-2 h-4 w-4\"\n fill=\"currentColor\"\n viewBox=\"0 0 24 24\"\n aria-hidden=\"true\"\n >\n <path d=\"M24 12.073C24 5.405 18.627 0 12 0S0 5.405 0 12.073C0 18.1 4.388 23.094 10.125 24v-8.435H7.078v-3.492h3.047V9.414c0-3.025 1.792-4.697 4.533-4.697 1.312 0 2.686.236 2.686.236v2.974h-1.513c-1.49 0-1.956.931-1.956 1.887v2.259h3.328l-.532 3.492h-2.796V24C19.612 23.094 24 18.1 24 12.073z\" />\n </svg>\n {labels.facebook}\n </button>\n <button\n type=\"button\"\n className={buttonClassName}\n onClick={() => {\n openShareWindow(buildLinkedInShareUrl(shareUrl()))\n }}\n >\n <svg\n className=\"mr-2 h-4 w-4\"\n fill=\"currentColor\"\n viewBox=\"0 0 24 24\"\n aria-hidden=\"true\"\n >\n <path d=\"M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z\" />\n </svg>\n {labels.linkedin}\n </button>\n <button\n type=\"button\"\n className={buttonClassName}\n onClick={() => void onCopy()}\n >\n <svg\n className=\"mr-2 h-4 w-4\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n aria-hidden=\"true\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z\"\n />\n </svg>\n {linkCopied ? labels.linkCopied : labels.copyLink}\n </button>\n </div>\n </div>\n )\n}\n","/** Lightweight social share URL builders (no host UI deps). */\n\nexport function requireAbsoluteHttpUrl(pageUrl: string): string {\n const trimmed = pageUrl.trim()\n if (!trimmed) {\n throw new Error('Share URL must be a non-empty absolute http(s) URL')\n }\n let parsed: URL\n try {\n parsed = new URL(trimmed)\n } catch {\n throw new Error(`Share URL is not a valid absolute URL: ${pageUrl}`)\n }\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {\n throw new Error(`Share URL must use http or https: ${pageUrl}`)\n }\n return trimmed\n}\n\nexport function buildFacebookSharerUrl(\n pageUrl: string,\n quote?: string\n): string {\n const params = new URLSearchParams({\n u: requireAbsoluteHttpUrl(pageUrl),\n })\n const trimmedQuote = quote?.trim()\n if (trimmedQuote) {\n params.set('quote', trimmedQuote)\n }\n return `https://www.facebook.com/sharer/sharer.php?${params.toString()}`\n}\n\n/** Blog article share (matches mindful-journal BlogPostShareButtons). */\nexport function buildXIntentTweetUrl(pageUrl: string, text: string): string {\n const params = new URLSearchParams({\n text,\n url: requireAbsoluteHttpUrl(pageUrl),\n })\n return `https://twitter.com/intent/tweet?${params.toString()}`\n}\n\nexport function buildLinkedInShareUrl(pageUrl: string): string {\n const params = new URLSearchParams({\n url: requireAbsoluteHttpUrl(pageUrl),\n })\n return `https://www.linkedin.com/sharing/share-offsite/?${params.toString()}`\n}\n\n/** Canonical page URL without query/hash — safer for social scrapers. */\nexport function getSharePageUrlFromLocation(\n location: Pick<Location, 'origin' | 'pathname'>\n): string {\n return `${location.origin}${location.pathname}`\n}\n"],"mappings":"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","names":["jsx","useEffect","jsx","jsxs","useCallback","useState","jsx","jsxs","useState","useCallback"]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/server/utils.ts","../src/markdown-link-target.ts"],"sourcesContent":["export type {\n BlogPostApiData,\n BlogPostLinkedLinkApi,\n BlogCategoryApi,\n BlogTagApi,\n BlogListQuery,\n BlogListPagination,\n MailooBlogFetchOptions,\n BlogPostReactionSentiment,\n BlogArticleMarkCounts,\n BlogArticleMarkState,\n SetBlogReactionInput,\n SetBlogFeedbackInput,\n} from './types.js'\nexport {\n BLOG_ARTICLE_PROSE_CLASSNAME,\n firstEmbedImageUrlFromArticle,\n} from './server/utils.js'\nexport { markdownHrefOpensInNewTab } from './markdown-link-target.js'\n","/**\n * First absolute image URL for OG/Twitter previews: prefer HTML from the API,\n * then markdown image syntax in content.\n */\nconst HTML_IMG_SRC = /<img[^>]+src=[\"']([^\"']+)[\"']/i\nconst MD_IMAGE = /!\\[[^\\]]*]\\(([^)\\s]+)\\)/\n\nfunction normalizeUrl(raw: string): string | null {\n const u = raw.trim()\n if (!u) return null\n if (\n u.startsWith('http://') ||\n u.startsWith('https://') ||\n u.startsWith('/')\n ) {\n return u\n }\n return null\n}\n\nexport function firstEmbedImageUrlFromArticle(\n htmlContent: string | null | undefined,\n markdownContent: string | null | undefined\n): string | null {\n if (htmlContent) {\n const m = htmlContent.match(HTML_IMG_SRC)\n if (m?.[1]) {\n const url = normalizeUrl(m[1])\n if (url) return url\n }\n }\n if (markdownContent) {\n const m = markdownContent.match(MD_IMAGE)\n if (m?.[1]) {\n const url = normalizeUrl(m[1])\n if (url) return url\n }\n }\n return null\n}\n\n/**\n * Default Tailwind Typography class string for article body.\n * Hosts may pass a custom `className` instead.\n */\nexport const BLOG_ARTICLE_PROSE_CLASSNAME = [\n 'prose',\n 'prose-gray',\n 'prose-lg',\n 'max-w-none',\n 'dark:prose-invert',\n 'prose-a:text-blue-600',\n 'hover:prose-a:text-blue-700',\n 'dark:prose-a:text-blue-400',\n 'dark:hover:prose-a:text-blue-300',\n 'prose-blockquote:border-blue-500',\n 'prose-img:rounded-lg',\n 'prose-img:shadow-sm',\n].join(' ')\n","export function markdownHrefOpensInNewTab(href: string): boolean {\n const h = href.trim()\n if (h === '') return false\n if (h.startsWith('#') || h.startsWith('?')) return false\n if (h.startsWith('//')) return true\n if (h.startsWith('/')) return false\n if (h.startsWith('./') || h.startsWith('../')) return false\n const lower = h.toLowerCase()\n return lower.startsWith('http://') || lower.startsWith('https://')\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACIA,IAAM,eAAe;AACrB,IAAM,WAAW;AAEjB,SAAS,aAAa,KAA4B;AAChD,QAAM,IAAI,IAAI,KAAK;AACnB,MAAI,CAAC,EAAG,QAAO;AACf,MACE,EAAE,WAAW,SAAS,KACtB,EAAE,WAAW,UAAU,KACvB,EAAE,WAAW,GAAG,GAChB;AACA,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAEO,SAAS,8BACd,aACA,iBACe;AACf,MAAI,aAAa;AACf,UAAM,IAAI,YAAY,MAAM,YAAY;AACxC,QAAI,IAAI,CAAC,GAAG;AACV,YAAM,MAAM,aAAa,EAAE,CAAC,CAAC;AAC7B,UAAI,IAAK,QAAO;AAAA,IAClB;AAAA,EACF;AACA,MAAI,iBAAiB;AACnB,UAAM,IAAI,gBAAgB,MAAM,QAAQ;AACxC,QAAI,IAAI,CAAC,GAAG;AACV,YAAM,MAAM,aAAa,EAAE,CAAC,CAAC;AAC7B,UAAI,IAAK,QAAO;AAAA,IAClB;AAAA,EACF;AACA,SAAO;AACT;AAMO,IAAM,+BAA+B;AAAA,EAC1C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,EAAE,KAAK,GAAG;;;AC1DH,SAAS,0BAA0B,MAAuB;AAC/D,QAAM,IAAI,KAAK,KAAK;AACpB,MAAI,MAAM,GAAI,QAAO;AACrB,MAAI,EAAE,WAAW,GAAG,KAAK,EAAE,WAAW,GAAG,EAAG,QAAO;AACnD,MAAI,EAAE,WAAW,IAAI,EAAG,QAAO;AAC/B,MAAI,EAAE,WAAW,GAAG,EAAG,QAAO;AAC9B,MAAI,EAAE,WAAW,IAAI,KAAK,EAAE,WAAW,KAAK,EAAG,QAAO;AACtD,QAAM,QAAQ,EAAE,YAAY;AAC5B,SAAO,MAAM,WAAW,SAAS,KAAK,MAAM,WAAW,UAAU;AACnE;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/server/utils.ts","../src/markdown-link-target.ts"],"sourcesContent":["export type {\n BlogPostApiData,\n BlogPostLinkedLinkApi,\n BlogCategoryApi,\n BlogTagApi,\n BlogListQuery,\n BlogListPagination,\n MailooBlogFetchOptions,\n BlogPostReactionSentiment,\n BlogArticleMarkCounts,\n BlogArticleMarkState,\n SetBlogReactionInput,\n SetBlogFeedbackInput,\n} from './types.js'\nexport {\n BLOG_ARTICLE_PROSE_CLASSNAME,\n firstEmbedImageUrlFromArticle,\n} from './server/utils.js'\nexport { markdownHrefOpensInNewTab } from './markdown-link-target.js'\n","/**\n * First cover-or-body image URL for OG/Twitter previews: prefer HTML from the\n * API, then markdown image syntax in content. May be absolute or a\n * site-relative path after rewrite. Resolve against the current request\n * origin before `og:image` / `twitter:image`.\n */\nconst HTML_IMG_SRC = /<img[^>]+src=[\"']([^\"']+)[\"']/i\nconst MD_IMAGE = /!\\[[^\\]]*]\\(([^)\\s]+)\\)/\n\nfunction normalizeUrl(raw: string): string | null {\n const u = raw.trim()\n if (!u) return null\n if (\n u.startsWith('http://') ||\n u.startsWith('https://') ||\n u.startsWith('/')\n ) {\n return u\n }\n return null\n}\n\nexport function firstEmbedImageUrlFromArticle(\n htmlContent: string | null | undefined,\n markdownContent: string | null | undefined\n): string | null {\n if (htmlContent) {\n const m = htmlContent.match(HTML_IMG_SRC)\n if (m?.[1]) {\n const url = normalizeUrl(m[1])\n if (url) return url\n }\n }\n if (markdownContent) {\n const m = markdownContent.match(MD_IMAGE)\n if (m?.[1]) {\n const url = normalizeUrl(m[1])\n if (url) return url\n }\n }\n return null\n}\n\n/**\n * Default Tailwind Typography class string for article body.\n * Hosts may pass a custom `className` instead.\n */\nexport const BLOG_ARTICLE_PROSE_CLASSNAME = [\n 'prose',\n 'prose-gray',\n 'prose-lg',\n 'max-w-none',\n 'dark:prose-invert',\n 'prose-a:text-blue-600',\n 'hover:prose-a:text-blue-700',\n 'dark:prose-a:text-blue-400',\n 'dark:hover:prose-a:text-blue-300',\n 'prose-blockquote:border-blue-500',\n 'prose-img:rounded-lg',\n 'prose-img:shadow-sm',\n].join(' ')\n","export function markdownHrefOpensInNewTab(href: string): boolean {\n const h = href.trim()\n if (h === '') return false\n if (h.startsWith('#') || h.startsWith('?')) return false\n if (h.startsWith('//')) return true\n if (h.startsWith('/')) return false\n if (h.startsWith('./') || h.startsWith('../')) return false\n const lower = h.toLowerCase()\n return lower.startsWith('http://') || lower.startsWith('https://')\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACMA,IAAM,eAAe;AACrB,IAAM,WAAW;AAEjB,SAAS,aAAa,KAA4B;AAChD,QAAM,IAAI,IAAI,KAAK;AACnB,MAAI,CAAC,EAAG,QAAO;AACf,MACE,EAAE,WAAW,SAAS,KACtB,EAAE,WAAW,UAAU,KACvB,EAAE,WAAW,GAAG,GAChB;AACA,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAEO,SAAS,8BACd,aACA,iBACe;AACf,MAAI,aAAa;AACf,UAAM,IAAI,YAAY,MAAM,YAAY;AACxC,QAAI,IAAI,CAAC,GAAG;AACV,YAAM,MAAM,aAAa,EAAE,CAAC,CAAC;AAC7B,UAAI,IAAK,QAAO;AAAA,IAClB;AAAA,EACF;AACA,MAAI,iBAAiB;AACnB,UAAM,IAAI,gBAAgB,MAAM,QAAQ;AACxC,QAAI,IAAI,CAAC,GAAG;AACV,YAAM,MAAM,aAAa,EAAE,CAAC,CAAC;AAC7B,UAAI,IAAK,QAAO;AAAA,IAClB;AAAA,EACF;AACA,SAAO;AACT;AAMO,IAAM,+BAA+B;AAAA,EAC1C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,EAAE,KAAK,GAAG;;;AC5DH,SAAS,0BAA0B,MAAuB;AAC/D,QAAM,IAAI,KAAK,KAAK;AACpB,MAAI,MAAM,GAAI,QAAO;AACrB,MAAI,EAAE,WAAW,GAAG,KAAK,EAAE,WAAW,GAAG,EAAG,QAAO;AACnD,MAAI,EAAE,WAAW,IAAI,EAAG,QAAO;AAC/B,MAAI,EAAE,WAAW,GAAG,EAAG,QAAO;AAC9B,MAAI,EAAE,WAAW,IAAI,KAAK,EAAE,WAAW,KAAK,EAAG,QAAO;AACtD,QAAM,QAAQ,EAAE,YAAY;AAC5B,SAAO,MAAM,WAAW,SAAS,KAAK,MAAM,WAAW,UAAU;AACnE;","names":[]}
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- export { j as BLOG_ARTICLE_PROSE_CLASSNAME, g as BlogArticleMarkCounts, h as BlogArticleMarkState, b as BlogCategoryApi, e as BlogListPagination, d as BlogListQuery, B as BlogPostApiData, a as BlogPostLinkedLinkApi, f as BlogPostReactionSentiment, c as BlogTagApi, M as MailooBlogFetchOptions, i as SetBlogFeedbackInput, S as SetBlogReactionInput, k as firstEmbedImageUrlFromArticle } from './utils-DXbN9MTi.cjs';
1
+ export { j as BLOG_ARTICLE_PROSE_CLASSNAME, g as BlogArticleMarkCounts, h as BlogArticleMarkState, b as BlogCategoryApi, e as BlogListPagination, d as BlogListQuery, B as BlogPostApiData, a as BlogPostLinkedLinkApi, f as BlogPostReactionSentiment, c as BlogTagApi, M as MailooBlogFetchOptions, i as SetBlogFeedbackInput, S as SetBlogReactionInput, k as firstEmbedImageUrlFromArticle } from './utils-694BRF5g.cjs';
2
2
 
3
3
  declare function markdownHrefOpensInNewTab(href: string): boolean;
4
4
 
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { j as BLOG_ARTICLE_PROSE_CLASSNAME, g as BlogArticleMarkCounts, h as BlogArticleMarkState, b as BlogCategoryApi, e as BlogListPagination, d as BlogListQuery, B as BlogPostApiData, a as BlogPostLinkedLinkApi, f as BlogPostReactionSentiment, c as BlogTagApi, M as MailooBlogFetchOptions, i as SetBlogFeedbackInput, S as SetBlogReactionInput, k as firstEmbedImageUrlFromArticle } from './utils-DXbN9MTi.js';
1
+ export { j as BLOG_ARTICLE_PROSE_CLASSNAME, g as BlogArticleMarkCounts, h as BlogArticleMarkState, b as BlogCategoryApi, e as BlogListPagination, d as BlogListQuery, B as BlogPostApiData, a as BlogPostLinkedLinkApi, f as BlogPostReactionSentiment, c as BlogTagApi, M as MailooBlogFetchOptions, i as SetBlogFeedbackInput, S as SetBlogReactionInput, k as firstEmbedImageUrlFromArticle } from './utils-694BRF5g.js';
2
2
 
3
3
  declare function markdownHrefOpensInNewTab(href: string): boolean;
4
4
 
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/server/utils.ts","../src/markdown-link-target.ts"],"sourcesContent":["/**\n * First absolute image URL for OG/Twitter previews: prefer HTML from the API,\n * then markdown image syntax in content.\n */\nconst HTML_IMG_SRC = /<img[^>]+src=[\"']([^\"']+)[\"']/i\nconst MD_IMAGE = /!\\[[^\\]]*]\\(([^)\\s]+)\\)/\n\nfunction normalizeUrl(raw: string): string | null {\n const u = raw.trim()\n if (!u) return null\n if (\n u.startsWith('http://') ||\n u.startsWith('https://') ||\n u.startsWith('/')\n ) {\n return u\n }\n return null\n}\n\nexport function firstEmbedImageUrlFromArticle(\n htmlContent: string | null | undefined,\n markdownContent: string | null | undefined\n): string | null {\n if (htmlContent) {\n const m = htmlContent.match(HTML_IMG_SRC)\n if (m?.[1]) {\n const url = normalizeUrl(m[1])\n if (url) return url\n }\n }\n if (markdownContent) {\n const m = markdownContent.match(MD_IMAGE)\n if (m?.[1]) {\n const url = normalizeUrl(m[1])\n if (url) return url\n }\n }\n return null\n}\n\n/**\n * Default Tailwind Typography class string for article body.\n * Hosts may pass a custom `className` instead.\n */\nexport const BLOG_ARTICLE_PROSE_CLASSNAME = [\n 'prose',\n 'prose-gray',\n 'prose-lg',\n 'max-w-none',\n 'dark:prose-invert',\n 'prose-a:text-blue-600',\n 'hover:prose-a:text-blue-700',\n 'dark:prose-a:text-blue-400',\n 'dark:hover:prose-a:text-blue-300',\n 'prose-blockquote:border-blue-500',\n 'prose-img:rounded-lg',\n 'prose-img:shadow-sm',\n].join(' ')\n","export function markdownHrefOpensInNewTab(href: string): boolean {\n const h = href.trim()\n if (h === '') return false\n if (h.startsWith('#') || h.startsWith('?')) return false\n if (h.startsWith('//')) return true\n if (h.startsWith('/')) return false\n if (h.startsWith('./') || h.startsWith('../')) return false\n const lower = h.toLowerCase()\n return lower.startsWith('http://') || lower.startsWith('https://')\n}\n"],"mappings":";AAIA,IAAM,eAAe;AACrB,IAAM,WAAW;AAEjB,SAAS,aAAa,KAA4B;AAChD,QAAM,IAAI,IAAI,KAAK;AACnB,MAAI,CAAC,EAAG,QAAO;AACf,MACE,EAAE,WAAW,SAAS,KACtB,EAAE,WAAW,UAAU,KACvB,EAAE,WAAW,GAAG,GAChB;AACA,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAEO,SAAS,8BACd,aACA,iBACe;AACf,MAAI,aAAa;AACf,UAAM,IAAI,YAAY,MAAM,YAAY;AACxC,QAAI,IAAI,CAAC,GAAG;AACV,YAAM,MAAM,aAAa,EAAE,CAAC,CAAC;AAC7B,UAAI,IAAK,QAAO;AAAA,IAClB;AAAA,EACF;AACA,MAAI,iBAAiB;AACnB,UAAM,IAAI,gBAAgB,MAAM,QAAQ;AACxC,QAAI,IAAI,CAAC,GAAG;AACV,YAAM,MAAM,aAAa,EAAE,CAAC,CAAC;AAC7B,UAAI,IAAK,QAAO;AAAA,IAClB;AAAA,EACF;AACA,SAAO;AACT;AAMO,IAAM,+BAA+B;AAAA,EAC1C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,EAAE,KAAK,GAAG;;;AC1DH,SAAS,0BAA0B,MAAuB;AAC/D,QAAM,IAAI,KAAK,KAAK;AACpB,MAAI,MAAM,GAAI,QAAO;AACrB,MAAI,EAAE,WAAW,GAAG,KAAK,EAAE,WAAW,GAAG,EAAG,QAAO;AACnD,MAAI,EAAE,WAAW,IAAI,EAAG,QAAO;AAC/B,MAAI,EAAE,WAAW,GAAG,EAAG,QAAO;AAC9B,MAAI,EAAE,WAAW,IAAI,KAAK,EAAE,WAAW,KAAK,EAAG,QAAO;AACtD,QAAM,QAAQ,EAAE,YAAY;AAC5B,SAAO,MAAM,WAAW,SAAS,KAAK,MAAM,WAAW,UAAU;AACnE;","names":[]}
1
+ {"version":3,"sources":["../src/server/utils.ts","../src/markdown-link-target.ts"],"sourcesContent":["/**\n * First cover-or-body image URL for OG/Twitter previews: prefer HTML from the\n * API, then markdown image syntax in content. May be absolute or a\n * site-relative path after rewrite. Resolve against the current request\n * origin before `og:image` / `twitter:image`.\n */\nconst HTML_IMG_SRC = /<img[^>]+src=[\"']([^\"']+)[\"']/i\nconst MD_IMAGE = /!\\[[^\\]]*]\\(([^)\\s]+)\\)/\n\nfunction normalizeUrl(raw: string): string | null {\n const u = raw.trim()\n if (!u) return null\n if (\n u.startsWith('http://') ||\n u.startsWith('https://') ||\n u.startsWith('/')\n ) {\n return u\n }\n return null\n}\n\nexport function firstEmbedImageUrlFromArticle(\n htmlContent: string | null | undefined,\n markdownContent: string | null | undefined\n): string | null {\n if (htmlContent) {\n const m = htmlContent.match(HTML_IMG_SRC)\n if (m?.[1]) {\n const url = normalizeUrl(m[1])\n if (url) return url\n }\n }\n if (markdownContent) {\n const m = markdownContent.match(MD_IMAGE)\n if (m?.[1]) {\n const url = normalizeUrl(m[1])\n if (url) return url\n }\n }\n return null\n}\n\n/**\n * Default Tailwind Typography class string for article body.\n * Hosts may pass a custom `className` instead.\n */\nexport const BLOG_ARTICLE_PROSE_CLASSNAME = [\n 'prose',\n 'prose-gray',\n 'prose-lg',\n 'max-w-none',\n 'dark:prose-invert',\n 'prose-a:text-blue-600',\n 'hover:prose-a:text-blue-700',\n 'dark:prose-a:text-blue-400',\n 'dark:hover:prose-a:text-blue-300',\n 'prose-blockquote:border-blue-500',\n 'prose-img:rounded-lg',\n 'prose-img:shadow-sm',\n].join(' ')\n","export function markdownHrefOpensInNewTab(href: string): boolean {\n const h = href.trim()\n if (h === '') return false\n if (h.startsWith('#') || h.startsWith('?')) return false\n if (h.startsWith('//')) return true\n if (h.startsWith('/')) return false\n if (h.startsWith('./') || h.startsWith('../')) return false\n const lower = h.toLowerCase()\n return lower.startsWith('http://') || lower.startsWith('https://')\n}\n"],"mappings":";AAMA,IAAM,eAAe;AACrB,IAAM,WAAW;AAEjB,SAAS,aAAa,KAA4B;AAChD,QAAM,IAAI,IAAI,KAAK;AACnB,MAAI,CAAC,EAAG,QAAO;AACf,MACE,EAAE,WAAW,SAAS,KACtB,EAAE,WAAW,UAAU,KACvB,EAAE,WAAW,GAAG,GAChB;AACA,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAEO,SAAS,8BACd,aACA,iBACe;AACf,MAAI,aAAa;AACf,UAAM,IAAI,YAAY,MAAM,YAAY;AACxC,QAAI,IAAI,CAAC,GAAG;AACV,YAAM,MAAM,aAAa,EAAE,CAAC,CAAC;AAC7B,UAAI,IAAK,QAAO;AAAA,IAClB;AAAA,EACF;AACA,MAAI,iBAAiB;AACnB,UAAM,IAAI,gBAAgB,MAAM,QAAQ;AACxC,QAAI,IAAI,CAAC,GAAG;AACV,YAAM,MAAM,aAAa,EAAE,CAAC,CAAC;AAC7B,UAAI,IAAK,QAAO;AAAA,IAClB;AAAA,EACF;AACA,SAAO;AACT;AAMO,IAAM,+BAA+B;AAAA,EAC1C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,EAAE,KAAK,GAAG;;;AC5DH,SAAS,0BAA0B,MAAuB;AAC/D,QAAM,IAAI,KAAK,KAAK;AACpB,MAAI,MAAM,GAAI,QAAO;AACrB,MAAI,EAAE,WAAW,GAAG,KAAK,EAAE,WAAW,GAAG,EAAG,QAAO;AACnD,MAAI,EAAE,WAAW,IAAI,EAAG,QAAO;AAC/B,MAAI,EAAE,WAAW,GAAG,EAAG,QAAO;AAC9B,MAAI,EAAE,WAAW,IAAI,KAAK,EAAE,WAAW,KAAK,EAAG,QAAO;AACtD,QAAM,QAAQ,EAAE,YAAY;AAC5B,SAAO,MAAM,WAAW,SAAS,KAAK,MAAM,WAAW,UAAU;AACnE;","names":[]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/routes/index.ts","../../src/server/client.ts","../../src/server/image-rewrite.ts"],"sourcesContent":["import type { NextRequest } from 'next/server'\nimport {\n defaultMailooLogger,\n jsonError,\n proxyMailooWebhookJsonPost,\n type MailooIntegrationConfig,\n type MailooLogger,\n} from '@mailoo/next-core'\nimport { getMailooBlogConfigFromEnv } from '../server/client.js'\nimport {\n rewriteBlogCategoryDescriptionEnvelope,\n rewriteBlogJsonLdEnvelope,\n rewriteBlogListEnvelope,\n rewriteBlogPostEnvelope,\n type BlogImageRewriteResult,\n} from '../server/image-rewrite.js'\n\nexport type CreateBlogRoutesOptions = {\n /** Explicit config; defaults to MAILOO_BLOG_* env */\n getConfig?: () => MailooIntegrationConfig | null\n logger?: MailooLogger\n /**\n * Integrator prefix ending with `?id=`. When set, list, slug, category\n * description, and JSON-LD responses rewrite canonical Mailoo embed URLs.\n * Example: `/api/v1/images/public/content?id=`\n */\n imageProxyPrefixWithIdEquals?: string\n /**\n * Used only when the query has no `locale`. Host maps Accept-Language\n * to its own locale list.\n */\n resolveLocale?: (request: NextRequest) => string | undefined\n}\n\nconst EMBED_BASE_ERROR =\n 'Blog API contract violation: imagePublicEmbedBaseUrl missing'\n\ntype BlogGetKind =\n 'list' | 'post' | 'category-description' | 'json-ld' | 'passthrough'\n\nfunction resolveConfig(\n options?: CreateBlogRoutesOptions\n): MailooIntegrationConfig | null {\n return options?.getConfig?.() ?? getMailooBlogConfigFromEnv()\n}\n\nfunction notConfigured(logger: MailooLogger): Response {\n logger.error(\n 'MAILOO_BLOG_PROJECT_UID, MAILOO_BLOG_INTEGRATION_ID, MAILOO_BLOG_API and MAILOO_BLOG_API_KEY must be set'\n )\n return jsonError({ error: 'Blog API not configured' }, 503)\n}\n\nfunction imageProxyPrefix(\n options?: CreateBlogRoutesOptions\n): string | undefined {\n const raw = options?.imageProxyPrefixWithIdEquals?.trim()\n return raw || undefined\n}\n\nfunction localeSearchParams(\n request: NextRequest,\n options: CreateBlogRoutesOptions | undefined,\n mode: 'all' | 'locale-only'\n): URLSearchParams {\n const source = request.nextUrl.searchParams\n const params =\n mode === 'all' ? new URLSearchParams(source) : new URLSearchParams()\n if (mode === 'locale-only') {\n const fromQuery = source.get('locale')\n if (fromQuery) params.set('locale', fromQuery)\n }\n if (!params.get('locale')) {\n const resolved = options?.resolveLocale?.(request)\n const locale = typeof resolved === 'string' ? resolved.trim() : ''\n if (locale) params.set('locale', locale)\n }\n return params\n}\n\nfunction jsonResponse(body: unknown, status: number): Response {\n return new Response(JSON.stringify(body), {\n status,\n headers: { 'Content-Type': 'application/json' },\n })\n}\n\nfunction rewriteProxiedBody(\n kind: BlogGetKind,\n body: unknown,\n proxyPrefix: string\n): BlogImageRewriteResult {\n switch (kind) {\n case 'list':\n return rewriteBlogListEnvelope(body, proxyPrefix)\n case 'post':\n return rewriteBlogPostEnvelope(body, proxyPrefix)\n case 'category-description':\n return rewriteBlogCategoryDescriptionEnvelope(body, proxyPrefix)\n case 'json-ld':\n return rewriteBlogJsonLdEnvelope(body, proxyPrefix)\n case 'passthrough':\n return { ok: true, body }\n }\n}\n\nasync function proxyGet(\n config: MailooIntegrationConfig,\n pathSuffix: string,\n searchParams: URLSearchParams | undefined,\n options: CreateBlogRoutesOptions | undefined,\n kind: BlogGetKind\n): Promise<Response> {\n const logger = options?.logger ?? defaultMailooLogger\n const url = new URL(\n `${config.apiBaseUrl}/api/v1/blog/${config.projectUid}/integrations/${config.integrationId}${pathSuffix}`\n )\n if (searchParams) {\n searchParams.forEach((value, key) => {\n url.searchParams.append(key, value)\n })\n }\n const response = await fetch(url.toString(), {\n method: 'GET',\n headers: {\n 'Content-Type': 'application/json',\n 'X-API-Key': config.apiKey,\n },\n })\n const data: unknown = await response.json()\n const prefix = imageProxyPrefix(options)\n if (!response.ok || !prefix || kind === 'passthrough') {\n return jsonResponse(data, response.status)\n }\n const rewritten = rewriteProxiedBody(kind, data, prefix)\n if (!rewritten.ok) {\n if (rewritten.reason === 'missing-embed-base') {\n logger.error(EMBED_BASE_ERROR)\n return jsonError({ error: EMBED_BASE_ERROR }, 502)\n }\n logger.error('Blog API JSON-LD rewrite failed')\n return jsonError({ error: 'Internal server error' }, 500)\n }\n return jsonResponse(rewritten.body, response.status)\n}\n\n/** Factory: GET list posts — mount at `app/api/v1/blog/route.ts` */\nexport function createBlogListHandler(options?: CreateBlogRoutesOptions) {\n const logger = options?.logger ?? defaultMailooLogger\n return async function GET(request: NextRequest): Promise<Response> {\n const config = resolveConfig(options)\n if (!config) return notConfigured(logger)\n try {\n return await proxyGet(\n config,\n '',\n localeSearchParams(request, options, 'all'),\n options,\n 'list'\n )\n } catch (error) {\n logger.logException?.(error, 'Blog API proxy error')\n return jsonError({ error: 'Internal server error' }, 500)\n }\n }\n}\n\n/** Factory: GET post by slug — mount at `app/api/v1/blog/slug/[slug]/route.ts` */\nexport function createBlogSlugHandler(options?: CreateBlogRoutesOptions) {\n const logger = options?.logger ?? defaultMailooLogger\n return async function GET(\n request: NextRequest,\n ctx: { params: Promise<{ slug: string }> }\n ): Promise<Response> {\n const config = resolveConfig(options)\n if (!config) return notConfigured(logger)\n const { slug } = await ctx.params\n try {\n return await proxyGet(\n config,\n `/slug/${encodeURIComponent(slug)}`,\n localeSearchParams(request, options, 'locale-only'),\n options,\n 'post'\n )\n } catch (error) {\n logger.logException?.(error, 'Blog slug API proxy error')\n return jsonError({ error: 'Internal server error' }, 500)\n }\n }\n}\n\n/** Factory: GET categories */\nexport function createBlogCategoriesHandler(options?: CreateBlogRoutesOptions) {\n const logger = options?.logger ?? defaultMailooLogger\n return async function GET(request: NextRequest): Promise<Response> {\n const config = resolveConfig(options)\n if (!config) return notConfigured(logger)\n try {\n return await proxyGet(\n config,\n '/categories',\n localeSearchParams(request, options, 'all'),\n options,\n 'passthrough'\n )\n } catch (error) {\n logger.logException?.(error, 'Blog categories API proxy error')\n return jsonError({ error: 'Internal server error' }, 500)\n }\n }\n}\n\n/** Factory: GET tags */\nexport function createBlogTagsHandler(options?: CreateBlogRoutesOptions) {\n const logger = options?.logger ?? defaultMailooLogger\n return async function GET(request: NextRequest): Promise<Response> {\n const config = resolveConfig(options)\n if (!config) return notConfigured(logger)\n try {\n return await proxyGet(\n config,\n '/tags',\n localeSearchParams(request, options, 'all'),\n options,\n 'passthrough'\n )\n } catch (error) {\n logger.logException?.(error, 'Blog tags API proxy error')\n return jsonError({ error: 'Internal server error' }, 500)\n }\n }\n}\n\n/** Factory: GET tag by slug */\nexport function createBlogTagSlugHandler(options?: CreateBlogRoutesOptions) {\n const logger = options?.logger ?? defaultMailooLogger\n return async function GET(\n request: NextRequest,\n ctx: { params: Promise<{ slug: string }> }\n ): Promise<Response> {\n const config = resolveConfig(options)\n if (!config) return notConfigured(logger)\n const { slug } = await ctx.params\n try {\n return await proxyGet(\n config,\n `/tags/slug/${encodeURIComponent(slug)}`,\n localeSearchParams(request, options, 'all'),\n options,\n 'passthrough'\n )\n } catch (error) {\n logger.logException?.(error, 'Blog tag slug API proxy error')\n return jsonError({ error: 'Internal server error' }, 500)\n }\n }\n}\n\n/** Factory: GET category description by slug */\nexport function createBlogCategoryDescriptionHandler(\n options?: CreateBlogRoutesOptions\n) {\n const logger = options?.logger ?? defaultMailooLogger\n return async function GET(\n request: NextRequest,\n ctx: { params: Promise<{ slug: string }> }\n ): Promise<Response> {\n const config = resolveConfig(options)\n if (!config) return notConfigured(logger)\n const { slug } = await ctx.params\n try {\n return await proxyGet(\n config,\n `/categories/slug/${encodeURIComponent(slug)}/description`,\n localeSearchParams(request, options, 'locale-only'),\n options,\n 'category-description'\n )\n } catch (error) {\n logger.logException?.(error, 'Blog category description API proxy error')\n return jsonError({ error: 'Internal server error' }, 500)\n }\n }\n}\n\n/** Factory: GET JSON-LD by slug — mount at `app/api/v1/blog/slug/[slug]/json-ld/route.ts` */\nexport function createBlogJsonLdHandler(options?: CreateBlogRoutesOptions) {\n const logger = options?.logger ?? defaultMailooLogger\n return async function GET(\n request: NextRequest,\n ctx: { params: Promise<{ slug: string }> }\n ): Promise<Response> {\n const config = resolveConfig(options)\n if (!config) return notConfigured(logger)\n const { slug } = await ctx.params\n try {\n return await proxyGet(\n config,\n `/slug/${encodeURIComponent(slug)}/json-ld`,\n localeSearchParams(request, options, 'locale-only'),\n options,\n 'json-ld'\n )\n } catch (error) {\n logger.logException?.(error, 'Blog JSON-LD API proxy error')\n return jsonError({ error: 'Internal server error' }, 500)\n }\n }\n}\n\nasync function proxyMarkPost(\n options: CreateBlogRoutesOptions | undefined,\n request: NextRequest,\n slug: string,\n pathKind: 'reaction' | 'feedback',\n logLabel: string\n): Promise<Response> {\n const logger = options?.logger ?? defaultMailooLogger\n const config = resolveConfig(options)\n if (!config) return notConfigured(logger)\n let body: unknown\n try {\n body = await request.json()\n } catch {\n return jsonError({ error: 'Invalid JSON body' }, 400)\n }\n const url = `${config.apiBaseUrl}/api/v1/blog/${config.projectUid}/integrations/${config.integrationId}/slug/${encodeURIComponent(slug)}/${pathKind}`\n try {\n return await proxyMailooWebhookJsonPost({\n request,\n url,\n apiKey: config.apiKey,\n body,\n })\n } catch (error) {\n logger.logException?.(error, logLabel)\n return jsonError({ error: 'Internal server error' }, 500)\n }\n}\n\n/** Factory: POST reaction — mount at `app/api/v1/blog/slug/[slug]/reaction/route.ts` */\nexport function createBlogReactionHandler(options?: CreateBlogRoutesOptions) {\n return async function POST(\n request: NextRequest,\n ctx: { params: Promise<{ slug: string }> }\n ): Promise<Response> {\n const { slug } = await ctx.params\n return proxyMarkPost(\n options,\n request,\n slug,\n 'reaction',\n 'Blog reaction proxy error'\n )\n }\n}\n\n/** Factory: POST feedback — mount at `app/api/v1/blog/slug/[slug]/feedback/route.ts` */\nexport function createBlogFeedbackHandler(options?: CreateBlogRoutesOptions) {\n return async function POST(\n request: NextRequest,\n ctx: { params: Promise<{ slug: string }> }\n ): Promise<Response> {\n const { slug } = await ctx.params\n return proxyMarkPost(\n options,\n request,\n slug,\n 'feedback',\n 'Blog feedback proxy error'\n )\n }\n}\n\nexport function createMailooBlogRoutes(options?: CreateBlogRoutesOptions) {\n return {\n list: createBlogListHandler(options),\n slug: createBlogSlugHandler(options),\n jsonLd: createBlogJsonLdHandler(options),\n categories: createBlogCategoriesHandler(options),\n tags: createBlogTagsHandler(options),\n tagSlug: createBlogTagSlugHandler(options),\n categoryDescription: createBlogCategoryDescriptionHandler(options),\n reaction: createBlogReactionHandler(options),\n feedback: createBlogFeedbackHandler(options),\n }\n}\n","import { unstable_rethrow } from 'next/navigation'\nimport type { MailooIntegrationConfig } from '@mailoo/next-core'\nimport { readMailooIntegrationConfig } from '@mailoo/next-core'\nimport {\n rewriteBlogPostMedia,\n rewriteCategoryDescriptionMedia,\n rewriteEmbedUrlsAnyHost,\n} from './image-rewrite.js'\nimport type {\n BlogArticleMarkState,\n BlogCategoryApi,\n BlogTagApi,\n BlogListPagination,\n BlogListQuery,\n BlogPostApiData,\n MailooBlogFetchOptions,\n SetBlogFeedbackInput,\n SetBlogReactionInput,\n} from '../types.js'\n\nexport type MailooBlogConfig = MailooIntegrationConfig\nexport type { MailooBlogFetchOptions }\n\n/** Host image proxy. Omit to keep canonical Mailoo embed URLs. */\nexport type CreateMailooBlogClientOptions = {\n /**\n * Integrator prefix ending with `?id=` (absolute or same-origin path).\n * Example: `/api/v1/images/public/content?id=`\n */\n imageProxyPrefixWithIdEquals?: string\n}\n\nconst DEFAULT_REVALIDATE = 60\n\n/** Read MAILOO_BLOG_* env (API, API_KEY, PROJECT_UID, INTEGRATION_ID). */\nexport function getMailooBlogConfigFromEnv(): MailooBlogConfig | null {\n return readMailooIntegrationConfig({ prefix: 'MAILOO_BLOG' })\n}\n\nfunction blogBasePath(config: MailooBlogConfig): string {\n return `${config.apiBaseUrl}/api/v1/blog/${config.projectUid}/integrations/${config.integrationId}`\n}\n\nfunction resolveRevalidate(options?: MailooBlogFetchOptions): number {\n return options?.revalidate ?? DEFAULT_REVALIDATE\n}\n\nasync function blogFetchJson(\n config: MailooBlogConfig,\n path: string,\n query?: Record<string, string | undefined>,\n options?: MailooBlogFetchOptions\n): Promise<\n { ok: true; json: unknown; status: number } | { ok: false; status: number }\n> {\n const url = new URL(`${blogBasePath(config)}${path}`)\n if (query) {\n for (const [k, v] of Object.entries(query)) {\n if (v !== undefined && v !== '') url.searchParams.set(k, v)\n }\n }\n try {\n const revalidate = resolveRevalidate(options)\n const res = await fetch(url.toString(), {\n method: 'GET',\n headers: {\n 'Content-Type': 'application/json',\n 'X-API-Key': config.apiKey,\n },\n next: { revalidate },\n } as RequestInit)\n if (!res.ok) {\n return { ok: false, status: res.status }\n }\n return { ok: true, json: await res.json(), status: res.status }\n } catch (error) {\n unstable_rethrow(error)\n return { ok: false, status: 500 }\n }\n}\n\nasync function blogPostJson(\n config: MailooBlogConfig,\n path: string,\n body: unknown\n): Promise<\n | { ok: true; data: BlogArticleMarkState; status: number }\n | { ok: false; status: number }\n> {\n try {\n const res = await fetch(`${blogBasePath(config)}${path}`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-API-Key': config.apiKey,\n },\n body: JSON.stringify(body),\n cache: 'no-store',\n })\n if (!res.ok) {\n return { ok: false, status: res.status }\n }\n const json = (await res.json()) as { data?: BlogArticleMarkState }\n if (!json.data || typeof json.data !== 'object') {\n return { ok: false, status: 500 }\n }\n return { ok: true, data: json.data, status: res.status }\n } catch (error) {\n unstable_rethrow(error)\n return { ok: false, status: 500 }\n }\n}\n\nasync function blogGet<T>(\n config: MailooBlogConfig,\n path: string,\n query?: Record<string, string | undefined>,\n options?: MailooBlogFetchOptions\n): Promise<\n { ok: true; data: T; status: number } | { ok: false; status: number }\n> {\n const fetched = await blogFetchJson(config, path, query, options)\n if (!fetched.ok) return fetched\n const json = fetched.json as { data?: T } & T\n const data =\n (json as { data?: T }).data !== undefined\n ? (json as { data: T }).data\n : (json as T)\n return { ok: true, data, status: fetched.status }\n}\n\nfunction isBlogListPagination(value: unknown): value is BlogListPagination {\n if (value === null || typeof value !== 'object') return false\n const p = value as Record<string, unknown>\n return (\n typeof p.page === 'number' &&\n typeof p.limit === 'number' &&\n typeof p.total === 'number' &&\n typeof p.totalPages === 'number' &&\n typeof p.hasNextPage === 'boolean' &&\n typeof p.hasPrevPage === 'boolean'\n )\n}\n\nfunction queryRecord(query: BlogListQuery): Record<string, string | undefined> {\n const q: Record<string, string | undefined> = {}\n for (const [k, v] of Object.entries(query)) {\n if (v === undefined) continue\n q[k] = String(v)\n }\n return q\n}\n\nfunction imageProxyPrefix(\n options?: CreateMailooBlogClientOptions\n): string | undefined {\n const raw = options?.imageProxyPrefixWithIdEquals?.trim()\n return raw || undefined\n}\n\nfunction rewritePost(\n post: BlogPostApiData,\n canonicalPrefixWithIdEquals: string,\n proxyPrefixWithIdEquals: string\n): BlogPostApiData {\n return rewriteBlogPostMedia(\n post,\n canonicalPrefixWithIdEquals,\n proxyPrefixWithIdEquals\n )\n}\n\ntype CategoryDescription = {\n description?: string\n descriptionHtml?: string\n imagePublicEmbedBaseUrl?: string\n}\n\nexport function createMailooBlogClient(\n config: MailooBlogConfig,\n clientOptions?: CreateMailooBlogClientOptions\n) {\n const proxyPrefix = imageProxyPrefix(clientOptions)\n return {\n async listPosts(\n query: BlogListQuery = {},\n options?: MailooBlogFetchOptions\n ): Promise<\n | {\n ok: true\n data: BlogPostApiData[]\n pagination: BlogListPagination\n imagePublicEmbedBaseUrl: string\n status: number\n }\n | { ok: false; status: number }\n > {\n const fetched = await blogFetchJson(\n config,\n '',\n queryRecord(query),\n options\n )\n if (!fetched.ok) return fetched\n const json = fetched.json\n if (json === null || typeof json !== 'object') {\n return { ok: false, status: 500 }\n }\n const body = json as {\n data?: unknown\n pagination?: unknown\n imagePublicEmbedBaseUrl?: unknown\n }\n const imagePublicEmbedBaseUrl =\n typeof body.imagePublicEmbedBaseUrl === 'string'\n ? body.imagePublicEmbedBaseUrl.trim()\n : ''\n if (\n !Array.isArray(body.data) ||\n !isBlogListPagination(body.pagination) ||\n !imagePublicEmbedBaseUrl\n ) {\n return { ok: false, status: 500 }\n }\n const posts = body.data as BlogPostApiData[]\n return {\n ok: true,\n data: proxyPrefix\n ? posts.map((post) =>\n rewritePost(post, imagePublicEmbedBaseUrl, proxyPrefix)\n )\n : posts,\n pagination: body.pagination,\n imagePublicEmbedBaseUrl,\n status: fetched.status,\n }\n },\n\n async getPostBySlug(\n slug: string,\n locale?: string,\n options?: MailooBlogFetchOptions\n ) {\n const result = await blogGet<BlogPostApiData>(\n config,\n `/slug/${encodeURIComponent(slug)}`,\n locale ? { locale } : undefined,\n options\n )\n if (!result.ok || !proxyPrefix) return result\n const canonical = result.data.imagePublicEmbedBaseUrl?.trim()\n if (!canonical) return { ok: false as const, status: 500 }\n return {\n ...result,\n data: rewritePost(result.data, canonical, proxyPrefix),\n }\n },\n\n async getPostJsonLd(\n slug: string,\n locale?: string,\n options?: MailooBlogFetchOptions\n ) {\n const result = await blogGet<{ jsonLd: Record<string, unknown> }>(\n config,\n `/slug/${encodeURIComponent(slug)}/json-ld`,\n locale ? { locale } : undefined,\n options\n )\n if (!result.ok || !proxyPrefix) return result\n try {\n return {\n ...result,\n data: {\n jsonLd: rewriteEmbedUrlsAnyHost(result.data.jsonLd, proxyPrefix),\n },\n }\n } catch {\n return { ok: false as const, status: 500 }\n }\n },\n\n async listCategories(locale?: string, options?: MailooBlogFetchOptions) {\n return blogGet<BlogCategoryApi[]>(\n config,\n '/categories',\n locale ? { locale } : undefined,\n options\n )\n },\n\n async listTags(locale?: string, options?: MailooBlogFetchOptions) {\n return blogGet<BlogTagApi[]>(\n config,\n '/tags',\n locale ? { locale } : undefined,\n options\n )\n },\n\n async getTagBySlug(\n slug: string,\n locale?: string,\n options?: MailooBlogFetchOptions\n ) {\n return blogGet<BlogTagApi>(\n config,\n `/tags/slug/${encodeURIComponent(slug)}`,\n locale ? { locale } : undefined,\n options\n )\n },\n\n async getCategoryDescription(\n slug: string,\n locale?: string,\n options?: MailooBlogFetchOptions\n ) {\n const result = await blogGet<CategoryDescription>(\n config,\n `/categories/slug/${encodeURIComponent(slug)}/description`,\n locale ? { locale } : undefined,\n options\n )\n if (!result.ok || !proxyPrefix) return result\n const canonical = result.data.imagePublicEmbedBaseUrl?.trim()\n if (!canonical) return { ok: false as const, status: 500 }\n return {\n ...result,\n data: rewriteCategoryDescriptionMedia(\n result.data,\n canonical,\n proxyPrefix\n ),\n }\n },\n\n async setReaction(slug: string, input: SetBlogReactionInput) {\n return blogPostJson(\n config,\n `/slug/${encodeURIComponent(slug)}/reaction`,\n input\n )\n },\n\n async setFeedback(slug: string, input: SetBlogFeedbackInput) {\n return blogPostJson(\n config,\n `/slug/${encodeURIComponent(slug)}/feedback`,\n input\n )\n },\n }\n}\n\nexport type MailooBlogClient = ReturnType<typeof createMailooBlogClient>\n","import {\n rewriteMailooBlogMediaUrls,\n rewriteMailooPublicImageUrls,\n rewriteMailooPublicImageUrlsAnyHost,\n} from '@mailoo/images'\nimport type { BlogPostApiData } from '../types.js'\n\nexport type BlogImageRewriteFailure = 'missing-embed-base' | 'invalid-json'\n\nexport type BlogImageRewriteResult =\n { ok: true; body: unknown } | { ok: false; reason: BlogImageRewriteFailure }\n\nfunction asRecord(value: unknown): Record<string, unknown> | null {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) {\n return null\n }\n return value as Record<string, unknown>\n}\n\nexport function readEmbedBase(...candidates: unknown[]): string | undefined {\n for (const candidate of candidates) {\n if (typeof candidate === 'string' && candidate.trim()) {\n return candidate.trim()\n }\n }\n return undefined\n}\n\nexport function rewriteBlogPostMedia<T extends BlogPostApiData>(\n post: T,\n canonicalPrefixWithIdEquals: string,\n proxyPrefixWithIdEquals: string\n): T {\n return rewriteMailooBlogMediaUrls(post, {\n canonicalPrefixWithIdEquals,\n proxyPrefixWithIdEquals,\n })\n}\n\nexport function rewriteCategoryDescriptionMedia<\n T extends {\n description?: string | null\n descriptionHtml?: string | null\n },\n>(\n data: T,\n canonicalPrefixWithIdEquals: string,\n proxyPrefixWithIdEquals: string\n): T {\n const opts = {\n canonicalPrefixWithIdEquals,\n proxyPrefixWithIdEquals,\n }\n return {\n ...data,\n description:\n typeof data.description === 'string'\n ? rewriteMailooPublicImageUrls(data.description, opts)\n : data.description,\n descriptionHtml:\n typeof data.descriptionHtml === 'string'\n ? rewriteMailooPublicImageUrls(data.descriptionHtml, opts)\n : data.descriptionHtml,\n }\n}\n\nexport function rewriteEmbedUrlsAnyHost<T>(value: T, proxyPrefix: string): T {\n const rewritten = rewriteMailooPublicImageUrlsAnyHost(\n JSON.stringify(value),\n proxyPrefix\n )\n return JSON.parse(rewritten) as T\n}\n\nfunction rewritePostRecord(\n post: Record<string, unknown>,\n canonical: string,\n proxy: string\n): Record<string, unknown> {\n return rewriteBlogPostMedia(\n post as unknown as BlogPostApiData,\n canonical,\n proxy\n ) as unknown as Record<string, unknown>\n}\n\n/** List envelope: one `imagePublicEmbedBaseUrl` rewrites every post. */\nexport function rewriteBlogListEnvelope(\n body: unknown,\n proxyPrefix: string\n): BlogImageRewriteResult {\n const record = asRecord(body)\n const canonical = readEmbedBase(record?.imagePublicEmbedBaseUrl)\n if (!record || !canonical) {\n return { ok: false, reason: 'missing-embed-base' }\n }\n if (!Array.isArray(record.data)) {\n return { ok: true, body }\n }\n return {\n ok: true,\n body: {\n ...record,\n data: record.data.map((item) => {\n const post = asRecord(item)\n if (!post) return item\n return rewritePostRecord(post, canonical, proxyPrefix)\n }),\n },\n }\n}\n\n/** Slug envelope: embed base on the article, or on the response root. */\nexport function rewriteBlogPostEnvelope(\n body: unknown,\n proxyPrefix: string\n): BlogImageRewriteResult {\n const record = asRecord(body)\n const data = asRecord(record?.data)\n const canonical = readEmbedBase(\n data?.imagePublicEmbedBaseUrl,\n record?.imagePublicEmbedBaseUrl\n )\n if (!record || !data || !canonical) {\n return { ok: false, reason: 'missing-embed-base' }\n }\n return {\n ok: true,\n body: {\n ...record,\n data: rewritePostRecord(data, canonical, proxyPrefix),\n },\n }\n}\n\n/** Category description envelope: embed base on `data` or the response root. */\nexport function rewriteBlogCategoryDescriptionEnvelope(\n body: unknown,\n proxyPrefix: string\n): BlogImageRewriteResult {\n const record = asRecord(body)\n const data = asRecord(record?.data)\n const canonical = readEmbedBase(\n data?.imagePublicEmbedBaseUrl,\n record?.imagePublicEmbedBaseUrl\n )\n if (!record || !data || !canonical) {\n return { ok: false, reason: 'missing-embed-base' }\n }\n return {\n ok: true,\n body: {\n ...record,\n data: rewriteCategoryDescriptionMedia(\n data as {\n description?: string | null\n descriptionHtml?: string | null\n },\n canonical,\n proxyPrefix\n ),\n },\n }\n}\n\nexport function rewriteBlogJsonLdEnvelope(\n body: unknown,\n proxyPrefix: string\n): BlogImageRewriteResult {\n try {\n return {\n ok: true,\n body: rewriteEmbedUrlsAnyHost(body, proxyPrefix),\n }\n } catch {\n return { ok: false, reason: 'invalid-json' }\n }\n}\n"],"mappings":"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","names":["import_next_core"]}
1
+ {"version":3,"sources":["../../src/routes/index.ts","../../src/server/client.ts","../../src/server/image-rewrite.ts"],"sourcesContent":["import type { NextRequest } from 'next/server'\nimport {\n defaultMailooLogger,\n jsonError,\n proxyMailooWebhookJsonPost,\n type MailooIntegrationConfig,\n type MailooLogger,\n} from '@mailoo/next-core'\nimport { getMailooBlogConfigFromEnv } from '../server/client.js'\nimport {\n rewriteBlogCategoryDescriptionEnvelope,\n rewriteBlogJsonLdEnvelope,\n rewriteBlogListEnvelope,\n rewriteBlogPostEnvelope,\n type BlogImageRewriteResult,\n} from '../server/image-rewrite.js'\n\nexport type CreateBlogRoutesOptions = {\n /** Explicit config; defaults to MAILOO_BLOG_* env */\n getConfig?: () => MailooIntegrationConfig | null\n logger?: MailooLogger\n /**\n * Integrator prefix ending with `?id=`. When set, list, slug, category\n * description, and JSON-LD responses rewrite canonical Mailoo embed URLs.\n * Example: `/api/v1/images/public/content?id=`\n * A site-relative prefix is copied onto rewritten `ogImageUrl` and body\n * image URLs. Resolve `ogImageUrl` against the current request origin\n * before `og:image` / JSON-LD; leave `htmlContent` relative.\n */\n imageProxyPrefixWithIdEquals?: string\n /**\n * Used only when the query has no `locale`. Host maps Accept-Language\n * to its own locale list.\n */\n resolveLocale?: (request: NextRequest) => string | undefined\n}\n\nconst EMBED_BASE_ERROR =\n 'Blog API contract violation: imagePublicEmbedBaseUrl missing'\n\ntype BlogGetKind =\n 'list' | 'post' | 'category-description' | 'json-ld' | 'passthrough'\n\nfunction resolveConfig(\n options?: CreateBlogRoutesOptions\n): MailooIntegrationConfig | null {\n return options?.getConfig?.() ?? getMailooBlogConfigFromEnv()\n}\n\nfunction notConfigured(logger: MailooLogger): Response {\n logger.error(\n 'MAILOO_BLOG_PROJECT_UID, MAILOO_BLOG_INTEGRATION_ID, MAILOO_BLOG_API and MAILOO_BLOG_API_KEY must be set'\n )\n return jsonError({ error: 'Blog API not configured' }, 503)\n}\n\nfunction imageProxyPrefix(\n options?: CreateBlogRoutesOptions\n): string | undefined {\n const raw = options?.imageProxyPrefixWithIdEquals?.trim()\n return raw || undefined\n}\n\nfunction localeSearchParams(\n request: NextRequest,\n options: CreateBlogRoutesOptions | undefined,\n mode: 'all' | 'locale-only'\n): URLSearchParams {\n const source = request.nextUrl.searchParams\n const params =\n mode === 'all' ? new URLSearchParams(source) : new URLSearchParams()\n if (mode === 'locale-only') {\n const fromQuery = source.get('locale')\n if (fromQuery) params.set('locale', fromQuery)\n }\n if (!params.get('locale')) {\n const resolved = options?.resolveLocale?.(request)\n const locale = typeof resolved === 'string' ? resolved.trim() : ''\n if (locale) params.set('locale', locale)\n }\n return params\n}\n\nfunction jsonResponse(body: unknown, status: number): Response {\n return new Response(JSON.stringify(body), {\n status,\n headers: { 'Content-Type': 'application/json' },\n })\n}\n\nfunction rewriteProxiedBody(\n kind: BlogGetKind,\n body: unknown,\n proxyPrefix: string\n): BlogImageRewriteResult {\n switch (kind) {\n case 'list':\n return rewriteBlogListEnvelope(body, proxyPrefix)\n case 'post':\n return rewriteBlogPostEnvelope(body, proxyPrefix)\n case 'category-description':\n return rewriteBlogCategoryDescriptionEnvelope(body, proxyPrefix)\n case 'json-ld':\n return rewriteBlogJsonLdEnvelope(body, proxyPrefix)\n case 'passthrough':\n return { ok: true, body }\n }\n}\n\nasync function proxyGet(\n config: MailooIntegrationConfig,\n pathSuffix: string,\n searchParams: URLSearchParams | undefined,\n options: CreateBlogRoutesOptions | undefined,\n kind: BlogGetKind\n): Promise<Response> {\n const logger = options?.logger ?? defaultMailooLogger\n const url = new URL(\n `${config.apiBaseUrl}/api/v1/blog/${config.projectUid}/integrations/${config.integrationId}${pathSuffix}`\n )\n if (searchParams) {\n searchParams.forEach((value, key) => {\n url.searchParams.append(key, value)\n })\n }\n const response = await fetch(url.toString(), {\n method: 'GET',\n headers: {\n 'Content-Type': 'application/json',\n 'X-API-Key': config.apiKey,\n },\n })\n const data: unknown = await response.json()\n const prefix = imageProxyPrefix(options)\n if (!response.ok || !prefix || kind === 'passthrough') {\n return jsonResponse(data, response.status)\n }\n const rewritten = rewriteProxiedBody(kind, data, prefix)\n if (!rewritten.ok) {\n if (rewritten.reason === 'missing-embed-base') {\n logger.error(EMBED_BASE_ERROR)\n return jsonError({ error: EMBED_BASE_ERROR }, 502)\n }\n logger.error('Blog API JSON-LD rewrite failed')\n return jsonError({ error: 'Internal server error' }, 500)\n }\n return jsonResponse(rewritten.body, response.status)\n}\n\n/** Factory: GET list posts — mount at `app/api/v1/blog/route.ts` */\nexport function createBlogListHandler(options?: CreateBlogRoutesOptions) {\n const logger = options?.logger ?? defaultMailooLogger\n return async function GET(request: NextRequest): Promise<Response> {\n const config = resolveConfig(options)\n if (!config) return notConfigured(logger)\n try {\n return await proxyGet(\n config,\n '',\n localeSearchParams(request, options, 'all'),\n options,\n 'list'\n )\n } catch (error) {\n logger.logException?.(error, 'Blog API proxy error')\n return jsonError({ error: 'Internal server error' }, 500)\n }\n }\n}\n\n/** Factory: GET post by slug — mount at `app/api/v1/blog/slug/[slug]/route.ts` */\nexport function createBlogSlugHandler(options?: CreateBlogRoutesOptions) {\n const logger = options?.logger ?? defaultMailooLogger\n return async function GET(\n request: NextRequest,\n ctx: { params: Promise<{ slug: string }> }\n ): Promise<Response> {\n const config = resolveConfig(options)\n if (!config) return notConfigured(logger)\n const { slug } = await ctx.params\n try {\n return await proxyGet(\n config,\n `/slug/${encodeURIComponent(slug)}`,\n localeSearchParams(request, options, 'locale-only'),\n options,\n 'post'\n )\n } catch (error) {\n logger.logException?.(error, 'Blog slug API proxy error')\n return jsonError({ error: 'Internal server error' }, 500)\n }\n }\n}\n\n/** Factory: GET categories */\nexport function createBlogCategoriesHandler(options?: CreateBlogRoutesOptions) {\n const logger = options?.logger ?? defaultMailooLogger\n return async function GET(request: NextRequest): Promise<Response> {\n const config = resolveConfig(options)\n if (!config) return notConfigured(logger)\n try {\n return await proxyGet(\n config,\n '/categories',\n localeSearchParams(request, options, 'all'),\n options,\n 'passthrough'\n )\n } catch (error) {\n logger.logException?.(error, 'Blog categories API proxy error')\n return jsonError({ error: 'Internal server error' }, 500)\n }\n }\n}\n\n/** Factory: GET tags */\nexport function createBlogTagsHandler(options?: CreateBlogRoutesOptions) {\n const logger = options?.logger ?? defaultMailooLogger\n return async function GET(request: NextRequest): Promise<Response> {\n const config = resolveConfig(options)\n if (!config) return notConfigured(logger)\n try {\n return await proxyGet(\n config,\n '/tags',\n localeSearchParams(request, options, 'all'),\n options,\n 'passthrough'\n )\n } catch (error) {\n logger.logException?.(error, 'Blog tags API proxy error')\n return jsonError({ error: 'Internal server error' }, 500)\n }\n }\n}\n\n/** Factory: GET tag by slug */\nexport function createBlogTagSlugHandler(options?: CreateBlogRoutesOptions) {\n const logger = options?.logger ?? defaultMailooLogger\n return async function GET(\n request: NextRequest,\n ctx: { params: Promise<{ slug: string }> }\n ): Promise<Response> {\n const config = resolveConfig(options)\n if (!config) return notConfigured(logger)\n const { slug } = await ctx.params\n try {\n return await proxyGet(\n config,\n `/tags/slug/${encodeURIComponent(slug)}`,\n localeSearchParams(request, options, 'all'),\n options,\n 'passthrough'\n )\n } catch (error) {\n logger.logException?.(error, 'Blog tag slug API proxy error')\n return jsonError({ error: 'Internal server error' }, 500)\n }\n }\n}\n\n/** Factory: GET category description by slug */\nexport function createBlogCategoryDescriptionHandler(\n options?: CreateBlogRoutesOptions\n) {\n const logger = options?.logger ?? defaultMailooLogger\n return async function GET(\n request: NextRequest,\n ctx: { params: Promise<{ slug: string }> }\n ): Promise<Response> {\n const config = resolveConfig(options)\n if (!config) return notConfigured(logger)\n const { slug } = await ctx.params\n try {\n return await proxyGet(\n config,\n `/categories/slug/${encodeURIComponent(slug)}/description`,\n localeSearchParams(request, options, 'locale-only'),\n options,\n 'category-description'\n )\n } catch (error) {\n logger.logException?.(error, 'Blog category description API proxy error')\n return jsonError({ error: 'Internal server error' }, 500)\n }\n }\n}\n\n/** Factory: GET JSON-LD by slug — mount at `app/api/v1/blog/slug/[slug]/json-ld/route.ts` */\nexport function createBlogJsonLdHandler(options?: CreateBlogRoutesOptions) {\n const logger = options?.logger ?? defaultMailooLogger\n return async function GET(\n request: NextRequest,\n ctx: { params: Promise<{ slug: string }> }\n ): Promise<Response> {\n const config = resolveConfig(options)\n if (!config) return notConfigured(logger)\n const { slug } = await ctx.params\n try {\n return await proxyGet(\n config,\n `/slug/${encodeURIComponent(slug)}/json-ld`,\n localeSearchParams(request, options, 'locale-only'),\n options,\n 'json-ld'\n )\n } catch (error) {\n logger.logException?.(error, 'Blog JSON-LD API proxy error')\n return jsonError({ error: 'Internal server error' }, 500)\n }\n }\n}\n\nasync function proxyMarkPost(\n options: CreateBlogRoutesOptions | undefined,\n request: NextRequest,\n slug: string,\n pathKind: 'reaction' | 'feedback',\n logLabel: string\n): Promise<Response> {\n const logger = options?.logger ?? defaultMailooLogger\n const config = resolveConfig(options)\n if (!config) return notConfigured(logger)\n let body: unknown\n try {\n body = await request.json()\n } catch {\n return jsonError({ error: 'Invalid JSON body' }, 400)\n }\n const url = `${config.apiBaseUrl}/api/v1/blog/${config.projectUid}/integrations/${config.integrationId}/slug/${encodeURIComponent(slug)}/${pathKind}`\n try {\n return await proxyMailooWebhookJsonPost({\n request,\n url,\n apiKey: config.apiKey,\n body,\n })\n } catch (error) {\n logger.logException?.(error, logLabel)\n return jsonError({ error: 'Internal server error' }, 500)\n }\n}\n\n/** Factory: POST reaction — mount at `app/api/v1/blog/slug/[slug]/reaction/route.ts` */\nexport function createBlogReactionHandler(options?: CreateBlogRoutesOptions) {\n return async function POST(\n request: NextRequest,\n ctx: { params: Promise<{ slug: string }> }\n ): Promise<Response> {\n const { slug } = await ctx.params\n return proxyMarkPost(\n options,\n request,\n slug,\n 'reaction',\n 'Blog reaction proxy error'\n )\n }\n}\n\n/** Factory: POST feedback — mount at `app/api/v1/blog/slug/[slug]/feedback/route.ts` */\nexport function createBlogFeedbackHandler(options?: CreateBlogRoutesOptions) {\n return async function POST(\n request: NextRequest,\n ctx: { params: Promise<{ slug: string }> }\n ): Promise<Response> {\n const { slug } = await ctx.params\n return proxyMarkPost(\n options,\n request,\n slug,\n 'feedback',\n 'Blog feedback proxy error'\n )\n }\n}\n\nexport function createMailooBlogRoutes(options?: CreateBlogRoutesOptions) {\n return {\n list: createBlogListHandler(options),\n slug: createBlogSlugHandler(options),\n jsonLd: createBlogJsonLdHandler(options),\n categories: createBlogCategoriesHandler(options),\n tags: createBlogTagsHandler(options),\n tagSlug: createBlogTagSlugHandler(options),\n categoryDescription: createBlogCategoryDescriptionHandler(options),\n reaction: createBlogReactionHandler(options),\n feedback: createBlogFeedbackHandler(options),\n }\n}\n","import { unstable_rethrow } from 'next/navigation'\nimport type { MailooIntegrationConfig } from '@mailoo/next-core'\nimport { readMailooIntegrationConfig } from '@mailoo/next-core'\nimport {\n rewriteBlogPostMedia,\n rewriteCategoryDescriptionMedia,\n rewriteEmbedUrlsAnyHost,\n} from './image-rewrite.js'\nimport type {\n BlogArticleMarkState,\n BlogCategoryApi,\n BlogTagApi,\n BlogListPagination,\n BlogListQuery,\n BlogPostApiData,\n MailooBlogFetchOptions,\n SetBlogFeedbackInput,\n SetBlogReactionInput,\n} from '../types.js'\n\nexport type MailooBlogConfig = MailooIntegrationConfig\nexport type { MailooBlogFetchOptions }\n\n/** Host image proxy. Omit to keep canonical Mailoo embed URLs. */\nexport type CreateMailooBlogClientOptions = {\n /**\n * Integrator prefix ending with `?id=` (absolute or same-origin path).\n * Example: `/api/v1/images/public/content?id=`\n * A site-relative prefix is copied onto rewritten `ogImageUrl` and body\n * image URLs. Resolve `ogImageUrl` against the current request origin\n * before `og:image` / JSON-LD; leave `htmlContent` relative.\n */\n imageProxyPrefixWithIdEquals?: string\n}\n\nconst DEFAULT_REVALIDATE = 60\n\n/** Read MAILOO_BLOG_* env (API, API_KEY, PROJECT_UID, INTEGRATION_ID). */\nexport function getMailooBlogConfigFromEnv(): MailooBlogConfig | null {\n return readMailooIntegrationConfig({ prefix: 'MAILOO_BLOG' })\n}\n\nfunction blogBasePath(config: MailooBlogConfig): string {\n return `${config.apiBaseUrl}/api/v1/blog/${config.projectUid}/integrations/${config.integrationId}`\n}\n\nfunction resolveRevalidate(options?: MailooBlogFetchOptions): number {\n return options?.revalidate ?? DEFAULT_REVALIDATE\n}\n\nasync function blogFetchJson(\n config: MailooBlogConfig,\n path: string,\n query?: Record<string, string | undefined>,\n options?: MailooBlogFetchOptions\n): Promise<\n { ok: true; json: unknown; status: number } | { ok: false; status: number }\n> {\n const url = new URL(`${blogBasePath(config)}${path}`)\n if (query) {\n for (const [k, v] of Object.entries(query)) {\n if (v !== undefined && v !== '') url.searchParams.set(k, v)\n }\n }\n try {\n const revalidate = resolveRevalidate(options)\n const res = await fetch(url.toString(), {\n method: 'GET',\n headers: {\n 'Content-Type': 'application/json',\n 'X-API-Key': config.apiKey,\n },\n next: { revalidate },\n } as RequestInit)\n if (!res.ok) {\n return { ok: false, status: res.status }\n }\n return { ok: true, json: await res.json(), status: res.status }\n } catch (error) {\n unstable_rethrow(error)\n return { ok: false, status: 500 }\n }\n}\n\nasync function blogPostJson(\n config: MailooBlogConfig,\n path: string,\n body: unknown\n): Promise<\n | { ok: true; data: BlogArticleMarkState; status: number }\n | { ok: false; status: number }\n> {\n try {\n const res = await fetch(`${blogBasePath(config)}${path}`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-API-Key': config.apiKey,\n },\n body: JSON.stringify(body),\n cache: 'no-store',\n })\n if (!res.ok) {\n return { ok: false, status: res.status }\n }\n const json = (await res.json()) as { data?: BlogArticleMarkState }\n if (!json.data || typeof json.data !== 'object') {\n return { ok: false, status: 500 }\n }\n return { ok: true, data: json.data, status: res.status }\n } catch (error) {\n unstable_rethrow(error)\n return { ok: false, status: 500 }\n }\n}\n\nasync function blogGet<T>(\n config: MailooBlogConfig,\n path: string,\n query?: Record<string, string | undefined>,\n options?: MailooBlogFetchOptions\n): Promise<\n { ok: true; data: T; status: number } | { ok: false; status: number }\n> {\n const fetched = await blogFetchJson(config, path, query, options)\n if (!fetched.ok) return fetched\n const json = fetched.json as { data?: T } & T\n const data =\n (json as { data?: T }).data !== undefined\n ? (json as { data: T }).data\n : (json as T)\n return { ok: true, data, status: fetched.status }\n}\n\nfunction isBlogListPagination(value: unknown): value is BlogListPagination {\n if (value === null || typeof value !== 'object') return false\n const p = value as Record<string, unknown>\n return (\n typeof p.page === 'number' &&\n typeof p.limit === 'number' &&\n typeof p.total === 'number' &&\n typeof p.totalPages === 'number' &&\n typeof p.hasNextPage === 'boolean' &&\n typeof p.hasPrevPage === 'boolean'\n )\n}\n\nfunction queryRecord(query: BlogListQuery): Record<string, string | undefined> {\n const q: Record<string, string | undefined> = {}\n for (const [k, v] of Object.entries(query)) {\n if (v === undefined) continue\n q[k] = String(v)\n }\n return q\n}\n\nfunction imageProxyPrefix(\n options?: CreateMailooBlogClientOptions\n): string | undefined {\n const raw = options?.imageProxyPrefixWithIdEquals?.trim()\n return raw || undefined\n}\n\nfunction rewritePost(\n post: BlogPostApiData,\n canonicalPrefixWithIdEquals: string,\n proxyPrefixWithIdEquals: string\n): BlogPostApiData {\n return rewriteBlogPostMedia(\n post,\n canonicalPrefixWithIdEquals,\n proxyPrefixWithIdEquals\n )\n}\n\ntype CategoryDescription = {\n description?: string\n descriptionHtml?: string\n imagePublicEmbedBaseUrl?: string\n}\n\nexport function createMailooBlogClient(\n config: MailooBlogConfig,\n clientOptions?: CreateMailooBlogClientOptions\n) {\n const proxyPrefix = imageProxyPrefix(clientOptions)\n return {\n async listPosts(\n query: BlogListQuery = {},\n options?: MailooBlogFetchOptions\n ): Promise<\n | {\n ok: true\n data: BlogPostApiData[]\n pagination: BlogListPagination\n imagePublicEmbedBaseUrl: string\n status: number\n }\n | { ok: false; status: number }\n > {\n const fetched = await blogFetchJson(\n config,\n '',\n queryRecord(query),\n options\n )\n if (!fetched.ok) return fetched\n const json = fetched.json\n if (json === null || typeof json !== 'object') {\n return { ok: false, status: 500 }\n }\n const body = json as {\n data?: unknown\n pagination?: unknown\n imagePublicEmbedBaseUrl?: unknown\n }\n const imagePublicEmbedBaseUrl =\n typeof body.imagePublicEmbedBaseUrl === 'string'\n ? body.imagePublicEmbedBaseUrl.trim()\n : ''\n if (\n !Array.isArray(body.data) ||\n !isBlogListPagination(body.pagination) ||\n !imagePublicEmbedBaseUrl\n ) {\n return { ok: false, status: 500 }\n }\n const posts = body.data as BlogPostApiData[]\n return {\n ok: true,\n data: proxyPrefix\n ? posts.map((post) =>\n rewritePost(post, imagePublicEmbedBaseUrl, proxyPrefix)\n )\n : posts,\n pagination: body.pagination,\n imagePublicEmbedBaseUrl,\n status: fetched.status,\n }\n },\n\n async getPostBySlug(\n slug: string,\n locale?: string,\n options?: MailooBlogFetchOptions\n ) {\n const result = await blogGet<BlogPostApiData>(\n config,\n `/slug/${encodeURIComponent(slug)}`,\n locale ? { locale } : undefined,\n options\n )\n if (!result.ok || !proxyPrefix) return result\n const canonical = result.data.imagePublicEmbedBaseUrl?.trim()\n if (!canonical) return { ok: false as const, status: 500 }\n return {\n ...result,\n data: rewritePost(result.data, canonical, proxyPrefix),\n }\n },\n\n async getPostJsonLd(\n slug: string,\n locale?: string,\n options?: MailooBlogFetchOptions\n ) {\n const result = await blogGet<{ jsonLd: Record<string, unknown> }>(\n config,\n `/slug/${encodeURIComponent(slug)}/json-ld`,\n locale ? { locale } : undefined,\n options\n )\n if (!result.ok || !proxyPrefix) return result\n try {\n return {\n ...result,\n data: {\n jsonLd: rewriteEmbedUrlsAnyHost(result.data.jsonLd, proxyPrefix),\n },\n }\n } catch {\n return { ok: false as const, status: 500 }\n }\n },\n\n async listCategories(locale?: string, options?: MailooBlogFetchOptions) {\n return blogGet<BlogCategoryApi[]>(\n config,\n '/categories',\n locale ? { locale } : undefined,\n options\n )\n },\n\n async listTags(locale?: string, options?: MailooBlogFetchOptions) {\n return blogGet<BlogTagApi[]>(\n config,\n '/tags',\n locale ? { locale } : undefined,\n options\n )\n },\n\n async getTagBySlug(\n slug: string,\n locale?: string,\n options?: MailooBlogFetchOptions\n ) {\n return blogGet<BlogTagApi>(\n config,\n `/tags/slug/${encodeURIComponent(slug)}`,\n locale ? { locale } : undefined,\n options\n )\n },\n\n async getCategoryDescription(\n slug: string,\n locale?: string,\n options?: MailooBlogFetchOptions\n ) {\n const result = await blogGet<CategoryDescription>(\n config,\n `/categories/slug/${encodeURIComponent(slug)}/description`,\n locale ? { locale } : undefined,\n options\n )\n if (!result.ok || !proxyPrefix) return result\n const canonical = result.data.imagePublicEmbedBaseUrl?.trim()\n if (!canonical) return { ok: false as const, status: 500 }\n return {\n ...result,\n data: rewriteCategoryDescriptionMedia(\n result.data,\n canonical,\n proxyPrefix\n ),\n }\n },\n\n async setReaction(slug: string, input: SetBlogReactionInput) {\n return blogPostJson(\n config,\n `/slug/${encodeURIComponent(slug)}/reaction`,\n input\n )\n },\n\n async setFeedback(slug: string, input: SetBlogFeedbackInput) {\n return blogPostJson(\n config,\n `/slug/${encodeURIComponent(slug)}/feedback`,\n input\n )\n },\n }\n}\n\nexport type MailooBlogClient = ReturnType<typeof createMailooBlogClient>\n","import {\n rewriteMailooBlogMediaUrls,\n rewriteMailooPublicImageUrls,\n rewriteMailooPublicImageUrlsAnyHost,\n} from '@mailoo/images'\nimport type { BlogPostApiData } from '../types.js'\n\nexport type BlogImageRewriteFailure = 'missing-embed-base' | 'invalid-json'\n\nexport type BlogImageRewriteResult =\n { ok: true; body: unknown } | { ok: false; reason: BlogImageRewriteFailure }\n\nfunction asRecord(value: unknown): Record<string, unknown> | null {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) {\n return null\n }\n return value as Record<string, unknown>\n}\n\nexport function readEmbedBase(...candidates: unknown[]): string | undefined {\n for (const candidate of candidates) {\n if (typeof candidate === 'string' && candidate.trim()) {\n return candidate.trim()\n }\n }\n return undefined\n}\n\nexport function rewriteBlogPostMedia<T extends BlogPostApiData>(\n post: T,\n canonicalPrefixWithIdEquals: string,\n proxyPrefixWithIdEquals: string\n): T {\n return rewriteMailooBlogMediaUrls(post, {\n canonicalPrefixWithIdEquals,\n proxyPrefixWithIdEquals,\n })\n}\n\nexport function rewriteCategoryDescriptionMedia<\n T extends {\n description?: string | null\n descriptionHtml?: string | null\n },\n>(\n data: T,\n canonicalPrefixWithIdEquals: string,\n proxyPrefixWithIdEquals: string\n): T {\n const opts = {\n canonicalPrefixWithIdEquals,\n proxyPrefixWithIdEquals,\n }\n return {\n ...data,\n description:\n typeof data.description === 'string'\n ? rewriteMailooPublicImageUrls(data.description, opts)\n : data.description,\n descriptionHtml:\n typeof data.descriptionHtml === 'string'\n ? rewriteMailooPublicImageUrls(data.descriptionHtml, opts)\n : data.descriptionHtml,\n }\n}\n\nexport function rewriteEmbedUrlsAnyHost<T>(value: T, proxyPrefix: string): T {\n const rewritten = rewriteMailooPublicImageUrlsAnyHost(\n JSON.stringify(value),\n proxyPrefix\n )\n return JSON.parse(rewritten) as T\n}\n\nfunction rewritePostRecord(\n post: Record<string, unknown>,\n canonical: string,\n proxy: string\n): Record<string, unknown> {\n return rewriteBlogPostMedia(\n post as unknown as BlogPostApiData,\n canonical,\n proxy\n ) as unknown as Record<string, unknown>\n}\n\n/** List envelope: one `imagePublicEmbedBaseUrl` rewrites every post. */\nexport function rewriteBlogListEnvelope(\n body: unknown,\n proxyPrefix: string\n): BlogImageRewriteResult {\n const record = asRecord(body)\n const canonical = readEmbedBase(record?.imagePublicEmbedBaseUrl)\n if (!record || !canonical) {\n return { ok: false, reason: 'missing-embed-base' }\n }\n if (!Array.isArray(record.data)) {\n return { ok: true, body }\n }\n return {\n ok: true,\n body: {\n ...record,\n data: record.data.map((item) => {\n const post = asRecord(item)\n if (!post) return item\n return rewritePostRecord(post, canonical, proxyPrefix)\n }),\n },\n }\n}\n\n/** Slug envelope: embed base on the article, or on the response root. */\nexport function rewriteBlogPostEnvelope(\n body: unknown,\n proxyPrefix: string\n): BlogImageRewriteResult {\n const record = asRecord(body)\n const data = asRecord(record?.data)\n const canonical = readEmbedBase(\n data?.imagePublicEmbedBaseUrl,\n record?.imagePublicEmbedBaseUrl\n )\n if (!record || !data || !canonical) {\n return { ok: false, reason: 'missing-embed-base' }\n }\n return {\n ok: true,\n body: {\n ...record,\n data: rewritePostRecord(data, canonical, proxyPrefix),\n },\n }\n}\n\n/** Category description envelope: embed base on `data` or the response root. */\nexport function rewriteBlogCategoryDescriptionEnvelope(\n body: unknown,\n proxyPrefix: string\n): BlogImageRewriteResult {\n const record = asRecord(body)\n const data = asRecord(record?.data)\n const canonical = readEmbedBase(\n data?.imagePublicEmbedBaseUrl,\n record?.imagePublicEmbedBaseUrl\n )\n if (!record || !data || !canonical) {\n return { ok: false, reason: 'missing-embed-base' }\n }\n return {\n ok: true,\n body: {\n ...record,\n data: rewriteCategoryDescriptionMedia(\n data as {\n description?: string | null\n descriptionHtml?: string | null\n },\n canonical,\n proxyPrefix\n ),\n },\n }\n}\n\nexport function rewriteBlogJsonLdEnvelope(\n body: unknown,\n proxyPrefix: string\n): BlogImageRewriteResult {\n try {\n return {\n ok: true,\n body: rewriteEmbedUrlsAnyHost(body, proxyPrefix),\n }\n } catch {\n return { ok: false, reason: 'invalid-json' }\n }\n}\n"],"mappings":"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","names":["import_next_core"]}
@@ -9,6 +9,9 @@ type CreateBlogRoutesOptions = {
9
9
  * Integrator prefix ending with `?id=`. When set, list, slug, category
10
10
  * description, and JSON-LD responses rewrite canonical Mailoo embed URLs.
11
11
  * Example: `/api/v1/images/public/content?id=`
12
+ * A site-relative prefix is copied onto rewritten `ogImageUrl` and body
13
+ * image URLs. Resolve `ogImageUrl` against the current request origin
14
+ * before `og:image` / JSON-LD; leave `htmlContent` relative.
12
15
  */
13
16
  imageProxyPrefixWithIdEquals?: string;
14
17
  /**
@@ -9,6 +9,9 @@ type CreateBlogRoutesOptions = {
9
9
  * Integrator prefix ending with `?id=`. When set, list, slug, category
10
10
  * description, and JSON-LD responses rewrite canonical Mailoo embed URLs.
11
11
  * Example: `/api/v1/images/public/content?id=`
12
+ * A site-relative prefix is copied onto rewritten `ogImageUrl` and body
13
+ * image URLs. Resolve `ogImageUrl` against the current request origin
14
+ * before `og:image` / JSON-LD; leave `htmlContent` relative.
12
15
  */
13
16
  imageProxyPrefixWithIdEquals?: string;
14
17
  /**