@stacksjs/cms 0.71.1 → 0.71.10

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 (50) hide show
  1. package/dist/authors/destroy.js +1 -0
  2. package/dist/authors/fetch.js +1 -0
  3. package/dist/authors/index.js +1 -0
  4. package/dist/authors/store.js +1 -0
  5. package/dist/authors/update.js +1 -0
  6. package/dist/blocks/defaults.js +1 -0
  7. package/dist/blocks/registry.js +1 -0
  8. package/dist/blocks/types.js +0 -0
  9. package/dist/build.d.ts +6 -0
  10. package/dist/build.js +876 -0
  11. package/dist/categorizables/destroy.js +1 -0
  12. package/dist/categorizables/fetch.js +1 -0
  13. package/dist/categorizables/index.js +1 -0
  14. package/dist/categorizables/store.js +1 -0
  15. package/dist/categorizables/update.js +1 -0
  16. package/dist/commentables/destroy.js +1 -0
  17. package/dist/commentables/fetch.js +1 -0
  18. package/dist/commentables/index.js +1 -0
  19. package/dist/commentables/store.js +1 -0
  20. package/dist/commentables/update.js +1 -0
  21. package/dist/database.js +1 -0
  22. package/dist/index.js +1 -3
  23. package/dist/menus/index.js +1 -0
  24. package/dist/pages/destroy.js +1 -0
  25. package/dist/pages/document.js +1 -0
  26. package/dist/pages/fetch.js +1 -0
  27. package/dist/pages/index.js +1 -0
  28. package/dist/pages/store.js +1 -0
  29. package/dist/pages/update.js +1 -0
  30. package/dist/posts/destroy.js +1 -0
  31. package/dist/posts/fetch.js +1 -0
  32. package/dist/posts/index.js +1 -0
  33. package/dist/posts/store.js +1 -0
  34. package/dist/posts/update.js +1 -0
  35. package/dist/public/fallback.js +1 -0
  36. package/dist/public/render.js +2 -0
  37. package/dist/public/resolve.js +1 -0
  38. package/dist/public/sanitize.js +1 -0
  39. package/dist/publish/index.js +1 -0
  40. package/dist/redirects/index.js +1 -0
  41. package/dist/results.js +1 -0
  42. package/dist/revisions/index.js +1 -0
  43. package/dist/taggables/destroy.js +1 -0
  44. package/dist/taggables/fetch.js +1 -0
  45. package/dist/taggables/index.js +1 -0
  46. package/dist/taggables/store.js +1 -0
  47. package/dist/taggables/update.js +1 -0
  48. package/dist/tests/setup.d.ts +5 -0
  49. package/dist/tests/setup.js +100 -0
  50. package/package.json +3 -3
package/dist/build.js ADDED
@@ -0,0 +1,876 @@
1
+ import{copyFileSync,existsSync,mkdirSync,readdirSync,writeFileSync}from"node:fs";import{join}from"node:path";import{path as p}from"@stacksjs/path";function getSlug(post){if(post.slug&&typeof post.slug==="string"&&post.slug!=="null")return post.slug;return`post-${post.id}`}function escapeHtml(str){return str.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#039;")}function escapeXml(str){return escapeHtml(str)}function formatDate(dateStr){return new Date(dateStr).toLocaleDateString("en-US",{year:"numeric",month:"long",day:"numeric"})}function formatRssDate(dateStr){return new Date(dateStr).toUTCString()}function estimateReadingTime(text){const words=text.split(/\s+/).length;return Math.max(1,Math.ceil(words/200))}function renderMarkdownish(text){const lines=text.split(`
2
+ `),result=[];let inCodeBlock=!1,codeBuffer=[],paragraphBuffer=[];function flushParagraph(){if(paragraphBuffer.length>0){const text=paragraphBuffer.join(" ");if(text.trim())result.push(`<p>${inlineFormat(text.trim())}</p>`);paragraphBuffer=[]}}function inlineFormat(str){str=str.replace(/\*\*(.+?)\*\*/g,"<strong>$1</strong>");str=str.replace(/`([^`]+)`/g,"<code>$1</code>");str=str.replace(/\[([^\]]+)\]\(([^)]+)\)/g,'<a href="$2">$1</a>');return str}for(const line of lines){if(line.startsWith("```")){if(inCodeBlock){result.push(`<pre><code>${escapeHtml(codeBuffer.join(`
3
+ `))}</code></pre>`);codeBuffer=[];inCodeBlock=!1}else{flushParagraph();inCodeBlock=!0}continue}if(inCodeBlock){codeBuffer.push(line);continue}if(line.trim()===""){flushParagraph();continue}if(line.startsWith("## ")){flushParagraph();result.push(`<h2>${inlineFormat(escapeHtml(line.slice(3)))}</h2>`)}else if(line.startsWith("### ")){flushParagraph();result.push(`<h3>${inlineFormat(escapeHtml(line.slice(4)))}</h3>`)}else if(line.startsWith("- ")||line.startsWith("* ")){flushParagraph();result.push(`<ul><li>${inlineFormat(escapeHtml(line.slice(2)))}</li></ul>`)}else paragraphBuffer.push(escapeHtml(line))}if(inCodeBlock&&codeBuffer.length)result.push(`<pre><code>${escapeHtml(codeBuffer.join(`
4
+ `))}</code></pre>`);flushParagraph();return result.join(`
5
+ `).replace(/<\/ul>\n<ul>/g,`
6
+ `)}function blogFontFaces(fontPath="/assets/fonts/nps"){return`@font-face {
7
+ font-family: 'Campmate Script';
8
+ src: url('${fontPath}/CampmateScript-Regular.woff2') format('woff2');
9
+ font-weight: 400;
10
+ font-style: normal;
11
+ font-display: swap;
12
+ }
13
+ @font-face {
14
+ font-family: 'Sequoia Sans';
15
+ src: url('${fontPath}/SequoiaSans-Regular.woff2') format('woff2');
16
+ font-weight: 400;
17
+ font-style: normal;
18
+ font-display: swap;
19
+ }
20
+ @font-face {
21
+ font-family: 'Switchback';
22
+ src: url('${fontPath}/Switchback-Regular.woff2') format('woff2');
23
+ font-weight: 400;
24
+ font-style: normal;
25
+ font-display: swap;
26
+ }`}function copyBlogFonts(outDir){const sourceDir=p.frameworkPath("defaults/resources/assets/fonts/nps");if(!existsSync(sourceDir))return;const targetDir=join(outDir,"assets","fonts","nps");ensureDir(targetDir);for(const file of readdirSync(sourceDir))if(file.endsWith(".woff2"))copyFileSync(join(sourceDir,file),join(targetDir,file))}function copyBlogImages(outDir){const sourceDir=p.frameworkPath("defaults/resources/assets/images");if(!existsSync(sourceDir))return;const targetDir=join(outDir,"assets","images");ensureDir(targetDir);for(const file of readdirSync(sourceDir))if(file.endsWith(".svg"))copyFileSync(join(sourceDir,file),join(targetDir,file))}function sqliteDatabasePath(){const configuredPath=process.env.DB_DATABASE_PATH||"database/stacks.sqlite";return configuredPath.startsWith("/")?configuredPath:p.projectPath(configuredPath)}function shouldUseLocalSqlite(){return(process.env.DB_CONNECTION||"sqlite")==="sqlite"&&existsSync(sqliteDatabasePath())}async function queryLocalSqlite(sql){if(!shouldUseLocalSqlite())return null;const{Database}=await import("bun:sqlite"),sqlite=new Database(sqliteDatabasePath(),{readonly:!0});try{return sqlite.query(sql).all()}finally{sqlite.close()}}async function withTimeout(promise,ms){let timeout;try{return await Promise.race([promise,new Promise((_,reject)=>{timeout=setTimeout(()=>reject(Error(`CMS database query timed out after ${ms}ms`)),ms)})])}finally{if(timeout)clearTimeout(timeout)}}async function fetchPublishedPosts(){const sqlitePosts=await queryLocalSqlite("select * from posts where status = 'published' order by published_at desc");if(sqlitePosts)return sqlitePosts;const{db}=await import("@stacksjs/database");return await withTimeout(db.selectFrom("posts").where("status","=","published").orderBy("published_at","desc").selectAll().execute(),3000)}async function fetchAuthors(){const sqliteAuthors=await queryLocalSqlite("select * from authors order by created_at desc");if(sqliteAuthors)return sqliteAuthors;const{db}=await import("@stacksjs/database");return await withTimeout(db.selectFrom("authors").selectAll().execute(),3000)}function generateLayout(config,title,content,_options){return`<!DOCTYPE html>
27
+ <html lang="en">
28
+ <head>
29
+ <meta charset="UTF-8">
30
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
31
+ <title>${title===config.title?config.title:`${escapeHtml(title)} | ${escapeHtml(config.title)}`}</title>
32
+ <meta name="description" content="${escapeHtml(config.description)}">
33
+ <link rel="alternate" type="application/rss+xml" title="${escapeHtml(config.title)}" href="/feed.xml">
34
+ <style>
35
+ ${blogFontFaces()}
36
+
37
+ :root {
38
+ --primary: ${config.theme.primaryColor};
39
+ --primary-hover: #24472d;
40
+ --primary-soft: #e4ead8;
41
+ --primary-deep: #18351f;
42
+ --on-primary: #fff7e7;
43
+ --on-primary-muted: #f3dfbd;
44
+ --accent: #b7792d;
45
+ --accent-hover: #96621f;
46
+ --accent-strong: #8b5316;
47
+ --accent-soft: #f5dfb8;
48
+ --river: #4e8f88;
49
+ --river-soft: #cfe8db;
50
+ --canopy: #326d43;
51
+ --post-hover-border: #b7792d;
52
+ --newsletter-border: #6e8f5f;
53
+ --newsletter-bg: #2d5938;
54
+ --newsletter-bg-end: #18351f;
55
+ --bg: #f7f0e3;
56
+ --paper: #fffaf0;
57
+ --bg-soft: #efe4d1;
58
+ --bg-muted: #eadbc1;
59
+ --text: #273128;
60
+ --text-light: #56624f;
61
+ --text-lighter: #7d806f;
62
+ --border: #d5c5a6;
63
+ --shadow: 0 14px 32px -24px rgba(53, 39, 19, 0.55);
64
+ --max-width: 820px;
65
+ --radius: 8px;
66
+ --focus-ring: 0 0 0 3px rgba(245, 223, 184, 0.45);
67
+ --font-display: 'Campmate Script', 'Sequoia Sans', system-ui, sans-serif;
68
+ --font-sans: 'Sequoia Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
69
+ --font-serif: 'Switchback', Georgia, serif;
70
+ }
71
+ @media (prefers-color-scheme: dark) {
72
+ :root {
73
+ --primary: #326d43;
74
+ --primary-hover: #5d9a66;
75
+ --primary-soft: #d9ead4;
76
+ --primary-deep: #142419;
77
+ --on-primary: #fff6e6;
78
+ --on-primary-muted: #eadcc0;
79
+ --accent: #d58a2e;
80
+ --accent-hover: #f0a846;
81
+ --accent-strong: #f2b04f;
82
+ --accent-soft: #ffe3ad;
83
+ --river: #6fb1a6;
84
+ --river-soft: #d9fff0;
85
+ --canopy: #4f8e55;
86
+ --post-hover-border: #d58a2e;
87
+ --newsletter-border: #6f955f;
88
+ --newsletter-bg: #2f6840;
89
+ --newsletter-bg-end: #142419;
90
+ --bg: #101811;
91
+ --paper: #19251b;
92
+ --bg-soft: #213321;
93
+ --bg-muted: #2a3d28;
94
+ --text: #fff2dc;
95
+ --text-light: #e8dcc3;
96
+ --text-lighter: #c9bda3;
97
+ --border: #4f7650;
98
+ --shadow: 0 18px 46px -26px rgba(0, 0, 0, 0.95);
99
+ --focus-ring: 0 0 0 3px rgba(255, 227, 173, 0.42);
100
+ }
101
+ }
102
+ * { margin: 0; padding: 0; box-sizing: border-box; }
103
+ body {
104
+ position: relative;
105
+ font-family: var(--font-sans);
106
+ background: var(--bg);
107
+ background-image:
108
+ radial-gradient(circle at top left, rgba(183, 121, 45, 0.09), transparent 24rem),
109
+ linear-gradient(180deg, rgba(255, 250, 240, 0.7), rgba(239, 228, 209, 0.48));
110
+ color: var(--text);
111
+ line-height: 1.7;
112
+ -webkit-font-smoothing: antialiased;
113
+ }
114
+ body::before {
115
+ content: '';
116
+ position: fixed;
117
+ inset: 0;
118
+ pointer-events: none;
119
+ background-image: url('/assets/images/topography.svg');
120
+ background-size: 520px 520px;
121
+ background-repeat: repeat;
122
+ opacity: 0.025;
123
+ z-index: 0;
124
+ }
125
+ body::after {
126
+ content: '';
127
+ position: fixed;
128
+ left: 0;
129
+ right: 0;
130
+ bottom: 0;
131
+ height: min(42vh, 26rem);
132
+ pointer-events: none;
133
+ background-image: url('/assets/images/park-ridge.svg');
134
+ background-position: center bottom;
135
+ background-repeat: no-repeat;
136
+ background-size: cover;
137
+ opacity: 0.12;
138
+ z-index: 0;
139
+ }
140
+ body > * {
141
+ position: relative;
142
+ z-index: 1;
143
+ }
144
+ @media (prefers-color-scheme: dark) {
145
+ body {
146
+ background-image:
147
+ radial-gradient(circle at 50% -10%, rgba(82, 126, 69, 0.18), transparent 32rem),
148
+ radial-gradient(circle at 15% 18%, rgba(213, 138, 46, 0.08), transparent 24rem),
149
+ linear-gradient(180deg, #142018 0%, #101811 56%, #0d150f 100%);
150
+ }
151
+ body::before { opacity: 0.045; }
152
+ body::after { opacity: 0.2; }
153
+ }
154
+ a { color: var(--primary); text-decoration: none; transition: color 0.15s, opacity 0.15s, border-color 0.15s; }
155
+ a:hover { color: var(--primary-hover); opacity: 1; }
156
+ a:focus-visible,
157
+ button:focus-visible,
158
+ input:focus-visible {
159
+ outline: none;
160
+ box-shadow: var(--focus-ring);
161
+ }
162
+
163
+ /* Header */
164
+ .header {
165
+ border-bottom: 3px solid var(--accent);
166
+ padding: 0.875rem 2rem;
167
+ display: flex;
168
+ align-items: center;
169
+ justify-content: space-between;
170
+ background: var(--primary);
171
+ position: sticky;
172
+ top: 0;
173
+ z-index: 10;
174
+ box-shadow: 0 12px 30px -20px rgba(0, 0, 0, 0.85);
175
+ }
176
+ .header .site-title {
177
+ font-family: var(--font-display);
178
+ font-size: 1.125rem;
179
+ font-weight: 750;
180
+ color: var(--on-primary);
181
+ display: flex;
182
+ align-items: center;
183
+ gap: 0.5rem;
184
+ }
185
+ .header .site-title svg { height: 28px; width: 28px; color: var(--accent-soft); }
186
+ .header nav { display: flex; align-items: center; gap: 1.25rem; }
187
+ .header nav a { color: var(--on-primary-muted); font-size: 0.875rem; font-weight: 700; }
188
+ .header nav a:hover { color: var(--on-primary); opacity: 1; }
189
+
190
+ /* Container */
191
+ .container { max-width: var(--max-width); margin: 0 auto; padding: 2.5rem 1.5rem 4rem; }
192
+
193
+ /* Hero */
194
+ .hero {
195
+ position: relative;
196
+ overflow: hidden;
197
+ text-align: center;
198
+ padding: 2.75rem 2rem 2.5rem;
199
+ border: 1px solid var(--border);
200
+ border-top: 4px solid var(--accent);
201
+ border-radius: var(--radius);
202
+ background: var(--paper);
203
+ box-shadow: var(--shadow);
204
+ margin-bottom: 1.5rem;
205
+ }
206
+ .hero::before {
207
+ content: '';
208
+ position: absolute;
209
+ inset: 0;
210
+ background-image: url('/assets/images/topography.svg');
211
+ background-size: 420px 420px;
212
+ opacity: 0.045;
213
+ pointer-events: none;
214
+ }
215
+ .hero::after {
216
+ content: '';
217
+ position: absolute;
218
+ left: 0;
219
+ right: 0;
220
+ bottom: -1px;
221
+ height: 46%;
222
+ background-image: url('/assets/images/park-ridge.svg');
223
+ background-position: center bottom;
224
+ background-repeat: no-repeat;
225
+ background-size: 110% auto;
226
+ opacity: 0.16;
227
+ pointer-events: none;
228
+ }
229
+ .hero > * {
230
+ position: relative;
231
+ z-index: 1;
232
+ }
233
+ .park-emblem {
234
+ width: 92px;
235
+ height: 92px;
236
+ margin: 0 auto 0.85rem;
237
+ color: var(--primary);
238
+ filter: drop-shadow(0 12px 18px rgba(0, 0, 0, 0.18));
239
+ }
240
+ .park-emblem svg {
241
+ width: 100%;
242
+ height: 100%;
243
+ display: block;
244
+ }
245
+ .hero h1 {
246
+ font-family: var(--font-display);
247
+ font-size: 2rem;
248
+ font-weight: 850;
249
+ margin-bottom: 0.5rem;
250
+ color: var(--text);
251
+ }
252
+ .hero p { color: var(--text-light); font-size: 1.05rem; max-width: 540px; margin: 0 auto; font-weight: 700; }
253
+
254
+ /* Post list */
255
+ .post-list { list-style: none; }
256
+ .post-item {
257
+ position: relative;
258
+ overflow: hidden;
259
+ padding: 1.5rem;
260
+ border: 1px solid var(--border);
261
+ border-left: 4px solid var(--primary);
262
+ border-radius: var(--radius);
263
+ background: var(--paper);
264
+ box-shadow: var(--shadow);
265
+ margin-bottom: 1rem;
266
+ transition: border-color 0.18s ease, box-shadow 0.18s ease, transform 0.18s ease;
267
+ }
268
+ .post-item:hover {
269
+ border-color: var(--post-hover-border);
270
+ box-shadow: 0 20px 48px -26px rgba(0, 0, 0, 0.72);
271
+ transform: translateY(-1px);
272
+ }
273
+ .post-item:last-child { border-bottom: 1px solid var(--border); }
274
+ .post-tag {
275
+ display: inline-block;
276
+ font-size: 0.7rem;
277
+ font-weight: 600;
278
+ text-transform: uppercase;
279
+ letter-spacing: 0.05em;
280
+ color: var(--primary);
281
+ background: var(--primary-soft);
282
+ padding: 0.15rem 0.5rem;
283
+ border-radius: 4px;
284
+ margin-bottom: 0.5rem;
285
+ }
286
+ .post-title { font-family: var(--font-display); font-size: 1.375rem; font-weight: 750; margin-bottom: 0.4rem; }
287
+ .post-title a { color: var(--text); }
288
+ .post-title a:hover { color: var(--accent-strong); opacity: 1; }
289
+ .post-meta {
290
+ color: var(--text-lighter);
291
+ font-size: 0.8125rem;
292
+ margin-bottom: 0.6rem;
293
+ display: flex;
294
+ align-items: center;
295
+ gap: 0.5rem;
296
+ flex-wrap: wrap;
297
+ font-weight: 800;
298
+ text-transform: uppercase;
299
+ }
300
+ .post-meta .sep { color: var(--border); }
301
+ .post-excerpt { font-family: var(--font-serif); color: var(--text-light); line-height: 1.72; font-size: 0.975rem; }
302
+ .read-more { display: inline-block; margin-top: 0.75rem; font-size: 0.875rem; font-weight: 800; color: var(--accent-strong); }
303
+ .read-more:hover { color: var(--primary-hover); }
304
+
305
+ /* Newsletter */
306
+ .newsletter-card {
307
+ position: relative;
308
+ overflow: hidden;
309
+ display: grid;
310
+ grid-template-columns: minmax(0, 1fr) minmax(360px, 0.9fr);
311
+ gap: 1.5rem;
312
+ align-items: center;
313
+ padding: 1.5rem;
314
+ margin: 0 0 1rem;
315
+ border: 1px solid var(--newsletter-border);
316
+ border-top: 4px solid var(--accent);
317
+ border-radius: var(--radius);
318
+ background:
319
+ linear-gradient(135deg, var(--newsletter-bg), var(--newsletter-bg-end));
320
+ color: var(--on-primary);
321
+ box-shadow: var(--shadow);
322
+ }
323
+ .newsletter-card::before {
324
+ content: '';
325
+ position: absolute;
326
+ inset: 0;
327
+ background-image: url('/assets/images/topography.svg');
328
+ background-size: 360px 360px;
329
+ opacity: 0.09;
330
+ pointer-events: none;
331
+ }
332
+ .newsletter-content,
333
+ .newsletter-form {
334
+ position: relative;
335
+ z-index: 1;
336
+ }
337
+ .newsletter-eyebrow {
338
+ color: var(--accent-soft);
339
+ font-size: 0.72rem;
340
+ font-weight: 700;
341
+ letter-spacing: 0.08em;
342
+ text-transform: uppercase;
343
+ margin-bottom: 0.35rem;
344
+ }
345
+ .newsletter-card h2 {
346
+ font-family: var(--font-display);
347
+ font-size: 1.35rem;
348
+ line-height: 1.2;
349
+ margin-bottom: 0.4rem;
350
+ color: var(--on-primary);
351
+ }
352
+ .newsletter-card p {
353
+ color: var(--on-primary-muted);
354
+ font-family: var(--font-serif);
355
+ font-size: 0.95rem;
356
+ line-height: 1.55;
357
+ }
358
+ .newsletter-form {
359
+ display: flex;
360
+ gap: 0.5rem;
361
+ align-items: center;
362
+ }
363
+ .newsletter-form input {
364
+ min-width: 13rem;
365
+ flex: 1;
366
+ height: 3rem;
367
+ border: 1px solid rgba(255, 247, 231, 0.65);
368
+ border-radius: 6px;
369
+ background: #fff8ea;
370
+ color: #263126;
371
+ padding: 0 0.85rem;
372
+ font: inherit;
373
+ line-height: normal;
374
+ font-weight: 700;
375
+ }
376
+ .newsletter-form input::placeholder { color: #6b6f65; opacity: 1; }
377
+ .newsletter-form button {
378
+ height: 3rem;
379
+ border: 0;
380
+ border-radius: 6px;
381
+ background: var(--accent);
382
+ color: #fff8ea;
383
+ cursor: pointer;
384
+ font: inherit;
385
+ line-height: normal;
386
+ font-weight: 800;
387
+ padding: 0 1rem;
388
+ white-space: nowrap;
389
+ }
390
+ .newsletter-form button:hover {
391
+ background: var(--accent-hover);
392
+ }
393
+
394
+ /* Featured badge */
395
+ .featured-badge {
396
+ display: inline-block;
397
+ background: var(--accent);
398
+ color: #fff8ea;
399
+ font-size: 0.6875rem;
400
+ font-weight: 600;
401
+ padding: 0.125rem 0.5rem;
402
+ border-radius: 4px;
403
+ margin-left: 0.5rem;
404
+ vertical-align: middle;
405
+ text-transform: uppercase;
406
+ letter-spacing: 0.03em;
407
+ }
408
+
409
+ /* Post page */
410
+ .back-link { display: inline-flex; align-items: center; gap: 0.35rem; color: var(--text-light); font-size: 0.875rem; font-weight: 500; margin-bottom: 1.5rem; }
411
+ .back-link:hover { color: var(--accent-strong); }
412
+ .post-header {
413
+ margin-bottom: 2rem;
414
+ padding-bottom: 1.5rem;
415
+ border-bottom: 1px solid var(--border);
416
+ }
417
+ .post-header h1 { font-family: var(--font-display); font-size: 2.25rem; font-weight: 850; margin-bottom: 0.75rem; line-height: 1.25; }
418
+ .post-poster { width: 100%; border-radius: 12px; margin-bottom: 2rem; aspect-ratio: 16/9; object-fit: cover; }
419
+ .author-info { display: flex; align-items: center; gap: 0.75rem; margin-top: 1rem; }
420
+ .author-avatar { width: 36px; height: 36px; border-radius: 50%; object-fit: cover; }
421
+ .author-name { font-weight: 600; font-size: 0.9rem; }
422
+ .author-bio { color: var(--text-light); font-size: 0.8125rem; }
423
+
424
+ /* Post content */
425
+ .post-content { font-family: var(--font-serif); line-height: 1.8; font-size: 1.0625rem; }
426
+ .post-content p { margin-bottom: 1.5rem; }
427
+ .post-content h2 { font-family: var(--font-sans); margin-top: 2.5rem; margin-bottom: 0.75rem; font-size: 1.5rem; font-weight: 700; }
428
+ .post-content h3 { font-family: var(--font-sans); margin-top: 2rem; margin-bottom: 0.5rem; font-size: 1.25rem; font-weight: 600; }
429
+ .post-content img { max-width: 100%; border-radius: var(--radius); margin: 1.5rem 0; }
430
+ .post-content pre { background: var(--bg-soft); color: var(--text); padding: 1.25rem; border-radius: var(--radius); overflow-x: auto; margin: 1.5rem 0; border: 1px solid var(--border); font-size: 0.875rem; }
431
+ .post-content code { font-family: 'Fira Code', 'JetBrains Mono', monospace; font-size: 0.875em; }
432
+ .post-content p code { background: var(--bg-muted); padding: 0.15em 0.4em; border-radius: 4px; font-size: 0.85em; }
433
+ .post-content ul { margin-bottom: 1.5rem; padding-left: 1.5rem; }
434
+ .post-content li { margin-bottom: 0.35rem; }
435
+ .post-content a { text-decoration: underline; text-underline-offset: 2px; }
436
+ .post-content strong { font-weight: 600; }
437
+
438
+ /* Post footer */
439
+ .post-footer { margin-top: 3rem; padding-top: 2rem; border-top: 1px solid var(--border); }
440
+ .post-footer .author-card { display: flex; gap: 1rem; align-items: flex-start; padding: 1.25rem; background: var(--paper); border: 1px solid var(--border); border-radius: var(--radius); }
441
+ .post-footer .author-card img { width: 48px; height: 48px; border-radius: 50%; }
442
+
443
+ /* Footer */
444
+ .footer {
445
+ border-top: 3px solid var(--accent);
446
+ padding: 2rem;
447
+ text-align: center;
448
+ color: var(--on-primary-muted);
449
+ font-size: 0.8125rem;
450
+ margin-top: 2rem;
451
+ background: var(--primary);
452
+ }
453
+ .footer a { color: var(--accent-soft); font-weight: 800; }
454
+
455
+ /* Pagination */
456
+ .pagination { display: flex; justify-content: center; gap: 0.75rem; margin-top: 2.5rem; }
457
+ .pagination a {
458
+ padding: 0.5rem 1.25rem;
459
+ border: 1px solid var(--border);
460
+ border-radius: 6px;
461
+ font-size: 0.875rem;
462
+ font-weight: 500;
463
+ color: var(--text);
464
+ background: var(--paper);
465
+ }
466
+ .pagination a:hover { border-color: var(--accent); color: var(--accent-strong); opacity: 1; }
467
+
468
+ @media (max-width: 640px) {
469
+ .header { padding: 0.75rem 1rem; gap: 0.75rem; }
470
+ .header .site-title { font-size: 0.95rem; white-space: nowrap; }
471
+ .header .site-title svg { height: 18px; width: 18px; }
472
+ .header nav { gap: 0.75rem; }
473
+ .header nav a { font-size: 0.75rem; }
474
+ .park-emblem { width: 76px; height: 76px; }
475
+ .hero h1 { font-size: 1.5rem; }
476
+ .hero::after { height: 38%; background-size: 180% auto; }
477
+ .newsletter-card { grid-template-columns: 1fr; padding: 1.25rem; }
478
+ .newsletter-form { flex-direction: column; align-items: stretch; }
479
+ .newsletter-form input,
480
+ .newsletter-form button { width: 100%; }
481
+ .post-title { font-size: 1.15rem; }
482
+ .post-header h1 { font-size: 1.75rem; }
483
+ .container { padding: 1.5rem 1rem 3rem; }
484
+ }
485
+ </style>
486
+ </head>
487
+ <body>
488
+ <header class="header">
489
+ <a href="/" class="site-title">
490
+ <svg viewBox="0 0 32 32" fill="none" aria-hidden="true">
491
+ <path d="M16 2.5 27.7 8v10.3c0 4.8-3.1 8.7-11.7 11.2C7.4 27 4.3 23.1 4.3 18.3V8L16 2.5Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
492
+ <path d="m8.7 19.2 5.2-6.6 3 3.8 2.3-2.8 4.2 5.6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
493
+ <path d="M11.4 21.8c2.4-1 4.3-.9 6.2 0 1.5.7 2.8.8 4.7 0" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>
494
+ <path d="M16 6.6v3.8M12.9 9.7h6.2" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>
495
+ </svg>
496
+ ${escapeHtml(config.title)}
497
+ </a>
498
+ <nav>
499
+ <a href="/">Posts</a>
500
+ <a href="https://stacksjs.com/docs">Docs</a>
501
+ ${config.enableRss?'<a href="/feed.xml">RSS</a>':""}
502
+ ${config.social.github?`<a href="https://github.com/${config.social.github}" target="_blank" rel="noopener">GitHub</a>`:""}
503
+ </nav>
504
+ </header>
505
+ <main class="container">
506
+ ${content}
507
+ </main>
508
+ <footer class="footer">
509
+ <p>&copy; ${new Date().getFullYear()} ${escapeHtml(config.title)}. Built with <a href="https://stacksjs.org">Stacks</a>.</p>
510
+ </footer>
511
+ </body>
512
+ </html>`}function generateNewsletterCapture(source="blog-static"){return`<section id="newsletter" class="newsletter-card" aria-label="Subscribe to Stacks updates">
513
+ <div class="newsletter-content">
514
+ <div class="newsletter-eyebrow">Trail dispatch</div>
515
+ <h2>Get new Stacks notes by email</h2>
516
+ <p>Short framework updates, release notes, and field guides for building with Stacks.</p>
517
+ </div>
518
+ <form class="newsletter-form" action="https://stacksjs.com/api/email/subscribe" method="POST">
519
+ <input type="hidden" name="source" value="${escapeHtml(source)}">
520
+ <input type="email" name="email" placeholder="you@example.com" autocomplete="email" required>
521
+ <button type="submit">Subscribe</button>
522
+ </form>
523
+ </section>`}function generatePostCard(post,author){const slug=getSlug(post),date=post.published_at?formatDate(post.published_at):"",authorName=author?.name||"Stacks Team",excerpt=(post.excerpt||(post.body||post.content||"").slice(0,220)).replace(/[#*`\[\]]/g,"").trim(),featured=post.is_featured?'<span class="featured-badge">Featured</span>':"",readTime=estimateReadingTime(post.body||post.content||"");return`<li class="post-item">
524
+ <h2 class="post-title"><a href="/posts/${escapeHtml(slug)}/">${escapeHtml(post.title)}${featured}</a></h2>
525
+ <div class="post-meta">
526
+ <span>${escapeHtml(authorName)}</span>
527
+ <span class="sep">&middot;</span>
528
+ <span>${date}</span>
529
+ <span class="sep">&middot;</span>
530
+ <span>${readTime} min read</span>
531
+ </div>
532
+ <p class="post-excerpt">${escapeHtml(excerpt)}${excerpt.length>=200?"...":""}</p>
533
+ <a href="/posts/${escapeHtml(slug)}/" class="read-more">Read more &rarr;</a>
534
+ </li>`}function generatePostPage(post,config,author){const date=post.published_at?formatDate(post.published_at):"",bodyContent=post.body||post.content||"",authorName=author?.name||"Stacks Team",readTime=estimateReadingTime(bodyContent),authorHtml=author?`<div class="author-info">
535
+ ${author.avatar?`<img src="${escapeHtml(author.avatar)}" alt="${escapeHtml(author.name)}" class="author-avatar" />`:""}
536
+ <div>
537
+ <div class="author-name">${escapeHtml(author.name)}</div>
538
+ ${author.bio?`<div class="author-bio">${escapeHtml(author.bio.slice(0,100))}</div>`:""}
539
+ </div>
540
+ </div>`:`<div class="author-info">
541
+ <div>
542
+ <div class="author-name">${escapeHtml(authorName)}</div>
543
+ </div>
544
+ </div>`,authorFooter=author?`<div class="post-footer">
545
+ <div class="author-card">
546
+ ${author.avatar?`<img src="${escapeHtml(author.avatar)}" alt="${escapeHtml(author.name)}" />`:""}
547
+ <div>
548
+ <div class="author-name">${escapeHtml(author.name)}</div>
549
+ ${author.bio?`<p style="color: var(--text-light); font-size: 0.875rem; margin-top: 0.25rem;">${escapeHtml(author.bio)}</p>`:""}
550
+ </div>
551
+ </div>
552
+ </div>`:"",content=`
553
+ <a href="/" class="back-link">&larr; All posts</a>
554
+ <article>
555
+ <div class="post-header">
556
+ <h1>${escapeHtml(post.title)}</h1>
557
+ <div class="post-meta">
558
+ <span>${escapeHtml(authorName)}</span>
559
+ <span class="sep">&middot;</span>
560
+ <span>${date}</span>
561
+ <span class="sep">&middot;</span>
562
+ <span>${readTime} min read</span>
563
+ </div>
564
+ ${authorHtml}
565
+ </div>
566
+ <div class="post-content">
567
+ ${renderMarkdownish(bodyContent)}
568
+ </div>
569
+ ${authorFooter}
570
+ </article>`;return generateLayout(config,post.title,content,{isPost:!0})}function generateIndexPage(posts,config,authors,page,totalPages){const postCards=posts.map((post)=>{const author=post.author_id?authors.get(post.author_id):void 0;return generatePostCard(post,author)}).join(`
571
+ `);let pagination="";if(totalPages>1){const prev=page>1?`<a href="${page===2?"/":`/page/${page-1}/`}">&larr; Newer</a>`:"",next=page<totalPages?`<a href="/page/${page+1}/">Older &rarr;</a>`:"";pagination=`<div class="pagination">${prev} ${next}</div>`}const content=`
572
+ ${page===1?`
573
+ <div class="hero">
574
+ <div class="park-emblem" aria-hidden="true">
575
+ <svg viewBox="0 0 96 96" fill="none">
576
+ <path d="M48 6 82 22v30c0 18-11 30-34 38C25 82 14 70 14 52V22L48 6Z" fill="var(--primary)" stroke="var(--accent)" stroke-width="3" stroke-linejoin="round"/>
577
+ <path d="M24 58 41 36l10 13 8-10 14 19" stroke="var(--accent-soft)" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
578
+ <path d="M29 69c8-5 15-5 23-1 6 3 12 3 20-1" stroke="var(--river-soft)" stroke-width="5" stroke-linecap="round"/>
579
+ <path d="M48 18v13M38 31h20" stroke="var(--accent-soft)" stroke-width="5" stroke-linecap="round"/>
580
+ <path d="m28 64 8-13 8 13H28ZM59 65l7-12 7 12H59Z" fill="var(--canopy)"/>
581
+ </svg>
582
+ </div>
583
+ <h1>${escapeHtml(config.title)}</h1>
584
+ <p>${escapeHtml(config.description)}</p>
585
+ </div>`:""}
586
+ ${page===1?generateNewsletterCapture():""}
587
+ <ul class="post-list">
588
+ ${postCards}
589
+ </ul>
590
+ ${pagination}`;return generateLayout(config,config.title,content)}function generateRssFeed(posts,config,domain){const items=posts.map((post)=>{const slug=getSlug(post),pubDate=post.published_at?formatRssDate(post.published_at):"",description=post.excerpt||(post.body||post.content||"").slice(0,300);return` <item>
591
+ <title>${escapeXml(post.title)}</title>
592
+ <link>https://${domain}/posts/${escapeXml(slug)}/</link>
593
+ <guid>https://${domain}/posts/${escapeXml(slug)}/</guid>
594
+ <description>${escapeXml(description)}</description>
595
+ ${pubDate?`<pubDate>${pubDate}</pubDate>`:""}
596
+ </item>`}).join(`
597
+ `);return`<?xml version="1.0" encoding="UTF-8"?>
598
+ <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
599
+ <channel>
600
+ <title>${escapeXml(config.title)}</title>
601
+ <link>https://${domain}/</link>
602
+ <description>${escapeXml(config.description)}</description>
603
+ <atom:link href="https://${domain}/feed.xml" rel="self" type="application/rss+xml"/>
604
+ <language>en-us</language>
605
+ <lastBuildDate>${new Date().toUTCString()}</lastBuildDate>
606
+ ${items}
607
+ </channel>
608
+ </rss>`}function generateSitemap(posts,_config,domain){const urls=posts.map((post)=>{const slug=getSlug(post),lastmod=post.updated_at||post.published_at||"";return` <url>
609
+ <loc>https://${domain}/posts/${escapeXml(slug)}/</loc>
610
+ ${lastmod?`<lastmod>${new Date(lastmod).toISOString().split("T")[0]}</lastmod>`:""}
611
+ <changefreq>weekly</changefreq>
612
+ </url>`}).join(`
613
+ `);return`<?xml version="1.0" encoding="UTF-8"?>
614
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
615
+ <url>
616
+ <loc>https://${domain}/</loc>
617
+ <changefreq>daily</changefreq>
618
+ <priority>1.0</priority>
619
+ </url>
620
+ ${urls}
621
+ </urlset>`}function ensureDir(dir){mkdirSync(dir,{recursive:!0})}function getDefaultBlogPosts(){return[{id:1,title:"Introducing Stacks: A Full-Stack Framework for the Modern Web",slug:"introducing-stacks",content:"We are thrilled to announce the official launch of Stacks, a full-stack framework.",body:`We are thrilled to announce the official launch of **Stacks**, a full-stack framework designed to make building web applications, APIs, cloud infrastructure, and libraries a delightful experience.
622
+
623
+ ## Why Stacks?
624
+
625
+ The JavaScript ecosystem is incredibly rich, but building a production-ready application still requires gluing together dozens of tools, configurations, and deployment pipelines. Stacks changes that.
626
+
627
+ With Stacks, you get a **unified, batteries-included framework** that handles everything from your database models and API routes to cloud infrastructure and documentation sites \u2014 all from a single, cohesive project.
628
+
629
+ ## What Makes Stacks Different
630
+
631
+ - **Model-View-Action (MVA)**: A fresh take on MVC that emphasizes clarity and simplicity
632
+ - **Type-Safe by Default**: Built on TypeScript with deep type inference throughout
633
+ - **Cloud-Native**: Define your AWS infrastructure in \`config/cloud.ts\` and deploy with \`./buddy deploy\`
634
+ - **Zero-Config DX**: Linting, testing, CI/CD pipelines, and documentation generation \u2014 all preconfigured
635
+ - **Library Extraction**: Build your app, then extract reusable STX components and TypeScript functions as publishable packages
636
+
637
+ ## Getting Started
638
+
639
+ Getting started with Stacks is simple:
640
+
641
+ \`\`\`bash
642
+ panx @stacksjs/buddy new my-app
643
+ cd my-app
644
+ ./buddy dev
645
+ \`\`\`
646
+
647
+ This gives you a fully configured project with a dev server, database, API routes, and more \u2014 all ready to go.
648
+
649
+ ## What's Next
650
+
651
+ We are actively working on expanding Stacks with more features, better documentation, and a growing ecosystem of plugins. Follow us on [GitHub](https://github.com/stacksjs/stacks) and join the conversation.
652
+
653
+ The future of full-stack development is here. Let's build something great together.`,excerpt:"We are thrilled to announce the official launch of Stacks, a full-stack framework designed to make building web applications, APIs, and cloud infrastructure a delightful experience.",status:"published",published_at:"2026-02-20T10:00:00.000Z",views:1250,is_featured:1,created_at:"2026-02-20T10:00:00.000Z",updated_at:"2026-02-20T10:00:00.000Z"},{id:2,title:"Deploying to AWS with a Single Command",slug:"deploying-to-aws",content:"Learn how Stacks makes cloud deployment as simple as running ./buddy deploy.",body:`One of the most powerful features of Stacks is its built-in cloud deployment pipeline. Instead of wrestling with Terraform, CDK, or manual AWS console clicks, you can deploy your entire application \u2014 API, frontend, docs, and blog \u2014 with a single command.
654
+
655
+ ## The Problem with Cloud Deployment
656
+
657
+ Most teams spend weeks setting up their deployment pipelines. You need to configure CloudFormation or Terraform templates, set up CI/CD, manage SSL certificates, configure CloudFront distributions, and handle database migrations. It's tedious and error-prone.
658
+
659
+ ## How Stacks Solves It
660
+
661
+ With Stacks, your cloud infrastructure is defined in a simple TypeScript configuration file:
662
+
663
+ \`\`\`typescript
664
+ // config/cloud.ts
665
+ export const tsCloud = {
666
+ project: { name: 'my-app', region: 'us-east-1' },
667
+ infrastructure: {
668
+ compute: { instances: 1, size: 'small' },
669
+ storage: {
670
+ public: { website: { indexDocument: 'index.html' } },
671
+ docs: { website: { indexDocument: 'index.html' } },
672
+ },
673
+ ssl: { enabled: true, domains: ['myapp.com', 'docs.myapp.com'] },
674
+ dns: { domain: 'myapp.com' },
675
+ },
676
+ }
677
+ \`\`\`
678
+
679
+ Then deploy everything:
680
+
681
+ \`\`\`bash
682
+ ./buddy deploy --yes
683
+ \`\`\`
684
+
685
+ ## What Happens Under the Hood
686
+
687
+ When you run \`./buddy deploy\`, Stacks:
688
+
689
+ - **Generates a CloudFormation template** from your config
690
+ - **Provisions EC2 instances** with your Bun application
691
+ - **Creates S3 buckets** for static sites (frontend, docs, blog)
692
+ - **Sets up CloudFront** distributions with SSL certificates
693
+ - **Configures Route53** DNS records
694
+ - **Runs database migrations** on the remote server
695
+ - **Uploads static assets** to S3 with proper cache headers
696
+ - **Invalidates CloudFront** caches for instant updates
697
+
698
+ All of this happens automatically, with progress output so you know exactly what's happening.
699
+
700
+ ## Zero-Downtime Updates
701
+
702
+ Subsequent deployments are incremental. Stacks detects what changed and only updates the necessary resources. Your users never experience downtime.
703
+
704
+ ## Try It Yourself
705
+
706
+ If you have an AWS account, you can deploy a Stacks app in under 10 minutes. Check out our [deployment guide](https://stacksjs.com/docs/bootcamp/deploy) to get started.`,excerpt:"Learn how Stacks makes cloud deployment as simple as running a single command. No Terraform, no CDK \u2014 just ./buddy deploy.",status:"published",published_at:"2026-02-18T14:00:00.000Z",views:840,is_featured:0,created_at:"2026-02-18T14:00:00.000Z",updated_at:"2026-02-18T14:00:00.000Z"},{id:3,title:"Building Type-Safe APIs with the Stacks ORM",slug:"type-safe-apis",content:"Discover how Stacks models auto-generate fully typed API endpoints.",body:`One of the most tedious parts of building a web application is writing CRUD boilerplate. With Stacks, your models automatically generate fully typed API endpoints, database migrations, factories, and seeders.
707
+
708
+ ## Define a Model, Get an API
709
+
710
+ Here's what a typical Stacks model looks like:
711
+
712
+ \`\`\`typescript
713
+ // app/Models/Post.ts
714
+ export default defineModel({
715
+ name: 'Post',
716
+ table: 'posts',
717
+ traits: {
718
+ useTimestamps: true,
719
+ useApi: {
720
+ uri: 'posts',
721
+ routes: ['index', 'store', 'show', 'update', 'destroy'],
722
+ },
723
+ },
724
+ attributes: {
725
+ title: {
726
+ validation: { rule: schema.string().min(3).max(255) },
727
+ factory: faker => faker.lorem.sentence(),
728
+ },
729
+ slug: {
730
+ unique: true,
731
+ validation: { rule: schema.string().min(3).max(255) },
732
+ },
733
+ body: {
734
+ validation: { rule: schema.string() },
735
+ },
736
+ },
737
+ })
738
+ \`\`\`
739
+
740
+ From this single file, Stacks generates:
741
+
742
+ - **API routes**: \`GET /api/posts\`, \`POST /api/posts\`, \`GET /api/posts/:id\`, \`PATCH /api/posts/:id\`, \`DELETE /api/posts/:id\`
743
+ - **Database migration**: Creates the \`posts\` table with all columns
744
+ - **Factory**: Generates realistic test data using Faker
745
+ - **Seeder**: Populates your database with sample data
746
+ - **TypeScript types**: Full type inference for queries and responses
747
+
748
+ ## Type-Safe Queries
749
+
750
+ The Stacks ORM is built on Kysely, giving you fully type-safe database queries:
751
+
752
+ \`\`\`typescript
753
+ const posts = await db
754
+ .selectFrom('posts')
755
+ .where('status', '=', 'published')
756
+ .orderBy('published_at', 'desc')
757
+ .selectAll()
758
+ .execute()
759
+ \`\`\`
760
+
761
+ Every column name, operator, and value is type-checked at compile time. Typos become compile errors, not runtime bugs.
762
+
763
+ ## Relationships
764
+
765
+ Stacks supports all common relationship types with a clean, declarative syntax:
766
+
767
+ \`\`\`typescript
768
+ export default defineModel({
769
+ name: 'Post',
770
+ belongsTo: ['Author'],
771
+ traits: { taggable: true, categorizable: true, commentables: true },
772
+ })
773
+ \`\`\`
774
+
775
+ ## What's Next
776
+
777
+ We are working on even more ORM features \u2014 real-time subscriptions, full-text search integration, and automatic OpenAPI documentation generation. Stay tuned.`,excerpt:"Discover how Stacks models auto-generate fully typed API endpoints, database migrations, and more from a single model definition.",status:"published",published_at:"2026-02-15T09:00:00.000Z",views:620,is_featured:0,created_at:"2026-02-15T09:00:00.000Z",updated_at:"2026-02-15T09:00:00.000Z"},{id:4,title:"Meet Buddy: Your CLI Companion for Stacks Development",slug:"meet-buddy-cli",content:"Buddy is the CLI tool that powers your entire Stacks development workflow.",body:`Every great framework needs a great CLI. In Stacks, that CLI is called **Buddy** \u2014 your companion for development, testing, deployment, and everything in between.
778
+
779
+ ## What Can Buddy Do?
780
+
781
+ Buddy is the single entry point for every task in your Stacks project:
782
+
783
+ \`\`\`bash
784
+ ./buddy dev # Start the development server
785
+ ./buddy build # Build for production
786
+ ./buddy test # Run your test suite
787
+ ./buddy deploy # Deploy to the cloud
788
+ ./buddy generate # Generate models, migrations, and more
789
+ ./buddy lint # Lint and format your code
790
+ ./buddy key:generate # Generate a new application key
791
+ \`\`\`
792
+
793
+ ## Developer Experience First
794
+
795
+ Buddy is designed to feel fast and intuitive. Some highlights:
796
+
797
+ - **Lazy-loaded commands**: Only the command you run is loaded, keeping startup under 100ms
798
+ - **Interactive mode**: Run \`./buddy\` with no arguments for a guided menu
799
+ - **Verbose mode**: Add \`--verbose\` to any command for detailed output
800
+ - **Tab completion**: Full shell completion support for bash and zsh
801
+
802
+ ## Extensible
803
+
804
+ You can add your own commands by creating files in \`app/Commands/\`:
805
+
806
+ \`\`\`typescript
807
+ // app/Commands/Greet.ts
808
+ export default function (buddy) {
809
+ buddy
810
+ .command('greet <name>', 'Greet someone')
811
+ .action((name) => {
812
+ console.log('Hello, ' + name + '!')
813
+ })
814
+ }
815
+ \`\`\`
816
+
817
+ Then run it:
818
+
819
+ \`\`\`bash
820
+ ./buddy greet World
821
+ # Hello, World!
822
+ \`\`\`
823
+
824
+ ## Built on Bun
825
+
826
+ Buddy runs on [Bun](https://bun.sh), giving it near-instant startup times and excellent TypeScript support without a build step. Your commands are executed directly from TypeScript source.
827
+
828
+ ## Try It
829
+
830
+ Start a new Stacks project and explore what Buddy can do. You might be surprised how much a good CLI can improve your workflow.`,excerpt:"Buddy is the CLI tool that powers your entire Stacks development workflow \u2014 from dev server to deployment, all in one place.",status:"published",published_at:"2026-02-12T11:30:00.000Z",views:450,is_featured:0,created_at:"2026-02-12T11:30:00.000Z",updated_at:"2026-02-12T11:30:00.000Z"},{id:5,title:"Documentation as a First-Class Citizen",slug:"documentation-first-class",content:"How Stacks makes writing and deploying documentation effortless.",body:`Good documentation is the difference between a framework people try and a framework people adopt. That's why Stacks treats documentation as a **first-class feature**, not an afterthought.
831
+
832
+ ## Write Docs, Deploy Docs
833
+
834
+ Every Stacks project comes with a \`docs/\` directory preconfigured. Write your documentation in Markdown, and Stacks builds it into a beautiful static site using [BunPress](https://github.com/stacksjs/bunpress).
835
+
836
+ \`\`\`bash
837
+ ./buddy dev:docs # Preview docs locally
838
+ ./buddy deploy # Docs deploy automatically to docs.yoursite.com
839
+ \`\`\`
840
+
841
+ ## What You Get
842
+
843
+ - **Beautiful defaults**: Clean, responsive design with dark mode support
844
+ - **Sidebar navigation**: Automatically generated from your file structure
845
+ - **Syntax highlighting**: Code blocks with proper language highlighting
846
+ - **Search**: Built-in search functionality
847
+ - **Automatic deployment**: Docs deploy to a dedicated S3 bucket + CloudFront CDN
848
+
849
+ ## Powered by BunPress
850
+
851
+ Under the hood, Stacks uses BunPress \u2014 a fast, minimal static site generator built on Bun. It takes your Markdown files and produces optimized HTML with:
852
+
853
+ - Table of contents generation
854
+ - Anchor links for headings
855
+ - Responsive images
856
+ - SEO-friendly output with sitemaps and meta tags
857
+
858
+ ## Documentation is Part of Your Deploy
859
+
860
+ When you run \`./buddy deploy\`, your documentation is automatically:
861
+
862
+ - Built from the \`docs/\` directory
863
+ - Uploaded to an S3 bucket
864
+ - Served via CloudFront at \`docs.yourdomain.com\`
865
+ - Cache-invalidated for instant updates
866
+
867
+ No separate CI/CD pipeline needed. No extra configuration. It just works.
868
+
869
+ ## Start Documenting
870
+
871
+ Great software deserves great documentation. With Stacks, there's no excuse not to write it.`,excerpt:"How Stacks makes writing and deploying documentation effortless \u2014 from Markdown to a deployed docs site in seconds.",status:"published",published_at:"2026-02-10T08:00:00.000Z",views:380,is_featured:0,created_at:"2026-02-10T08:00:00.000Z",updated_at:"2026-02-10T08:00:00.000Z"}]}export async function buildBlogSite(options){const{config,outDir}=options,domain=`${config.subdomain}.stacksjs.com`;ensureDir(outDir);ensureDir(join(outDir,"posts"));copyBlogFonts(outDir);copyBlogImages(outDir);let posts,usedDefaults=!1;try{const dbPosts=await fetchPublishedPosts(),hasRealContent=dbPosts.some((p)=>p.slug&&p.slug!=="null"&&!p.title.endsWith("."));if(dbPosts.length===0||!hasRealContent){posts=getDefaultBlogPosts();usedDefaults=!0}else posts=dbPosts}catch{console.log(" Database not available, using default blog posts");posts=getDefaultBlogPosts();usedDefaults=!0}if(usedDefaults)console.log(" Using built-in blog posts (seed your database for custom content)");const authors=new Map;try{const authorRows=await fetchAuthors();for(const author of authorRows)authors.set(author.id,author)}catch{}const postsPerPage=config.postsPerPage||10,totalPages=Math.max(1,Math.ceil(posts.length/postsPerPage));for(let page=1;page<=totalPages;page++){const start=(page-1)*postsPerPage,pagePosts=posts.slice(start,start+postsPerPage),html=generateIndexPage(pagePosts,config,authors,page,totalPages);if(page===1)writeFileSync(join(outDir,"index.html"),html);if(totalPages>1){const pageDir=join(outDir,"page",String(page));ensureDir(pageDir);writeFileSync(join(pageDir,"index.html"),html)}}const generatedSlugs=new Set;for(const post of posts){const slug=getSlug(post);if(generatedSlugs.has(slug))continue;generatedSlugs.add(slug);const postDir=join(outDir,"posts",slug);ensureDir(postDir);const author=post.author_id?authors.get(post.author_id):void 0,html=generatePostPage(post,config,author);writeFileSync(join(postDir,"index.html"),html)}if(config.enableRss){const rss=generateRssFeed(posts.slice(0,20),config,domain);writeFileSync(join(outDir,"feed.xml"),rss)}if(config.enableSitemap){const sitemap=generateSitemap(posts,config,domain);writeFileSync(join(outDir,"sitemap.xml"),sitemap)}const notFoundContent=`
872
+ <div style="text-align: center; padding: 6rem 0;">
873
+ <p style="font-size: 5rem; font-weight: 800; letter-spacing: -0.03em; margin-bottom: 0.5rem;">404</p>
874
+ <p style="color: var(--text-light); margin-bottom: 1.5rem; font-size: 1.125rem;">This page could not be found.</p>
875
+ <a href="/" style="display: inline-block; padding: 0.6rem 1.5rem; background: var(--primary); color: white; border-radius: 6px; font-weight: 500; font-size: 0.9rem;">Back to blog</a>
876
+ </div>`;writeFileSync(join(outDir,"404.html"),generateLayout(config,"404 - Not Found",notFoundContent));console.log(` Generated ${generatedSlugs.size} post pages, ${totalPages} index page(s)`);if(config.enableRss)console.log(" Generated feed.xml");if(config.enableSitemap)console.log(" Generated sitemap.xml")}