@roottale/cms-client 0.25.0 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -2,346 +2,49 @@ import {
2
2
  parseAttributionJson
3
3
  } from "./chunk-FRCLSU55.js";
4
4
 
5
- // src/seo.ts
6
- function generateRssXml(input) {
7
- const lang = input.language ?? "ko-KR";
8
- const lastBuild = input.items.length > 0 ? toRfc822(input.items[0].pubDate) : "";
9
- const lines = [];
10
- lines.push('<?xml version="1.0" encoding="UTF-8"?>');
11
- lines.push(
12
- '<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">'
13
- );
14
- lines.push("<channel>");
15
- lines.push(`<title>${escapeXml(input.title)}</title>`);
16
- lines.push(`<link>${escapeXml(input.link)}</link>`);
17
- lines.push(
18
- `<atom:link href="${escapeXml(input.feedUrl)}" rel="self" type="application/rss+xml" />`
19
- );
20
- lines.push(
21
- `<description>${escapeXml(input.description ?? input.title)}</description>`
22
- );
23
- lines.push(`<language>${escapeXml(lang)}</language>`);
24
- if (lastBuild) lines.push(`<lastBuildDate>${lastBuild}</lastBuildDate>`);
25
- for (const item of input.items) {
26
- const guid = item.guid ?? item.link;
27
- lines.push("<item>");
28
- lines.push(`<title>${escapeXml(item.title)}</title>`);
29
- lines.push(`<link>${escapeXml(item.link)}</link>`);
30
- lines.push(`<guid isPermaLink="false">${escapeXml(guid)}</guid>`);
31
- if (item.description) {
32
- lines.push(`<description>${cdata(item.description)}</description>`);
33
- }
34
- const pub = toRfc822(item.pubDate);
35
- if (pub) lines.push(`<pubDate>${pub}</pubDate>`);
36
- lines.push("</item>");
37
- }
38
- lines.push("</channel>");
39
- lines.push("</rss>");
40
- return lines.join("\n");
41
- }
42
- function rssItemsFromPosts(posts, opts) {
43
- return posts.map((p) => ({
44
- title: p.title,
45
- link: opts.postUrl(p),
46
- guid: opts.postUrl(p),
47
- description: p.excerpt,
48
- pubDate: p.publishedAt
49
- }));
50
- }
51
- function toRfc822(value) {
52
- if (!value) return "";
53
- const d = value instanceof Date ? value : new Date(value);
54
- return Number.isNaN(d.getTime()) ? "" : d.toUTCString();
55
- }
56
- function articleSchema(input) {
57
- const schema = {
58
- "@context": "https://schema.org",
59
- "@type": "Article",
60
- headline: input.headline,
61
- mainEntityOfPage: { "@type": "WebPage", "@id": input.url }
62
- };
63
- if (input.description) schema.description = input.description;
64
- if (input.image) schema.image = input.image;
65
- const pub = toIso(input.datePublished);
66
- if (pub) schema.datePublished = pub;
67
- const mod = toIso(input.dateModified);
68
- if (mod) schema.dateModified = mod;
69
- if (input.authorName)
70
- schema.author = { "@type": "Person", name: input.authorName };
71
- if (input.publisherName) {
72
- schema.publisher = {
73
- "@type": "Organization",
74
- name: input.publisherName,
75
- ...input.publisherLogo ? { logo: { "@type": "ImageObject", url: input.publisherLogo } } : {}
76
- };
77
- }
78
- return schema;
79
- }
80
- function breadcrumbSchema(items) {
81
- return {
82
- "@context": "https://schema.org",
83
- "@type": "BreadcrumbList",
84
- itemListElement: items.map((it, i) => ({
85
- "@type": "ListItem",
86
- position: i + 1,
87
- name: it.name,
88
- item: it.url
89
- }))
90
- };
91
- }
92
- function organizationSchema(input) {
93
- return {
94
- "@context": "https://schema.org",
95
- "@type": "Organization",
96
- name: input.name,
97
- url: input.url,
98
- ...input.logo ? { logo: input.logo } : {},
99
- ...input.sameAs && input.sameAs.length > 0 ? { sameAs: input.sameAs } : {}
100
- };
101
- }
102
- var DAY_OF_WEEK = {
103
- Mo: "Monday",
104
- Tu: "Tuesday",
105
- We: "Wednesday",
106
- Th: "Thursday",
107
- Fr: "Friday",
108
- Sa: "Saturday",
109
- Su: "Sunday"
110
- };
111
- function localBusinessSchema(profile, opts) {
112
- const input = profile;
113
- const types = Array.from(
114
- /* @__PURE__ */ new Set([input.businessType || "LocalBusiness", "LocalBusiness"])
115
- );
116
- const schema = {
117
- "@context": "https://schema.org",
118
- "@type": types,
119
- name: input.name,
120
- url: opts.url
121
- };
122
- if (input.legalName) schema.legalName = input.legalName;
123
- if (opts.image) schema.image = opts.image;
124
- if (input.telephone) schema.telephone = input.telephone;
125
- if (input.email) schema.email = input.email;
126
- if (input.address) {
127
- schema.address = {
128
- "@type": "PostalAddress",
129
- streetAddress: input.address.streetAddress,
130
- addressLocality: input.address.addressLocality,
131
- ...input.address.addressRegion ? { addressRegion: input.address.addressRegion } : {},
132
- ...input.address.postalCode ? { postalCode: input.address.postalCode } : {},
133
- addressCountry: "KR"
134
- };
135
- }
136
- if (input.geo) {
137
- schema.geo = {
138
- "@type": "GeoCoordinates",
139
- latitude: input.geo.latitude,
140
- longitude: input.geo.longitude
141
- };
142
- }
143
- const openingHours = (input.openingHours ?? []).filter(
144
- (row) => row.days.length > 0 && row.opens && row.closes
145
- );
146
- if (openingHours.length > 0) {
147
- schema.openingHoursSpecification = openingHours.map((row) => ({
148
- "@type": "OpeningHoursSpecification",
149
- dayOfWeek: row.days.map((d) => DAY_OF_WEEK[d] ?? d),
150
- opens: row.opens,
151
- closes: row.closes
152
- }));
153
- }
154
- if (input.priceRange) schema.priceRange = input.priceRange;
155
- if (input.areaServed && input.areaServed.length > 0) {
156
- schema.areaServed = input.areaServed;
157
- }
158
- const sameAs = Object.values(input.profiles ?? {}).filter(
159
- (u) => typeof u === "string" && u.length > 0
160
- );
161
- if (sameAs.length > 0) schema.sameAs = sameAs;
162
- return schema;
163
- }
164
- function websiteSchema(input) {
165
- return {
166
- "@context": "https://schema.org",
167
- "@type": "WebSite",
168
- name: input.name,
169
- url: input.url,
170
- ...input.searchUrl ? {
171
- potentialAction: {
172
- "@type": "SearchAction",
173
- target: `${input.searchUrl}{search_term_string}`,
174
- "query-input": "required name=search_term_string"
175
- }
176
- } : {}
177
- };
178
- }
179
- function faqSchema(qa) {
180
- return {
181
- "@context": "https://schema.org",
182
- "@type": "FAQPage",
183
- mainEntity: qa.map((x) => ({
184
- "@type": "Question",
185
- name: x.question,
186
- acceptedAnswer: { "@type": "Answer", text: x.answer }
187
- }))
188
- };
189
- }
190
- function jsonLdScript(schema) {
191
- const json = JSON.stringify(schema).replace(/</g, "\\u003c");
192
- return `<script type="application/ld+json">${json}</script>`;
193
- }
194
- function toIso(value) {
195
- if (!value) return null;
196
- const d = value instanceof Date ? value : new Date(value);
197
- return Number.isNaN(d.getTime()) ? null : d.toISOString();
198
- }
199
- function escapeXml(value) {
200
- return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
201
- }
202
- function cdata(value) {
203
- return `<![CDATA[${value.replace(/]]>/g, "]]]]><![CDATA[>")}]]>`;
204
- }
205
-
206
- // src/inquiry.ts
207
- var DEFAULT_BASE_URL = "https://api.roottale.com";
208
- var INQUIRY_ERROR_MESSAGES = {
209
- consent_privacy: "\uAC1C\uC778\uC815\uBCF4 \uC218\uC9D1\xB7\uC774\uC6A9 \uB3D9\uC758\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.",
210
- consent_overseas: "\uC758\uB8CC \uBD84\uC57C\uB294 \uAD6D\uC678\uC774\uC804 \uB3D9\uC758\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.",
211
- invalid_vertical: "\uC5C5\uC885 \uBD84\uB958\uAC00 \uC720\uD6A8\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.",
212
- missing_fields: "\uD544\uC218 \uD56D\uBAA9\uC774 \uBE44\uC5B4 \uC788\uC2B5\uB2C8\uB2E4.",
213
- invalid_email: "\uC774\uBA54\uC77C \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.",
214
- invalid_api_key: "\uC5F0\uB3D9 \uC778\uC99D\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4. API \uD0A4\uB97C \uD655\uC778\uD574\uC8FC\uC138\uC694.",
215
- internal: "\uC804\uC1A1\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4. \uC7A0\uC2DC \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694."
216
- };
217
- var RESERVED_INQUIRY_KEYS = /* @__PURE__ */ new Set([
218
- "vertical",
219
- "contact_name",
220
- "business_name",
221
- "email",
222
- "phone",
223
- "message",
224
- "consultation_field",
225
- "current_site_url",
226
- "privacy_consent",
227
- "overseas_transfer_consent",
228
- "lead_kind"
229
- ]);
230
- function inquiryErrorMessage(code) {
231
- return code && INQUIRY_ERROR_MESSAGES[code] || INQUIRY_ERROR_MESSAGES.internal;
232
- }
233
- function parseInquiryRedirect(location, base = DEFAULT_BASE_URL) {
234
- if (!location) {
235
- return { ok: false, code: null, message: inquiryErrorMessage(null) };
236
- }
237
- let url;
238
- try {
239
- url = new URL(location, base);
240
- } catch {
241
- return { ok: false, code: null, message: inquiryErrorMessage(null) };
242
- }
243
- if (url.searchParams.get("ok") === "1") return { ok: true };
244
- const code = url.searchParams.get("err");
245
- return { ok: false, code, message: inquiryErrorMessage(code) };
246
- }
247
- function formatKoreanPhoneNumber(input) {
248
- let digits = input.replace(/\D/g, "");
249
- if (digits.startsWith("82") && digits.length >= 11) {
250
- digits = `0${digits.slice(2)}`;
251
- }
252
- if (digits.length === 11) {
253
- return `${digits.slice(0, 3)}-${digits.slice(3, 7)}-${digits.slice(7)}`;
254
- }
255
- if (digits.length === 10) {
256
- if (digits.startsWith("02")) {
257
- return `${digits.slice(0, 2)}-${digits.slice(2, 6)}-${digits.slice(6)}`;
258
- }
259
- return `${digits.slice(0, 3)}-${digits.slice(3, 6)}-${digits.slice(6)}`;
260
- }
261
- if (digits.length === 8) {
262
- return `${digits.slice(0, 4)}-${digits.slice(4)}`;
263
- }
264
- return input.trim();
265
- }
266
- async function submitInquiry(options) {
267
- assertServer();
268
- const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
269
- const f = options.fields;
270
- const body = new FormData();
271
- body.set("vertical", f.vertical);
272
- body.set("contact_name", f.contactName);
273
- body.set("business_name", f.businessName);
274
- body.set("email", f.email);
275
- body.set("phone", formatKoreanPhoneNumber(f.phone));
276
- if (f.message) body.set("message", f.message);
277
- if (f.consultationField) body.set("consultation_field", f.consultationField);
278
- if (f.currentSiteUrl) body.set("current_site_url", f.currentSiteUrl);
279
- if (f.privacyConsent) body.set("privacy_consent", "on");
280
- if (f.overseasTransferConsent) body.set("overseas_transfer_consent", "on");
281
- if (f.leadKind) body.set("lead_kind", f.leadKind);
282
- if (f.attribution) {
283
- for (const [key, value] of Object.entries(f.attribution)) {
284
- if (typeof value !== "string" || !value.trim()) continue;
285
- body.set(`attr_${key}`, value.trim());
286
- }
287
- }
288
- if (f.extras) {
289
- for (const [rawKey, rawValue] of Object.entries(f.extras)) {
290
- const key = rawKey.trim();
291
- const value = String(rawValue ?? "").trim();
292
- if (!key || !value) continue;
293
- if (key.startsWith("_") || RESERVED_INQUIRY_KEYS.has(key)) continue;
294
- if (key.startsWith("attr_")) continue;
295
- if (body.has(key)) continue;
296
- body.set(key, value);
297
- }
5
+ // src/server-base.ts
6
+ var DEFAULT_BASE_URL = "https://api.roottale.com";
7
+ var CLIENT_VERSION = true ? "0.33.0" : "dev";
8
+ var CMS_CLIENT_VERSION = CLIENT_VERSION;
9
+ function authHeaders(apiKey) {
10
+ return {
11
+ authorization: `Bearer ${apiKey}`,
12
+ "x-roottale-client": `cms-client/${CLIENT_VERSION}`
13
+ };
14
+ }
15
+ var CmsApiError = class extends Error {
16
+ status;
17
+ code;
18
+ constructor(message, status, code) {
19
+ super(message);
20
+ this.name = "CmsApiError";
21
+ this.status = status;
22
+ this.code = code;
298
23
  }
299
- let response;
24
+ };
25
+ async function toApiError(response) {
26
+ let code = "request_failed";
27
+ let message = `RootTale CMS API error: ${response.status}`;
300
28
  try {
301
- response = await fetch(`${baseUrl}/v1/public/inquiries`, {
302
- method: "POST",
303
- headers: { authorization: `Bearer ${options.apiKey}` },
304
- body,
305
- // intake 는 302 로 결과 신호 — 자동 추적 금지하고 Location 직접 읽음.
306
- redirect: "manual",
307
- signal: options.signal
308
- });
29
+ const body = await response.json();
30
+ if (body.code) code = body.code;
31
+ if (body.message) message = body.message;
309
32
  } catch {
310
- return { ok: false, code: "internal", message: inquiryErrorMessage("internal") };
311
- }
312
- if (response.status === 401) {
313
- return {
314
- ok: false,
315
- code: "invalid_api_key",
316
- message: inquiryErrorMessage("invalid_api_key")
317
- };
318
33
  }
319
- const location = response.headers.get("location");
320
- if (location) return parseInquiryRedirect(location, baseUrl);
321
- if (response.ok) return { ok: true };
322
- return { ok: false, code: "internal", message: inquiryErrorMessage("internal") };
34
+ return new CmsApiError(message, response.status, code);
323
35
  }
324
36
  function assertServer() {
325
37
  if (typeof window !== "undefined") {
326
38
  throw new Error(
327
- "@roottale/cms-client submitInquiry must not run in the browser. Call it from a Server Action / Route Handler. Shipping the API key to the browser breaks the security model (ADR-0023 \xA75.1 #15)."
39
+ "@roottale/cms-client/server must not run in the browser. Move this call to a Server Component, getServerSideProps, or a Route Handler. Shipping API keys to the browser breaks the security model (ADR-0023 \xA75.1 #15)."
328
40
  );
329
41
  }
330
42
  }
331
43
 
332
- // src/server.ts
333
- var DEFAULT_BASE_URL2 = "https://api.roottale.com";
334
- var CLIENT_VERSION = true ? "0.25.0" : "dev";
335
- var CMS_CLIENT_VERSION = CLIENT_VERSION;
336
- function authHeaders(apiKey) {
337
- return {
338
- authorization: `Bearer ${apiKey}`,
339
- "x-roottale-client": `cms-client/${CLIENT_VERSION}`
340
- };
341
- }
44
+ // src/server-posts-theme.ts
342
45
  async function fetchTheme(options) {
343
- assertServer2();
344
- const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL2).replace(/\/+$/, "");
46
+ assertServer();
47
+ const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
345
48
  const params = new URLSearchParams();
346
49
  if (options.siteId) params.set("site_id", options.siteId);
347
50
  const qs = params.toString();
@@ -362,8 +65,8 @@ async function fetchTheme(options) {
362
65
  return theme;
363
66
  }
364
67
  async function fetchAnalyticsConfig(options) {
365
- assertServer2();
366
- const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL2).replace(/\/+$/, "");
68
+ assertServer();
69
+ const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
367
70
  const params = new URLSearchParams();
368
71
  if (options.siteId) params.set("site_id", options.siteId);
369
72
  const qs = params.toString();
@@ -379,9 +82,81 @@ async function fetchAnalyticsConfig(options) {
379
82
  const json = await response.json();
380
83
  return { siteId: json.site_id, tags: json.tags ?? [] };
381
84
  }
85
+ async function fetchPosts(options) {
86
+ assertServer();
87
+ const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
88
+ const params = new URLSearchParams();
89
+ if (options.limit !== void 0) params.set("limit", String(options.limit));
90
+ if (options.cursor) params.set("cursor", options.cursor);
91
+ if (options.type) params.set("type", options.type);
92
+ if (options.siteId) params.set("site_id", options.siteId);
93
+ const qs = params.toString();
94
+ const url = `${baseUrl}/v1/cms/public/posts${qs ? `?${qs}` : ""}`;
95
+ const response = await fetch(url, {
96
+ method: "GET",
97
+ headers: authHeaders(options.apiKey),
98
+ signal: options.signal
99
+ });
100
+ if (!response.ok) {
101
+ throw await toApiError(response);
102
+ }
103
+ const json = await response.json();
104
+ return {
105
+ items: json.items.map(fromWire),
106
+ hasMore: json.has_more,
107
+ nextCursor: json.next_cursor
108
+ };
109
+ }
110
+ async function fetchPost(options) {
111
+ assertServer();
112
+ const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
113
+ const params = new URLSearchParams();
114
+ if (options.siteId) params.set("site_id", options.siteId);
115
+ const qs = params.toString();
116
+ const url = `${baseUrl}/v1/cms/public/posts/${encodeURIComponent(options.slugOrId)}${qs ? `?${qs}` : ""}`;
117
+ const response = await fetch(url, {
118
+ method: "GET",
119
+ headers: authHeaders(options.apiKey),
120
+ signal: options.signal
121
+ });
122
+ if (response.status === 404) return null;
123
+ if (!response.ok) {
124
+ throw await toApiError(response);
125
+ }
126
+ const json = await response.json();
127
+ return fromWire(json);
128
+ }
129
+ function fromWire(wire) {
130
+ return {
131
+ id: wire.id,
132
+ tenantId: wire.tenant_id,
133
+ siteId: wire.site_id,
134
+ type: wire.type,
135
+ collectionKey: wire.collection_key ?? null,
136
+ slug: wire.slug,
137
+ title: wire.title,
138
+ excerpt: wire.excerpt,
139
+ featuredMediaId: wire.featured_media_id,
140
+ featuredImageUrl: wire.featured_media_url ?? null,
141
+ authorId: wire.author_id,
142
+ authorName: wire.author_name ?? null,
143
+ authorImageUrl: wire.author_image_url ?? null,
144
+ authorBio: wire.author_bio ?? null,
145
+ bodyJson: wire.body_json,
146
+ status: wire.status,
147
+ metaJson: wire.meta_json,
148
+ fields: wire.fields ?? null,
149
+ terms: wire.terms ?? [],
150
+ publishedAt: wire.published_at,
151
+ createdAt: wire.created_at,
152
+ updatedAt: wire.updated_at
153
+ };
154
+ }
155
+
156
+ // src/server-blog.ts
382
157
  async function fetchBlogSettings(options) {
383
- assertServer2();
384
- const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL2).replace(/\/+$/, "");
158
+ assertServer();
159
+ const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
385
160
  const params = new URLSearchParams();
386
161
  if (options.siteId) params.set("site_id", options.siteId);
387
162
  const qs = params.toString();
@@ -413,6 +188,45 @@ async function fetchBlogSettings(options) {
413
188
  authorProfileImagePositionY: percent(json.author_profile_image_position_y)
414
189
  };
415
190
  }
191
+ async function fetchCollections(options) {
192
+ assertServer();
193
+ const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
194
+ const params = new URLSearchParams();
195
+ if (options.siteId) params.set("site_id", options.siteId);
196
+ const qs = params.toString();
197
+ const url = `${baseUrl}/v1/cms/public/collections${qs ? `?${qs}` : ""}`;
198
+ const response = await fetch(url, {
199
+ method: "GET",
200
+ headers: authHeaders(options.apiKey),
201
+ signal: options.signal
202
+ });
203
+ if (!response.ok) {
204
+ throw await toApiError(response);
205
+ }
206
+ const json = await response.json();
207
+ const items = Array.isArray(json.collections) ? json.collections : [];
208
+ return items.flatMap((o) => {
209
+ const key = typeof o.key === "string" ? o.key : "";
210
+ const basePath = typeof o.base_path === "string" ? o.base_path : "";
211
+ if (!key || !basePath) return [];
212
+ return [
213
+ {
214
+ key,
215
+ basePath,
216
+ label: typeof o.label === "string" ? o.label : key,
217
+ layout: typeof o.layout === "string" ? o.layout : "grid",
218
+ categories: Array.isArray(o.categories) ? o.categories.filter((s) => typeof s === "string") : [],
219
+ feed: o.feed === true,
220
+ archives: o.archives === true,
221
+ og: o.og === true,
222
+ requireSubcategory: o.require_subcategory === true,
223
+ pinning: o.pinning === true,
224
+ icon: typeof o.icon === "string" ? o.icon : null
225
+ }
226
+ ];
227
+ });
228
+ }
229
+ var fetchCollectionsSafeDefault = [];
416
230
  var DEFAULT_BLOG_SETTINGS = {
417
231
  showTableOfContents: false,
418
232
  showAuthor: true,
@@ -428,6 +242,27 @@ var DEFAULT_BLOG_SETTINGS = {
428
242
  authorProfileImagePositionX: 50,
429
243
  authorProfileImagePositionY: 50
430
244
  };
245
+ function resolvePostDisplay(post, settings, explicit) {
246
+ const display = post.metaJson && typeof post.metaJson === "object" ? post.metaJson.display : void 0;
247
+ const ov = (key) => display && typeof display[key] === "boolean" ? display[key] : void 0;
248
+ return {
249
+ showTableOfContents: explicit?.showTableOfContents ?? ov("toc") ?? settings.showTableOfContents,
250
+ showAuthor: ov("author") ?? settings.showAuthor,
251
+ showDate: ov("date") ?? settings.showDate,
252
+ showAuthorCard: settings.showAuthorCard,
253
+ tocTitle: explicit?.tableOfContentsTitle ?? settings.tocTitle ?? "\uBAA9\uCC28",
254
+ authorCardEyebrow: settings.authorCardEyebrow ?? "\uC791\uC131\uC790",
255
+ authorCardDescription: settings.authorCardDescription,
256
+ authorProfileName: settings.authorProfileName,
257
+ authorProfileBio: settings.authorProfileBio,
258
+ authorProfileImageUrl: settings.authorProfileImageUrl,
259
+ authorProfileImageRadius: settings.authorProfileImageRadius,
260
+ authorProfileImagePositionX: settings.authorProfileImagePositionX,
261
+ authorProfileImagePositionY: settings.authorProfileImagePositionY
262
+ };
263
+ }
264
+
265
+ // src/server-business.ts
431
266
  function businessProfileFromWire(json) {
432
267
  if (json.configured === false) return null;
433
268
  if (typeof json.name !== "string" || json.name.length === 0) return null;
@@ -464,13 +299,80 @@ function businessProfileFromWire(json) {
464
299
  }
465
300
  };
466
301
  }
467
- async function fetchBusinessProfile(options) {
468
- assertServer2();
469
- const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL2).replace(/\/+$/, "");
302
+ async function fetchBusinessProfile(options) {
303
+ assertServer();
304
+ const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
305
+ const params = new URLSearchParams();
306
+ if (options.siteId) params.set("site_id", options.siteId);
307
+ const qs = params.toString();
308
+ const url = `${baseUrl}/v1/cms/public/business-profile${qs ? `?${qs}` : ""}`;
309
+ const response = await fetch(url, {
310
+ method: "GET",
311
+ headers: authHeaders(options.apiKey),
312
+ signal: options.signal
313
+ });
314
+ if (response.status === 404) return null;
315
+ if (!response.ok) {
316
+ throw await toApiError(response);
317
+ }
318
+ const json = await response.json();
319
+ return businessProfileFromWire(json);
320
+ }
321
+
322
+ // src/server-discovery.ts
323
+ async function searchPosts(options) {
324
+ assertServer();
325
+ const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
326
+ const params = new URLSearchParams();
327
+ params.set("q", options.query);
328
+ if (options.limit !== void 0) params.set("limit", String(options.limit));
329
+ if (options.type) params.set("type", options.type);
330
+ if (options.siteId) params.set("site_id", options.siteId);
331
+ const url = `${baseUrl}/v1/cms/public/search?${params.toString()}`;
332
+ const response = await fetch(url, {
333
+ method: "GET",
334
+ headers: authHeaders(options.apiKey),
335
+ signal: options.signal
336
+ });
337
+ if (response.status === 404) return [];
338
+ if (!response.ok) {
339
+ throw await toApiError(response);
340
+ }
341
+ const json = await response.json();
342
+ return json.items.map((w) => ({
343
+ id: w.id,
344
+ type: w.type,
345
+ title: w.title,
346
+ slug: w.slug,
347
+ excerpt: w.excerpt,
348
+ featuredImageUrl: w.featured_media_url ?? null,
349
+ publishedAt: w.published_at
350
+ }));
351
+ }
352
+ function menuItemFromWire(w) {
353
+ const item = { id: w.id, label: w.label, url: w.url };
354
+ if (w.new_tab) item.newTab = true;
355
+ if (w.children && w.children.length > 0) {
356
+ item.children = w.children.map(menuItemFromWire);
357
+ }
358
+ return item;
359
+ }
360
+ function menuFromWire(w) {
361
+ return {
362
+ id: w.id,
363
+ name: w.name,
364
+ slug: w.slug,
365
+ items: (w.items ?? []).map(menuItemFromWire),
366
+ updatedAt: w.updated_at ?? null
367
+ };
368
+ }
369
+ async function fetchMenu(options) {
370
+ assertServer();
371
+ const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
470
372
  const params = new URLSearchParams();
471
373
  if (options.siteId) params.set("site_id", options.siteId);
472
374
  const qs = params.toString();
473
- const url = `${baseUrl}/v1/cms/public/business-profile${qs ? `?${qs}` : ""}`;
375
+ const url = `${baseUrl}/v1/cms/public/menus/${encodeURIComponent(options.slug)}${qs ? `?${qs}` : ""}`;
474
376
  const response = await fetch(url, {
475
377
  method: "GET",
476
378
  headers: authHeaders(options.apiKey),
@@ -481,205 +383,351 @@ async function fetchBusinessProfile(options) {
481
383
  throw await toApiError(response);
482
384
  }
483
385
  const json = await response.json();
484
- return businessProfileFromWire(json);
386
+ return menuFromWire(json);
485
387
  }
486
- function resolvePostDisplay(post, settings, explicit) {
487
- const display = post.metaJson && typeof post.metaJson === "object" ? post.metaJson.display : void 0;
488
- const ov = (key) => display && typeof display[key] === "boolean" ? display[key] : void 0;
388
+ async function fetchMenus(options) {
389
+ assertServer();
390
+ const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
391
+ const params = new URLSearchParams();
392
+ if (options.siteId) params.set("site_id", options.siteId);
393
+ const qs = params.toString();
394
+ const url = `${baseUrl}/v1/cms/public/menus${qs ? `?${qs}` : ""}`;
395
+ const response = await fetch(url, {
396
+ method: "GET",
397
+ headers: authHeaders(options.apiKey),
398
+ signal: options.signal
399
+ });
400
+ if (response.status === 404) return [];
401
+ if (!response.ok) {
402
+ throw await toApiError(response);
403
+ }
404
+ const json = await response.json();
405
+ return (json.items ?? []).map(menuFromWire);
406
+ }
407
+
408
+ // src/seo.ts
409
+ function generateRssXml(input) {
410
+ const lang = input.language ?? "ko-KR";
411
+ const lastBuild = input.items.length > 0 ? toRfc822(input.items[0].pubDate) : "";
412
+ const lines = [];
413
+ lines.push('<?xml version="1.0" encoding="UTF-8"?>');
414
+ lines.push(
415
+ '<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">'
416
+ );
417
+ lines.push("<channel>");
418
+ lines.push(`<title>${escapeXml(input.title)}</title>`);
419
+ lines.push(`<link>${escapeXml(input.link)}</link>`);
420
+ lines.push(
421
+ `<atom:link href="${escapeXml(input.feedUrl)}" rel="self" type="application/rss+xml" />`
422
+ );
423
+ lines.push(
424
+ `<description>${escapeXml(input.description ?? input.title)}</description>`
425
+ );
426
+ lines.push(`<language>${escapeXml(lang)}</language>`);
427
+ if (lastBuild) lines.push(`<lastBuildDate>${lastBuild}</lastBuildDate>`);
428
+ for (const item of input.items) {
429
+ const guid = item.guid ?? item.link;
430
+ lines.push("<item>");
431
+ lines.push(`<title>${escapeXml(item.title)}</title>`);
432
+ lines.push(`<link>${escapeXml(item.link)}</link>`);
433
+ lines.push(`<guid isPermaLink="false">${escapeXml(guid)}</guid>`);
434
+ if (item.description) {
435
+ lines.push(`<description>${cdata(item.description)}</description>`);
436
+ }
437
+ const pub = toRfc822(item.pubDate);
438
+ if (pub) lines.push(`<pubDate>${pub}</pubDate>`);
439
+ lines.push("</item>");
440
+ }
441
+ lines.push("</channel>");
442
+ lines.push("</rss>");
443
+ return lines.join("\n");
444
+ }
445
+ function rssItemsFromPosts(posts, opts) {
446
+ return posts.map((p) => ({
447
+ title: p.title,
448
+ link: opts.postUrl(p),
449
+ guid: opts.postUrl(p),
450
+ description: p.excerpt,
451
+ pubDate: p.publishedAt
452
+ }));
453
+ }
454
+ function toRfc822(value) {
455
+ if (!value) return "";
456
+ const d = value instanceof Date ? value : new Date(value);
457
+ return Number.isNaN(d.getTime()) ? "" : d.toUTCString();
458
+ }
459
+ function articleSchema(input) {
460
+ const schema = {
461
+ "@context": "https://schema.org",
462
+ "@type": "Article",
463
+ headline: input.headline,
464
+ mainEntityOfPage: { "@type": "WebPage", "@id": input.url }
465
+ };
466
+ if (input.description) schema.description = input.description;
467
+ if (input.image) schema.image = input.image;
468
+ const pub = toIso(input.datePublished);
469
+ if (pub) schema.datePublished = pub;
470
+ const mod = toIso(input.dateModified);
471
+ if (mod) schema.dateModified = mod;
472
+ if (input.authorName)
473
+ schema.author = { "@type": "Person", name: input.authorName };
474
+ if (input.publisherName) {
475
+ schema.publisher = {
476
+ "@type": "Organization",
477
+ name: input.publisherName,
478
+ ...input.publisherLogo ? { logo: { "@type": "ImageObject", url: input.publisherLogo } } : {}
479
+ };
480
+ }
481
+ return schema;
482
+ }
483
+ function breadcrumbSchema(items) {
489
484
  return {
490
- showTableOfContents: explicit?.showTableOfContents ?? ov("toc") ?? settings.showTableOfContents,
491
- showAuthor: ov("author") ?? settings.showAuthor,
492
- showDate: ov("date") ?? settings.showDate,
493
- showAuthorCard: settings.showAuthorCard,
494
- tocTitle: explicit?.tableOfContentsTitle ?? settings.tocTitle ?? "\uBAA9\uCC28",
495
- authorCardEyebrow: settings.authorCardEyebrow ?? "\uC791\uC131\uC790",
496
- authorCardDescription: settings.authorCardDescription,
497
- authorProfileName: settings.authorProfileName,
498
- authorProfileBio: settings.authorProfileBio,
499
- authorProfileImageUrl: settings.authorProfileImageUrl,
500
- authorProfileImageRadius: settings.authorProfileImageRadius,
501
- authorProfileImagePositionX: settings.authorProfileImagePositionX,
502
- authorProfileImagePositionY: settings.authorProfileImagePositionY
485
+ "@context": "https://schema.org",
486
+ "@type": "BreadcrumbList",
487
+ itemListElement: items.map((it, i) => ({
488
+ "@type": "ListItem",
489
+ position: i + 1,
490
+ name: it.name,
491
+ item: it.url
492
+ }))
493
+ };
494
+ }
495
+ function organizationSchema(input) {
496
+ return {
497
+ "@context": "https://schema.org",
498
+ "@type": "Organization",
499
+ name: input.name,
500
+ url: input.url,
501
+ ...input.logo ? { logo: input.logo } : {},
502
+ ...input.sameAs && input.sameAs.length > 0 ? { sameAs: input.sameAs } : {}
503
+ };
504
+ }
505
+ var DAY_OF_WEEK = {
506
+ Mo: "Monday",
507
+ Tu: "Tuesday",
508
+ We: "Wednesday",
509
+ Th: "Thursday",
510
+ Fr: "Friday",
511
+ Sa: "Saturday",
512
+ Su: "Sunday"
513
+ };
514
+ function localBusinessSchema(profile, opts) {
515
+ const input = profile;
516
+ const types = Array.from(
517
+ /* @__PURE__ */ new Set([input.businessType || "LocalBusiness", "LocalBusiness"])
518
+ );
519
+ const schema = {
520
+ "@context": "https://schema.org",
521
+ "@type": types,
522
+ name: input.name,
523
+ url: opts.url
524
+ };
525
+ if (input.legalName) schema.legalName = input.legalName;
526
+ if (opts.image) schema.image = opts.image;
527
+ if (input.telephone) schema.telephone = input.telephone;
528
+ if (input.email) schema.email = input.email;
529
+ if (input.address) {
530
+ schema.address = {
531
+ "@type": "PostalAddress",
532
+ streetAddress: input.address.streetAddress,
533
+ addressLocality: input.address.addressLocality,
534
+ ...input.address.addressRegion ? { addressRegion: input.address.addressRegion } : {},
535
+ ...input.address.postalCode ? { postalCode: input.address.postalCode } : {},
536
+ addressCountry: "KR"
537
+ };
538
+ }
539
+ if (input.geo) {
540
+ schema.geo = {
541
+ "@type": "GeoCoordinates",
542
+ latitude: input.geo.latitude,
543
+ longitude: input.geo.longitude
544
+ };
545
+ }
546
+ const openingHours = (input.openingHours ?? []).filter(
547
+ (row) => row.days.length > 0 && row.opens && row.closes
548
+ );
549
+ if (openingHours.length > 0) {
550
+ schema.openingHoursSpecification = openingHours.map((row) => ({
551
+ "@type": "OpeningHoursSpecification",
552
+ dayOfWeek: row.days.map((d) => DAY_OF_WEEK[d] ?? d),
553
+ opens: row.opens,
554
+ closes: row.closes
555
+ }));
556
+ }
557
+ if (input.priceRange) schema.priceRange = input.priceRange;
558
+ if (input.areaServed && input.areaServed.length > 0) {
559
+ schema.areaServed = input.areaServed;
560
+ }
561
+ const sameAs = Object.values(input.profiles ?? {}).filter(
562
+ (u) => typeof u === "string" && u.length > 0
563
+ );
564
+ if (sameAs.length > 0) schema.sameAs = sameAs;
565
+ return schema;
566
+ }
567
+ function websiteSchema(input) {
568
+ return {
569
+ "@context": "https://schema.org",
570
+ "@type": "WebSite",
571
+ name: input.name,
572
+ url: input.url,
573
+ ...input.searchUrl ? {
574
+ potentialAction: {
575
+ "@type": "SearchAction",
576
+ target: `${input.searchUrl}{search_term_string}`,
577
+ "query-input": "required name=search_term_string"
578
+ }
579
+ } : {}
580
+ };
581
+ }
582
+ function faqSchema(qa) {
583
+ return {
584
+ "@context": "https://schema.org",
585
+ "@type": "FAQPage",
586
+ mainEntity: qa.map((x) => ({
587
+ "@type": "Question",
588
+ name: x.question,
589
+ acceptedAnswer: { "@type": "Answer", text: x.answer }
590
+ }))
503
591
  };
504
592
  }
505
- var CmsApiError = class extends Error {
506
- status;
507
- code;
508
- constructor(message, status, code) {
509
- super(message);
510
- this.name = "CmsApiError";
511
- this.status = status;
512
- this.code = code;
593
+ function jsonLdScript(schema) {
594
+ const json = JSON.stringify(schema).replace(/</g, "\\u003c");
595
+ return `<script type="application/ld+json">${json}</script>`;
596
+ }
597
+ function toIso(value) {
598
+ if (!value) return null;
599
+ const d = value instanceof Date ? value : new Date(value);
600
+ return Number.isNaN(d.getTime()) ? null : d.toISOString();
601
+ }
602
+ function escapeXml(value) {
603
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
604
+ }
605
+ function cdata(value) {
606
+ return `<![CDATA[${value.replace(/]]>/g, "]]]]><![CDATA[>")}]]>`;
607
+ }
608
+
609
+ // src/inquiry.ts
610
+ var DEFAULT_BASE_URL2 = "https://api.roottale.com";
611
+ var INQUIRY_ERROR_MESSAGES = {
612
+ consent_privacy: "\uAC1C\uC778\uC815\uBCF4 \uC218\uC9D1\xB7\uC774\uC6A9 \uB3D9\uC758\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.",
613
+ consent_overseas: "\uC758\uB8CC \uBD84\uC57C\uB294 \uAD6D\uC678\uC774\uC804 \uB3D9\uC758\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.",
614
+ invalid_vertical: "\uC5C5\uC885 \uBD84\uB958\uAC00 \uC720\uD6A8\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.",
615
+ missing_fields: "\uD544\uC218 \uD56D\uBAA9\uC774 \uBE44\uC5B4 \uC788\uC2B5\uB2C8\uB2E4.",
616
+ invalid_email: "\uC774\uBA54\uC77C \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.",
617
+ invalid_api_key: "\uC5F0\uB3D9 \uC778\uC99D\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4. API \uD0A4\uB97C \uD655\uC778\uD574\uC8FC\uC138\uC694.",
618
+ internal: "\uC804\uC1A1\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4. \uC7A0\uC2DC \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694."
619
+ };
620
+ var RESERVED_INQUIRY_KEYS = /* @__PURE__ */ new Set([
621
+ "vertical",
622
+ "contact_name",
623
+ "business_name",
624
+ "email",
625
+ "phone",
626
+ "message",
627
+ "consultation_field",
628
+ "current_site_url",
629
+ "privacy_consent",
630
+ "overseas_transfer_consent",
631
+ "lead_kind"
632
+ ]);
633
+ function inquiryErrorMessage(code) {
634
+ return code && INQUIRY_ERROR_MESSAGES[code] || INQUIRY_ERROR_MESSAGES.internal;
635
+ }
636
+ function parseInquiryRedirect(location, base = DEFAULT_BASE_URL2) {
637
+ if (!location) {
638
+ return { ok: false, code: null, message: inquiryErrorMessage(null) };
513
639
  }
514
- };
515
- async function fetchPosts(options) {
516
- assertServer2();
517
- const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL2).replace(/\/+$/, "");
518
- const params = new URLSearchParams();
519
- if (options.limit !== void 0) params.set("limit", String(options.limit));
520
- if (options.cursor) params.set("cursor", options.cursor);
521
- if (options.type) params.set("type", options.type);
522
- if (options.siteId) params.set("site_id", options.siteId);
523
- const qs = params.toString();
524
- const url = `${baseUrl}/v1/cms/public/posts${qs ? `?${qs}` : ""}`;
525
- const response = await fetch(url, {
526
- method: "GET",
527
- headers: authHeaders(options.apiKey),
528
- signal: options.signal
529
- });
530
- if (!response.ok) {
531
- throw await toApiError(response);
640
+ let url;
641
+ try {
642
+ url = new URL(location, base);
643
+ } catch {
644
+ return { ok: false, code: null, message: inquiryErrorMessage(null) };
532
645
  }
533
- const json = await response.json();
534
- return {
535
- items: json.items.map(fromWire),
536
- hasMore: json.has_more,
537
- nextCursor: json.next_cursor
538
- };
646
+ if (url.searchParams.get("ok") === "1") return { ok: true };
647
+ const code = url.searchParams.get("err");
648
+ return { ok: false, code, message: inquiryErrorMessage(code) };
539
649
  }
540
- async function fetchPost(options) {
541
- assertServer2();
542
- const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL2).replace(/\/+$/, "");
543
- const params = new URLSearchParams();
544
- if (options.siteId) params.set("site_id", options.siteId);
545
- const qs = params.toString();
546
- const url = `${baseUrl}/v1/cms/public/posts/${encodeURIComponent(options.slugOrId)}${qs ? `?${qs}` : ""}`;
547
- const response = await fetch(url, {
548
- method: "GET",
549
- headers: authHeaders(options.apiKey),
550
- signal: options.signal
551
- });
552
- if (response.status === 404) return null;
553
- if (!response.ok) {
554
- throw await toApiError(response);
650
+ function formatKoreanPhoneNumber(input) {
651
+ let digits = input.replace(/\D/g, "");
652
+ if (digits.startsWith("82") && digits.length >= 11) {
653
+ digits = `0${digits.slice(2)}`;
555
654
  }
556
- const json = await response.json();
557
- return fromWire(json);
558
- }
559
- async function searchPosts(options) {
560
- assertServer2();
561
- const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL2).replace(/\/+$/, "");
562
- const params = new URLSearchParams();
563
- params.set("q", options.query);
564
- if (options.limit !== void 0) params.set("limit", String(options.limit));
565
- if (options.type) params.set("type", options.type);
566
- if (options.siteId) params.set("site_id", options.siteId);
567
- const url = `${baseUrl}/v1/cms/public/search?${params.toString()}`;
568
- const response = await fetch(url, {
569
- method: "GET",
570
- headers: authHeaders(options.apiKey),
571
- signal: options.signal
572
- });
573
- if (response.status === 404) return [];
574
- if (!response.ok) {
575
- throw await toApiError(response);
655
+ if (digits.length === 11) {
656
+ return `${digits.slice(0, 3)}-${digits.slice(3, 7)}-${digits.slice(7)}`;
576
657
  }
577
- const json = await response.json();
578
- return json.items.map((w) => ({
579
- id: w.id,
580
- type: w.type,
581
- title: w.title,
582
- slug: w.slug,
583
- excerpt: w.excerpt,
584
- featuredImageUrl: w.featured_media_url ?? null,
585
- publishedAt: w.published_at
586
- }));
587
- }
588
- function menuItemFromWire(w) {
589
- const item = { id: w.id, label: w.label, url: w.url };
590
- if (w.new_tab) item.newTab = true;
591
- if (w.children && w.children.length > 0) {
592
- item.children = w.children.map(menuItemFromWire);
658
+ if (digits.length === 10) {
659
+ if (digits.startsWith("02")) {
660
+ return `${digits.slice(0, 2)}-${digits.slice(2, 6)}-${digits.slice(6)}`;
661
+ }
662
+ return `${digits.slice(0, 3)}-${digits.slice(3, 6)}-${digits.slice(6)}`;
593
663
  }
594
- return item;
595
- }
596
- function menuFromWire(w) {
597
- return {
598
- id: w.id,
599
- name: w.name,
600
- slug: w.slug,
601
- items: (w.items ?? []).map(menuItemFromWire),
602
- updatedAt: w.updated_at ?? null
603
- };
604
- }
605
- async function fetchMenu(options) {
606
- assertServer2();
607
- const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL2).replace(/\/+$/, "");
608
- const params = new URLSearchParams();
609
- if (options.siteId) params.set("site_id", options.siteId);
610
- const qs = params.toString();
611
- const url = `${baseUrl}/v1/cms/public/menus/${encodeURIComponent(options.slug)}${qs ? `?${qs}` : ""}`;
612
- const response = await fetch(url, {
613
- method: "GET",
614
- headers: authHeaders(options.apiKey),
615
- signal: options.signal
616
- });
617
- if (response.status === 404) return null;
618
- if (!response.ok) {
619
- throw await toApiError(response);
664
+ if (digits.length === 8) {
665
+ return `${digits.slice(0, 4)}-${digits.slice(4)}`;
620
666
  }
621
- const json = await response.json();
622
- return menuFromWire(json);
667
+ return input.trim();
623
668
  }
624
- async function fetchMenus(options) {
669
+ async function submitInquiry(options) {
625
670
  assertServer2();
626
671
  const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL2).replace(/\/+$/, "");
627
- const params = new URLSearchParams();
628
- if (options.siteId) params.set("site_id", options.siteId);
629
- const qs = params.toString();
630
- const url = `${baseUrl}/v1/cms/public/menus${qs ? `?${qs}` : ""}`;
631
- const response = await fetch(url, {
632
- method: "GET",
633
- headers: authHeaders(options.apiKey),
634
- signal: options.signal
635
- });
636
- if (response.status === 404) return [];
637
- if (!response.ok) {
638
- throw await toApiError(response);
672
+ const f = options.fields;
673
+ const body = new FormData();
674
+ body.set("vertical", f.vertical);
675
+ body.set("contact_name", f.contactName);
676
+ body.set("business_name", f.businessName);
677
+ body.set("email", f.email);
678
+ body.set("phone", formatKoreanPhoneNumber(f.phone));
679
+ if (f.message) body.set("message", f.message);
680
+ if (f.consultationField) body.set("consultation_field", f.consultationField);
681
+ if (f.currentSiteUrl) body.set("current_site_url", f.currentSiteUrl);
682
+ if (f.privacyConsent) body.set("privacy_consent", "on");
683
+ if (f.overseasTransferConsent) body.set("overseas_transfer_consent", "on");
684
+ if (f.leadKind) body.set("lead_kind", f.leadKind);
685
+ if (f.attribution) {
686
+ for (const [key, value] of Object.entries(f.attribution)) {
687
+ if (typeof value !== "string" || !value.trim()) continue;
688
+ body.set(`attr_${key}`, value.trim());
689
+ }
639
690
  }
640
- const json = await response.json();
641
- return (json.items ?? []).map(menuFromWire);
642
- }
643
- function fromWire(wire) {
644
- return {
645
- id: wire.id,
646
- tenantId: wire.tenant_id,
647
- siteId: wire.site_id,
648
- type: wire.type,
649
- slug: wire.slug,
650
- title: wire.title,
651
- excerpt: wire.excerpt,
652
- featuredMediaId: wire.featured_media_id,
653
- featuredImageUrl: wire.featured_media_url ?? null,
654
- authorId: wire.author_id,
655
- authorName: wire.author_name ?? null,
656
- authorImageUrl: wire.author_image_url ?? null,
657
- authorBio: wire.author_bio ?? null,
658
- bodyJson: wire.body_json,
659
- status: wire.status,
660
- metaJson: wire.meta_json,
661
- fields: wire.fields ?? null,
662
- terms: wire.terms ?? [],
663
- publishedAt: wire.published_at,
664
- createdAt: wire.created_at,
665
- updatedAt: wire.updated_at
666
- };
667
- }
668
- async function toApiError(response) {
669
- let code = "request_failed";
670
- let message = `RootTale CMS API error: ${response.status}`;
691
+ if (f.extras) {
692
+ for (const [rawKey, rawValue] of Object.entries(f.extras)) {
693
+ const key = rawKey.trim();
694
+ const value = String(rawValue ?? "").trim();
695
+ if (!key || !value) continue;
696
+ if (key.startsWith("_") || RESERVED_INQUIRY_KEYS.has(key)) continue;
697
+ if (key.startsWith("attr_")) continue;
698
+ if (body.has(key)) continue;
699
+ body.set(key, value);
700
+ }
701
+ }
702
+ let response;
671
703
  try {
672
- const body = await response.json();
673
- if (body.code) code = body.code;
674
- if (body.message) message = body.message;
704
+ response = await fetch(`${baseUrl}/v1/public/inquiries`, {
705
+ method: "POST",
706
+ headers: { authorization: `Bearer ${options.apiKey}` },
707
+ body,
708
+ // intake 는 302 로 결과 신호 — 자동 추적 금지하고 Location 직접 읽음.
709
+ redirect: "manual",
710
+ signal: options.signal
711
+ });
675
712
  } catch {
713
+ return { ok: false, code: "internal", message: inquiryErrorMessage("internal") };
676
714
  }
677
- return new CmsApiError(message, response.status, code);
715
+ if (response.status === 401) {
716
+ return {
717
+ ok: false,
718
+ code: "invalid_api_key",
719
+ message: inquiryErrorMessage("invalid_api_key")
720
+ };
721
+ }
722
+ const location = response.headers.get("location");
723
+ if (location) return parseInquiryRedirect(location, baseUrl);
724
+ if (response.ok) return { ok: true };
725
+ return { ok: false, code: "internal", message: inquiryErrorMessage("internal") };
678
726
  }
679
727
  function assertServer2() {
680
728
  if (typeof window !== "undefined") {
681
729
  throw new Error(
682
- "@roottale/cms-client/server must not run in the browser. Move this call to a Server Component, getServerSideProps, or a Route Handler. Shipping API keys to the browser breaks the security model (ADR-0023 \xA75.1 #15)."
730
+ "@roottale/cms-client submitInquiry must not run in the browser. Call it from a Server Action / Route Handler. Shipping the API key to the browser breaks the security model (ADR-0023 \xA75.1 #15)."
683
731
  );
684
732
  }
685
733
  }
@@ -695,6 +743,8 @@ export {
695
743
  fetchAnalyticsConfig,
696
744
  fetchBlogSettings,
697
745
  fetchBusinessProfile,
746
+ fetchCollections,
747
+ fetchCollectionsSafeDefault,
698
748
  fetchMenu,
699
749
  fetchMenus,
700
750
  fetchPost,