@leadcms/sdk 1.2.85-pre
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/LICENSE +21 -0
- package/README.md +261 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +205 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +38 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/cms.d.ts +104 -0
- package/dist/lib/cms.d.ts.map +1 -0
- package/dist/lib/cms.js +532 -0
- package/dist/lib/cms.js.map +1 -0
- package/dist/lib/config.d.ts +45 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +194 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/scripts/fetch-leadcms-content.mjs +361 -0
- package/dist/scripts/generate-env-js.mjs +24 -0
- package/dist/scripts/leadcms-helpers.mjs +208 -0
- package/dist/scripts/sse-watcher.mjs +325 -0
- package/dist/templates/docker/Dockerfile +34 -0
- package/dist/templates/docker/nginx.conf +70 -0
- package/dist/templates/docker/preview/Dockerfile +75 -0
- package/dist/templates/docker/preview/nginx.conf +128 -0
- package/dist/templates/docker/preview/supervisord.conf +63 -0
- package/dist/templates/scripts/inject-runtime-env.sh +33 -0
- package/leadcms.config.json.sample +8 -0
- package/package.json +76 -0
|
@@ -0,0 +1,208 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import "dotenv/config"
|
|
2
|
+
import { EventSource } from "eventsource"
|
|
3
|
+
import { exec } from "child_process"
|
|
4
|
+
import {
|
|
5
|
+
saveContentFile,
|
|
6
|
+
leadCMSUrl,
|
|
7
|
+
leadCMSApiKey,
|
|
8
|
+
defaultLanguage,
|
|
9
|
+
CONTENT_DIR,
|
|
10
|
+
} from "./leadcms-helpers.mjs"
|
|
11
|
+
import { fetchContentTypes } from "./leadcms-helpers.mjs"
|
|
12
|
+
|
|
13
|
+
// Log environment configuration for debugging
|
|
14
|
+
console.log(`[SSE ENV] LeadCMS URL: ${leadCMSUrl}`)
|
|
15
|
+
console.log(
|
|
16
|
+
`[SSE ENV] LeadCMS API Key: ${leadCMSApiKey ? `${leadCMSApiKey.substring(0, 8)}...` : "NOT_SET"}`
|
|
17
|
+
)
|
|
18
|
+
console.log(`[SSE ENV] Default Language: ${defaultLanguage}`)
|
|
19
|
+
console.log(`[SSE ENV] Content Dir: ${CONTENT_DIR}`)
|
|
20
|
+
|
|
21
|
+
function buildSSEUrl() {
|
|
22
|
+
console.log(`[SSE URL] Building SSE URL with base: ${leadCMSUrl}`)
|
|
23
|
+
const url = new URL("/api/sse/stream", leadCMSUrl)
|
|
24
|
+
url.searchParams.set("entities", "Content")
|
|
25
|
+
url.searchParams.set("includeContent", "true")
|
|
26
|
+
url.searchParams.set("includeLiveDrafts", "true")
|
|
27
|
+
const finalUrl = url.toString()
|
|
28
|
+
console.log(`[SSE URL] Final SSE URL: ${finalUrl}`)
|
|
29
|
+
return finalUrl
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function startSSEWatcher() {
|
|
33
|
+
console.log(`[SSE] Starting SSE watcher...`)
|
|
34
|
+
const typeMap = await fetchContentTypes()
|
|
35
|
+
const sseUrl = buildSSEUrl()
|
|
36
|
+
const eventSourceOptions = {}
|
|
37
|
+
|
|
38
|
+
if (leadCMSApiKey) {
|
|
39
|
+
console.log(`[SSE] Using API key for authentication`)
|
|
40
|
+
eventSourceOptions.fetch = (input, init) => {
|
|
41
|
+
console.log(`[SSE FETCH] Making authenticated request to: ${input}`)
|
|
42
|
+
console.log(
|
|
43
|
+
`[SSE FETCH] Headers:`,
|
|
44
|
+
JSON.stringify(
|
|
45
|
+
{
|
|
46
|
+
...init?.headers,
|
|
47
|
+
Authorization: `Bearer ${leadCMSApiKey.substring(0, 8)}...`,
|
|
48
|
+
},
|
|
49
|
+
null,
|
|
50
|
+
2
|
|
51
|
+
)
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
return fetch(input, {
|
|
55
|
+
...init,
|
|
56
|
+
headers: {
|
|
57
|
+
...init?.headers,
|
|
58
|
+
Authorization: `Bearer ${leadCMSApiKey}`,
|
|
59
|
+
},
|
|
60
|
+
})
|
|
61
|
+
.then((response) => {
|
|
62
|
+
console.log(`[SSE FETCH] Response status: ${response.status} ${response.statusText}`)
|
|
63
|
+
console.log(
|
|
64
|
+
`[SSE FETCH] Response headers:`,
|
|
65
|
+
JSON.stringify(Object.fromEntries(response.headers.entries()), null, 2)
|
|
66
|
+
)
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
console.error(`[SSE FETCH] Failed response: ${response.status} ${response.statusText}`)
|
|
69
|
+
}
|
|
70
|
+
return response
|
|
71
|
+
})
|
|
72
|
+
.catch((error) => {
|
|
73
|
+
console.error(`[SSE FETCH] Fetch error:`, error.message)
|
|
74
|
+
throw error
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
console.warn(`[SSE] No API key provided - attempting unauthenticated connection`)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
console.log(`[SSE] Connecting to: ${sseUrl}`)
|
|
82
|
+
console.log(`[SSE] Event source options:`, JSON.stringify(eventSourceOptions, null, 2))
|
|
83
|
+
const es = new EventSource(sseUrl, eventSourceOptions)
|
|
84
|
+
|
|
85
|
+
es.onopen = () => {
|
|
86
|
+
console.log("[SSE] Connection opened successfully")
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
es.onmessage = (event) => {
|
|
90
|
+
console.log(`[SSE] Received message:`, event.data)
|
|
91
|
+
try {
|
|
92
|
+
const data = JSON.parse(event.data)
|
|
93
|
+
console.log(`[SSE] Parsed message data:`, JSON.stringify(data, null, 2))
|
|
94
|
+
|
|
95
|
+
if (data.entityType === "Content") {
|
|
96
|
+
console.log(`[SSE] Content message - Operation: ${data.operation}`)
|
|
97
|
+
console.log(`[SSE] Content change detected - triggering full fetch`)
|
|
98
|
+
exec("npm run fetch:leadcms", (err, stdout, stderr) => {
|
|
99
|
+
if (err) {
|
|
100
|
+
console.error("[SSE] fetch:leadcms failed:", err.message)
|
|
101
|
+
console.error("[SSE] fetch:leadcms stderr:", stderr)
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
console.log("[SSE] fetch:leadcms completed successfully")
|
|
105
|
+
console.log("[SSE] fetch:leadcms output:\n", stdout)
|
|
106
|
+
if (stderr) console.warn("[SSE] fetch:leadcms stderr:", stderr)
|
|
107
|
+
})
|
|
108
|
+
} else {
|
|
109
|
+
console.log(`[SSE] Non-content message - Entity type: ${data.entityType}`)
|
|
110
|
+
}
|
|
111
|
+
} catch (e) {
|
|
112
|
+
console.warn("[SSE] Failed to parse SSE message:", e.message)
|
|
113
|
+
console.warn("[SSE] Raw event data:", event.data)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
es.addEventListener("connected", (event) => {
|
|
118
|
+
console.log(`[SSE] Received 'connected' event:`, event.data)
|
|
119
|
+
try {
|
|
120
|
+
const data = JSON.parse(event.data)
|
|
121
|
+
console.log(
|
|
122
|
+
`[SSE] Connected successfully - Client ID: ${data.clientId}, Starting change log ID: ${data.startingChangeLogId}`
|
|
123
|
+
)
|
|
124
|
+
} catch (e) {
|
|
125
|
+
console.warn("[SSE] Failed to parse connected event:", e.message)
|
|
126
|
+
console.warn("[SSE] Raw connected event data:", event.data)
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
es.addEventListener("heartbeat", (event) => {
|
|
131
|
+
console.log(`[SSE] Received heartbeat:`, event.data)
|
|
132
|
+
try {
|
|
133
|
+
const data = JSON.parse(event.data)
|
|
134
|
+
console.log(`[SSE] Heartbeat at ${data.timestamp}`)
|
|
135
|
+
} catch (e) {
|
|
136
|
+
console.warn("[SSE] Failed to parse heartbeat event:", e.message)
|
|
137
|
+
console.warn("[SSE] Raw heartbeat event data:", event.data)
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
es.addEventListener("draft-updated", (event) => {
|
|
142
|
+
console.log(`[SSE] Received 'draft-updated' event:`, event.data)
|
|
143
|
+
try {
|
|
144
|
+
const data = JSON.parse(event.data)
|
|
145
|
+
console.log(`[SSE] Draft updated data:`, JSON.stringify(data, null, 2))
|
|
146
|
+
|
|
147
|
+
if (data.createdById && data.data) {
|
|
148
|
+
console.log(`[SSE] Processing draft update for user: ${data.createdById}`)
|
|
149
|
+
let contentData
|
|
150
|
+
try {
|
|
151
|
+
contentData = typeof data.data === "string" ? JSON.parse(data.data) : data.data
|
|
152
|
+
console.log(`[SSE] Draft content data:`, JSON.stringify(contentData, null, 2))
|
|
153
|
+
} catch (e) {
|
|
154
|
+
console.warn("[SSE] Failed to parse draft content data:", e.message)
|
|
155
|
+
console.warn("[SSE] Raw data:", data.data)
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Determine content type for draft handling
|
|
160
|
+
let contentType = undefined
|
|
161
|
+
if (contentData && contentData.type && typeMap && typeMap[contentData.type]) {
|
|
162
|
+
contentType = typeMap[contentData.type]
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
console.log(`[SSE] Draft updated - triggering full fetch`)
|
|
166
|
+
exec("npm run fetch:leadcms", (err, stdout, stderr) => {
|
|
167
|
+
if (err) {
|
|
168
|
+
console.error("[SSE] fetch:leadcms failed:", err.message)
|
|
169
|
+
console.error("[SSE] fetch:leadcms stderr:", stderr)
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
console.log("[SSE] fetch:leadcms completed successfully")
|
|
173
|
+
console.log("[SSE] fetch:leadcms output:\n", stdout)
|
|
174
|
+
if (stderr) console.warn("[SSE] fetch:leadcms stderr:", stderr)
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
if (contentType === "MDX" || contentType === "JSON") {
|
|
178
|
+
if (contentData && typeof contentData === "object") {
|
|
179
|
+
const previewSlug = `${contentData.slug}-${data.createdById}`
|
|
180
|
+
console.log(`[SSE] Saving draft content file for preview: ${previewSlug}`)
|
|
181
|
+
;(async () => {
|
|
182
|
+
try {
|
|
183
|
+
await saveContentFile({
|
|
184
|
+
content: contentData,
|
|
185
|
+
typeMap,
|
|
186
|
+
contentDir: CONTENT_DIR,
|
|
187
|
+
previewSlug: previewSlug,
|
|
188
|
+
})
|
|
189
|
+
console.log(`[SSE] Saved draft preview for ${previewSlug}`)
|
|
190
|
+
} catch (error) {
|
|
191
|
+
console.error(`[SSE] Error processing draft update:`, error.message)
|
|
192
|
+
}
|
|
193
|
+
})()
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
console.log(`[SSE] Draft is not MDX or JSON (type: ${contentType}), skipping file save.`)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
} catch (e) {
|
|
200
|
+
console.warn("[SSE] Failed to parse draft-updated event:", e.message)
|
|
201
|
+
console.warn("[SSE] Raw draft-updated event data:", event.data)
|
|
202
|
+
}
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
// Handle legacy DraftModified messages for backward compatibility
|
|
206
|
+
es.addEventListener("message", (event) => {
|
|
207
|
+
try {
|
|
208
|
+
const data = JSON.parse(event.data)
|
|
209
|
+
|
|
210
|
+
// Only handle DraftModified operations here for backward compatibility
|
|
211
|
+
if (data.entityType === "Content" && data.operation === "DraftModified" && data.createdById && data.data) {
|
|
212
|
+
console.log(`[SSE] Received legacy 'DraftModified' message for user: ${data.createdById}`)
|
|
213
|
+
let contentData
|
|
214
|
+
try {
|
|
215
|
+
contentData = typeof data.data === "string" ? JSON.parse(data.data) : data.data
|
|
216
|
+
console.log(`[SSE] Legacy draft content data:`, JSON.stringify(contentData, null, 2))
|
|
217
|
+
} catch (e) {
|
|
218
|
+
console.warn("[SSE] Failed to parse legacy draft content data:", e.message)
|
|
219
|
+
console.warn("[SSE] Raw data:", data.data)
|
|
220
|
+
return
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Determine content type for legacy draft handling
|
|
224
|
+
let contentType = undefined
|
|
225
|
+
if (contentData && contentData.type && typeMap && typeMap[contentData.type]) {
|
|
226
|
+
contentType = typeMap[contentData.type]
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
console.log(`[SSE] Legacy draft modified - triggering full fetch`)
|
|
230
|
+
exec("npm run fetch:leadcms", (err, stdout, stderr) => {
|
|
231
|
+
if (err) {
|
|
232
|
+
console.error("[SSE] fetch:leadcms failed:", err.message)
|
|
233
|
+
console.error("[SSE] fetch:leadcms stderr:", stderr)
|
|
234
|
+
return
|
|
235
|
+
}
|
|
236
|
+
console.log("[SSE] fetch:leadcms completed successfully")
|
|
237
|
+
console.log("[SSE] fetch:leadcms output:\n", stdout)
|
|
238
|
+
if (stderr) console.warn("[SSE] fetch:leadcms stderr:", stderr)
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
if (contentType === "MDX" || contentType === "JSON") {
|
|
242
|
+
if (contentData && typeof contentData === "object") {
|
|
243
|
+
const previewSlug = `${contentData.slug}-${data.createdById}`
|
|
244
|
+
console.log(`[SSE] Saving legacy draft content file for preview: ${previewSlug}`)
|
|
245
|
+
;(async () => {
|
|
246
|
+
try {
|
|
247
|
+
await saveContentFile({
|
|
248
|
+
content: contentData,
|
|
249
|
+
typeMap,
|
|
250
|
+
contentDir: CONTENT_DIR,
|
|
251
|
+
previewSlug: previewSlug,
|
|
252
|
+
})
|
|
253
|
+
console.log(`[SSE] Saved legacy draft preview for ${previewSlug}`)
|
|
254
|
+
} catch (error) {
|
|
255
|
+
console.error(`[SSE] Error processing legacy draft modification:`, error.message)
|
|
256
|
+
}
|
|
257
|
+
})()
|
|
258
|
+
}
|
|
259
|
+
} else {
|
|
260
|
+
console.log(`[SSE] Legacy draft is not MDX or JSON (type: ${contentType}), skipping file save.`)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
} catch {
|
|
264
|
+
// Silently ignore parse errors for non-JSON messages
|
|
265
|
+
}
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
es.addEventListener("content-updated", (event) => {
|
|
269
|
+
console.log(`[SSE] Received 'content-updated' event:`, event.data)
|
|
270
|
+
try {
|
|
271
|
+
const data = JSON.parse(event.data)
|
|
272
|
+
console.log(`[SSE] Content updated data:`, JSON.stringify(data, null, 2))
|
|
273
|
+
|
|
274
|
+
console.log(`[SSE] Content updated - triggering full fetch`)
|
|
275
|
+
exec("npm run fetch:leadcms", (err, stdout, stderr) => {
|
|
276
|
+
if (err) {
|
|
277
|
+
console.error("[SSE] fetch:leadcms failed:", err.message)
|
|
278
|
+
console.error("[SSE] fetch:leadcms stderr:", stderr)
|
|
279
|
+
return
|
|
280
|
+
}
|
|
281
|
+
console.log("[SSE] fetch:leadcms completed successfully")
|
|
282
|
+
console.log("[SSE] fetch:leadcms output:\n", stdout)
|
|
283
|
+
if (stderr) console.warn("[SSE] fetch:leadcms stderr:", stderr)
|
|
284
|
+
})
|
|
285
|
+
} catch (e) {
|
|
286
|
+
console.warn("[SSE] Failed to parse content-updated event:", e.message)
|
|
287
|
+
console.warn("[SSE] Raw content-updated event data:", event.data)
|
|
288
|
+
}
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
es.onerror = (err) => {
|
|
292
|
+
console.error("[SSE] Connection error occurred:", {
|
|
293
|
+
type: err.type,
|
|
294
|
+
message: err.message,
|
|
295
|
+
code: err.code,
|
|
296
|
+
timestamp: new Date().toISOString(),
|
|
297
|
+
readyState: es.readyState,
|
|
298
|
+
url: es.url,
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
// Log specific error types
|
|
302
|
+
if (err.code === 401) {
|
|
303
|
+
console.error("[SSE] Authentication failed (401) - check your LEADCMS_API_KEY")
|
|
304
|
+
console.error(
|
|
305
|
+
"[SSE] Current API Key (first 8 chars):",
|
|
306
|
+
leadCMSApiKey ? leadCMSApiKey.substring(0, 8) : "NOT_SET"
|
|
307
|
+
)
|
|
308
|
+
} else if (err.code === 403) {
|
|
309
|
+
console.error("[SSE] Forbidden (403) - insufficient permissions")
|
|
310
|
+
} else if (err.code === 404) {
|
|
311
|
+
console.error("[SSE] Not Found (404) - check your LEADCMS_URL and endpoint path")
|
|
312
|
+
} else if (err.code >= 500) {
|
|
313
|
+
console.error("[SSE] Server error (5xx) - LeadCMS server issue")
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
console.log("[SSE] Closing connection and will reconnect in 5s")
|
|
317
|
+
es.close()
|
|
318
|
+
setTimeout(() => {
|
|
319
|
+
console.log("[SSE] Attempting to reconnect...")
|
|
320
|
+
startSSEWatcher()
|
|
321
|
+
}, 5000)
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
startSSEWatcher()
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# LeadCMS Static Site Production Dockerfile
|
|
2
|
+
# This Dockerfile serves pre-built static files using nginx
|
|
3
|
+
# Works with any static site generator (Next.js, Astro, Gatsby, Nuxt, etc.)
|
|
4
|
+
#
|
|
5
|
+
# Usage from project root:
|
|
6
|
+
# 1. Build your static site to 'out' or 'dist' directory
|
|
7
|
+
# 2. docker build -t my-leadcms-site .
|
|
8
|
+
# 3. docker run -p 80:80 my-leadcms-site
|
|
9
|
+
|
|
10
|
+
FROM nginx:alpine
|
|
11
|
+
|
|
12
|
+
# Copy pre-built static site from local 'out' folder
|
|
13
|
+
# Note: Adjust the source directory based on your static site generator:
|
|
14
|
+
# - Next.js: out/
|
|
15
|
+
# - Astro: dist/
|
|
16
|
+
# - Gatsby: public/
|
|
17
|
+
# - Nuxt: dist/
|
|
18
|
+
COPY out /usr/share/nginx/html
|
|
19
|
+
|
|
20
|
+
# Copy nginx configuration and runtime environment injection script
|
|
21
|
+
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
|
22
|
+
COPY scripts/inject-runtime-env.sh /app/scripts/inject-runtime-env.sh
|
|
23
|
+
|
|
24
|
+
# Set appropriate permissions
|
|
25
|
+
RUN chmod -R 755 /usr/share/nginx/html && chmod +x /app/scripts/inject-runtime-env.sh
|
|
26
|
+
|
|
27
|
+
EXPOSE 80
|
|
28
|
+
|
|
29
|
+
# Health check
|
|
30
|
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
|
31
|
+
CMD wget --spider -q http://localhost:80 || exit 1
|
|
32
|
+
|
|
33
|
+
# Start nginx with runtime environment injection
|
|
34
|
+
CMD ["/bin/sh", "-c", "/app/scripts/inject-runtime-env.sh && nginx -g 'daemon off;'"]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# LeadCMS Static Site nginx Configuration
|
|
2
|
+
# Optimized for serving static sites with proper caching headers
|
|
3
|
+
# Works with any static site generator
|
|
4
|
+
|
|
5
|
+
server {
|
|
6
|
+
listen 80;
|
|
7
|
+
server_name localhost;
|
|
8
|
+
root /usr/share/nginx/html;
|
|
9
|
+
index index.html;
|
|
10
|
+
|
|
11
|
+
# Security headers
|
|
12
|
+
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
13
|
+
add_header X-Content-Type-Options "nosniff" always;
|
|
14
|
+
add_header X-XSS-Protection "1; mode=block" always;
|
|
15
|
+
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
16
|
+
|
|
17
|
+
# HTML files: no cache for content freshness
|
|
18
|
+
location ~* \.html$ {
|
|
19
|
+
add_header Cache-Control "public, max-age=0, must-revalidate";
|
|
20
|
+
try_files $uri $uri/index.html =404;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
# Runtime environment file: never cache
|
|
24
|
+
location = /__env.js {
|
|
25
|
+
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
|
|
26
|
+
try_files $uri =404;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
# Static assets: cache forever with immutable flag
|
|
30
|
+
location ~* \.(js|css|woff2?|ttf|eot|svg|jpg|jpeg|png|avif|webp|gif|ico|pdf)$ {
|
|
31
|
+
add_header Cache-Control "public, max-age=31536000, immutable";
|
|
32
|
+
try_files $uri =404;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# JSON files (potentially dynamic content): short cache
|
|
36
|
+
location ~* \.json$ {
|
|
37
|
+
add_header Cache-Control "public, max-age=300"; # 5 minutes
|
|
38
|
+
try_files $uri =404;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Default location: try files with index.html fallback
|
|
42
|
+
location / {
|
|
43
|
+
try_files $uri $uri/index.html =404;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Custom error pages
|
|
47
|
+
error_page 404 /404.html;
|
|
48
|
+
error_page 500 502 503 504 /50x.html;
|
|
49
|
+
|
|
50
|
+
location = /50x.html {
|
|
51
|
+
root /usr/share/nginx/html;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# Gzip compression
|
|
55
|
+
gzip on;
|
|
56
|
+
gzip_vary on;
|
|
57
|
+
gzip_min_length 1024;
|
|
58
|
+
gzip_proxied any;
|
|
59
|
+
gzip_comp_level 6;
|
|
60
|
+
gzip_types
|
|
61
|
+
text/plain
|
|
62
|
+
text/css
|
|
63
|
+
text/xml
|
|
64
|
+
text/javascript
|
|
65
|
+
application/json
|
|
66
|
+
application/javascript
|
|
67
|
+
application/xml+rss
|
|
68
|
+
application/atom+xml
|
|
69
|
+
image/svg+xml;
|
|
70
|
+
}
|