@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.
@@ -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
+ }