@mailoo/blog 1.33.0 → 1.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,29 @@
1
+ type BlogPrintEvent = {
2
+ section: HTMLElement;
3
+ printTitle: string;
4
+ };
1
5
  interface BlogPrintableButtonsProps {
2
6
  /** Button label (host provides i18n) */
3
7
  printLabel?: string;
4
8
  buttonClassName?: string;
9
+ /**
10
+ * Limit enhancement to `[data-mailoo-print]` inside this root.
11
+ * When the root is missing, no buttons are added.
12
+ * Omit to enhance every section in the document.
13
+ */
14
+ containerSelector?: string;
15
+ /** Called before the print window opens. The package does not call analytics. */
16
+ onPrint?: (event: BlogPrintEvent) => void;
17
+ /**
18
+ * When set, the print document appends a source block (label, document
19
+ * title, page URL). Host provides the locale string.
20
+ */
21
+ sourceLabel?: string;
5
22
  }
6
23
  /**
7
24
  * Enhances `[data-mailoo-print]` sections from API-generated article HTML with
8
25
  * a print button.
9
26
  */
10
- declare function BlogPrintableButtons({ printLabel, buttonClassName, }?: BlogPrintableButtonsProps): null;
27
+ declare function BlogPrintableButtons({ printLabel, buttonClassName, containerSelector, onPrint, sourceLabel, }?: BlogPrintableButtonsProps): null;
11
28
 
12
- export { BlogPrintableButtons, type BlogPrintableButtonsProps };
29
+ export { type BlogPrintEvent, BlogPrintableButtons, type BlogPrintableButtonsProps };
@@ -1,12 +1,29 @@
1
+ type BlogPrintEvent = {
2
+ section: HTMLElement;
3
+ printTitle: string;
4
+ };
1
5
  interface BlogPrintableButtonsProps {
2
6
  /** Button label (host provides i18n) */
3
7
  printLabel?: string;
4
8
  buttonClassName?: string;
9
+ /**
10
+ * Limit enhancement to `[data-mailoo-print]` inside this root.
11
+ * When the root is missing, no buttons are added.
12
+ * Omit to enhance every section in the document.
13
+ */
14
+ containerSelector?: string;
15
+ /** Called before the print window opens. The package does not call analytics. */
16
+ onPrint?: (event: BlogPrintEvent) => void;
17
+ /**
18
+ * When set, the print document appends a source block (label, document
19
+ * title, page URL). Host provides the locale string.
20
+ */
21
+ sourceLabel?: string;
5
22
  }
6
23
  /**
7
24
  * Enhances `[data-mailoo-print]` sections from API-generated article HTML with
8
25
  * a print button.
9
26
  */
10
- declare function BlogPrintableButtons({ printLabel, buttonClassName, }?: BlogPrintableButtonsProps): null;
27
+ declare function BlogPrintableButtons({ printLabel, buttonClassName, containerSelector, onPrint, sourceLabel, }?: BlogPrintableButtonsProps): null;
11
28
 
12
- export { BlogPrintableButtons, type BlogPrintableButtonsProps };
29
+ export { type BlogPrintEvent, BlogPrintableButtons, type BlogPrintableButtonsProps };
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  // src/components/blog-printable-buttons.tsx
4
- import { useEffect } from "react";
4
+ import { useEffect, useRef } from "react";
5
5
  var PRINT_BUTTON_CLASSNAME = [
6
6
  "inline-flex items-center justify-center rounded-lg font-medium transition-colors",
7
7
  "focus:outline-none focus:ring-2 focus:ring-offset-2",
@@ -9,58 +9,117 @@ var PRINT_BUTTON_CLASSNAME = [
9
9
  "dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-800",
10
10
  "px-3 py-1.5 text-sm"
11
11
  ].join(" ");
12
- function escapeHtml(text) {
13
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
14
- }
15
- function printMailooSection(section) {
16
- const body = section.querySelector(".mailoo-printable-body");
17
- if (!body) return;
18
- const printTitle = section.getAttribute("data-print-title")?.trim() || document.title;
19
- const win = window.open("", "_blank", "noopener,noreferrer");
20
- if (!win) return;
21
- const html = `<!DOCTYPE html>
22
- <html lang="en">
23
- <head>
24
- <meta charset="utf-8">
25
- <title>${escapeHtml(printTitle)}</title>
26
- <style>
12
+ var ENHANCED_ATTR = "data-mailoo-print-enhanced";
13
+ var SECTION_SELECTOR = `[data-mailoo-print]:not([${ENHANCED_ATTR}])`;
14
+ var PRINT_WINDOW_STYLES = `
27
15
  body {
28
- font-family: system-ui, -apple-system, sans-serif;
16
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
29
17
  line-height: 1.6;
30
- max-width: 48rem;
31
- margin: 2rem auto;
32
- padding: 0 1rem;
18
+ margin: 1.5rem;
33
19
  color: #111;
34
20
  }
21
+ h1, h2, h3, h4 {
22
+ margin-top: 1.25em;
23
+ margin-bottom: 0.5em;
24
+ line-height: 1.25;
25
+ }
26
+ p { margin: 0.75em 0; }
27
+ ul, ol { padding-left: 1.5em; margin: 0.75em 0; }
28
+ li { margin: 0.25em 0; }
35
29
  img { max-width: 100%; height: auto; }
36
30
  a { color: #2563eb; }
37
- </style>
38
- </head>
39
- <body>
40
- ${body.innerHTML}
41
- </body>
42
- </html>`;
31
+ blockquote {
32
+ margin: 1em 0;
33
+ padding-left: 1em;
34
+ border-left: 3px solid #d4d4d4;
35
+ color: #525252;
36
+ }
37
+ .mailoo-print-source {
38
+ margin-top: 2rem;
39
+ padding: 0.75rem 1rem;
40
+ border: 1px solid #e5e5e5;
41
+ border-radius: 0.5rem;
42
+ background: #fafafa;
43
+ font-size: 0.8125rem;
44
+ line-height: 1.45;
45
+ color: #525252;
46
+ }
47
+ .mailoo-print-source-label {
48
+ display: block;
49
+ margin-bottom: 0.35rem;
50
+ font-size: 0.6875rem;
51
+ font-weight: 650;
52
+ letter-spacing: 0.06em;
53
+ text-transform: uppercase;
54
+ color: #737373;
55
+ }
56
+ .mailoo-print-source-title {
57
+ display: block;
58
+ margin: 0;
59
+ font-size: 0.875rem;
60
+ font-weight: 600;
61
+ color: #171717;
62
+ text-wrap: pretty;
63
+ }
64
+ .mailoo-print-source-url {
65
+ display: block;
66
+ margin-top: 0.25rem;
67
+ word-break: break-all;
68
+ color: #525252;
69
+ text-decoration: none;
70
+ }
71
+ `;
72
+ function escapeHtml(text) {
73
+ return text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
74
+ }
75
+ function buildSourceBlock(sourceLabel) {
76
+ const pageTitle = document.title;
77
+ const pageUrl = window.location.href;
78
+ return `<aside class="mailoo-print-source"><span class="mailoo-print-source-label">${escapeHtml(sourceLabel)}</span><span class="mailoo-print-source-title">${escapeHtml(pageTitle)}</span><a class="mailoo-print-source-url" href="${escapeHtml(pageUrl)}">${escapeHtml(pageUrl)}</a></aside>`;
79
+ }
80
+ function printMailooSection(section, sourceLabel) {
81
+ const body = section.querySelector(".mailoo-printable-body");
82
+ if (!body) return;
83
+ const printTitle = section.getAttribute("data-print-title")?.trim() || document.title;
84
+ const win = window.open("", "_blank");
85
+ if (!win) return;
86
+ win.opener = null;
87
+ const lang = document.documentElement.lang;
88
+ const source = sourceLabel?.trim() ? buildSourceBlock(sourceLabel.trim()) : "";
89
+ const html = `<!DOCTYPE html><html lang="${escapeHtml(lang)}"><head><meta charset="utf-8"><title>${escapeHtml(printTitle)}</title><style>${PRINT_WINDOW_STYLES}</style></head><body>${body.innerHTML}${source}</body></html>`;
43
90
  win.document.open();
44
91
  win.document.write(html);
45
92
  win.document.close();
46
93
  win.focus();
47
- win.onload = () => {
48
- win.print();
94
+ const triggerPrint = () => {
95
+ try {
96
+ win.print();
97
+ } catch {
98
+ }
49
99
  };
50
- setTimeout(() => {
51
- win.print();
52
- }, 300);
100
+ if (win.document.readyState === "complete") {
101
+ win.setTimeout(triggerPrint, 0);
102
+ } else {
103
+ win.addEventListener("load", triggerPrint, { once: true });
104
+ }
53
105
  }
54
106
  function BlogPrintableButtons({
55
107
  printLabel = "Print summary",
56
- buttonClassName = PRINT_BUTTON_CLASSNAME
108
+ buttonClassName = PRINT_BUTTON_CLASSNAME,
109
+ containerSelector,
110
+ onPrint,
111
+ sourceLabel
57
112
  } = {}) {
113
+ const onPrintRef = useRef(onPrint);
114
+ const sourceLabelRef = useRef(sourceLabel);
115
+ onPrintRef.current = onPrint;
116
+ sourceLabelRef.current = sourceLabel;
58
117
  useEffect(() => {
59
- const sections = document.querySelectorAll(
60
- "[data-mailoo-print]:not([data-mailoo-print-enhanced])"
61
- );
118
+ const root = containerSelector?.trim() ? document.querySelector(containerSelector) : document;
119
+ if (!root) return;
120
+ const sections = root.querySelectorAll(SECTION_SELECTOR);
62
121
  for (const section of sections) {
63
- section.setAttribute("data-mailoo-print-enhanced", "true");
122
+ section.setAttribute(ENHANCED_ATTR, "true");
64
123
  const actions = document.createElement("div");
65
124
  actions.className = "mailoo-printable-actions";
66
125
  const button = document.createElement("button");
@@ -68,12 +127,16 @@ function BlogPrintableButtons({
68
127
  button.className = buttonClassName;
69
128
  button.textContent = printLabel;
70
129
  button.addEventListener("click", () => {
71
- printMailooSection(section);
130
+ const body = section.querySelector(".mailoo-printable-body");
131
+ if (!body) return;
132
+ const printTitle = section.getAttribute("data-print-title")?.trim() || document.title;
133
+ onPrintRef.current?.({ section, printTitle });
134
+ printMailooSection(section, sourceLabelRef.current);
72
135
  });
73
136
  actions.appendChild(button);
74
137
  section.insertBefore(actions, section.firstChild);
75
138
  }
76
- }, [printLabel, buttonClassName]);
139
+ }, [printLabel, buttonClassName, containerSelector]);
77
140
  return null;
78
141
  }
79
142
  export {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/blog-printable-buttons.tsx"],"sourcesContent":["'use client'\n\nimport { useEffect } 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\nfunction escapeHtml(text: string): string {\n return text\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n}\n\nfunction printMailooSection(section: HTMLElement): 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 const win = window.open('', '_blank', 'noopener,noreferrer')\n if (!win) return\n\n const html = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>${escapeHtml(printTitle)}</title>\n<style>\n body {\n font-family: system-ui, -apple-system, sans-serif;\n line-height: 1.6;\n max-width: 48rem;\n margin: 2rem auto;\n padding: 0 1rem;\n color: #111;\n }\n img { max-width: 100%; height: auto; }\n a { color: #2563eb; }\n</style>\n</head>\n<body>\n${body.innerHTML}\n</body>\n</html>`\n\n win.document.open()\n win.document.write(html)\n win.document.close()\n win.focus()\n win.onload = () => {\n win.print()\n }\n setTimeout(() => {\n win.print()\n }, 300)\n}\n\nexport interface BlogPrintableButtonsProps {\n /** Button label (host provides i18n) */\n printLabel?: string\n buttonClassName?: 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}: BlogPrintableButtonsProps = {}) {\n useEffect(() => {\n const sections = document.querySelectorAll<HTMLElement>(\n '[data-mailoo-print]:not([data-mailoo-print-enhanced])'\n )\n\n for (const section of sections) {\n section.setAttribute('data-mailoo-print-enhanced', '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 printMailooSection(section)\n })\n\n actions.appendChild(button)\n section.insertBefore(actions, section.firstChild)\n }\n }, [printLabel, buttonClassName])\n\n return null\n}\n"],"mappings":";;;AAEA,SAAS,iBAAiB;AAE1B,IAAM,yBAAyB;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,EAAE,KAAK,GAAG;AAEV,SAAS,WAAW,MAAsB;AACxC,SAAO,KACJ,QAAQ,MAAM,OAAO,EACrB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,QAAQ;AAC3B;AAEA,SAAS,mBAAmB,SAA4B;AACtD,QAAM,OAAO,QAAQ,cAAc,wBAAwB;AAC3D,MAAI,CAAC,KAAM;AAEX,QAAM,aACJ,QAAQ,aAAa,kBAAkB,GAAG,KAAK,KAAK,SAAS;AAC/D,QAAM,MAAM,OAAO,KAAK,IAAI,UAAU,qBAAqB;AAC3D,MAAI,CAAC,IAAK;AAEV,QAAM,OAAO;AAAA;AAAA;AAAA;AAAA,SAIN,WAAW,UAAU,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAe7B,KAAK,SAAS;AAAA;AAAA;AAId,MAAI,SAAS,KAAK;AAClB,MAAI,SAAS,MAAM,IAAI;AACvB,MAAI,SAAS,MAAM;AACnB,MAAI,MAAM;AACV,MAAI,SAAS,MAAM;AACjB,QAAI,MAAM;AAAA,EACZ;AACA,aAAW,MAAM;AACf,QAAI,MAAM;AAAA,EACZ,GAAG,GAAG;AACR;AAYO,SAAS,qBAAqB;AAAA,EACnC,aAAa;AAAA,EACb,kBAAkB;AACpB,IAA+B,CAAC,GAAG;AACjC,YAAU,MAAM;AACd,UAAM,WAAW,SAAS;AAAA,MACxB;AAAA,IACF;AAEA,eAAW,WAAW,UAAU;AAC9B,cAAQ,aAAa,8BAA8B,MAAM;AAEzD,YAAM,UAAU,SAAS,cAAc,KAAK;AAC5C,cAAQ,YAAY;AAEpB,YAAM,SAAS,SAAS,cAAc,QAAQ;AAC9C,aAAO,OAAO;AACd,aAAO,YAAY;AACnB,aAAO,cAAc;AACrB,aAAO,iBAAiB,SAAS,MAAM;AACrC,2BAAmB,OAAO;AAAA,MAC5B,CAAC;AAED,cAAQ,YAAY,MAAM;AAC1B,cAAQ,aAAa,SAAS,QAAQ,UAAU;AAAA,IAClD;AAAA,EACF,GAAG,CAAC,YAAY,eAAe,CAAC;AAEhC,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../../src/components/blog-printable-buttons.tsx"],"sourcesContent":["'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"],"mappings":"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","names":[]}
@@ -37,12 +37,133 @@ var import_next_core2 = require("@mailoo/next-core");
37
37
  // src/server/client.ts
38
38
  var import_navigation = require("next/navigation");
39
39
  var import_next_core = require("@mailoo/next-core");
40
+
41
+ // src/server/image-rewrite.ts
40
42
  var import_images = require("@mailoo/images");
43
+ function asRecord(value) {
44
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
45
+ return null;
46
+ }
47
+ return value;
48
+ }
49
+ function readEmbedBase(...candidates) {
50
+ for (const candidate of candidates) {
51
+ if (typeof candidate === "string" && candidate.trim()) {
52
+ return candidate.trim();
53
+ }
54
+ }
55
+ return void 0;
56
+ }
57
+ function rewriteBlogPostMedia(post, canonicalPrefixWithIdEquals, proxyPrefixWithIdEquals) {
58
+ return (0, import_images.rewriteMailooBlogMediaUrls)(post, {
59
+ canonicalPrefixWithIdEquals,
60
+ proxyPrefixWithIdEquals
61
+ });
62
+ }
63
+ function rewriteCategoryDescriptionMedia(data, canonicalPrefixWithIdEquals, proxyPrefixWithIdEquals) {
64
+ const opts = {
65
+ canonicalPrefixWithIdEquals,
66
+ proxyPrefixWithIdEquals
67
+ };
68
+ return {
69
+ ...data,
70
+ description: typeof data.description === "string" ? (0, import_images.rewriteMailooPublicImageUrls)(data.description, opts) : data.description,
71
+ descriptionHtml: typeof data.descriptionHtml === "string" ? (0, import_images.rewriteMailooPublicImageUrls)(data.descriptionHtml, opts) : data.descriptionHtml
72
+ };
73
+ }
74
+ function rewriteEmbedUrlsAnyHost(value, proxyPrefix) {
75
+ const rewritten = (0, import_images.rewriteMailooPublicImageUrlsAnyHost)(
76
+ JSON.stringify(value),
77
+ proxyPrefix
78
+ );
79
+ return JSON.parse(rewritten);
80
+ }
81
+ function rewritePostRecord(post, canonical, proxy) {
82
+ return rewriteBlogPostMedia(
83
+ post,
84
+ canonical,
85
+ proxy
86
+ );
87
+ }
88
+ function rewriteBlogListEnvelope(body, proxyPrefix) {
89
+ const record = asRecord(body);
90
+ const canonical = readEmbedBase(record?.imagePublicEmbedBaseUrl);
91
+ if (!record || !canonical) {
92
+ return { ok: false, reason: "missing-embed-base" };
93
+ }
94
+ if (!Array.isArray(record.data)) {
95
+ return { ok: true, body };
96
+ }
97
+ return {
98
+ ok: true,
99
+ body: {
100
+ ...record,
101
+ data: record.data.map((item) => {
102
+ const post = asRecord(item);
103
+ if (!post) return item;
104
+ return rewritePostRecord(post, canonical, proxyPrefix);
105
+ })
106
+ }
107
+ };
108
+ }
109
+ function rewriteBlogPostEnvelope(body, proxyPrefix) {
110
+ const record = asRecord(body);
111
+ const data = asRecord(record?.data);
112
+ const canonical = readEmbedBase(
113
+ data?.imagePublicEmbedBaseUrl,
114
+ record?.imagePublicEmbedBaseUrl
115
+ );
116
+ if (!record || !data || !canonical) {
117
+ return { ok: false, reason: "missing-embed-base" };
118
+ }
119
+ return {
120
+ ok: true,
121
+ body: {
122
+ ...record,
123
+ data: rewritePostRecord(data, canonical, proxyPrefix)
124
+ }
125
+ };
126
+ }
127
+ function rewriteBlogCategoryDescriptionEnvelope(body, proxyPrefix) {
128
+ const record = asRecord(body);
129
+ const data = asRecord(record?.data);
130
+ const canonical = readEmbedBase(
131
+ data?.imagePublicEmbedBaseUrl,
132
+ record?.imagePublicEmbedBaseUrl
133
+ );
134
+ if (!record || !data || !canonical) {
135
+ return { ok: false, reason: "missing-embed-base" };
136
+ }
137
+ return {
138
+ ok: true,
139
+ body: {
140
+ ...record,
141
+ data: rewriteCategoryDescriptionMedia(
142
+ data,
143
+ canonical,
144
+ proxyPrefix
145
+ )
146
+ }
147
+ };
148
+ }
149
+ function rewriteBlogJsonLdEnvelope(body, proxyPrefix) {
150
+ try {
151
+ return {
152
+ ok: true,
153
+ body: rewriteEmbedUrlsAnyHost(body, proxyPrefix)
154
+ };
155
+ } catch {
156
+ return { ok: false, reason: "invalid-json" };
157
+ }
158
+ }
159
+
160
+ // src/server/client.ts
41
161
  function getMailooBlogConfigFromEnv() {
42
162
  return (0, import_next_core.readMailooIntegrationConfig)({ prefix: "MAILOO_BLOG" });
43
163
  }
44
164
 
45
165
  // src/routes/index.ts
166
+ var EMBED_BASE_ERROR = "Blog API contract violation: imagePublicEmbedBaseUrl missing";
46
167
  function resolveConfig(options) {
47
168
  return options?.getConfig?.() ?? getMailooBlogConfigFromEnv();
48
169
  }
@@ -52,7 +173,46 @@ function notConfigured(logger) {
52
173
  );
53
174
  return (0, import_next_core2.jsonError)({ error: "Blog API not configured" }, 503);
54
175
  }
55
- async function proxyGet(config, pathSuffix, searchParams) {
176
+ function imageProxyPrefix(options) {
177
+ const raw = options?.imageProxyPrefixWithIdEquals?.trim();
178
+ return raw || void 0;
179
+ }
180
+ function localeSearchParams(request, options, mode) {
181
+ const source = request.nextUrl.searchParams;
182
+ const params = mode === "all" ? new URLSearchParams(source) : new URLSearchParams();
183
+ if (mode === "locale-only") {
184
+ const fromQuery = source.get("locale");
185
+ if (fromQuery) params.set("locale", fromQuery);
186
+ }
187
+ if (!params.get("locale")) {
188
+ const resolved = options?.resolveLocale?.(request);
189
+ const locale = typeof resolved === "string" ? resolved.trim() : "";
190
+ if (locale) params.set("locale", locale);
191
+ }
192
+ return params;
193
+ }
194
+ function jsonResponse(body, status) {
195
+ return new Response(JSON.stringify(body), {
196
+ status,
197
+ headers: { "Content-Type": "application/json" }
198
+ });
199
+ }
200
+ function rewriteProxiedBody(kind, body, proxyPrefix) {
201
+ switch (kind) {
202
+ case "list":
203
+ return rewriteBlogListEnvelope(body, proxyPrefix);
204
+ case "post":
205
+ return rewriteBlogPostEnvelope(body, proxyPrefix);
206
+ case "category-description":
207
+ return rewriteBlogCategoryDescriptionEnvelope(body, proxyPrefix);
208
+ case "json-ld":
209
+ return rewriteBlogJsonLdEnvelope(body, proxyPrefix);
210
+ case "passthrough":
211
+ return { ok: true, body };
212
+ }
213
+ }
214
+ async function proxyGet(config, pathSuffix, searchParams, options, kind) {
215
+ const logger = options?.logger ?? import_next_core2.defaultMailooLogger;
56
216
  const url = new URL(
57
217
  `${config.apiBaseUrl}/api/v1/blog/${config.projectUid}/integrations/${config.integrationId}${pathSuffix}`
58
218
  );
@@ -69,10 +229,20 @@ async function proxyGet(config, pathSuffix, searchParams) {
69
229
  }
70
230
  });
71
231
  const data = await response.json();
72
- return new Response(JSON.stringify(data), {
73
- status: response.status,
74
- headers: { "Content-Type": "application/json" }
75
- });
232
+ const prefix = imageProxyPrefix(options);
233
+ if (!response.ok || !prefix || kind === "passthrough") {
234
+ return jsonResponse(data, response.status);
235
+ }
236
+ const rewritten = rewriteProxiedBody(kind, data, prefix);
237
+ if (!rewritten.ok) {
238
+ if (rewritten.reason === "missing-embed-base") {
239
+ logger.error(EMBED_BASE_ERROR);
240
+ return (0, import_next_core2.jsonError)({ error: EMBED_BASE_ERROR }, 502);
241
+ }
242
+ logger.error("Blog API JSON-LD rewrite failed");
243
+ return (0, import_next_core2.jsonError)({ error: "Internal server error" }, 500);
244
+ }
245
+ return jsonResponse(rewritten.body, response.status);
76
246
  }
77
247
  function createBlogListHandler(options) {
78
248
  const logger = options?.logger ?? import_next_core2.defaultMailooLogger;
@@ -80,7 +250,13 @@ function createBlogListHandler(options) {
80
250
  const config = resolveConfig(options);
81
251
  if (!config) return notConfigured(logger);
82
252
  try {
83
- return await proxyGet(config, "", request.nextUrl.searchParams);
253
+ return await proxyGet(
254
+ config,
255
+ "",
256
+ localeSearchParams(request, options, "all"),
257
+ options,
258
+ "list"
259
+ );
84
260
  } catch (error) {
85
261
  logger.logException?.(error, "Blog API proxy error");
86
262
  return (0, import_next_core2.jsonError)({ error: "Internal server error" }, 500);
@@ -94,10 +270,13 @@ function createBlogSlugHandler(options) {
94
270
  if (!config) return notConfigured(logger);
95
271
  const { slug } = await ctx.params;
96
272
  try {
97
- const locale = request.nextUrl.searchParams.get("locale");
98
- const qs = new URLSearchParams();
99
- if (locale) qs.set("locale", locale);
100
- return await proxyGet(config, `/slug/${encodeURIComponent(slug)}`, qs);
273
+ return await proxyGet(
274
+ config,
275
+ `/slug/${encodeURIComponent(slug)}`,
276
+ localeSearchParams(request, options, "locale-only"),
277
+ options,
278
+ "post"
279
+ );
101
280
  } catch (error) {
102
281
  logger.logException?.(error, "Blog slug API proxy error");
103
282
  return (0, import_next_core2.jsonError)({ error: "Internal server error" }, 500);
@@ -110,7 +289,13 @@ function createBlogCategoriesHandler(options) {
110
289
  const config = resolveConfig(options);
111
290
  if (!config) return notConfigured(logger);
112
291
  try {
113
- return await proxyGet(config, "/categories", request.nextUrl.searchParams);
292
+ return await proxyGet(
293
+ config,
294
+ "/categories",
295
+ localeSearchParams(request, options, "all"),
296
+ options,
297
+ "passthrough"
298
+ );
114
299
  } catch (error) {
115
300
  logger.logException?.(error, "Blog categories API proxy error");
116
301
  return (0, import_next_core2.jsonError)({ error: "Internal server error" }, 500);
@@ -123,7 +308,13 @@ function createBlogTagsHandler(options) {
123
308
  const config = resolveConfig(options);
124
309
  if (!config) return notConfigured(logger);
125
310
  try {
126
- return await proxyGet(config, "/tags", request.nextUrl.searchParams);
311
+ return await proxyGet(
312
+ config,
313
+ "/tags",
314
+ localeSearchParams(request, options, "all"),
315
+ options,
316
+ "passthrough"
317
+ );
127
318
  } catch (error) {
128
319
  logger.logException?.(error, "Blog tags API proxy error");
129
320
  return (0, import_next_core2.jsonError)({ error: "Internal server error" }, 500);
@@ -140,7 +331,9 @@ function createBlogTagSlugHandler(options) {
140
331
  return await proxyGet(
141
332
  config,
142
333
  `/tags/slug/${encodeURIComponent(slug)}`,
143
- request.nextUrl.searchParams
334
+ localeSearchParams(request, options, "all"),
335
+ options,
336
+ "passthrough"
144
337
  );
145
338
  } catch (error) {
146
339
  logger.logException?.(error, "Blog tag slug API proxy error");
@@ -155,13 +348,12 @@ function createBlogCategoryDescriptionHandler(options) {
155
348
  if (!config) return notConfigured(logger);
156
349
  const { slug } = await ctx.params;
157
350
  try {
158
- const locale = request.nextUrl.searchParams.get("locale");
159
- const qs = new URLSearchParams();
160
- if (locale) qs.set("locale", locale);
161
351
  return await proxyGet(
162
352
  config,
163
353
  `/categories/slug/${encodeURIComponent(slug)}/description`,
164
- qs
354
+ localeSearchParams(request, options, "locale-only"),
355
+ options,
356
+ "category-description"
165
357
  );
166
358
  } catch (error) {
167
359
  logger.logException?.(error, "Blog category description API proxy error");
@@ -176,13 +368,12 @@ function createBlogJsonLdHandler(options) {
176
368
  if (!config) return notConfigured(logger);
177
369
  const { slug } = await ctx.params;
178
370
  try {
179
- const locale = request.nextUrl.searchParams.get("locale");
180
- const qs = new URLSearchParams();
181
- if (locale) qs.set("locale", locale);
182
371
  return await proxyGet(
183
372
  config,
184
373
  `/slug/${encodeURIComponent(slug)}/json-ld`,
185
- qs
374
+ localeSearchParams(request, options, "locale-only"),
375
+ options,
376
+ "json-ld"
186
377
  );
187
378
  } catch (error) {
188
379
  logger.logException?.(error, "Blog JSON-LD API proxy error");