nextblogkit 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +951 -0
  3. package/dist/admin/index.cjs +2465 -0
  4. package/dist/admin/index.cjs.map +1 -0
  5. package/dist/admin/index.d.cts +44 -0
  6. package/dist/admin/index.d.ts +44 -0
  7. package/dist/admin/index.js +2438 -0
  8. package/dist/admin/index.js.map +1 -0
  9. package/dist/api/categories.cjs +82 -0
  10. package/dist/api/categories.cjs.map +1 -0
  11. package/dist/api/categories.d.cts +27 -0
  12. package/dist/api/categories.d.ts +27 -0
  13. package/dist/api/categories.js +77 -0
  14. package/dist/api/categories.js.map +1 -0
  15. package/dist/api/media.cjs +113 -0
  16. package/dist/api/media.cjs.map +1 -0
  17. package/dist/api/media.d.cts +22 -0
  18. package/dist/api/media.d.ts +22 -0
  19. package/dist/api/media.js +109 -0
  20. package/dist/api/media.js.map +1 -0
  21. package/dist/api/posts.cjs +103 -0
  22. package/dist/api/posts.cjs.map +1 -0
  23. package/dist/api/posts.d.cts +27 -0
  24. package/dist/api/posts.d.ts +27 -0
  25. package/dist/api/posts.js +98 -0
  26. package/dist/api/posts.js.map +1 -0
  27. package/dist/api/rss.cjs +25 -0
  28. package/dist/api/rss.cjs.map +1 -0
  29. package/dist/api/rss.d.cts +5 -0
  30. package/dist/api/rss.d.ts +5 -0
  31. package/dist/api/rss.js +23 -0
  32. package/dist/api/rss.js.map +1 -0
  33. package/dist/api/settings.cjs +40 -0
  34. package/dist/api/settings.cjs.map +1 -0
  35. package/dist/api/settings.d.cts +17 -0
  36. package/dist/api/settings.d.ts +17 -0
  37. package/dist/api/settings.js +37 -0
  38. package/dist/api/settings.js.map +1 -0
  39. package/dist/api/sitemap.cjs +25 -0
  40. package/dist/api/sitemap.cjs.map +1 -0
  41. package/dist/api/sitemap.d.cts +5 -0
  42. package/dist/api/sitemap.d.ts +5 -0
  43. package/dist/api/sitemap.js +23 -0
  44. package/dist/api/sitemap.js.map +1 -0
  45. package/dist/chunk-4NKOJYWJ.js +68 -0
  46. package/dist/chunk-4NKOJYWJ.js.map +1 -0
  47. package/dist/chunk-4PY224XM.js +103 -0
  48. package/dist/chunk-4PY224XM.js.map +1 -0
  49. package/dist/chunk-64HUVJOZ.js +446 -0
  50. package/dist/chunk-64HUVJOZ.js.map +1 -0
  51. package/dist/chunk-6HKMZOI4.cjs +48 -0
  52. package/dist/chunk-6HKMZOI4.cjs.map +1 -0
  53. package/dist/chunk-A2S32RZN.js +138 -0
  54. package/dist/chunk-A2S32RZN.js.map +1 -0
  55. package/dist/chunk-E2QLTHKN.cjs +70 -0
  56. package/dist/chunk-E2QLTHKN.cjs.map +1 -0
  57. package/dist/chunk-JLPJKNRZ.js +37 -0
  58. package/dist/chunk-JLPJKNRZ.js.map +1 -0
  59. package/dist/chunk-JM7QRXXK.js +330 -0
  60. package/dist/chunk-JM7QRXXK.js.map +1 -0
  61. package/dist/chunk-KDZER3PU.cjs +43 -0
  62. package/dist/chunk-KDZER3PU.cjs.map +1 -0
  63. package/dist/chunk-N5MKAD7J.cjs +109 -0
  64. package/dist/chunk-N5MKAD7J.cjs.map +1 -0
  65. package/dist/chunk-QE4VLQYN.cjs +337 -0
  66. package/dist/chunk-QE4VLQYN.cjs.map +1 -0
  67. package/dist/chunk-R6MO3QIP.js +46 -0
  68. package/dist/chunk-R6MO3QIP.js.map +1 -0
  69. package/dist/chunk-U2ROR6AY.cjs +476 -0
  70. package/dist/chunk-U2ROR6AY.cjs.map +1 -0
  71. package/dist/chunk-ZP5XRVVH.cjs +141 -0
  72. package/dist/chunk-ZP5XRVVH.cjs.map +1 -0
  73. package/dist/cli/index.cjs +1308 -0
  74. package/dist/components/index.cjs +541 -0
  75. package/dist/components/index.cjs.map +1 -0
  76. package/dist/components/index.d.cts +165 -0
  77. package/dist/components/index.d.ts +165 -0
  78. package/dist/components/index.js +527 -0
  79. package/dist/components/index.js.map +1 -0
  80. package/dist/editor/index.cjs +1083 -0
  81. package/dist/editor/index.cjs.map +1 -0
  82. package/dist/editor/index.d.cts +133 -0
  83. package/dist/editor/index.d.ts +133 -0
  84. package/dist/editor/index.js +1051 -0
  85. package/dist/editor/index.js.map +1 -0
  86. package/dist/index-Cgzphklp.d.ts +266 -0
  87. package/dist/index-vjlZDWNr.d.cts +266 -0
  88. package/dist/index.cjs +368 -0
  89. package/dist/index.cjs.map +1 -0
  90. package/dist/index.d.cts +27 -0
  91. package/dist/index.d.ts +27 -0
  92. package/dist/index.js +208 -0
  93. package/dist/index.js.map +1 -0
  94. package/dist/lib/index.cjs +120 -0
  95. package/dist/lib/index.cjs.map +1 -0
  96. package/dist/lib/index.d.cts +4 -0
  97. package/dist/lib/index.d.ts +4 -0
  98. package/dist/lib/index.js +7 -0
  99. package/dist/lib/index.js.map +1 -0
  100. package/dist/styles/admin.css +657 -0
  101. package/dist/styles/blog.css +851 -0
  102. package/dist/styles/editor.css +452 -0
  103. package/dist/styles/globals.css +270 -0
  104. package/dist/styles/prose.css +299 -0
  105. package/dist/types-CBEEBR4A.d.cts +732 -0
  106. package/dist/types-CBEEBR4A.d.ts +732 -0
  107. package/package.json +134 -0
@@ -0,0 +1,1308 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __esm = (fn, res) => function __init() {
10
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
11
+ };
12
+ var __export = (target, all) => {
13
+ for (var name in all)
14
+ __defProp(target, name, { get: all[name], enumerable: true });
15
+ };
16
+ var __copyProps = (to, from, except, desc) => {
17
+ if (from && typeof from === "object" || typeof from === "function") {
18
+ for (let key of __getOwnPropNames(from))
19
+ if (!__hasOwnProp.call(to, key) && key !== except)
20
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
21
+ }
22
+ return to;
23
+ };
24
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
25
+ // If the importer is in node compatibility mode or this is not an ESM
26
+ // file that has been converted to a CommonJS file using a Babel-
27
+ // compatible transform (i.e. "__esModule" has not been set), then set
28
+ // "default" to the CommonJS "module.exports" for node compatibility.
29
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
30
+ mod
31
+ ));
32
+
33
+ // src/lib/config.ts
34
+ var config_exports = {};
35
+ __export(config_exports, {
36
+ defineConfig: () => defineConfig,
37
+ getBlogConfig: () => getBlogConfig,
38
+ getConfig: () => getConfig,
39
+ getEnvConfig: () => getEnvConfig
40
+ });
41
+ function getEnvConfig() {
42
+ if (cachedEnv) return cachedEnv;
43
+ const result = envSchema.safeParse(process.env);
44
+ if (!result.success) {
45
+ const missing = result.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`);
46
+ throw new Error(`NextBlogKit configuration error:
47
+ ${missing.join("\n")}`);
48
+ }
49
+ cachedEnv = result.data;
50
+ return cachedEnv;
51
+ }
52
+ function defineConfig(config) {
53
+ return config;
54
+ }
55
+ function getConfig(userConfig) {
56
+ if (cachedConfig && !userConfig) return cachedConfig;
57
+ const merged = {
58
+ ...defaultConfig,
59
+ ...userConfig,
60
+ editor: { ...defaultConfig.editor, ...userConfig?.editor },
61
+ seo: { ...defaultConfig.seo, ...userConfig?.seo },
62
+ auth: { ...defaultConfig.auth, ...userConfig?.auth },
63
+ features: { ...defaultConfig.features, ...userConfig?.features },
64
+ theme: {
65
+ ...defaultConfig.theme,
66
+ ...userConfig?.theme,
67
+ variables: { ...defaultConfig.theme.variables, ...userConfig?.theme?.variables }
68
+ },
69
+ hooks: { ...defaultConfig.hooks, ...userConfig?.hooks }
70
+ };
71
+ if (!userConfig) {
72
+ cachedConfig = merged;
73
+ }
74
+ return merged;
75
+ }
76
+ function getBlogConfig() {
77
+ const env = getEnvConfig();
78
+ const config = getConfig();
79
+ return {
80
+ ...config,
81
+ siteUrl: env.NEXTBLOGKIT_SITE_URL,
82
+ siteName: env.NEXTBLOGKIT_SITE_NAME,
83
+ metadata: {
84
+ title: `Blog | ${env.NEXTBLOGKIT_SITE_NAME}`,
85
+ description: `Latest posts from ${env.NEXTBLOGKIT_SITE_NAME}`,
86
+ openGraph: {
87
+ title: `Blog | ${env.NEXTBLOGKIT_SITE_NAME}`,
88
+ description: `Latest posts from ${env.NEXTBLOGKIT_SITE_NAME}`,
89
+ url: `${env.NEXTBLOGKIT_SITE_URL}${config.basePath}`,
90
+ siteName: env.NEXTBLOGKIT_SITE_NAME,
91
+ type: "website"
92
+ }
93
+ }
94
+ };
95
+ }
96
+ var import_zod, envSchema, cachedEnv, defaultConfig, cachedConfig;
97
+ var init_config = __esm({
98
+ "src/lib/config.ts"() {
99
+ "use strict";
100
+ import_zod = require("zod");
101
+ envSchema = import_zod.z.object({
102
+ NEXTBLOGKIT_MONGODB_URI: import_zod.z.string().min(1, "MongoDB URI is required"),
103
+ NEXTBLOGKIT_R2_ACCOUNT_ID: import_zod.z.string().min(1, "R2 Account ID is required"),
104
+ NEXTBLOGKIT_R2_ACCESS_KEY: import_zod.z.string().min(1, "R2 Access Key is required"),
105
+ NEXTBLOGKIT_R2_SECRET_KEY: import_zod.z.string().min(1, "R2 Secret Key is required"),
106
+ NEXTBLOGKIT_R2_BUCKET: import_zod.z.string().min(1, "R2 Bucket name is required"),
107
+ NEXTBLOGKIT_R2_PUBLIC_URL: import_zod.z.string().url("R2 Public URL must be a valid URL"),
108
+ NEXTBLOGKIT_API_KEY: import_zod.z.string().min(32, "API key must be at least 32 characters"),
109
+ NEXTBLOGKIT_SITE_URL: import_zod.z.string().url("Site URL must be a valid URL"),
110
+ NEXTBLOGKIT_SITE_NAME: import_zod.z.string().min(1, "Site name is required")
111
+ });
112
+ cachedEnv = null;
113
+ defaultConfig = {
114
+ basePath: "/blog",
115
+ adminPath: "/admin/blog",
116
+ apiPath: "/api/blog",
117
+ postsPerPage: 10,
118
+ excerptLength: 160,
119
+ codeHighlighter: "shiki",
120
+ editor: {
121
+ blocks: [
122
+ "paragraph",
123
+ "heading",
124
+ "image",
125
+ "codeBlock",
126
+ "blockquote",
127
+ "bulletList",
128
+ "orderedList",
129
+ "taskList",
130
+ "table",
131
+ "embed",
132
+ "horizontalRule",
133
+ "callout",
134
+ "tableOfContents",
135
+ "faq",
136
+ "html"
137
+ ],
138
+ maxImageSize: 10 * 1024 * 1024,
139
+ imageFormats: ["jpg", "jpeg", "png", "gif", "webp", "svg"],
140
+ autosaveInterval: 3e4
141
+ },
142
+ seo: {
143
+ titleTemplate: "%s | %siteName%",
144
+ generateRSS: true,
145
+ generateSitemap: true,
146
+ structuredData: true,
147
+ minContentLength: 300
148
+ },
149
+ auth: {
150
+ strategy: "api-key"
151
+ },
152
+ features: {
153
+ search: true,
154
+ relatedPosts: true,
155
+ readingProgress: true,
156
+ tableOfContents: true,
157
+ shareButtons: true,
158
+ darkMode: true,
159
+ scheduling: true,
160
+ revisionHistory: true,
161
+ imageOptimization: true
162
+ },
163
+ theme: {
164
+ darkMode: true,
165
+ variables: {
166
+ "--nbk-primary": "#2563eb",
167
+ "--nbk-primary-hover": "#1d4ed8",
168
+ "--nbk-text": "#1f2937",
169
+ "--nbk-text-muted": "#6b7280",
170
+ "--nbk-bg": "#ffffff",
171
+ "--nbk-bg-secondary": "#f9fafb",
172
+ "--nbk-card-bg": "#ffffff",
173
+ "--nbk-border": "#e5e7eb",
174
+ "--nbk-radius": "0.5rem",
175
+ "--nbk-font-heading": '"Inter", system-ui, sans-serif',
176
+ "--nbk-font-body": '"Inter", system-ui, sans-serif',
177
+ "--nbk-font-code": '"JetBrains Mono", "Fira Code", monospace'
178
+ }
179
+ },
180
+ hooks: {}
181
+ };
182
+ cachedConfig = null;
183
+ }
184
+ });
185
+
186
+ // src/lib/slug.ts
187
+ function generateSlug(text) {
188
+ return text.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
189
+ }
190
+ async function ensureUniqueSlug(slug, collection, excludeId) {
191
+ let candidate = slug;
192
+ let counter = 1;
193
+ while (true) {
194
+ const query = { slug: candidate };
195
+ if (excludeId) {
196
+ query._id = { $ne: excludeId };
197
+ }
198
+ const existing = await collection.findOne(query);
199
+ if (!existing) return candidate;
200
+ candidate = `${slug}-${counter}`;
201
+ counter++;
202
+ }
203
+ }
204
+ var init_slug = __esm({
205
+ "src/lib/slug.ts"() {
206
+ "use strict";
207
+ }
208
+ });
209
+
210
+ // src/lib/reading-time.ts
211
+ function calculateReadingTime(text) {
212
+ const words = countWords(text);
213
+ return Math.max(1, Math.ceil(words / WORDS_PER_MINUTE));
214
+ }
215
+ function countWords(text) {
216
+ if (!text || !text.trim()) return 0;
217
+ return text.trim().split(/\s+/).length;
218
+ }
219
+ function extractTextFromBlocks(blocks) {
220
+ if (!Array.isArray(blocks)) return "";
221
+ const parts = [];
222
+ function walk(node) {
223
+ if (!node || typeof node !== "object") return;
224
+ const n = node;
225
+ if (typeof n.text === "string") {
226
+ parts.push(n.text);
227
+ }
228
+ if (Array.isArray(n.content)) {
229
+ for (const child of n.content) {
230
+ walk(child);
231
+ }
232
+ }
233
+ }
234
+ for (const block of blocks) {
235
+ walk(block);
236
+ }
237
+ return parts.join(" ");
238
+ }
239
+ var WORDS_PER_MINUTE;
240
+ var init_reading_time = __esm({
241
+ "src/lib/reading-time.ts"() {
242
+ "use strict";
243
+ WORDS_PER_MINUTE = 200;
244
+ }
245
+ });
246
+
247
+ // src/lib/db.ts
248
+ var db_exports = {};
249
+ __export(db_exports, {
250
+ createCategory: () => createCategory,
251
+ createMedia: () => createMedia,
252
+ createPost: () => createPost,
253
+ deleteCategory: () => deleteCategory,
254
+ deleteMedia: () => deleteMedia,
255
+ deletePost: () => deletePost,
256
+ ensureIndexes: () => ensureIndexes,
257
+ getCategoryBySlug: () => getCategoryBySlug,
258
+ getCollection: () => getCollection,
259
+ getDb: () => getDb,
260
+ getPostById: () => getPostById,
261
+ getPostBySlug: () => getPostBySlug,
262
+ getSettings: () => getSettings,
263
+ hardDeletePost: () => hardDeletePost,
264
+ listCategories: () => listCategories,
265
+ listMedia: () => listMedia,
266
+ listPosts: () => listPosts,
267
+ updateCategory: () => updateCategory,
268
+ updateCategoryPostCount: () => updateCategoryPostCount,
269
+ updatePost: () => updatePost,
270
+ updateSettings: () => updateSettings
271
+ });
272
+ async function getDb() {
273
+ if (db) return db;
274
+ const env = getEnvConfig();
275
+ client = new import_mongodb.MongoClient(env.NEXTBLOGKIT_MONGODB_URI);
276
+ await client.connect();
277
+ db = client.db();
278
+ return db;
279
+ }
280
+ async function getCollection(name) {
281
+ const database = await getDb();
282
+ return database.collection(name);
283
+ }
284
+ async function ensureIndexes() {
285
+ const database = await getDb();
286
+ const posts = database.collection("nbk_posts");
287
+ await posts.createIndex({ slug: 1 }, { unique: true });
288
+ await posts.createIndex({ status: 1, publishedAt: -1 });
289
+ await posts.createIndex({ categories: 1, status: 1 });
290
+ await posts.createIndex({ tags: 1, status: 1 });
291
+ await posts.createIndex({ "seo.focusKeyword": 1 });
292
+ await posts.createIndex({ contentText: "text", title: "text", excerpt: "text" });
293
+ const categories = database.collection("nbk_categories");
294
+ await categories.createIndex({ slug: 1 }, { unique: true });
295
+ await categories.createIndex({ order: 1 });
296
+ const media = database.collection("nbk_media");
297
+ await media.createIndex({ createdAt: -1 });
298
+ await media.createIndex({ r2Key: 1 }, { unique: true });
299
+ }
300
+ async function createPost(input, defaultAuthor) {
301
+ const col = await getCollection("nbk_posts");
302
+ const slug = await ensureUniqueSlug(
303
+ input.slug || generateSlug(input.title),
304
+ col
305
+ );
306
+ const contentText = input.contentText || extractTextFromBlocks(input.content || []);
307
+ const wordCount = countWords(contentText);
308
+ const readingTime = calculateReadingTime(contentText);
309
+ const excerpt = input.excerpt || contentText.slice(0, 160).replace(/\s+\S*$/, "") + "...";
310
+ const now = /* @__PURE__ */ new Date();
311
+ const doc = {
312
+ title: input.title,
313
+ slug,
314
+ excerpt,
315
+ content: input.content || [],
316
+ contentHTML: input.contentHTML || "",
317
+ contentText,
318
+ coverImage: input.coverImage,
319
+ categories: input.categories || [],
320
+ tags: input.tags || [],
321
+ author: input.author || defaultAuthor || { name: "Admin" },
322
+ seo: {
323
+ ogType: "article",
324
+ noIndex: false,
325
+ ...input.seo
326
+ },
327
+ status: input.status || "draft",
328
+ publishedAt: input.status === "published" ? now : input.publishedAt,
329
+ scheduledAt: input.scheduledAt,
330
+ readingTime,
331
+ wordCount,
332
+ version: 1,
333
+ revisions: [],
334
+ createdAt: now,
335
+ updatedAt: now
336
+ };
337
+ const result = await col.insertOne(doc);
338
+ return { _id: result.insertedId, ...doc };
339
+ }
340
+ async function updatePost(id, input) {
341
+ const col = await getCollection("nbk_posts");
342
+ const objectId = new import_mongodb.ObjectId(id);
343
+ const existing = await col.findOne({ _id: objectId });
344
+ if (!existing) return null;
345
+ const updates = { ...input, updatedAt: /* @__PURE__ */ new Date() };
346
+ if (input.slug && input.slug !== existing.slug) {
347
+ updates.slug = await ensureUniqueSlug(input.slug, col, id);
348
+ }
349
+ if (input.content) {
350
+ const contentText = input.contentText || extractTextFromBlocks(input.content);
351
+ updates.contentText = contentText;
352
+ updates.wordCount = countWords(contentText);
353
+ updates.readingTime = calculateReadingTime(contentText);
354
+ if (!input.excerpt) {
355
+ updates.excerpt = contentText.slice(0, 160).replace(/\s+\S*$/, "") + "...";
356
+ }
357
+ const revision = {
358
+ version: existing.version || 1,
359
+ title: existing.title,
360
+ content: existing.content,
361
+ contentHTML: existing.contentHTML,
362
+ savedAt: /* @__PURE__ */ new Date()
363
+ };
364
+ const revisions = [...existing.revisions || [], revision].slice(-10);
365
+ updates.revisions = revisions;
366
+ updates.version = (existing.version || 1) + 1;
367
+ }
368
+ if (input.status === "published" && existing.status !== "published") {
369
+ updates.publishedAt = /* @__PURE__ */ new Date();
370
+ }
371
+ await col.updateOne({ _id: objectId }, { $set: updates });
372
+ return await col.findOne({ _id: objectId });
373
+ }
374
+ async function deletePost(id) {
375
+ const col = await getCollection("nbk_posts");
376
+ const result = await col.updateOne(
377
+ { _id: new import_mongodb.ObjectId(id) },
378
+ { $set: { status: "archived", updatedAt: /* @__PURE__ */ new Date() } }
379
+ );
380
+ return result.modifiedCount > 0;
381
+ }
382
+ async function hardDeletePost(id) {
383
+ const col = await getCollection("nbk_posts");
384
+ const result = await col.deleteOne({ _id: new import_mongodb.ObjectId(id) });
385
+ return result.deletedCount > 0;
386
+ }
387
+ async function getPostBySlug(slug) {
388
+ const col = await getCollection("nbk_posts");
389
+ return await col.findOne({ slug });
390
+ }
391
+ async function getPostById(id) {
392
+ const col = await getCollection("nbk_posts");
393
+ return await col.findOne({ _id: new import_mongodb.ObjectId(id) });
394
+ }
395
+ async function listPosts(query = {}) {
396
+ const col = await getCollection("nbk_posts");
397
+ const {
398
+ page = 1,
399
+ limit = 10,
400
+ category,
401
+ tag,
402
+ status,
403
+ search,
404
+ sortBy = "publishedAt",
405
+ sortOrder = "desc"
406
+ } = query;
407
+ const filter = {};
408
+ if (status) {
409
+ filter.status = status;
410
+ } else {
411
+ filter.status = { $ne: "archived" };
412
+ }
413
+ if (category) filter.categories = category;
414
+ if (tag) filter.tags = tag;
415
+ if (search) {
416
+ filter.$text = { $search: search };
417
+ }
418
+ const sort = {
419
+ [sortBy]: sortOrder === "asc" ? 1 : -1
420
+ };
421
+ const skip = (page - 1) * limit;
422
+ const [posts, total] = await Promise.all([
423
+ col.find(filter).sort(sort).skip(skip).limit(limit).toArray(),
424
+ col.countDocuments(filter)
425
+ ]);
426
+ return {
427
+ posts,
428
+ total
429
+ };
430
+ }
431
+ async function createCategory(input) {
432
+ const col = await getCollection("nbk_categories");
433
+ const slug = await ensureUniqueSlug(
434
+ input.slug || generateSlug(input.name),
435
+ col
436
+ );
437
+ const doc = {
438
+ name: input.name,
439
+ slug,
440
+ description: input.description,
441
+ seo: input.seo,
442
+ order: input.order ?? 0,
443
+ parentId: input.parentId ? new import_mongodb.ObjectId(input.parentId) : void 0,
444
+ postCount: 0
445
+ };
446
+ const result = await col.insertOne(doc);
447
+ return { _id: result.insertedId, ...doc };
448
+ }
449
+ async function updateCategory(id, input) {
450
+ const col = await getCollection("nbk_categories");
451
+ const objectId = new import_mongodb.ObjectId(id);
452
+ const updates = { ...input };
453
+ if (input.slug) {
454
+ updates.slug = await ensureUniqueSlug(input.slug, col, id);
455
+ }
456
+ if (input.parentId) {
457
+ updates.parentId = new import_mongodb.ObjectId(input.parentId);
458
+ }
459
+ await col.updateOne({ _id: objectId }, { $set: updates });
460
+ return await col.findOne({ _id: objectId });
461
+ }
462
+ async function deleteCategory(id) {
463
+ const col = await getCollection("nbk_categories");
464
+ const result = await col.deleteOne({ _id: new import_mongodb.ObjectId(id) });
465
+ return result.deletedCount > 0;
466
+ }
467
+ async function listCategories() {
468
+ const col = await getCollection("nbk_categories");
469
+ return await col.find({}).sort({ order: 1 }).toArray();
470
+ }
471
+ async function getCategoryBySlug(slug) {
472
+ const col = await getCollection("nbk_categories");
473
+ return await col.findOne({ slug });
474
+ }
475
+ async function updateCategoryPostCount(categorySlug) {
476
+ const posts = await getCollection("nbk_posts");
477
+ const categories = await getCollection("nbk_categories");
478
+ const count = await posts.countDocuments({
479
+ categories: categorySlug,
480
+ status: "published"
481
+ });
482
+ await categories.updateOne({ slug: categorySlug }, { $set: { postCount: count } });
483
+ }
484
+ async function createMedia(data) {
485
+ const col = await getCollection("nbk_media");
486
+ const result = await col.insertOne(data);
487
+ return { _id: result.insertedId, ...data };
488
+ }
489
+ async function deleteMedia(id) {
490
+ const col = await getCollection("nbk_media");
491
+ const media = await col.findOne({ _id: new import_mongodb.ObjectId(id) });
492
+ if (!media) return null;
493
+ await col.deleteOne({ _id: new import_mongodb.ObjectId(id) });
494
+ return media;
495
+ }
496
+ async function listMedia(query = {}) {
497
+ const col = await getCollection("nbk_media");
498
+ const { page = 1, limit = 20, mimeType } = query;
499
+ const filter = {};
500
+ if (mimeType) filter.mimeType = { $regex: mimeType };
501
+ const skip = (page - 1) * limit;
502
+ const [media, total] = await Promise.all([
503
+ col.find(filter).sort({ createdAt: -1 }).skip(skip).limit(limit).toArray(),
504
+ col.countDocuments(filter)
505
+ ]);
506
+ return { media, total };
507
+ }
508
+ async function getSettings() {
509
+ const col = await getCollection("nbk_settings");
510
+ const settings = await col.findOne({ _id: "global" });
511
+ if (!settings) {
512
+ const defaults = {
513
+ _id: "global",
514
+ postsPerPage: 10,
515
+ commentSystem: "none"
516
+ };
517
+ await col.insertOne(defaults);
518
+ return defaults;
519
+ }
520
+ return settings;
521
+ }
522
+ async function updateSettings(data) {
523
+ const col = await getCollection("nbk_settings");
524
+ const { _id, ...updates } = data;
525
+ await col.updateOne(
526
+ { _id: "global" },
527
+ { $set: updates },
528
+ { upsert: true }
529
+ );
530
+ return getSettings();
531
+ }
532
+ var import_mongodb, client, db;
533
+ var init_db = __esm({
534
+ "src/lib/db.ts"() {
535
+ "use strict";
536
+ import_mongodb = require("mongodb");
537
+ init_config();
538
+ init_slug();
539
+ init_reading_time();
540
+ client = null;
541
+ db = null;
542
+ }
543
+ });
544
+
545
+ // src/lib/storage.ts
546
+ var storage_exports = {};
547
+ __export(storage_exports, {
548
+ R2StorageProvider: () => R2StorageProvider
549
+ });
550
+ function getS3Client() {
551
+ if (clientInstance) return clientInstance;
552
+ const env = getEnvConfig();
553
+ clientInstance = new import_client_s3.S3Client({
554
+ region: "auto",
555
+ endpoint: `https://${env.NEXTBLOGKIT_R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
556
+ credentials: {
557
+ accessKeyId: env.NEXTBLOGKIT_R2_ACCESS_KEY,
558
+ secretAccessKey: env.NEXTBLOGKIT_R2_SECRET_KEY
559
+ }
560
+ });
561
+ return clientInstance;
562
+ }
563
+ function generateKey(filename) {
564
+ const now = /* @__PURE__ */ new Date();
565
+ const year = now.getFullYear();
566
+ const month = String(now.getMonth() + 1).padStart(2, "0");
567
+ const uuid = (0, import_crypto.randomUUID)().split("-")[0];
568
+ const safeName = filename.replace(/[^a-zA-Z0-9._-]/g, "-").toLowerCase();
569
+ return `blog/${year}/${month}/${uuid}-${safeName}`;
570
+ }
571
+ var import_client_s3, import_crypto, clientInstance, R2StorageProvider;
572
+ var init_storage = __esm({
573
+ "src/lib/storage.ts"() {
574
+ "use strict";
575
+ import_client_s3 = require("@aws-sdk/client-s3");
576
+ init_config();
577
+ import_crypto = require("crypto");
578
+ clientInstance = null;
579
+ R2StorageProvider = class {
580
+ async upload(file, filename, contentType) {
581
+ const env = getEnvConfig();
582
+ const client2 = getS3Client();
583
+ const key = generateKey(filename);
584
+ await client2.send(
585
+ new import_client_s3.PutObjectCommand({
586
+ Bucket: env.NEXTBLOGKIT_R2_BUCKET,
587
+ Key: key,
588
+ Body: file,
589
+ ContentType: contentType
590
+ })
591
+ );
592
+ return {
593
+ key,
594
+ url: `${env.NEXTBLOGKIT_R2_PUBLIC_URL}/${key}`,
595
+ size: file.length,
596
+ contentType
597
+ };
598
+ }
599
+ async delete(key) {
600
+ const env = getEnvConfig();
601
+ const client2 = getS3Client();
602
+ await client2.send(
603
+ new import_client_s3.DeleteObjectCommand({
604
+ Bucket: env.NEXTBLOGKIT_R2_BUCKET,
605
+ Key: key
606
+ })
607
+ );
608
+ }
609
+ async list(prefix) {
610
+ const env = getEnvConfig();
611
+ const client2 = getS3Client();
612
+ const response = await client2.send(
613
+ new import_client_s3.ListObjectsV2Command({
614
+ Bucket: env.NEXTBLOGKIT_R2_BUCKET,
615
+ Prefix: prefix || "blog/"
616
+ })
617
+ );
618
+ return (response.Contents || []).map((obj) => ({
619
+ key: obj.Key || "",
620
+ size: obj.Size || 0,
621
+ lastModified: obj.LastModified || /* @__PURE__ */ new Date()
622
+ }));
623
+ }
624
+ };
625
+ }
626
+ });
627
+
628
+ // src/cli/index.ts
629
+ var import_node_fs = require("fs");
630
+ var import_node_path = require("path");
631
+ var import_commander = require("commander");
632
+
633
+ // src/cli/init.ts
634
+ var import_fs = __toESM(require("fs"), 1);
635
+ var import_path = __toESM(require("path"), 1);
636
+
637
+ // src/cli/templates.ts
638
+ function getTemplates(options) {
639
+ return [
640
+ // ---- Blog Pages ----
641
+ {
642
+ path: "blog/layout.tsx",
643
+ content: `import 'nextblogkit/styles/blog.css';
644
+
645
+ export default function BlogLayout({ children }: { children: React.ReactNode }) {
646
+ return <>{children}</>;
647
+ }
648
+ `
649
+ },
650
+ {
651
+ path: "blog/page.tsx",
652
+ content: `import { BlogCard, BlogSearch, Pagination, CategoryList } from 'nextblogkit/components';
653
+ import { listPosts, listCategories } from 'nextblogkit/lib';
654
+
655
+ interface Props {
656
+ searchParams: Promise<{ page?: string; category?: string; q?: string }>;
657
+ }
658
+
659
+ export default async function Blog({ searchParams }: Props) {
660
+ const params = await searchParams;
661
+ const page = parseInt(params.page || '1', 10);
662
+
663
+ const [{ posts, total }, categories] = await Promise.all([
664
+ listPosts({
665
+ page,
666
+ limit: 10,
667
+ status: 'published',
668
+ category: params.category || undefined,
669
+ search: params.q || undefined,
670
+ sortBy: 'publishedAt',
671
+ sortOrder: 'desc',
672
+ }),
673
+ listCategories(),
674
+ ]);
675
+
676
+ const totalPages = Math.ceil(total / 10);
677
+
678
+ return (
679
+ <div className="nbk-blog-list">
680
+ <div className="nbk-blog-header">
681
+ <BlogSearch apiPath="/api/blog" />
682
+ </div>
683
+
684
+ <div className="nbk-blog-layout">
685
+ <div className="nbk-blog-main">
686
+ {posts.length === 0 ? (
687
+ <div className="nbk-empty-state">
688
+ <p>No posts found.</p>
689
+ </div>
690
+ ) : (
691
+ <div className="nbk-posts-grid">
692
+ {posts.map((post) => (
693
+ <BlogCard
694
+ key={String(post._id || post.slug)}
695
+ post={JSON.parse(JSON.stringify(post))}
696
+ basePath="/blog"
697
+ />
698
+ ))}
699
+ </div>
700
+ )}
701
+
702
+ {totalPages > 1 && (
703
+ <Pagination
704
+ currentPage={page}
705
+ totalPages={totalPages}
706
+ basePath="/blog"
707
+ category={params.category}
708
+ />
709
+ )}
710
+ </div>
711
+
712
+ {categories.length > 0 && (
713
+ <aside className="nbk-blog-sidebar">
714
+ <CategoryList
715
+ categories={JSON.parse(JSON.stringify(categories))}
716
+ activeCategory={params.category}
717
+ basePath="/blog"
718
+ />
719
+ </aside>
720
+ )}
721
+ </div>
722
+ </div>
723
+ );
724
+ }
725
+ `
726
+ },
727
+ {
728
+ path: "blog/[slug]/page.tsx",
729
+ content: `import { AuthorCard, BreadcrumbNav, ShareButtons, ReadingProgressBar, BlogCard } from 'nextblogkit/components';
730
+ import { getPostBySlug, listPosts, generateMetaTags } from 'nextblogkit/lib';
731
+ import type { Metadata } from 'next';
732
+
733
+ interface Props {
734
+ params: Promise<{ slug: string }>;
735
+ }
736
+
737
+ export async function generateMetadata({ params }: Props): Promise<Metadata> {
738
+ const { slug } = await params;
739
+ const post = await getPostBySlug(slug);
740
+ if (!post) return { title: 'Post Not Found' };
741
+ const meta = generateMetaTags(post);
742
+ return {
743
+ title: meta.title,
744
+ description: meta.description,
745
+ openGraph: meta.openGraph,
746
+ twitter: meta.twitter,
747
+ alternates: { canonical: meta.canonical },
748
+ };
749
+ }
750
+
751
+ export default async function BlogPost({ params }: Props) {
752
+ const { slug } = await params;
753
+ const post = await getPostBySlug(slug);
754
+
755
+ if (!post) {
756
+ return <div className="nbk-not-found">Post not found</div>;
757
+ }
758
+
759
+ let relatedPosts: any[] = [];
760
+ if (post.categories?.length) {
761
+ const { posts } = await listPosts({
762
+ status: 'published',
763
+ category: post.categories[0],
764
+ limit: 4,
765
+ });
766
+ relatedPosts = posts
767
+ .filter((p) => String(p._id) !== String(post._id))
768
+ .slice(0, 3);
769
+ }
770
+
771
+ const date = post.publishedAt
772
+ ? new Date(post.publishedAt).toLocaleDateString('en-US', {
773
+ year: 'numeric',
774
+ month: 'long',
775
+ day: 'numeric',
776
+ })
777
+ : '';
778
+
779
+ return (
780
+ <article className="nbk-post">
781
+ <ReadingProgressBar />
782
+
783
+ <BreadcrumbNav
784
+ items={[
785
+ { label: 'Home', href: '/' },
786
+ { label: 'Blog', href: '/blog' },
787
+ ...(post.categories?.[0]
788
+ ? [{ label: post.categories[0], href: \\\`/blog/category/\\\${post.categories[0]}\\\` }]
789
+ : []),
790
+ { label: post.title },
791
+ ]}
792
+ />
793
+
794
+ <header className="nbk-post-header">
795
+ {post.categories?.length > 0 && (
796
+ <div className="nbk-post-categories">
797
+ {post.categories.map((cat: string) => (
798
+ <a key={cat} href={\\\`/blog/category/\\\${cat}\\\`} className="nbk-post-category">
799
+ {cat}
800
+ </a>
801
+ ))}
802
+ </div>
803
+ )}
804
+ <h1 className="nbk-post-title">{post.title}</h1>
805
+ <div className="nbk-post-meta">
806
+ <span className="nbk-post-author">{post.author?.name}</span>
807
+ {date && (
808
+ <>
809
+ <span className="nbk-post-sep">&middot;</span>
810
+ <time className="nbk-post-date">{date}</time>
811
+ </>
812
+ )}
813
+ <span className="nbk-post-sep">&middot;</span>
814
+ <span className="nbk-post-reading-time">{post.readingTime} min read</span>
815
+ </div>
816
+ </header>
817
+
818
+ {post.coverImage?.url && (
819
+ <div className="nbk-post-cover">
820
+ <img src={post.coverImage.url} alt={post.coverImage.alt || post.title} />
821
+ </div>
822
+ )}
823
+
824
+ <div
825
+ className="nbk-post-content"
826
+ dangerouslySetInnerHTML={{ __html: post.contentHTML || '' }}
827
+ />
828
+
829
+ <ShareButtons url={\\\`/blog/\\\${slug}\\\`} title={post.title} />
830
+
831
+ {post.author && <AuthorCard author={post.author} />}
832
+
833
+ {relatedPosts.length > 0 && (
834
+ <section className="nbk-related">
835
+ <h2 className="nbk-related-title">Related Posts</h2>
836
+ <div className="nbk-related-grid">
837
+ {relatedPosts.map((p) => (
838
+ <BlogCard key={String(p._id)} post={JSON.parse(JSON.stringify(p))} basePath="/blog" />
839
+ ))}
840
+ </div>
841
+ </section>
842
+ )}
843
+ </article>
844
+ );
845
+ }
846
+ `
847
+ },
848
+ {
849
+ path: "blog/category/[slug]/page.tsx",
850
+ content: `import { BlogCard, BlogSearch, Pagination, CategoryList } from 'nextblogkit/components';
851
+ import { listPosts, listCategories } from 'nextblogkit/lib';
852
+
853
+ interface Props {
854
+ params: Promise<{ slug: string }>;
855
+ searchParams: Promise<{ page?: string }>;
856
+ }
857
+
858
+ export default async function CategoryPage({ params, searchParams }: Props) {
859
+ const { slug } = await params;
860
+ const sp = await searchParams;
861
+ const page = parseInt(sp.page || '1', 10);
862
+
863
+ const [{ posts, total }, categories] = await Promise.all([
864
+ listPosts({
865
+ page,
866
+ limit: 10,
867
+ status: 'published',
868
+ category: slug,
869
+ sortBy: 'publishedAt',
870
+ sortOrder: 'desc',
871
+ }),
872
+ listCategories(),
873
+ ]);
874
+
875
+ const totalPages = Math.ceil(total / 10);
876
+
877
+ return (
878
+ <div className="nbk-blog-list">
879
+ <div className="nbk-blog-header">
880
+ <BlogSearch apiPath="/api/blog" />
881
+ </div>
882
+
883
+ <div className="nbk-blog-layout">
884
+ <div className="nbk-blog-main">
885
+ {posts.length === 0 ? (
886
+ <div className="nbk-empty-state">
887
+ <p>No posts found in this category.</p>
888
+ </div>
889
+ ) : (
890
+ <div className="nbk-posts-grid">
891
+ {posts.map((post) => (
892
+ <BlogCard
893
+ key={String(post._id || post.slug)}
894
+ post={JSON.parse(JSON.stringify(post))}
895
+ basePath="/blog"
896
+ />
897
+ ))}
898
+ </div>
899
+ )}
900
+
901
+ {totalPages > 1 && (
902
+ <Pagination
903
+ currentPage={page}
904
+ totalPages={totalPages}
905
+ basePath="/blog"
906
+ category={slug}
907
+ />
908
+ )}
909
+ </div>
910
+
911
+ {categories.length > 0 && (
912
+ <aside className="nbk-blog-sidebar">
913
+ <CategoryList
914
+ categories={JSON.parse(JSON.stringify(categories))}
915
+ activeCategory={slug}
916
+ basePath="/blog"
917
+ />
918
+ </aside>
919
+ )}
920
+ </div>
921
+ </div>
922
+ );
923
+ }
924
+ `
925
+ },
926
+ // ---- Admin Pages ----
927
+ {
928
+ path: "admin/blog/layout.tsx",
929
+ content: `import { AdminLayout } from 'nextblogkit/admin';
930
+ import 'nextblogkit/styles/admin.css';
931
+
932
+ export default function AdminBlogLayout({ children }: { children: React.ReactNode }) {
933
+ return <AdminLayout>{children}</AdminLayout>;
934
+ }
935
+ `
936
+ },
937
+ {
938
+ path: "admin/blog/page.tsx",
939
+ content: `import { Dashboard } from 'nextblogkit/admin';
940
+
941
+ export default function AdminDashboard() {
942
+ return <Dashboard />;
943
+ }
944
+ `
945
+ },
946
+ {
947
+ path: "admin/blog/posts/page.tsx",
948
+ content: `import { PostList } from 'nextblogkit/admin';
949
+
950
+ export default function AdminPosts() {
951
+ return <PostList />;
952
+ }
953
+ `
954
+ },
955
+ {
956
+ path: "admin/blog/new/page.tsx",
957
+ content: `import { PostEditor } from 'nextblogkit/admin';
958
+
959
+ export default function NewPost() {
960
+ return <PostEditor />;
961
+ }
962
+ `
963
+ },
964
+ {
965
+ path: "admin/blog/[id]/edit/page.tsx",
966
+ content: `import { PostEditor } from 'nextblogkit/admin';
967
+
968
+ interface Props {
969
+ params: Promise<{ id: string }>;
970
+ }
971
+
972
+ export default async function EditPost({ params }: Props) {
973
+ const { id } = await params;
974
+ return <PostEditor postId={id} />;
975
+ }
976
+ `
977
+ },
978
+ {
979
+ path: "admin/blog/media/page.tsx",
980
+ content: `import { MediaLibrary } from 'nextblogkit/admin';
981
+
982
+ export default function AdminMedia() {
983
+ return <MediaLibrary />;
984
+ }
985
+ `
986
+ },
987
+ {
988
+ path: "admin/blog/categories/page.tsx",
989
+ content: `import { CategoryManager } from 'nextblogkit/admin';
990
+
991
+ export default function AdminCategories() {
992
+ return <CategoryManager />;
993
+ }
994
+ `
995
+ },
996
+ {
997
+ path: "admin/blog/settings/page.tsx",
998
+ content: `import { SettingsPage } from 'nextblogkit/admin';
999
+
1000
+ export default function AdminSettings() {
1001
+ return <SettingsPage />;
1002
+ }
1003
+ `
1004
+ },
1005
+ // ---- API Routes ----
1006
+ {
1007
+ path: "api/blog/posts/route.ts",
1008
+ content: `export { GET, POST, PUT, DELETE } from 'nextblogkit/api/posts';
1009
+ `
1010
+ },
1011
+ {
1012
+ path: "api/blog/media/route.ts",
1013
+ content: `export { GET, POST, DELETE } from 'nextblogkit/api/media';
1014
+ `
1015
+ },
1016
+ {
1017
+ path: "api/blog/categories/route.ts",
1018
+ content: `export { GET, POST, PUT, DELETE } from 'nextblogkit/api/categories';
1019
+ `
1020
+ },
1021
+ {
1022
+ path: "api/blog/settings/route.ts",
1023
+ content: `export { GET, PUT } from 'nextblogkit/api/settings';
1024
+ `
1025
+ },
1026
+ {
1027
+ path: "api/blog/sitemap.xml/route.ts",
1028
+ content: `export { GET } from 'nextblogkit/api/sitemap';
1029
+ `
1030
+ },
1031
+ {
1032
+ path: "api/blog/rss.xml/route.ts",
1033
+ content: `export { GET } from 'nextblogkit/api/rss';
1034
+ `
1035
+ }
1036
+ ];
1037
+ }
1038
+
1039
+ // src/cli/init.ts
1040
+ async function init(options) {
1041
+ const cwd2 = process.cwd();
1042
+ const packageJsonPath = import_path.default.join(cwd2, "package.json");
1043
+ if (!import_fs.default.existsSync(packageJsonPath)) {
1044
+ console.error("Error: No package.json found. Run this in a Next.js project root.");
1045
+ process.exit(1);
1046
+ }
1047
+ const pkg = JSON.parse(import_fs.default.readFileSync(packageJsonPath, "utf-8"));
1048
+ const hasNext = pkg.dependencies?.next || pkg.devDependencies?.next;
1049
+ if (!hasNext) {
1050
+ console.error("Error: next is not a dependency. This must be a Next.js project.");
1051
+ process.exit(1);
1052
+ }
1053
+ const appDir = import_fs.default.existsSync(import_path.default.join(cwd2, "src", "app")) ? import_path.default.join(cwd2, "src", "app") : import_path.default.join(cwd2, "app");
1054
+ if (!import_fs.default.existsSync(appDir)) {
1055
+ console.error("Error: No app/ directory found. NextBlogKit requires the Next.js App Router.");
1056
+ process.exit(1);
1057
+ }
1058
+ console.log("");
1059
+ console.log(" NextBlogKit \u2014 Initializing...");
1060
+ console.log("");
1061
+ const templates = getTemplates(options);
1062
+ for (const template of templates) {
1063
+ const filePath = import_path.default.join(appDir, template.path);
1064
+ const dir = import_path.default.dirname(filePath);
1065
+ if (!import_fs.default.existsSync(dir)) {
1066
+ import_fs.default.mkdirSync(dir, { recursive: true });
1067
+ }
1068
+ if (import_fs.default.existsSync(filePath)) {
1069
+ console.log(` \u26A0 Skipping ${template.path} (already exists)`);
1070
+ continue;
1071
+ }
1072
+ import_fs.default.writeFileSync(filePath, template.content);
1073
+ console.log(` \u2713 ${template.path}`);
1074
+ }
1075
+ const configPath = import_path.default.join(cwd2, "nextblogkit.config.ts");
1076
+ if (!import_fs.default.existsSync(configPath)) {
1077
+ import_fs.default.writeFileSync(
1078
+ configPath,
1079
+ `import { defineConfig } from 'nextblogkit';
1080
+
1081
+ export default defineConfig({
1082
+ basePath: '${options.blogPath}',
1083
+ adminPath: '${options.adminPath}',
1084
+ apiPath: '${options.apiPath}',
1085
+ auth: {
1086
+ strategy: 'api-key',
1087
+ },
1088
+ });
1089
+ `
1090
+ );
1091
+ console.log(" \u2713 nextblogkit.config.ts");
1092
+ }
1093
+ const envExamplePath = import_path.default.join(cwd2, ".env.local.example");
1094
+ if (!import_fs.default.existsSync(envExamplePath)) {
1095
+ import_fs.default.writeFileSync(
1096
+ envExamplePath,
1097
+ `# NextBlogKit Configuration
1098
+ # Copy this to .env.local and fill in your values
1099
+
1100
+ # MongoDB Connection
1101
+ NEXTBLOGKIT_MONGODB_URI=mongodb+srv://user:pass@cluster.mongodb.net/mydb
1102
+
1103
+ # Cloudflare R2 Storage
1104
+ NEXTBLOGKIT_R2_ACCOUNT_ID=your-account-id
1105
+ NEXTBLOGKIT_R2_ACCESS_KEY=your-access-key
1106
+ NEXTBLOGKIT_R2_SECRET_KEY=your-secret-key
1107
+ NEXTBLOGKIT_R2_BUCKET=blog-media
1108
+ NEXTBLOGKIT_R2_PUBLIC_URL=https://media.yourdomain.com
1109
+
1110
+ # Authentication
1111
+ NEXTBLOGKIT_API_KEY=your-secure-api-key-must-be-at-least-32-characters-long
1112
+
1113
+ # Site Info
1114
+ NEXTBLOGKIT_SITE_URL=https://yourdomain.com
1115
+ NEXTBLOGKIT_SITE_NAME="Your Site Name"
1116
+ `
1117
+ );
1118
+ console.log(" \u2713 .env.local.example");
1119
+ }
1120
+ console.log("");
1121
+ console.log(" Done! Next steps:");
1122
+ console.log("");
1123
+ console.log(" 1. Copy .env.local.example to .env.local and fill in your values");
1124
+ console.log(" 2. Run: npm run dev");
1125
+ console.log(` 3. Visit: http://localhost:3000${options.adminPath}`);
1126
+ console.log("");
1127
+ }
1128
+
1129
+ // src/cli/seed.ts
1130
+ async function seed() {
1131
+ console.log("");
1132
+ console.log(" NextBlogKit \u2014 Seeding example content...");
1133
+ console.log("");
1134
+ try {
1135
+ const { getDb: getDb2, ensureIndexes: ensureIndexes2 } = await Promise.resolve().then(() => (init_db(), db_exports));
1136
+ const { createPost: createPost2, createCategory: createCategory2 } = await Promise.resolve().then(() => (init_db(), db_exports));
1137
+ await ensureIndexes2();
1138
+ console.log(" \u2713 Database indexes created");
1139
+ const categories = [
1140
+ { name: "Technology", slug: "technology", description: "Tech tutorials and updates", order: 0 },
1141
+ { name: "Design", slug: "design", description: "UI/UX and design tips", order: 1 },
1142
+ { name: "Tutorials", slug: "tutorials", description: "Step-by-step guides", order: 2 }
1143
+ ];
1144
+ for (const cat of categories) {
1145
+ try {
1146
+ await createCategory2(cat);
1147
+ console.log(` \u2713 Category: ${cat.name}`);
1148
+ } catch (err) {
1149
+ if (err.code === 11e3) {
1150
+ console.log(` \u26A0 Category "${cat.name}" already exists`);
1151
+ } else {
1152
+ throw err;
1153
+ }
1154
+ }
1155
+ }
1156
+ try {
1157
+ await createPost2({
1158
+ title: "Welcome to NextBlogKit",
1159
+ slug: "welcome-to-nextblogkit",
1160
+ excerpt: "Your new blog is ready! This is an example post to get you started with NextBlogKit.",
1161
+ content: [
1162
+ {
1163
+ type: "heading",
1164
+ attrs: { level: 2 },
1165
+ content: [{ type: "text", text: "Getting Started" }]
1166
+ },
1167
+ {
1168
+ type: "paragraph",
1169
+ content: [
1170
+ { type: "text", text: "Congratulations! Your NextBlogKit blog is up and running. This example post shows you what\u2019s possible." }
1171
+ ]
1172
+ },
1173
+ {
1174
+ type: "heading",
1175
+ attrs: { level: 2 },
1176
+ content: [{ type: "text", text: "Features" }]
1177
+ },
1178
+ {
1179
+ type: "bulletList",
1180
+ content: [
1181
+ { type: "listItem", content: [{ type: "paragraph", content: [{ type: "text", text: "Block editor with slash commands" }] }] },
1182
+ { type: "listItem", content: [{ type: "paragraph", content: [{ type: "text", text: "Full SEO optimization out of the box" }] }] },
1183
+ { type: "listItem", content: [{ type: "paragraph", content: [{ type: "text", text: "Media library with R2 storage" }] }] },
1184
+ { type: "listItem", content: [{ type: "paragraph", content: [{ type: "text", text: "Dynamic sitemap and RSS feed" }] }] }
1185
+ ]
1186
+ },
1187
+ {
1188
+ type: "callout",
1189
+ attrs: { type: "tip" },
1190
+ content: [
1191
+ { type: "paragraph", content: [{ type: "text", text: "Head to the admin panel to start creating your own posts!" }] }
1192
+ ]
1193
+ }
1194
+ ],
1195
+ contentHTML: "<h2>Getting Started</h2><p>Congratulations! Your NextBlogKit blog is up and running.</p><h2>Features</h2><ul><li>Block editor with slash commands</li><li>Full SEO optimization out of the box</li><li>Media library with R2 storage</li><li>Dynamic sitemap and RSS feed</li></ul>",
1196
+ status: "published",
1197
+ categories: ["technology"],
1198
+ tags: ["nextjs", "blog", "getting-started"]
1199
+ });
1200
+ console.log(' \u2713 Example post: "Welcome to NextBlogKit"');
1201
+ } catch (err) {
1202
+ if (err.code === 11e3) {
1203
+ console.log(" \u26A0 Example post already exists");
1204
+ } else {
1205
+ throw err;
1206
+ }
1207
+ }
1208
+ console.log("");
1209
+ console.log(" Seeding complete!");
1210
+ console.log("");
1211
+ process.exit(0);
1212
+ } catch (error) {
1213
+ console.error(" \u2717 Seeding failed:", error);
1214
+ process.exit(1);
1215
+ }
1216
+ }
1217
+
1218
+ // src/cli/health.ts
1219
+ async function health() {
1220
+ console.log("");
1221
+ console.log(" NextBlogKit \u2014 Health Check");
1222
+ console.log("");
1223
+ let allPassed = true;
1224
+ try {
1225
+ const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), db_exports));
1226
+ const db2 = await getDb2();
1227
+ await db2.command({ ping: 1 });
1228
+ console.log(" \u2713 MongoDB: Connected");
1229
+ } catch (err) {
1230
+ console.log(` \u2717 MongoDB: ${err.message}`);
1231
+ allPassed = false;
1232
+ }
1233
+ try {
1234
+ const { R2StorageProvider: R2StorageProvider2 } = await Promise.resolve().then(() => (init_storage(), storage_exports));
1235
+ const storage = new R2StorageProvider2();
1236
+ await storage.list("blog/");
1237
+ console.log(" \u2713 Cloudflare R2: Connected");
1238
+ } catch (err) {
1239
+ console.log(` \u2717 Cloudflare R2: ${err.message}`);
1240
+ allPassed = false;
1241
+ }
1242
+ try {
1243
+ const { getEnvConfig: getEnvConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
1244
+ getEnvConfig2();
1245
+ console.log(" \u2713 Environment: All variables set");
1246
+ } catch (err) {
1247
+ console.log(` \u2717 Environment: ${err.message}`);
1248
+ allPassed = false;
1249
+ }
1250
+ console.log("");
1251
+ if (allPassed) {
1252
+ console.log(" All checks passed!");
1253
+ } else {
1254
+ console.log(" Some checks failed. Please review your configuration.");
1255
+ }
1256
+ console.log("");
1257
+ process.exit(allPassed ? 0 : 1);
1258
+ }
1259
+
1260
+ // src/cli/migrate.ts
1261
+ async function migrateCommand() {
1262
+ console.log("");
1263
+ console.log(" NextBlogKit \u2014 Database Migration");
1264
+ console.log("");
1265
+ try {
1266
+ const { ensureIndexes: ensureIndexes2 } = await Promise.resolve().then(() => (init_db(), db_exports));
1267
+ console.log(" Creating/updating indexes...");
1268
+ await ensureIndexes2();
1269
+ console.log(" \u2713 All indexes created");
1270
+ console.log("");
1271
+ console.log(" Migration complete!");
1272
+ console.log("");
1273
+ process.exit(0);
1274
+ } catch (error) {
1275
+ console.error(" \u2717 Migration failed:", error);
1276
+ process.exit(1);
1277
+ }
1278
+ }
1279
+
1280
+ // src/cli/index.ts
1281
+ function loadEnvFile(filePath) {
1282
+ if (!(0, import_node_fs.existsSync)(filePath)) return;
1283
+ const content = (0, import_node_fs.readFileSync)(filePath, "utf-8");
1284
+ for (const line of content.split("\n")) {
1285
+ const trimmed = line.trim();
1286
+ if (!trimmed || trimmed.startsWith("#")) continue;
1287
+ const eqIndex = trimmed.indexOf("=");
1288
+ if (eqIndex === -1) continue;
1289
+ const key = trimmed.slice(0, eqIndex).trim();
1290
+ let value = trimmed.slice(eqIndex + 1).trim();
1291
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
1292
+ value = value.slice(1, -1);
1293
+ }
1294
+ if (process.env[key] === void 0) {
1295
+ process.env[key] = value;
1296
+ }
1297
+ }
1298
+ }
1299
+ var cwd = process.cwd();
1300
+ loadEnvFile((0, import_node_path.resolve)(cwd, ".env.local"));
1301
+ loadEnvFile((0, import_node_path.resolve)(cwd, ".env"));
1302
+ var program = new import_commander.Command();
1303
+ program.name("nextblogkit").description("NextBlogKit \u2014 Blog engine for Next.js").version("0.1.0");
1304
+ program.command("init").description("Initialize NextBlogKit in your Next.js project").option("--blog-path <path>", "Blog base path", "/blog").option("--admin-path <path>", "Admin panel path", "/admin/blog").option("--api-path <path>", "API base path", "/api/blog").option("--no-example", "Skip creating example post").action(init);
1305
+ program.command("seed").description("Seed example blog content").action(seed);
1306
+ program.command("health").description("Check database and storage connectivity").action(health);
1307
+ program.command("migrate").description("Run database migrations").action(migrateCommand);
1308
+ program.parse();