@leadcms/sdk 1.3.0-pre → 2.1.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.
Files changed (56) hide show
  1. package/README.md +82 -3
  2. package/dist/cli/index.js +55 -62
  3. package/dist/cli/index.js.map +1 -1
  4. package/dist/index.d.ts +2 -2
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +2 -18
  7. package/dist/index.js.map +1 -1
  8. package/dist/lib/cms.d.ts +1 -1
  9. package/dist/lib/cms.d.ts.map +1 -1
  10. package/dist/lib/cms.js +49 -72
  11. package/dist/lib/cms.js.map +1 -1
  12. package/dist/lib/config.d.ts +0 -8
  13. package/dist/lib/config.d.ts.map +1 -1
  14. package/dist/lib/config.js +13 -40
  15. package/dist/lib/config.js.map +1 -1
  16. package/dist/lib/console-colors.d.ts +49 -0
  17. package/dist/lib/console-colors.d.ts.map +1 -0
  18. package/dist/lib/console-colors.js +121 -0
  19. package/dist/lib/console-colors.js.map +1 -0
  20. package/dist/lib/content-transformation.d.ts +90 -0
  21. package/dist/lib/content-transformation.d.ts.map +1 -0
  22. package/dist/lib/content-transformation.js +335 -0
  23. package/dist/lib/content-transformation.js.map +1 -0
  24. package/dist/lib/data-service.d.ts +97 -0
  25. package/dist/lib/data-service.d.ts.map +1 -0
  26. package/dist/lib/data-service.js +389 -0
  27. package/dist/lib/data-service.js.map +1 -0
  28. package/dist/scripts/fetch-leadcms-content.d.ts +19 -0
  29. package/dist/scripts/fetch-leadcms-content.d.ts.map +1 -0
  30. package/dist/scripts/fetch-leadcms-content.js +301 -0
  31. package/dist/scripts/fetch-leadcms-content.js.map +1 -0
  32. package/dist/scripts/generate-env-js.d.ts +2 -0
  33. package/dist/scripts/generate-env-js.d.ts.map +1 -0
  34. package/dist/scripts/generate-env-js.js +22 -0
  35. package/dist/scripts/generate-env-js.js.map +1 -0
  36. package/dist/scripts/leadcms-helpers.d.ts +25 -0
  37. package/dist/scripts/leadcms-helpers.d.ts.map +1 -0
  38. package/dist/scripts/leadcms-helpers.js +78 -0
  39. package/dist/scripts/leadcms-helpers.js.map +1 -0
  40. package/dist/scripts/push-leadcms-content.d.ts +50 -0
  41. package/dist/scripts/push-leadcms-content.d.ts.map +1 -0
  42. package/dist/scripts/push-leadcms-content.js +1022 -0
  43. package/dist/scripts/push-leadcms-content.js.map +1 -0
  44. package/dist/scripts/sse-watcher.d.ts +20 -0
  45. package/dist/scripts/sse-watcher.d.ts.map +1 -0
  46. package/dist/scripts/sse-watcher.js +268 -0
  47. package/dist/scripts/sse-watcher.js.map +1 -0
  48. package/dist/scripts/status-leadcms-content.d.ts +4 -0
  49. package/dist/scripts/status-leadcms-content.d.ts.map +1 -0
  50. package/dist/scripts/status-leadcms-content.js +36 -0
  51. package/dist/scripts/status-leadcms-content.js.map +1 -0
  52. package/package.json +14 -12
  53. package/dist/scripts/fetch-leadcms-content.mjs +0 -367
  54. package/dist/scripts/generate-env-js.mjs +0 -24
  55. package/dist/scripts/leadcms-helpers.mjs +0 -208
  56. package/dist/scripts/sse-watcher.mjs +0 -300
@@ -1,367 +0,0 @@
1
- import "dotenv/config"
2
- import fs from "fs/promises"
3
- import path from "path"
4
- import axios from "axios"
5
- import {
6
- extractMediaUrlsFromContent,
7
- downloadMediaFileDirect,
8
- saveContentFile,
9
- leadCMSUrl,
10
- leadCMSApiKey,
11
- defaultLanguage,
12
- CONTENT_DIR,
13
- MEDIA_DIR,
14
- fetchContentTypes,
15
- } from "./leadcms-helpers.mjs"
16
-
17
- // Add axios request/response interceptors for debugging
18
- axios.interceptors.request.use(
19
- (config) => {
20
- console.log(`[AXIOS REQUEST] ${config.method?.toUpperCase()} ${config.url}`)
21
-
22
- // Mask the Authorization header for security
23
- const maskedHeaders = { ...config.headers }
24
- if (maskedHeaders.Authorization && typeof maskedHeaders.Authorization === "string") {
25
- const authParts = maskedHeaders.Authorization.split(" ")
26
- if (authParts.length === 2 && authParts[0] === "Bearer") {
27
- maskedHeaders.Authorization = `Bearer ${authParts[1].substring(0, 8)}...`
28
- }
29
- }
30
-
31
- return config
32
- },
33
- (error) => {
34
- console.error(`[AXIOS REQUEST ERROR]`, error)
35
- return Promise.reject(error)
36
- }
37
- )
38
-
39
- axios.interceptors.response.use(
40
- (response) => {
41
- return response
42
- },
43
- (error) => {
44
- console.error(
45
- `[AXIOS RESPONSE ERROR] ${error.response?.status || "NO_STATUS"} ${error.response?.statusText || "NO_STATUS_TEXT"} for ${error.config?.url || "NO_URL"}`
46
- )
47
- if (error.response) {
48
- console.error(`[AXIOS RESPONSE ERROR] Response data:`, error.response.data)
49
- console.error(
50
- `[AXIOS RESPONSE ERROR] Response headers:`,
51
- JSON.stringify(error.response.headers, null, 2)
52
- )
53
- }
54
- console.error(`[AXIOS RESPONSE ERROR] Full error:`, error.message)
55
- return Promise.reject(error)
56
- }
57
- )
58
-
59
- const SYNC_TOKEN_PATH = path.resolve(".leadcms/sync-token.txt")
60
- const MEDIA_SYNC_TOKEN_PATH = path.resolve(".leadcms/media-sync-token.txt")
61
-
62
- async function readSyncToken() {
63
- try {
64
- return (await fs.readFile(SYNC_TOKEN_PATH, "utf8")).trim()
65
- } catch {
66
- return undefined
67
- }
68
- }
69
-
70
- async function writeSyncToken(token) {
71
- await fs.mkdir(path.dirname(SYNC_TOKEN_PATH), { recursive: true })
72
- await fs.writeFile(SYNC_TOKEN_PATH, token, "utf8")
73
- }
74
-
75
- async function readMediaSyncToken() {
76
- try {
77
- return (await fs.readFile(MEDIA_SYNC_TOKEN_PATH, "utf8")).trim()
78
- } catch {
79
- return undefined
80
- }
81
- }
82
-
83
- async function writeMediaSyncToken(token) {
84
- await fs.mkdir(path.dirname(MEDIA_SYNC_TOKEN_PATH), { recursive: true })
85
- await fs.writeFile(MEDIA_SYNC_TOKEN_PATH, token, "utf8")
86
- }
87
-
88
- async function fetchContentSync(syncToken) {
89
- console.log(`[FETCH_CONTENT_SYNC] Starting with syncToken: ${syncToken || "NONE"}`)
90
- let allItems = []
91
- let allDeleted = []
92
- let token = syncToken || ""
93
- let nextSyncToken = undefined
94
- let page = 0
95
-
96
- while (true) {
97
- const url = new URL("/api/content/sync", leadCMSUrl)
98
- url.searchParams.set("filter[limit]", "100")
99
- url.searchParams.set("syncToken", token)
100
-
101
- console.log(`[FETCH_CONTENT_SYNC] Page ${page}, URL: ${url.toString()}`)
102
-
103
- try {
104
- const res = await axios.get(url.toString(), {
105
- headers: { Authorization: `Bearer ${leadCMSApiKey}` },
106
- })
107
-
108
- if (res.status === 204) {
109
- console.log(`[FETCH_CONTENT_SYNC] Got 204 No Content - ending sync`)
110
- break
111
- }
112
-
113
- const data = res.data
114
- console.log(
115
- `[FETCH_CONTENT_SYNC] Page ${page} - Got ${data.items?.length || 0} items, ${data.deleted?.length || 0} deleted`
116
- )
117
-
118
- if (data.items && Array.isArray(data.items)) allItems.push(...data.items)
119
-
120
- if (data.deleted && Array.isArray(data.deleted)) {
121
- allDeleted.push(...data.deleted)
122
- }
123
-
124
- const newSyncToken = res.headers["x-next-sync-token"] || token
125
- console.log(`[FETCH_CONTENT_SYNC] Next sync token: ${newSyncToken}`)
126
-
127
- if (!newSyncToken || newSyncToken === token) {
128
- nextSyncToken = newSyncToken || token
129
- console.log(`[FETCH_CONTENT_SYNC] No new sync token - ending sync`)
130
- break
131
- }
132
-
133
- nextSyncToken = newSyncToken
134
- token = newSyncToken
135
- page++
136
- } catch (error) {
137
- console.error(`[FETCH_CONTENT_SYNC] Failed on page ${page}:`, error.message)
138
- throw error
139
- }
140
- }
141
-
142
- console.log(
143
- `[FETCH_CONTENT_SYNC] Completed - Total items: ${allItems.length}, deleted: ${allDeleted.length}`
144
- )
145
- return {
146
- items: allItems,
147
- deleted: allDeleted,
148
- nextSyncToken: nextSyncToken || token,
149
- }
150
- }
151
-
152
- async function fetchMediaSync(syncToken) {
153
- console.log(`[FETCH_MEDIA_SYNC] Starting with syncToken: ${syncToken || "NONE"}`)
154
- let allItems = []
155
- let token = syncToken || ""
156
- let nextSyncToken = undefined
157
- let page = 0
158
-
159
- while (true) {
160
- const url = new URL("/api/media/sync", leadCMSUrl)
161
- url.searchParams.set("filter[limit]", "100")
162
- url.searchParams.set("syncToken", token)
163
-
164
- console.log(`[FETCH_MEDIA_SYNC] Page ${page}, URL: ${url.toString()}`)
165
-
166
- try {
167
- const res = await axios.get(url.toString(), {
168
- headers: { Authorization: `Bearer ${leadCMSApiKey}` },
169
- })
170
-
171
- if (res.status === 204) {
172
- console.log(`[FETCH_MEDIA_SYNC] Got 204 No Content - ending sync`)
173
- break
174
- }
175
-
176
- const data = res.data
177
- console.log(
178
- `[FETCH_MEDIA_SYNC] Page ${page} - Got ${data.items?.length || 0} items`
179
- )
180
-
181
- if (data.items && Array.isArray(data.items)) allItems.push(...data.items)
182
-
183
- const newSyncToken = res.headers["x-next-sync-token"] || token
184
- console.log(`[FETCH_MEDIA_SYNC] Next sync token: ${newSyncToken}`)
185
-
186
- if (!newSyncToken || newSyncToken === token) {
187
- nextSyncToken = newSyncToken || token
188
- console.log(`[FETCH_MEDIA_SYNC] No new sync token - ending sync`)
189
- break
190
- }
191
-
192
- nextSyncToken = newSyncToken
193
- token = newSyncToken
194
- page++
195
- } catch (error) {
196
- console.error(`[FETCH_MEDIA_SYNC] Failed on page ${page}:`, error.message)
197
- throw error
198
- }
199
- }
200
-
201
- console.log(
202
- `[FETCH_MEDIA_SYNC] Completed - Total items: ${allItems.length}`
203
- )
204
- return {
205
- items: allItems,
206
- nextSyncToken: nextSyncToken || token,
207
- }
208
- }
209
-
210
- async function main() {
211
- // Log environment configuration for debugging
212
- console.log(`[ENV] LeadCMS URL: ${leadCMSUrl}`)
213
- console.log(
214
- `[ENV] LeadCMS API Key: ${leadCMSApiKey ? `${leadCMSApiKey.substring(0, 8)}...` : "NOT_SET"}`
215
- )
216
- console.log(`[ENV] Default Language: ${defaultLanguage}`)
217
- console.log(`[ENV] Content Dir: ${CONTENT_DIR}`)
218
- console.log(`[ENV] Media Dir: ${MEDIA_DIR}`)
219
-
220
- await fs.mkdir(CONTENT_DIR, { recursive: true })
221
- await fs.mkdir(MEDIA_DIR, { recursive: true })
222
-
223
- const typeMap = await fetchContentTypes()
224
-
225
- const lastSyncToken = await readSyncToken()
226
- const lastMediaSyncToken = await readMediaSyncToken()
227
-
228
- let items = [],
229
- deleted = [],
230
- nextSyncToken
231
-
232
- let mediaItems = [],
233
- nextMediaSyncToken
234
-
235
- // Sync content
236
- try {
237
- if (lastSyncToken) {
238
- console.log(`Syncing content from LeadCMS using sync token: ${lastSyncToken}`)
239
- ;({ items, deleted, nextSyncToken } = await fetchContentSync(lastSyncToken))
240
- } else {
241
- console.log("No content sync token found. Doing full fetch from LeadCMS...")
242
- ;({ items, deleted, nextSyncToken } = await fetchContentSync(undefined))
243
- }
244
- } catch (error) {
245
- console.error(`[MAIN] Failed to fetch content:`, error.message)
246
- if (error.response?.status === 401) {
247
- console.error(`[MAIN] Authentication failed - check your LEADCMS_API_KEY`)
248
- }
249
- throw error
250
- }
251
-
252
- // Sync media
253
- try {
254
- if (lastMediaSyncToken) {
255
- console.log(`Syncing media from LeadCMS using sync token: ${lastMediaSyncToken}`)
256
- ;({ items: mediaItems, nextSyncToken: nextMediaSyncToken } = await fetchMediaSync(lastMediaSyncToken))
257
- } else {
258
- console.log("No media sync token found. Doing full fetch from LeadCMS...")
259
- ;({ items: mediaItems, nextSyncToken: nextMediaSyncToken } = await fetchMediaSync(undefined))
260
- }
261
- } catch (error) {
262
- console.error(`[MAIN] Failed to fetch media:`, error.message)
263
- if (error.response?.status === 401) {
264
- console.error(`[MAIN] Authentication failed - check your LEADCMS_API_KEY`)
265
- }
266
- // Don't throw here, continue with content sync even if media sync fails
267
- console.warn(`[MAIN] Continuing without media sync...`)
268
- }
269
-
270
- console.log(`Fetched ${items.length} content items, ${deleted.length} deleted.`)
271
- console.log(`Fetched ${mediaItems.length} media items.`)
272
-
273
- // Save content files and collect all media URLs from content
274
- const allMediaUrls = new Set()
275
- for (const content of items) {
276
- if (content && typeof content === "object") {
277
- await saveContentFile({
278
- content,
279
- typeMap,
280
- contentDir: CONTENT_DIR,
281
- })
282
- for (const url of extractMediaUrlsFromContent(content)) {
283
- allMediaUrls.add(url)
284
- }
285
- }
286
- }
287
-
288
- // Remove deleted content files from all language directories
289
- for (const id of deleted) {
290
- const idStr = String(id)
291
-
292
- // Function to recursively search for files in a directory
293
- async function findAndDeleteContentFile(dir) {
294
- try {
295
- const entries = await fs.readdir(dir, { withFileTypes: true })
296
- for (const entry of entries) {
297
- const fullPath = path.join(dir, entry.name)
298
- if (entry.isDirectory()) {
299
- // Recursively search subdirectories
300
- await findAndDeleteContentFile(fullPath)
301
- } else if (entry.isFile()) {
302
- try {
303
- const content = await fs.readFile(fullPath, "utf8")
304
- // Exact-match YAML frontmatter: lines like `id: 10` or `id: '10'`
305
- const yamlRegex = new RegExp(`(^|\\n)id:\\s*['\"]?${idStr}['\"]?(\\n|$)`)
306
- // Exact-match JSON: "id": 10 or "id": "10"
307
- const jsonRegex = new RegExp(`\\"id\\"\\s*:\\s*['\"]?${idStr}['\"]?\\s*(,|\\}|\\n|$)`)
308
- if (yamlRegex.test(content) || jsonRegex.test(content)) {
309
- await fs.unlink(fullPath)
310
- console.log(`Deleted: ${fullPath}`)
311
- }
312
- } catch {}
313
- }
314
- }
315
- } catch (err) {
316
- // Directory might not exist, that's okay
317
- if (err.code !== 'ENOENT') {
318
- console.warn(`Warning: Could not read directory ${dir}:`, err.message)
319
- }
320
- }
321
- }
322
-
323
- await findAndDeleteContentFile(CONTENT_DIR)
324
- }
325
-
326
- // Handle media sync results
327
- if (mediaItems.length > 0) {
328
- console.log(`\nProcessing media changes...`)
329
-
330
- // Download new/updated media files
331
- let downloaded = 0
332
- for (const mediaItem of mediaItems) {
333
- if (mediaItem.location) {
334
- const relPath = mediaItem.location.replace(/^\/api\/media\//, "")
335
- const destPath = path.join(MEDIA_DIR, relPath)
336
- const didDownload = await downloadMediaFileDirect(mediaItem.location, destPath, leadCMSUrl, leadCMSApiKey)
337
- if (didDownload) {
338
- console.log(`Downloaded: ${mediaItem.location} -> ${destPath}`)
339
- downloaded++
340
- }
341
- }
342
- }
343
- console.log(`\nDone. ${downloaded} media files downloaded.\n`)
344
- } else {
345
- console.log(`\nNo media changes detected.\n`)
346
- } // Save new sync tokens
347
- if (nextSyncToken) {
348
- await writeSyncToken(nextSyncToken)
349
- console.log(`Content sync token updated: ${nextSyncToken}`)
350
- }
351
-
352
- if (nextMediaSyncToken) {
353
- await writeMediaSyncToken(nextMediaSyncToken)
354
- console.log(`Media sync token updated: ${nextMediaSyncToken}`)
355
- }
356
- }
357
-
358
- // Export the main function so it can be imported by other modules
359
- export { main as fetchLeadCMSContent }
360
-
361
- // If this script is run directly (not imported), execute the main function
362
- if (import.meta.url === `file://${process.argv[1]}`) {
363
- main().catch((error) => {
364
- console.error('Error running LeadCMS fetch:', error.message);
365
- process.exit(1);
366
- });
367
- }
@@ -1,24 +0,0 @@
1
- // This script generates a __env.js file with all NEXT_PUBLIC_ env variables
2
- // Usage: node ./scripts/generate-env-js.mjs
3
- import fs from "fs"
4
- import path from "path"
5
- import dotenv from "dotenv"
6
-
7
- // Load env from .env, .env.local, etc. (dotenv will not overwrite existing process.env)
8
- dotenv.config({ path: path.resolve(process.cwd(), ".env") })
9
- try {
10
- dotenv.config({ path: path.resolve(process.cwd(), ".env.local") })
11
- } catch {}
12
-
13
- const envVars = Object.keys(process.env)
14
- .filter((key) => key.startsWith("NEXT_PUBLIC_"))
15
- .reduce((acc, key) => {
16
- acc[key] = process.env[key]
17
- return acc
18
- }, {})
19
-
20
- const jsContent = `window.__env = ${JSON.stringify(envVars, null, 2)};\n`
21
-
22
- const outPath = path.resolve(process.cwd(), "public", "__env.js")
23
- fs.writeFileSync(outPath, jsContent)
24
- console.log("Generated public/__env.js with NEXT_PUBLIC_ env variables.")
@@ -1,208 +0,0 @@
1
- import fs from "fs/promises"
2
- import path from "path"
3
- import yaml from "js-yaml"
4
- import axios from "axios"
5
-
6
- import { existsSync, readFileSync } from "fs"
7
-
8
- // Simple configuration loader for the scripts
9
- function loadConfig() {
10
- const possiblePaths = [
11
- "leadcms.config.json",
12
- ".leadcmsrc.json",
13
- ".leadcmsrc"
14
- ];
15
-
16
- for (const configPath of possiblePaths) {
17
- try {
18
- if (existsSync(configPath)) {
19
- const content = readFileSync(configPath, "utf-8");
20
- const config = JSON.parse(content);
21
- console.log(`[LeadCMS] Loaded configuration from: ${configPath}`);
22
- return config;
23
- }
24
- } catch (error) {
25
- // Continue to next config file
26
- }
27
- }
28
-
29
- return {};
30
- }
31
-
32
- const config = loadConfig();
33
-
34
- // Use configuration with environment variable fallbacks
35
- export const leadCMSUrl = config.url || process.env.LEADCMS_URL || process.env.NEXT_PUBLIC_LEADCMS_URL
36
- export const leadCMSApiKey = config.apiKey || process.env.LEADCMS_API_KEY
37
- export const defaultLanguage = config.defaultLanguage || process.env.LEADCMS_DEFAULT_LANGUAGE || process.env.NEXT_PUBLIC_LEADCMS_DEFAULT_LANGUAGE || "en"
38
- export const CONTENT_DIR = path.resolve(config.contentDir || ".leadcms/content")
39
- export const MEDIA_DIR = path.resolve(config.mediaDir || "public/media")
40
-
41
- // Fetch content types dynamically from LeadCMS API to build typeMap
42
- // Content types are automatically detected and don't need to be configured
43
- export async function fetchContentTypes() {
44
- console.log(`[LeadCMS] Fetching content types from API...`)
45
- const url = new URL("/api/content-types", leadCMSUrl)
46
- url.searchParams.set("filter[limit]", "100")
47
- try {
48
- const res = await axios.get(url.toString(), {
49
- headers: { Authorization: `Bearer ${leadCMSApiKey}` },
50
- })
51
- const types = res.data
52
- const typeMap = {}
53
- for (const t of types) {
54
- typeMap[t.uid] = t.format
55
- }
56
- console.log(`[LeadCMS] Detected ${Object.keys(typeMap).length} content types:`, Object.keys(typeMap).join(', '))
57
- return typeMap
58
- } catch (error) {
59
- console.error(`[LeadCMS] Failed to fetch content types:`, error.message)
60
- return {}
61
- }
62
- }
63
-
64
- export function extractMediaUrlsFromContent(content) {
65
- console.log(`[LeadCMS] Extracting media URLs from content: ${content}`)
66
- const urls = new Set()
67
- const body = content.body || ""
68
- const regex = /["'\(](\/api\/media\/[^"'\)\s]+)/g
69
- let match
70
- while ((match = regex.exec(body))) {
71
- urls.add(match[1])
72
- }
73
- if (content.coverImageUrl && content.coverImageUrl.startsWith("/api/media/")) {
74
- urls.add(content.coverImageUrl)
75
- }
76
-
77
- return Array.from(urls)
78
- }
79
-
80
- // Direct media download without meta.json dependency
81
- export async function downloadMediaFileDirect(mediaUrl, destPath, leadCMSUrl, leadCMSApiKey) {
82
- await fs.mkdir(path.dirname(destPath), { recursive: true })
83
-
84
- const fullUrl = mediaUrl.startsWith("http") ? mediaUrl : leadCMSUrl.replace(/\/$/, "") + mediaUrl
85
- const headers = { Authorization: `Bearer ${leadCMSApiKey}` }
86
-
87
- try {
88
- const res = await axios.get(fullUrl, {
89
- responseType: "arraybuffer",
90
- headers,
91
- validateStatus: (status) =>
92
- (status >= 200 && status < 300) || status === 404,
93
- })
94
-
95
- if (res.status === 404) {
96
- // Remove file if not found on server
97
- try {
98
- await fs.unlink(destPath)
99
- console.log(`Deleted missing file: ${destPath}`)
100
- } catch {}
101
- return false
102
- }
103
-
104
- await fs.writeFile(destPath, res.data)
105
- return true
106
- } catch (err) {
107
- console.error(`Failed to download ${mediaUrl}:`, err.message)
108
- throw err
109
- }
110
- }
111
-
112
- // Old downloadMediaFile function removed - replaced with downloadMediaFileDirect
113
- // No longer using meta.json for media file caching, using sync API instead
114
-
115
- export function buildFrontmatter(content) {
116
- const omit = ["body"]
117
- const fm = Object.fromEntries(
118
- Object.entries(content).filter(([k, v]) => !omit.includes(k) && v !== undefined && v !== null)
119
- )
120
- return `---\n${yaml.dump(fm)}---`
121
- }
122
-
123
- export function replaceApiMediaPaths(obj) {
124
- if (typeof obj === "string") {
125
- return obj.replace(/\/api\/media\//g, "/media/")
126
- } else if (Array.isArray(obj)) {
127
- return obj.map(replaceApiMediaPaths)
128
- } else if (typeof obj === "object" && obj !== null) {
129
- const out = {}
130
- for (const [k, v] of Object.entries(obj)) {
131
- out[k] = replaceApiMediaPaths(v)
132
- }
133
- return out
134
- }
135
- return obj
136
- }
137
-
138
- export async function saveContentFile({ content, typeMap, contentDir, previewSlug }) {
139
- if (!content || typeof content !== "object") {
140
- console.warn("[LeadCMS] Skipping undefined or invalid content:", content)
141
- return
142
- }
143
- const slug = previewSlug || content.slug
144
- if (!slug) {
145
- console.warn("[LeadCMS] Skipping content with missing slug:", content)
146
- return
147
- }
148
- const contentType = typeMap
149
- ? typeMap[content.type]
150
- : content.format || (content.body ? "MDX" : "JSON")
151
- const cleanedContent = replaceApiMediaPaths(content)
152
-
153
- // Inject draft: true when previewSlug is provided (indicates draft content)
154
- if (previewSlug) {
155
- cleanedContent.draft = true
156
- }
157
-
158
- // Determine the target directory based on language
159
- let targetContentDir = contentDir
160
- const contentLanguage = content.language || defaultLanguage
161
-
162
- if (contentLanguage !== defaultLanguage) {
163
- // Save non-default language content in language-specific folder
164
- targetContentDir = path.join(contentDir, contentLanguage)
165
- }
166
-
167
- if (contentType === "MDX") {
168
- const filePath = path.join(targetContentDir, `${slug}.mdx`)
169
- let body = cleanedContent.body || ""
170
- let bodyFrontmatter = {}
171
- let bodyContent = body
172
- // Extract frontmatter from body if present
173
- const fmMatch = body.match(/^---\n([\s\S]*?)\n---\n?/)
174
- if (fmMatch) {
175
- try {
176
- bodyFrontmatter = yaml.load(fmMatch[1]) || {}
177
- } catch (error) {
178
- console.warn(`[LeadCMS] Failed to parse frontmatter in body for ${slug}:`, error.message)
179
- }
180
- bodyContent = body.slice(fmMatch[0].length)
181
- }
182
-
183
- // Merge frontmatters, body frontmatter takes precedence over content metadata
184
- const mergedFrontmatter = { ...cleanedContent, ...bodyFrontmatter }
185
- delete mergedFrontmatter.body
186
- const frontmatterStr = buildFrontmatter(mergedFrontmatter)
187
- const mdx = `${frontmatterStr}\n\n${bodyContent.replace(/\/api\/media\//g, "/media/").trim()}\n`
188
- await fs.mkdir(path.dirname(filePath), { recursive: true })
189
- await fs.writeFile(filePath, mdx, "utf8")
190
- return filePath
191
- } else {
192
- let bodyObj = {}
193
- try {
194
- bodyObj = cleanedContent.body ? JSON.parse(cleanedContent.body) : {}
195
- } catch {
196
- bodyObj = {}
197
- }
198
- const merged = { ...bodyObj }
199
- for (const [k, v] of Object.entries(cleanedContent)) {
200
- if (k !== "body") merged[k] = v
201
- }
202
- const filePath = path.join(targetContentDir, `${slug}.json`)
203
- const jsonStr = JSON.stringify(merged, null, 2)
204
- await fs.mkdir(path.dirname(filePath), { recursive: true })
205
- await fs.writeFile(filePath, jsonStr, "utf8")
206
- return filePath
207
- }
208
- }