next-blogpanel 1.0.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 (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +211 -0
  3. package/dist/admin/index.cjs +3062 -0
  4. package/dist/admin/index.cjs.map +1 -0
  5. package/dist/admin/index.d.cts +51 -0
  6. package/dist/admin/index.d.ts +51 -0
  7. package/dist/admin/index.js +3029 -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-file.cjs +46 -0
  16. package/dist/api/media-file.cjs.map +1 -0
  17. package/dist/api/media-file.d.cts +7 -0
  18. package/dist/api/media-file.d.ts +7 -0
  19. package/dist/api/media-file.js +44 -0
  20. package/dist/api/media-file.js.map +1 -0
  21. package/dist/api/media.cjs +180 -0
  22. package/dist/api/media.cjs.map +1 -0
  23. package/dist/api/media.d.cts +22 -0
  24. package/dist/api/media.d.ts +22 -0
  25. package/dist/api/media.js +176 -0
  26. package/dist/api/media.js.map +1 -0
  27. package/dist/api/posts.cjs +103 -0
  28. package/dist/api/posts.cjs.map +1 -0
  29. package/dist/api/posts.d.cts +27 -0
  30. package/dist/api/posts.d.ts +27 -0
  31. package/dist/api/posts.js +98 -0
  32. package/dist/api/posts.js.map +1 -0
  33. package/dist/api/rss.cjs +25 -0
  34. package/dist/api/rss.cjs.map +1 -0
  35. package/dist/api/rss.d.cts +5 -0
  36. package/dist/api/rss.d.ts +5 -0
  37. package/dist/api/rss.js +23 -0
  38. package/dist/api/rss.js.map +1 -0
  39. package/dist/api/settings.cjs +40 -0
  40. package/dist/api/settings.cjs.map +1 -0
  41. package/dist/api/settings.d.cts +17 -0
  42. package/dist/api/settings.d.ts +17 -0
  43. package/dist/api/settings.js +37 -0
  44. package/dist/api/settings.js.map +1 -0
  45. package/dist/api/sitemap.cjs +25 -0
  46. package/dist/api/sitemap.cjs.map +1 -0
  47. package/dist/api/sitemap.d.cts +5 -0
  48. package/dist/api/sitemap.d.ts +5 -0
  49. package/dist/api/sitemap.js +23 -0
  50. package/dist/api/sitemap.js.map +1 -0
  51. package/dist/api/tokens.cjs +56 -0
  52. package/dist/api/tokens.cjs.map +1 -0
  53. package/dist/api/tokens.d.cts +22 -0
  54. package/dist/api/tokens.d.ts +22 -0
  55. package/dist/api/tokens.js +52 -0
  56. package/dist/api/tokens.js.map +1 -0
  57. package/dist/chunk-2M46MMWN.cjs +76 -0
  58. package/dist/chunk-2M46MMWN.cjs.map +1 -0
  59. package/dist/chunk-4GK22Q3R.cjs +229 -0
  60. package/dist/chunk-4GK22Q3R.cjs.map +1 -0
  61. package/dist/chunk-6PZ772NQ.js +595 -0
  62. package/dist/chunk-6PZ772NQ.js.map +1 -0
  63. package/dist/chunk-BZXPRGLN.cjs +351 -0
  64. package/dist/chunk-BZXPRGLN.cjs.map +1 -0
  65. package/dist/chunk-DAX5C6SC.cjs +86 -0
  66. package/dist/chunk-DAX5C6SC.cjs.map +1 -0
  67. package/dist/chunk-DWNMHQ4I.cjs +57 -0
  68. package/dist/chunk-DWNMHQ4I.cjs.map +1 -0
  69. package/dist/chunk-FPGVR2BX.cjs +121 -0
  70. package/dist/chunk-FPGVR2BX.cjs.map +1 -0
  71. package/dist/chunk-JBP3G4QJ.js +50 -0
  72. package/dist/chunk-JBP3G4QJ.js.map +1 -0
  73. package/dist/chunk-JDP4DIHI.js +215 -0
  74. package/dist/chunk-JDP4DIHI.js.map +1 -0
  75. package/dist/chunk-LIZOVA4W.js +118 -0
  76. package/dist/chunk-LIZOVA4W.js.map +1 -0
  77. package/dist/chunk-QLBTOCT3.js +118 -0
  78. package/dist/chunk-QLBTOCT3.js.map +1 -0
  79. package/dist/chunk-UI2KDZ5H.js +344 -0
  80. package/dist/chunk-UI2KDZ5H.js.map +1 -0
  81. package/dist/chunk-V7QPRX4A.cjs +125 -0
  82. package/dist/chunk-V7QPRX4A.cjs.map +1 -0
  83. package/dist/chunk-VEJFN65Y.js +82 -0
  84. package/dist/chunk-VEJFN65Y.js.map +1 -0
  85. package/dist/chunk-WYZFAI6O.js +74 -0
  86. package/dist/chunk-WYZFAI6O.js.map +1 -0
  87. package/dist/chunk-XLYTATRQ.cjs +637 -0
  88. package/dist/chunk-XLYTATRQ.cjs.map +1 -0
  89. package/dist/cli/index.cjs +2036 -0
  90. package/dist/components/index.cjs +647 -0
  91. package/dist/components/index.cjs.map +1 -0
  92. package/dist/components/index.d.cts +187 -0
  93. package/dist/components/index.d.ts +187 -0
  94. package/dist/components/index.js +633 -0
  95. package/dist/components/index.js.map +1 -0
  96. package/dist/db-M3KXLBUP.js +3 -0
  97. package/dist/db-M3KXLBUP.js.map +1 -0
  98. package/dist/db-TDCZNY3Q.cjs +112 -0
  99. package/dist/db-TDCZNY3Q.cjs.map +1 -0
  100. package/dist/editor/index.cjs +1126 -0
  101. package/dist/editor/index.cjs.map +1 -0
  102. package/dist/editor/index.d.cts +137 -0
  103. package/dist/editor/index.d.ts +137 -0
  104. package/dist/editor/index.js +1093 -0
  105. package/dist/editor/index.js.map +1 -0
  106. package/dist/index-BcNka7gK.d.cts +475 -0
  107. package/dist/index-DPRgaNEE.d.ts +475 -0
  108. package/dist/index.cjs +433 -0
  109. package/dist/index.cjs.map +1 -0
  110. package/dist/index.d.cts +27 -0
  111. package/dist/index.d.ts +27 -0
  112. package/dist/index.js +209 -0
  113. package/dist/index.js.map +1 -0
  114. package/dist/lib/index.cjs +193 -0
  115. package/dist/lib/index.cjs.map +1 -0
  116. package/dist/lib/index.d.cts +4 -0
  117. package/dist/lib/index.d.ts +4 -0
  118. package/dist/lib/index.js +8 -0
  119. package/dist/lib/index.js.map +1 -0
  120. package/dist/styles/admin.css +667 -0
  121. package/dist/styles/blog.css +851 -0
  122. package/dist/styles/editor.css +452 -0
  123. package/dist/styles/globals.css +270 -0
  124. package/dist/styles/prose.css +299 -0
  125. package/dist/types-CgrBuig-.d.cts +778 -0
  126. package/dist/types-CgrBuig-.d.ts +778 -0
  127. package/package.json +140 -0
@@ -0,0 +1,2036 @@
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/limits.ts
34
+ var DEFAULT_MAX_IMAGE_SIZE, DEFAULT_MAX_UPLOAD_SIZE;
35
+ var init_limits = __esm({
36
+ "src/lib/limits.ts"() {
37
+ "use strict";
38
+ DEFAULT_MAX_IMAGE_SIZE = 2 * 1024 * 1024;
39
+ DEFAULT_MAX_UPLOAD_SIZE = 10 * 1024 * 1024;
40
+ }
41
+ });
42
+
43
+ // src/lib/config.ts
44
+ var config_exports = {};
45
+ __export(config_exports, {
46
+ ConfigNotInstalledError: () => ConfigNotInstalledError,
47
+ defineConfig: () => defineConfig,
48
+ getBlogConfig: () => getBlogConfig,
49
+ getConfig: () => getConfig,
50
+ getEnvConfig: () => getEnvConfig,
51
+ isConfigInstalled: () => isConfigInstalled,
52
+ isR2Configured: () => isR2Configured,
53
+ requireApiPath: () => requireApiPath,
54
+ resetConfig: () => resetConfig,
55
+ resetEnvCache: () => resetEnvCache,
56
+ setConfig: () => setConfig
57
+ });
58
+ function getEnvConfig() {
59
+ if (cachedEnv) return cachedEnv;
60
+ const result = envSchema.safeParse(process.env);
61
+ if (!result.success) {
62
+ const missing = result.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`);
63
+ throw new Error(`Next BlogPanel configuration error:
64
+ ${missing.join("\n")}`);
65
+ }
66
+ cachedEnv = result.data;
67
+ return cachedEnv;
68
+ }
69
+ function resetEnvCache() {
70
+ cachedEnv = null;
71
+ }
72
+ function isR2Configured() {
73
+ const env = getEnvConfig();
74
+ return !!(env.BLOGPANEL_R2_ACCOUNT_ID && env.BLOGPANEL_R2_ACCESS_KEY && env.BLOGPANEL_R2_SECRET_KEY && env.BLOGPANEL_R2_BUCKET && env.BLOGPANEL_R2_PUBLIC_URL);
75
+ }
76
+ function mergeConfig(base, userConfig) {
77
+ return {
78
+ ...base,
79
+ ...userConfig,
80
+ editor: { ...base.editor, ...userConfig?.editor },
81
+ seo: { ...base.seo, ...userConfig?.seo },
82
+ auth: { ...base.auth, ...userConfig?.auth },
83
+ features: { ...base.features, ...userConfig?.features },
84
+ theme: {
85
+ ...base.theme,
86
+ ...userConfig?.theme,
87
+ variables: { ...base.theme.variables, ...userConfig?.theme?.variables }
88
+ },
89
+ hooks: { ...base.hooks, ...userConfig?.hooks }
90
+ };
91
+ }
92
+ function slot() {
93
+ const store = globalThis;
94
+ let current = store[CONFIG_SLOT];
95
+ if (!current) {
96
+ current = { installed: false, config: mergeConfig(defaultConfig) };
97
+ store[CONFIG_SLOT] = current;
98
+ }
99
+ return current;
100
+ }
101
+ function warnApiPathConflict(previous, next) {
102
+ const store = globalThis;
103
+ const seen = store[CONFIG_WARNED] || (store[CONFIG_WARNED] = /* @__PURE__ */ new Set());
104
+ const key = `${previous}=>${next}`;
105
+ if (seen.has(key)) return;
106
+ seen.add(key);
107
+ console.warn(
108
+ `[blogpanel] setConfig() was called again with a different apiPath ('${next}', previously '${previous}'). apiPath is baked into every GridFS media URL already written to the database and into post bodies that embed those images, so this does not retroactively fix or break anything already stored \u2014 but every upload from this point on will use the new value. If that is not what you intended, look for more than one setConfig() call with a hardcoded apiPath (for example, your own call alongside the generated next-blogpanel.setup.ts).`
109
+ );
110
+ }
111
+ function defineConfig(config) {
112
+ return config;
113
+ }
114
+ function setConfig(userConfig) {
115
+ const current = slot();
116
+ const base = current.installed ? current.config : defaultConfig;
117
+ if (current.installed && userConfig.apiPath !== void 0 && userConfig.apiPath !== current.config.apiPath) {
118
+ warnApiPathConflict(current.config.apiPath, userConfig.apiPath);
119
+ }
120
+ const merged = mergeConfig(base, userConfig);
121
+ current.config = merged;
122
+ current.installed = true;
123
+ return merged;
124
+ }
125
+ function isConfigInstalled() {
126
+ return slot().installed;
127
+ }
128
+ function getConfig(userConfig) {
129
+ if (userConfig) return mergeConfig(defaultConfig, userConfig);
130
+ return slot().config;
131
+ }
132
+ function requireApiPath(context) {
133
+ const current = slot();
134
+ if (!current.installed) {
135
+ throw new ConfigNotInstalledError(context);
136
+ }
137
+ return current.config.apiPath;
138
+ }
139
+ function resetConfig() {
140
+ const store = globalThis;
141
+ delete store[CONFIG_SLOT];
142
+ delete store[CONFIG_WARNED];
143
+ }
144
+ function getBlogConfig() {
145
+ const env = getEnvConfig();
146
+ const config = getConfig();
147
+ return {
148
+ ...config,
149
+ siteUrl: env.BLOGPANEL_SITE_URL,
150
+ siteName: env.BLOGPANEL_SITE_NAME,
151
+ metadata: {
152
+ title: `Blog | ${env.BLOGPANEL_SITE_NAME}`,
153
+ description: `Latest posts from ${env.BLOGPANEL_SITE_NAME}`,
154
+ openGraph: {
155
+ title: `Blog | ${env.BLOGPANEL_SITE_NAME}`,
156
+ description: `Latest posts from ${env.BLOGPANEL_SITE_NAME}`,
157
+ url: `${env.BLOGPANEL_SITE_URL}${config.basePath}`,
158
+ siteName: env.BLOGPANEL_SITE_NAME,
159
+ type: "website"
160
+ }
161
+ }
162
+ };
163
+ }
164
+ var import_zod, envSchema, cachedEnv, defaultConfig, CONFIG_SLOT, CONFIG_WARNED, ConfigNotInstalledError;
165
+ var init_config = __esm({
166
+ "src/lib/config.ts"() {
167
+ "use strict";
168
+ import_zod = require("zod");
169
+ init_limits();
170
+ envSchema = import_zod.z.object({
171
+ // Required
172
+ BLOGPANEL_MONGODB_URI: import_zod.z.string().min(1, "MongoDB URI is required"),
173
+ BLOGPANEL_API_KEY: import_zod.z.string().min(32, "API key must be at least 32 characters"),
174
+ // Optional — Database name (defaults to the database in your connection URI)
175
+ BLOGPANEL_MONGODB_DB: import_zod.z.string().optional(),
176
+ // Optional — Cloudflare R2 (image storage)
177
+ BLOGPANEL_R2_ACCOUNT_ID: import_zod.z.string().optional(),
178
+ BLOGPANEL_R2_ACCESS_KEY: import_zod.z.string().optional(),
179
+ BLOGPANEL_R2_SECRET_KEY: import_zod.z.string().optional(),
180
+ BLOGPANEL_R2_BUCKET: import_zod.z.string().optional(),
181
+ BLOGPANEL_R2_PUBLIC_URL: import_zod.z.string().optional(),
182
+ // Optional — force a storage backend regardless of what is configured.
183
+ //
184
+ // Not a z.enum(['gridfs','r2']) any more: a custom provider registered via
185
+ // registerStorageProvider() is selectable here by its own name, and an enum
186
+ // could not know those names — they are supplied by application code at
187
+ // runtime, long after this schema is evaluated.
188
+ //
189
+ // The enum did buy loud detection of a typo, and that is not given up, only
190
+ // moved: getStorageProviderByName() throws StorageProviderNotRegisteredError
191
+ // and names every resolvable provider. That is a strictly better failure
192
+ // than the enum's, which rejected the whole environment — taking down
193
+ // routes that never touch storage — and could only say "expected 'gridfs' |
194
+ // 'r2'". Left as a plain optional string rather than .min(1) so that an
195
+ // empty `BLOGPANEL_STORAGE=` line, which reads as "unset" in every .env
196
+ // file, does not fail config parsing process-wide; getStorageProvider()
197
+ // treats blank as absent.
198
+ BLOGPANEL_STORAGE: import_zod.z.string().optional(),
199
+ // Optional — Site info (defaults provided)
200
+ BLOGPANEL_SITE_URL: import_zod.z.string().optional().default(""),
201
+ BLOGPANEL_SITE_NAME: import_zod.z.string().optional().default("Blog")
202
+ });
203
+ cachedEnv = null;
204
+ defaultConfig = {
205
+ basePath: "/blog",
206
+ adminPath: "/admin/blog",
207
+ apiPath: "/api/blog",
208
+ postsPerPage: 10,
209
+ excerptLength: 160,
210
+ codeHighlighter: "shiki",
211
+ editor: {
212
+ blocks: [
213
+ "paragraph",
214
+ "heading",
215
+ "image",
216
+ "codeBlock",
217
+ "blockquote",
218
+ "bulletList",
219
+ "orderedList",
220
+ "taskList",
221
+ "table",
222
+ "embed",
223
+ "horizontalRule",
224
+ "callout",
225
+ "tableOfContents",
226
+ "faq",
227
+ "html"
228
+ ],
229
+ maxImageSize: DEFAULT_MAX_IMAGE_SIZE,
230
+ // stored result ceiling — compression target
231
+ maxUploadSize: DEFAULT_MAX_UPLOAD_SIZE,
232
+ // raw ingest ceiling — rejection threshold
233
+ imageFormats: ["jpg", "jpeg", "png", "gif", "webp", "svg"],
234
+ autosaveInterval: 3e4
235
+ },
236
+ seo: {
237
+ titleTemplate: "%s | %siteName%",
238
+ generateRSS: true,
239
+ generateSitemap: true,
240
+ structuredData: true,
241
+ minContentLength: 300
242
+ },
243
+ auth: {
244
+ strategy: "api-key"
245
+ },
246
+ features: {
247
+ search: true,
248
+ relatedPosts: true,
249
+ readingProgress: true,
250
+ tableOfContents: true,
251
+ shareButtons: true,
252
+ darkMode: true,
253
+ scheduling: true,
254
+ revisionHistory: true,
255
+ imageOptimization: true
256
+ },
257
+ theme: {
258
+ darkMode: true,
259
+ variables: {
260
+ "--nbp-primary": "#2563eb",
261
+ "--nbp-primary-hover": "#1d4ed8",
262
+ "--nbp-text": "#1f2937",
263
+ "--nbp-text-muted": "#6b7280",
264
+ "--nbp-bg": "#ffffff",
265
+ "--nbp-bg-secondary": "#f9fafb",
266
+ "--nbp-card-bg": "#ffffff",
267
+ "--nbp-border": "#e5e7eb",
268
+ "--nbp-radius": "0.5rem",
269
+ "--nbp-font-heading": '"Inter", system-ui, sans-serif',
270
+ "--nbp-font-body": '"Inter", system-ui, sans-serif',
271
+ "--nbp-font-code": '"JetBrains Mono", "Fira Code", monospace'
272
+ }
273
+ },
274
+ hooks: {}
275
+ };
276
+ CONFIG_SLOT = /* @__PURE__ */ Symbol.for("next-blogpanel.config.v1");
277
+ CONFIG_WARNED = /* @__PURE__ */ Symbol.for("next-blogpanel.config.warned");
278
+ ConfigNotInstalledError = class extends Error {
279
+ constructor(context) {
280
+ super(
281
+ `Next BlogPanel: no configuration has been installed in this server runtime, so ${context} cannot be resolved.
282
+ This value is built from \`apiPath\` and then stored permanently, so falling back to the default ('${defaultConfig.apiPath}') would write a URL that may never resolve.
283
+ Fix: import the setup module \`next-blogpanel init\` generated next to your app directory (app/next-blogpanel.setup.ts) from the route handler that reaches this code, or call setConfig(config) yourself before serving requests.
284
+ See docs/configuration.md \u2014 "Getting the config to the server".`
285
+ );
286
+ this.name = "ConfigNotInstalledError";
287
+ }
288
+ };
289
+ }
290
+ });
291
+
292
+ // src/lib/slug.ts
293
+ function generateSlug(text) {
294
+ return text.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
295
+ }
296
+ async function ensureUniqueSlug(slug, collection, excludeId) {
297
+ let candidate = slug;
298
+ let counter = 1;
299
+ while (true) {
300
+ const query = { slug: candidate };
301
+ if (excludeId) {
302
+ query._id = { $ne: excludeId };
303
+ }
304
+ const existing = await collection.findOne(query);
305
+ if (!existing) return candidate;
306
+ candidate = `${slug}-${counter}`;
307
+ counter++;
308
+ }
309
+ }
310
+ var init_slug = __esm({
311
+ "src/lib/slug.ts"() {
312
+ "use strict";
313
+ }
314
+ });
315
+
316
+ // src/lib/reading-time.ts
317
+ function calculateReadingTime(text) {
318
+ const words = countWords(text);
319
+ return Math.max(1, Math.ceil(words / WORDS_PER_MINUTE));
320
+ }
321
+ function countWords(text) {
322
+ if (!text || !text.trim()) return 0;
323
+ return text.trim().split(/\s+/).length;
324
+ }
325
+ function extractTextFromBlocks(blocks) {
326
+ if (!Array.isArray(blocks)) return "";
327
+ const parts = [];
328
+ function walk(node) {
329
+ if (!node || typeof node !== "object") return;
330
+ const n = node;
331
+ if (typeof n.text === "string") {
332
+ parts.push(n.text);
333
+ }
334
+ if (Array.isArray(n.content)) {
335
+ for (const child of n.content) {
336
+ walk(child);
337
+ }
338
+ }
339
+ }
340
+ for (const block of blocks) {
341
+ walk(block);
342
+ }
343
+ return parts.join(" ");
344
+ }
345
+ var WORDS_PER_MINUTE;
346
+ var init_reading_time = __esm({
347
+ "src/lib/reading-time.ts"() {
348
+ "use strict";
349
+ WORDS_PER_MINUTE = 200;
350
+ }
351
+ });
352
+
353
+ // src/lib/db.ts
354
+ var db_exports = {};
355
+ __export(db_exports, {
356
+ createApiToken: () => createApiToken,
357
+ createCategory: () => createCategory,
358
+ createMedia: () => createMedia,
359
+ createPost: () => createPost,
360
+ deleteApiToken: () => deleteApiToken,
361
+ deleteCategory: () => deleteCategory,
362
+ deleteMedia: () => deleteMedia,
363
+ deletePost: () => deletePost,
364
+ ensureIndexes: () => ensureIndexes,
365
+ getCategoryBySlug: () => getCategoryBySlug,
366
+ getCollection: () => getCollection,
367
+ getDb: () => getDb,
368
+ getMediaById: () => getMediaById,
369
+ getPostById: () => getPostById,
370
+ getPostBySlug: () => getPostBySlug,
371
+ getSettings: () => getSettings,
372
+ hardDeletePost: () => hardDeletePost,
373
+ listApiTokens: () => listApiTokens,
374
+ listCategories: () => listCategories,
375
+ listMedia: () => listMedia,
376
+ listPosts: () => listPosts,
377
+ updateCategory: () => updateCategory,
378
+ updateCategoryPostCount: () => updateCategoryPostCount,
379
+ updatePost: () => updatePost,
380
+ updateSettings: () => updateSettings,
381
+ verifyApiToken: () => verifyApiToken
382
+ });
383
+ async function getDb() {
384
+ if (db) return db;
385
+ const env = getEnvConfig();
386
+ client = new import_mongodb.MongoClient(env.BLOGPANEL_MONGODB_URI);
387
+ await client.connect();
388
+ db = client.db(env.BLOGPANEL_MONGODB_DB || void 0);
389
+ return db;
390
+ }
391
+ async function getCollection(name) {
392
+ const database = await getDb();
393
+ return database.collection(name);
394
+ }
395
+ async function ensureIndexes() {
396
+ const database = await getDb();
397
+ const posts = database.collection("nbk_posts");
398
+ await posts.createIndex({ slug: 1 }, { unique: true });
399
+ await posts.createIndex({ status: 1, publishedAt: -1 });
400
+ await posts.createIndex({ categories: 1, status: 1 });
401
+ await posts.createIndex({ tags: 1, status: 1 });
402
+ await posts.createIndex({ "seo.focusKeyword": 1 });
403
+ await posts.createIndex({ contentText: "text", title: "text", excerpt: "text" });
404
+ const categories = database.collection("nbk_categories");
405
+ await categories.createIndex({ slug: 1 }, { unique: true });
406
+ await categories.createIndex({ order: 1 });
407
+ const media = database.collection("nbk_media");
408
+ await media.createIndex({ createdAt: -1 });
409
+ await media.createIndex({ storageKey: 1 }, { unique: true });
410
+ const tokens = database.collection("nbk_api_tokens");
411
+ await tokens.createIndex({ tokenHash: 1 }, { unique: true });
412
+ await tokens.createIndex({ createdAt: -1 });
413
+ }
414
+ async function createPost(input, defaultAuthor) {
415
+ const col = await getCollection("nbk_posts");
416
+ const slug = await ensureUniqueSlug(
417
+ input.slug || generateSlug(input.title),
418
+ col
419
+ );
420
+ const contentText = input.contentText || extractTextFromBlocks(input.content || []);
421
+ const wordCount = countWords(contentText);
422
+ const readingTime = calculateReadingTime(contentText);
423
+ const excerpt = input.excerpt || contentText.slice(0, 160).replace(/\s+\S*$/, "") + "...";
424
+ const now = /* @__PURE__ */ new Date();
425
+ const doc = {
426
+ title: input.title,
427
+ slug,
428
+ excerpt,
429
+ content: input.content || [],
430
+ contentHTML: input.contentHTML || "",
431
+ contentText,
432
+ coverImage: input.coverImage,
433
+ categories: input.categories || [],
434
+ tags: input.tags || [],
435
+ author: input.author || defaultAuthor || { name: "Admin" },
436
+ seo: {
437
+ ogType: "article",
438
+ noIndex: false,
439
+ ...input.seo
440
+ },
441
+ status: input.status || "draft",
442
+ publishedAt: input.status === "published" ? now : input.publishedAt,
443
+ scheduledAt: input.scheduledAt,
444
+ readingTime,
445
+ wordCount,
446
+ version: 1,
447
+ revisions: [],
448
+ createdAt: now,
449
+ updatedAt: now
450
+ };
451
+ const result = await col.insertOne(doc);
452
+ return { _id: result.insertedId, ...doc };
453
+ }
454
+ async function updatePost(id, input) {
455
+ const col = await getCollection("nbk_posts");
456
+ const objectId = new import_mongodb.ObjectId(id);
457
+ const existing = await col.findOne({ _id: objectId });
458
+ if (!existing) return null;
459
+ const updates = { ...input, updatedAt: /* @__PURE__ */ new Date() };
460
+ if (input.slug && input.slug !== existing.slug) {
461
+ updates.slug = await ensureUniqueSlug(input.slug, col, id);
462
+ }
463
+ if (input.content) {
464
+ const contentText = input.contentText || extractTextFromBlocks(input.content);
465
+ updates.contentText = contentText;
466
+ updates.wordCount = countWords(contentText);
467
+ updates.readingTime = calculateReadingTime(contentText);
468
+ if (!input.excerpt) {
469
+ updates.excerpt = contentText.slice(0, 160).replace(/\s+\S*$/, "") + "...";
470
+ }
471
+ const revision = {
472
+ version: existing.version || 1,
473
+ title: existing.title,
474
+ content: existing.content,
475
+ contentHTML: existing.contentHTML,
476
+ savedAt: /* @__PURE__ */ new Date()
477
+ };
478
+ const revisions = [...existing.revisions || [], revision].slice(-10);
479
+ updates.revisions = revisions;
480
+ updates.version = (existing.version || 1) + 1;
481
+ }
482
+ if (input.status === "published" && existing.status !== "published") {
483
+ updates.publishedAt = /* @__PURE__ */ new Date();
484
+ }
485
+ await col.updateOne({ _id: objectId }, { $set: updates });
486
+ return await col.findOne({ _id: objectId });
487
+ }
488
+ async function deletePost(id) {
489
+ const col = await getCollection("nbk_posts");
490
+ const result = await col.updateOne(
491
+ { _id: new import_mongodb.ObjectId(id) },
492
+ { $set: { status: "archived", updatedAt: /* @__PURE__ */ new Date() } }
493
+ );
494
+ return result.modifiedCount > 0;
495
+ }
496
+ async function hardDeletePost(id) {
497
+ const col = await getCollection("nbk_posts");
498
+ const result = await col.deleteOne({ _id: new import_mongodb.ObjectId(id) });
499
+ return result.deletedCount > 0;
500
+ }
501
+ async function getPostBySlug(slug) {
502
+ const col = await getCollection("nbk_posts");
503
+ return await col.findOne({ slug });
504
+ }
505
+ async function getPostById(id) {
506
+ const col = await getCollection("nbk_posts");
507
+ return await col.findOne({ _id: new import_mongodb.ObjectId(id) });
508
+ }
509
+ async function listPosts(query = {}) {
510
+ const col = await getCollection("nbk_posts");
511
+ const {
512
+ page = 1,
513
+ limit = 10,
514
+ category,
515
+ tag,
516
+ status,
517
+ search,
518
+ sortBy = "publishedAt",
519
+ sortOrder = "desc"
520
+ } = query;
521
+ const filter = {};
522
+ if (status) {
523
+ filter.status = status;
524
+ } else {
525
+ filter.status = { $ne: "archived" };
526
+ }
527
+ if (category) filter.categories = category;
528
+ if (tag) filter.tags = tag;
529
+ if (search) {
530
+ filter.$text = { $search: search };
531
+ }
532
+ const sort = {
533
+ [sortBy]: sortOrder === "asc" ? 1 : -1
534
+ };
535
+ const skip = (page - 1) * limit;
536
+ const [posts, total] = await Promise.all([
537
+ col.find(filter).sort(sort).skip(skip).limit(limit).toArray(),
538
+ col.countDocuments(filter)
539
+ ]);
540
+ return {
541
+ posts,
542
+ total
543
+ };
544
+ }
545
+ async function createCategory(input) {
546
+ const col = await getCollection("nbk_categories");
547
+ const slug = await ensureUniqueSlug(
548
+ input.slug || generateSlug(input.name),
549
+ col
550
+ );
551
+ const doc = {
552
+ name: input.name,
553
+ slug,
554
+ description: input.description,
555
+ seo: input.seo,
556
+ order: input.order ?? 0,
557
+ parentId: input.parentId ? new import_mongodb.ObjectId(input.parentId) : void 0,
558
+ postCount: 0
559
+ };
560
+ const result = await col.insertOne(doc);
561
+ return { _id: result.insertedId, ...doc };
562
+ }
563
+ async function updateCategory(id, input) {
564
+ const col = await getCollection("nbk_categories");
565
+ const objectId = new import_mongodb.ObjectId(id);
566
+ const updates = { ...input };
567
+ if (input.slug) {
568
+ updates.slug = await ensureUniqueSlug(input.slug, col, id);
569
+ }
570
+ if (input.parentId) {
571
+ updates.parentId = new import_mongodb.ObjectId(input.parentId);
572
+ }
573
+ await col.updateOne({ _id: objectId }, { $set: updates });
574
+ return await col.findOne({ _id: objectId });
575
+ }
576
+ async function deleteCategory(id) {
577
+ const col = await getCollection("nbk_categories");
578
+ const result = await col.deleteOne({ _id: new import_mongodb.ObjectId(id) });
579
+ return result.deletedCount > 0;
580
+ }
581
+ async function listCategories() {
582
+ const col = await getCollection("nbk_categories");
583
+ return await col.find({}).sort({ order: 1 }).toArray();
584
+ }
585
+ async function getCategoryBySlug(slug) {
586
+ const col = await getCollection("nbk_categories");
587
+ return await col.findOne({ slug });
588
+ }
589
+ async function updateCategoryPostCount(categorySlug) {
590
+ const posts = await getCollection("nbk_posts");
591
+ const categories = await getCollection("nbk_categories");
592
+ const count = await posts.countDocuments({
593
+ categories: categorySlug,
594
+ status: "published"
595
+ });
596
+ await categories.updateOne({ slug: categorySlug }, { $set: { postCount: count } });
597
+ }
598
+ async function createMedia(data) {
599
+ const col = await getCollection("nbk_media");
600
+ const result = await col.insertOne(data);
601
+ return { _id: result.insertedId, ...data };
602
+ }
603
+ async function getMediaById(id) {
604
+ const col = await getCollection("nbk_media");
605
+ const media = await col.findOne({ _id: new import_mongodb.ObjectId(id) });
606
+ return media || null;
607
+ }
608
+ async function deleteMedia(id) {
609
+ const col = await getCollection("nbk_media");
610
+ const media = await col.findOne({ _id: new import_mongodb.ObjectId(id) });
611
+ if (!media) return null;
612
+ await col.deleteOne({ _id: new import_mongodb.ObjectId(id) });
613
+ return media;
614
+ }
615
+ async function listMedia(query = {}) {
616
+ const col = await getCollection("nbk_media");
617
+ const { page = 1, limit = 20, mimeType } = query;
618
+ const filter = {};
619
+ if (mimeType) filter.mimeType = { $regex: mimeType };
620
+ const skip = (page - 1) * limit;
621
+ const [media, total] = await Promise.all([
622
+ col.find(filter).sort({ createdAt: -1 }).skip(skip).limit(limit).toArray(),
623
+ col.countDocuments(filter)
624
+ ]);
625
+ return { media, total };
626
+ }
627
+ async function getSettings() {
628
+ const col = await getCollection("nbk_settings");
629
+ const settings = await col.findOne({ _id: "global" });
630
+ if (!settings) {
631
+ const defaults = {
632
+ _id: "global",
633
+ postsPerPage: 10,
634
+ commentSystem: "none"
635
+ };
636
+ await col.insertOne(defaults);
637
+ return defaults;
638
+ }
639
+ return settings;
640
+ }
641
+ async function updateSettings(data) {
642
+ const col = await getCollection("nbk_settings");
643
+ const { _id, ...updates } = data;
644
+ await col.updateOne(
645
+ { _id: "global" },
646
+ { $set: updates },
647
+ { upsert: true }
648
+ );
649
+ return getSettings();
650
+ }
651
+ function hashToken(plain) {
652
+ return (0, import_crypto.createHash)("sha256").update(plain).digest("hex");
653
+ }
654
+ async function createApiToken(name) {
655
+ const col = await getCollection("nbk_api_tokens");
656
+ const plainToken = "nbk_" + (0, import_crypto.randomBytes)(24).toString("hex");
657
+ const tokenHash = hashToken(plainToken);
658
+ const prefix = plainToken.slice(0, 8);
659
+ const now = /* @__PURE__ */ new Date();
660
+ const doc = {
661
+ name,
662
+ tokenHash,
663
+ prefix,
664
+ createdAt: now
665
+ };
666
+ const result = await col.insertOne(doc);
667
+ const token = { _id: result.insertedId, ...doc };
668
+ return { token, plainToken };
669
+ }
670
+ async function listApiTokens() {
671
+ const col = await getCollection("nbk_api_tokens");
672
+ return await col.find({}, { projection: { tokenHash: 0 } }).sort({ createdAt: -1 }).toArray();
673
+ }
674
+ async function deleteApiToken(id) {
675
+ const col = await getCollection("nbk_api_tokens");
676
+ const result = await col.deleteOne({ _id: new import_mongodb.ObjectId(id) });
677
+ return result.deletedCount > 0;
678
+ }
679
+ async function verifyApiToken(plainToken) {
680
+ const col = await getCollection("nbk_api_tokens");
681
+ const tokenHash = hashToken(plainToken);
682
+ const token = await col.findOneAndUpdate(
683
+ { tokenHash },
684
+ { $set: { lastUsedAt: /* @__PURE__ */ new Date() } },
685
+ { returnDocument: "after" }
686
+ );
687
+ return token;
688
+ }
689
+ var import_mongodb, import_crypto, client, db;
690
+ var init_db = __esm({
691
+ "src/lib/db.ts"() {
692
+ "use strict";
693
+ import_mongodb = require("mongodb");
694
+ import_crypto = require("crypto");
695
+ init_config();
696
+ init_slug();
697
+ init_reading_time();
698
+ client = null;
699
+ db = null;
700
+ }
701
+ });
702
+
703
+ // src/lib/storage.ts
704
+ var storage_exports = {};
705
+ __export(storage_exports, {
706
+ BUILT_IN_PROVIDER_NAMES: () => BUILT_IN_PROVIDER_NAMES,
707
+ GRIDFS_BUCKET: () => GRIDFS_BUCKET,
708
+ GridFSStorageProvider: () => GridFSStorageProvider,
709
+ R2StorageProvider: () => R2StorageProvider,
710
+ StorageProviderNotRegisteredError: () => StorageProviderNotRegisteredError,
711
+ generateKey: () => generateKey,
712
+ getStorageProvider: () => getStorageProvider,
713
+ getStorageProviderByName: () => getStorageProviderByName,
714
+ isStorageProviderRegistered: () => isStorageProviderRegistered,
715
+ listStorageProviderNames: () => listStorageProviderNames,
716
+ registerStorageProvider: () => registerStorageProvider,
717
+ resetStorageProviders: () => resetStorageProviders,
718
+ rollbackUploads: () => rollbackUploads,
719
+ unregisterStorageProvider: () => unregisterStorageProvider
720
+ });
721
+ function getS3Client() {
722
+ if (clientInstance) return clientInstance;
723
+ if (!isR2Configured()) {
724
+ throw new Error(
725
+ "Cloudflare R2 is not configured. Set BLOGPANEL_R2_ACCOUNT_ID, BLOGPANEL_R2_ACCESS_KEY, BLOGPANEL_R2_SECRET_KEY, BLOGPANEL_R2_BUCKET, and BLOGPANEL_R2_PUBLIC_URL environment variables to enable image uploads."
726
+ );
727
+ }
728
+ const env = getEnvConfig();
729
+ clientInstance = new import_client_s3.S3Client({
730
+ region: "auto",
731
+ endpoint: `https://${env.BLOGPANEL_R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
732
+ credentials: {
733
+ accessKeyId: env.BLOGPANEL_R2_ACCESS_KEY,
734
+ secretAccessKey: env.BLOGPANEL_R2_SECRET_KEY
735
+ }
736
+ });
737
+ return clientInstance;
738
+ }
739
+ function generateKey(filename) {
740
+ const now = /* @__PURE__ */ new Date();
741
+ const year = now.getFullYear();
742
+ const month = String(now.getMonth() + 1).padStart(2, "0");
743
+ const uuid = (0, import_crypto2.randomUUID)().split("-")[0];
744
+ const safeName = filename.replace(/[^a-zA-Z0-9._-]/g, "-").toLowerCase();
745
+ return `media/${year}/${month}/${uuid}-${safeName}`;
746
+ }
747
+ function registry() {
748
+ const store = globalThis;
749
+ let current = store[REGISTRY_SLOT];
750
+ if (!current) {
751
+ current = /* @__PURE__ */ new Map();
752
+ store[REGISTRY_SLOT] = current;
753
+ }
754
+ return current;
755
+ }
756
+ function registerStorageProvider(provider) {
757
+ if (!provider || typeof provider !== "object") {
758
+ throw new Error(
759
+ "Next BlogPanel: registerStorageProvider() expects a StorageProvider instance."
760
+ );
761
+ }
762
+ const name = provider.name;
763
+ if (typeof name !== "string" || name.trim() === "") {
764
+ throw new Error(
765
+ "Next BlogPanel: a storage provider must expose a non-empty `name`. That value is written to Media.provider on every upload and read back to route the delete, so a provider without one cannot have its objects deleted."
766
+ );
767
+ }
768
+ if (BUILT_IN_PROVIDER_NAMES.includes(name)) {
769
+ throw new Error(
770
+ `Next BlogPanel: '${name}' is reserved by a built-in storage provider and cannot be registered over. Media documents already record '${name}', so replacing it would redirect their deletes to a backend that does not hold them \u2014 which strands the objects silently. Give your provider a distinct name.`
771
+ );
772
+ }
773
+ for (const method of ["upload", "delete", "list"]) {
774
+ if (typeof provider[method] !== "function") {
775
+ throw new Error(
776
+ `Next BlogPanel: storage provider '${name}' is missing a \`${method}()\` method. upload(), delete() and list() are all required \u2014 see docs/storage.md.`
777
+ );
778
+ }
779
+ }
780
+ registry().set(name, provider);
781
+ return provider;
782
+ }
783
+ function unregisterStorageProvider(name) {
784
+ return registry().delete(name);
785
+ }
786
+ function listStorageProviderNames() {
787
+ return [...BUILT_IN_PROVIDER_NAMES, ...registry().keys()];
788
+ }
789
+ function isStorageProviderRegistered(name) {
790
+ return BUILT_IN_PROVIDER_NAMES.includes(name) || registry().has(name);
791
+ }
792
+ function resetStorageProviders() {
793
+ const store = globalThis;
794
+ delete store[REGISTRY_SLOT];
795
+ }
796
+ function getStorageProvider() {
797
+ const env = getEnvConfig();
798
+ const forced = env.BLOGPANEL_STORAGE?.trim();
799
+ if (forced) return getStorageProviderByName(forced);
800
+ return isR2Configured() ? new R2StorageProvider() : new GridFSStorageProvider();
801
+ }
802
+ function getStorageProviderByName(name) {
803
+ if (name === "gridfs") return new GridFSStorageProvider();
804
+ if (name === "r2") return new R2StorageProvider();
805
+ const registered = registry().get(name);
806
+ if (registered) return registered;
807
+ throw new StorageProviderNotRegisteredError(name, listStorageProviderNames());
808
+ }
809
+ async function rollbackUploads(storage, keys, context) {
810
+ await Promise.allSettled(
811
+ keys.map(
812
+ (key) => storage.delete(key).catch((cleanupError) => {
813
+ console.error(
814
+ `[blogpanel] Failed to clean up orphaned upload after ${context} (provider=${storage.name}, key=${key}):`,
815
+ cleanupError
816
+ );
817
+ })
818
+ )
819
+ );
820
+ }
821
+ var import_client_s3, import_mongodb2, import_crypto2, BUILT_IN_PROVIDER_NAMES, clientInstance, R2StorageProvider, GRIDFS_BUCKET, GridFSStorageProvider, REGISTRY_SLOT, StorageProviderNotRegisteredError;
822
+ var init_storage = __esm({
823
+ "src/lib/storage.ts"() {
824
+ "use strict";
825
+ import_client_s3 = require("@aws-sdk/client-s3");
826
+ import_mongodb2 = require("mongodb");
827
+ init_config();
828
+ init_db();
829
+ import_crypto2 = require("crypto");
830
+ BUILT_IN_PROVIDER_NAMES = ["gridfs", "r2"];
831
+ clientInstance = null;
832
+ R2StorageProvider = class {
833
+ constructor() {
834
+ this.name = "r2";
835
+ }
836
+ async upload(file, filename, contentType) {
837
+ const env = getEnvConfig();
838
+ const client2 = getS3Client();
839
+ const key = generateKey(filename);
840
+ await client2.send(
841
+ new import_client_s3.PutObjectCommand({
842
+ Bucket: env.BLOGPANEL_R2_BUCKET,
843
+ Key: key,
844
+ Body: file,
845
+ ContentType: contentType
846
+ })
847
+ );
848
+ return {
849
+ key,
850
+ url: `${env.BLOGPANEL_R2_PUBLIC_URL}/${key}`,
851
+ size: file.length,
852
+ contentType
853
+ };
854
+ }
855
+ async delete(key) {
856
+ const env = getEnvConfig();
857
+ const client2 = getS3Client();
858
+ await client2.send(
859
+ new import_client_s3.DeleteObjectCommand({
860
+ Bucket: env.BLOGPANEL_R2_BUCKET,
861
+ Key: key
862
+ })
863
+ );
864
+ }
865
+ async list(prefix) {
866
+ const env = getEnvConfig();
867
+ const client2 = getS3Client();
868
+ const response = await client2.send(
869
+ new import_client_s3.ListObjectsV2Command({
870
+ Bucket: env.BLOGPANEL_R2_BUCKET,
871
+ Prefix: prefix || "media/"
872
+ })
873
+ );
874
+ return (response.Contents || []).map((obj) => ({
875
+ key: obj.Key || "",
876
+ size: obj.Size || 0,
877
+ lastModified: obj.LastModified || /* @__PURE__ */ new Date()
878
+ }));
879
+ }
880
+ };
881
+ GRIDFS_BUCKET = "media";
882
+ GridFSStorageProvider = class {
883
+ constructor() {
884
+ this.name = "gridfs";
885
+ }
886
+ async bucket() {
887
+ const db2 = await getDb();
888
+ return new import_mongodb2.GridFSBucket(db2, { bucketName: GRIDFS_BUCKET });
889
+ }
890
+ async upload(file, filename, contentType) {
891
+ const apiPath = requireApiPath("the URL for this GridFS upload");
892
+ const bucket = await this.bucket();
893
+ const key = generateKey(filename);
894
+ await new Promise((resolve2, reject) => {
895
+ const stream = bucket.openUploadStream(key, { contentType });
896
+ stream.on("error", reject);
897
+ stream.on("finish", () => resolve2());
898
+ stream.end(file);
899
+ });
900
+ return {
901
+ key,
902
+ url: `${apiPath}/media/file/${key}`,
903
+ size: file.length,
904
+ contentType
905
+ };
906
+ }
907
+ async delete(key) {
908
+ const bucket = await this.bucket();
909
+ const files = await bucket.find({ filename: key }).toArray();
910
+ for (const file of files) {
911
+ await bucket.delete(file._id);
912
+ }
913
+ }
914
+ async list(prefix) {
915
+ const bucket = await this.bucket();
916
+ const pattern = new RegExp("^" + (prefix || "media/").replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
917
+ const files = await bucket.find({ filename: { $regex: pattern } }).toArray();
918
+ return files.map((f) => ({
919
+ key: f.filename,
920
+ size: f.length,
921
+ lastModified: f.uploadDate
922
+ }));
923
+ }
924
+ };
925
+ REGISTRY_SLOT = /* @__PURE__ */ Symbol.for("next-blogpanel.storage.registry.v1");
926
+ StorageProviderNotRegisteredError = class extends Error {
927
+ constructor(providerName, available) {
928
+ super(
929
+ `Next BlogPanel: no storage provider named '${providerName}' is registered in this server runtime.
930
+ Resolvable here: ${available.map((n) => `'${n}'`).join(", ")}.
931
+ This is refused rather than quietly falling back to the configured default, because every backend generates identically-shaped keys: a default asked to delete a key it does not hold finds nothing and returns cleanly, silently stranding the real object.
932
+ Fix: call registerStorageProvider(new MyProvider()) before any request is served \u2014 the generated setup module (app/next-blogpanel.setup.ts) is the place, since every scaffolded route imports it. If '${providerName}' is a typo in BLOGPANEL_STORAGE or in a stored Media.provider value, correct that instead.
933
+ See docs/storage.md \u2014 "Custom storage providers".`
934
+ );
935
+ this.name = "StorageProviderNotRegisteredError";
936
+ this.providerName = providerName;
937
+ this.available = available;
938
+ }
939
+ };
940
+ }
941
+ });
942
+
943
+ // src/cli/index.ts
944
+ var import_node_fs = require("fs");
945
+ var import_node_path = require("path");
946
+ var import_commander = require("commander");
947
+
948
+ // src/cli/init.ts
949
+ var import_fs = __toESM(require("fs"), 1);
950
+ var import_path = __toESM(require("path"), 1);
951
+
952
+ // src/cli/templates.ts
953
+ var SETUP_MODULE = "next-blogpanel.setup";
954
+ function setupSpecifier(templatePath) {
955
+ const depth = templatePath.split("/").length - 1;
956
+ return depth === 0 ? `./${SETUP_MODULE}` : `${"../".repeat(depth)}${SETUP_MODULE}`;
957
+ }
958
+ function toDir(urlPath) {
959
+ return urlPath.replace(/^\/+/, "").replace(/\/+$/, "");
960
+ }
961
+ function toUrl(urlPath) {
962
+ return "/" + toDir(urlPath);
963
+ }
964
+ function getEnvExample() {
965
+ return `# Next BlogPanel Configuration
966
+ # Copy this to .env.local and fill in your values
967
+
968
+ # MongoDB Connection (required)
969
+ BLOGPANEL_MONGODB_URI=mongodb+srv://user:pass@cluster.mongodb.net/mydb
970
+
971
+ # Authentication (required \u2014 must be at least 32 characters)
972
+ BLOGPANEL_API_KEY=your-secure-api-key-must-be-at-least-32-characters-long
973
+
974
+ # Image storage
975
+ # GridFS (stored inside your MongoDB database) is the DEFAULT and needs no
976
+ # setup \u2014 leave the block below commented out to use it. Uncomment ALL FIVE
977
+ # lines to switch the storage backend to Cloudflare R2 instead. They are
978
+ # only read as a set: isR2Configured() requires every one of them to be
979
+ # present, so partially filling them in has no effect and GridFS keeps
980
+ # being used.
981
+ # BLOGPANEL_R2_ACCOUNT_ID=your-account-id
982
+ # BLOGPANEL_R2_ACCESS_KEY=your-access-key
983
+ # BLOGPANEL_R2_SECRET_KEY=your-secret-key
984
+ # BLOGPANEL_R2_BUCKET=blog-media
985
+ # BLOGPANEL_R2_PUBLIC_URL=https://media.yourdomain.com
986
+
987
+ # Force a specific storage backend, overriding the automatic choice above.
988
+ # Accepts 'gridfs', 'r2', or the name of a provider you registered yourself
989
+ # with registerStorageProvider() \u2014 see docs/storage.md, "Custom storage
990
+ # providers". A name that cannot be resolved fails loudly and names the ones
991
+ # that can, rather than quietly falling back. Leave commented out to let the
992
+ # automatic choice apply.
993
+ # BLOGPANEL_STORAGE=gridfs
994
+
995
+ # Site info \u2014 REQUIRED before you deploy, even though the schema marks
996
+ # them optional. The sitemap, RSS feed, and JSON-LD structured data all
997
+ # build absolute URLs from BLOGPANEL_SITE_URL. Leaving the placeholder
998
+ # below means your sitemap.xml and RSS feed will advertise pages on
999
+ # "yourdomain.com" \u2014 a domain you don't own \u2014 to search engines and
1000
+ # feed readers. If you comment this out instead, it's worse: it defaults
1001
+ # to an empty string, so every one of those absolute URLs becomes a bare
1002
+ # relative path, which is invalid per the sitemap protocol and schema.org.
1003
+ # Set this to your real domain (no trailing slash) before going live.
1004
+ BLOGPANEL_SITE_URL=https://yourdomain.com
1005
+ BLOGPANEL_SITE_NAME="Your Site Name"
1006
+ `;
1007
+ }
1008
+ function getTemplates(options) {
1009
+ const blogDir = toDir(options.blogPath);
1010
+ const adminDir = toDir(options.adminPath);
1011
+ const apiDir = toDir(options.apiPath);
1012
+ const blogUrl = toUrl(options.blogPath);
1013
+ const adminUrl = toUrl(options.adminPath);
1014
+ const apiUrl = toUrl(options.apiPath);
1015
+ const configImport = options.configImport || "../next-blogpanel.config";
1016
+ const setup = {
1017
+ path: `${SETUP_MODULE}.ts`,
1018
+ content: `// Generated by \`next-blogpanel init\`. Do not delete.
1019
+ //
1020
+ // This module hands your next-blogpanel.config.ts to the server at runtime.
1021
+ // Nothing else reads that file: without this import chain the package would
1022
+ // silently run on its built-in defaults, and \`apiPath\` in particular would be
1023
+ // wrong in URLs that get written permanently into the database.
1024
+ //
1025
+ // Every scaffolded route handler, layout and page imports this module. That
1026
+ // looks redundant, and on a single server it is \u2014 but Next.js instantiates
1027
+ // server modules per route entry, so a value installed while handling one
1028
+ // route is not guaranteed to be visible to the module instance serving
1029
+ // another. setConfig() stores the config on globalThis, which every bundle in
1030
+ // the process shares, and importing this module everywhere guarantees some
1031
+ // entry point has installed it before any request is served, whichever route
1032
+ // gets hit first.
1033
+ //
1034
+ // If you add your own route handler or server component that calls
1035
+ // next-blogpanel APIs, import this module at the top of it too:
1036
+ //
1037
+ // import '<relative path>/${SETUP_MODULE}';
1038
+ import { setConfig } from 'next-blogpanel';
1039
+ import config from '${configImport}';
1040
+
1041
+ setConfig(config);
1042
+
1043
+ // ---- Custom storage backend (optional) ----
1044
+ //
1045
+ // GridFS is the default and needs nothing here. Cloudflare R2 is selected by
1046
+ // its five environment variables and needs nothing here either.
1047
+ //
1048
+ // To store uploads somewhere else \u2014 Vercel Blob, Supabase Storage, Backblaze
1049
+ // B2, ImageKit, UploadThing, S3, anything \u2014 implement the StorageProvider
1050
+ // interface and register the instance below, then set BLOGPANEL_STORAGE to
1051
+ // the same name your provider reports.
1052
+ //
1053
+ // This module is the right home for it: registration has to happen before any
1054
+ // request is served, and every scaffolded route imports this file, so
1055
+ // whichever route a request lands on first has already run it.
1056
+ //
1057
+ // The name matters more than it looks. It is written to \`provider\` on every
1058
+ // media document and read back to decide which backend to ask when that item
1059
+ // is deleted. Keep it stable, and never reuse it for a different backend \u2014
1060
+ // see docs/storage.md, "Custom storage providers".
1061
+ //
1062
+ // import { registerStorageProvider } from 'next-blogpanel';
1063
+ // import { MyStorageProvider } from '../lib/my-storage-provider';
1064
+ //
1065
+ // registerStorageProvider(new MyStorageProvider());
1066
+ `
1067
+ };
1068
+ const files = [
1069
+ // ---- Blog Pages ----
1070
+ {
1071
+ path: `${blogDir}/layout.tsx`,
1072
+ content: `import 'next-blogpanel/styles/blog.css';
1073
+
1074
+ export default function BlogLayout({ children }: { children: React.ReactNode }) {
1075
+ return <>{children}</>;
1076
+ }
1077
+ `
1078
+ },
1079
+ {
1080
+ path: `${blogDir}/page.tsx`,
1081
+ content: `import { BlogCard, BlogSearch, Pagination, CategoryList } from 'next-blogpanel/components';
1082
+ import { listPosts, listCategories } from 'next-blogpanel/lib';
1083
+
1084
+ interface Props {
1085
+ searchParams: Promise<{ page?: string; category?: string; q?: string }>;
1086
+ }
1087
+
1088
+ export default async function Blog({ searchParams }: Props) {
1089
+ const params = await searchParams;
1090
+ const page = parseInt(params.page || '1', 10);
1091
+
1092
+ const [{ posts, total }, categories] = await Promise.all([
1093
+ listPosts({
1094
+ page,
1095
+ limit: 10,
1096
+ status: 'published',
1097
+ category: params.category || undefined,
1098
+ search: params.q || undefined,
1099
+ sortBy: 'publishedAt',
1100
+ sortOrder: 'desc',
1101
+ }),
1102
+ listCategories(),
1103
+ ]);
1104
+
1105
+ const totalPages = Math.ceil(total / 10);
1106
+
1107
+ return (
1108
+ <div className="nbp-blog-list">
1109
+ <div className="nbp-blog-header">
1110
+ <BlogSearch apiPath="${apiUrl}" />
1111
+ </div>
1112
+
1113
+ <div className="nbp-blog-layout">
1114
+ <div className="nbp-blog-main">
1115
+ {posts.length === 0 ? (
1116
+ <div className="nbp-empty-state">
1117
+ <p>No posts found.</p>
1118
+ </div>
1119
+ ) : (
1120
+ <div className="nbp-posts-grid">
1121
+ {posts.map((post) => (
1122
+ <BlogCard
1123
+ key={String(post._id || post.slug)}
1124
+ post={JSON.parse(JSON.stringify(post))}
1125
+ basePath="${blogUrl}"
1126
+ />
1127
+ ))}
1128
+ </div>
1129
+ )}
1130
+
1131
+ {totalPages > 1 && (
1132
+ <Pagination
1133
+ currentPage={page}
1134
+ totalPages={totalPages}
1135
+ basePath="${blogUrl}"
1136
+ category={params.category}
1137
+ />
1138
+ )}
1139
+ </div>
1140
+
1141
+ {categories.length > 0 && (
1142
+ <aside className="nbp-blog-sidebar">
1143
+ <CategoryList
1144
+ categories={JSON.parse(JSON.stringify(categories))}
1145
+ activeCategory={params.category}
1146
+ basePath="${blogUrl}"
1147
+ />
1148
+ </aside>
1149
+ )}
1150
+ </div>
1151
+ </div>
1152
+ );
1153
+ }
1154
+ `
1155
+ },
1156
+ {
1157
+ path: `${blogDir}/[slug]/page.tsx`,
1158
+ content: `import { AuthorCard, BreadcrumbNav, ShareButtons, ReadingProgressBar, BlogCard } from 'next-blogpanel/components';
1159
+ import { getPostBySlug, listPosts, generateMetaTags } from 'next-blogpanel/lib';
1160
+ import type { Metadata } from 'next';
1161
+
1162
+ interface Props {
1163
+ params: Promise<{ slug: string }>;
1164
+ }
1165
+
1166
+ export async function generateMetadata({ params }: Props): Promise<Metadata> {
1167
+ const { slug } = await params;
1168
+ const post = await getPostBySlug(slug);
1169
+ if (!post) return { title: 'Post Not Found' };
1170
+ const meta = generateMetaTags(post);
1171
+ return {
1172
+ title: meta.title,
1173
+ description: meta.description,
1174
+ openGraph: meta.openGraph,
1175
+ twitter: meta.twitter,
1176
+ alternates: { canonical: meta.canonical },
1177
+ };
1178
+ }
1179
+
1180
+ export default async function BlogPost({ params }: Props) {
1181
+ const { slug } = await params;
1182
+ const post = await getPostBySlug(slug);
1183
+
1184
+ if (!post) {
1185
+ return <div className="nbp-not-found">Post not found</div>;
1186
+ }
1187
+
1188
+ let relatedPosts: any[] = [];
1189
+ if (post.categories?.length) {
1190
+ const { posts } = await listPosts({
1191
+ status: 'published',
1192
+ category: post.categories[0],
1193
+ limit: 4,
1194
+ });
1195
+ relatedPosts = posts
1196
+ .filter((p) => String(p._id) !== String(post._id))
1197
+ .slice(0, 3);
1198
+ }
1199
+
1200
+ const date = post.publishedAt
1201
+ ? new Date(post.publishedAt).toLocaleDateString('en-US', {
1202
+ year: 'numeric',
1203
+ month: 'long',
1204
+ day: 'numeric',
1205
+ })
1206
+ : '';
1207
+
1208
+ return (
1209
+ <article className="nbp-post">
1210
+ <ReadingProgressBar />
1211
+
1212
+ <BreadcrumbNav
1213
+ items={[
1214
+ { label: 'Home', href: '/' },
1215
+ { label: 'Blog', href: '${blogUrl}' },
1216
+ ...(post.categories?.[0]
1217
+ ? [{ label: post.categories[0], href: \`${blogUrl}/category/\${post.categories[0]}\` }]
1218
+ : []),
1219
+ { label: post.title },
1220
+ ]}
1221
+ />
1222
+
1223
+ <header className="nbp-post-header">
1224
+ {post.categories?.length > 0 && (
1225
+ <div className="nbp-post-categories">
1226
+ {post.categories.map((cat: string) => (
1227
+ <a key={cat} href={\`${blogUrl}/category/\${cat}\`} className="nbp-post-category">
1228
+ {cat}
1229
+ </a>
1230
+ ))}
1231
+ </div>
1232
+ )}
1233
+ <h1 className="nbp-post-title">{post.title}</h1>
1234
+ <div className="nbp-post-meta">
1235
+ <span className="nbp-post-author">{post.author?.name}</span>
1236
+ {date && (
1237
+ <>
1238
+ <span className="nbp-post-sep">&middot;</span>
1239
+ <time className="nbp-post-date">{date}</time>
1240
+ </>
1241
+ )}
1242
+ <span className="nbp-post-sep">&middot;</span>
1243
+ <span className="nbp-post-reading-time">{post.readingTime} min read</span>
1244
+ </div>
1245
+ </header>
1246
+
1247
+ {post.coverImage?.url && (
1248
+ <div className="nbp-post-cover">
1249
+ <img src={post.coverImage.url} alt={post.coverImage.alt || post.title} />
1250
+ </div>
1251
+ )}
1252
+
1253
+ <div
1254
+ className="nbp-post-content"
1255
+ dangerouslySetInnerHTML={{ __html: post.contentHTML || '' }}
1256
+ />
1257
+
1258
+ <ShareButtons url={\`${blogUrl}/\${slug}\`} title={post.title} />
1259
+
1260
+ {post.author && <AuthorCard author={post.author} />}
1261
+
1262
+ {relatedPosts.length > 0 && (
1263
+ <section className="nbp-related">
1264
+ <h2 className="nbp-related-title">Related Posts</h2>
1265
+ <div className="nbp-related-grid">
1266
+ {relatedPosts.map((p) => (
1267
+ <BlogCard key={String(p._id)} post={JSON.parse(JSON.stringify(p))} basePath="${blogUrl}" />
1268
+ ))}
1269
+ </div>
1270
+ </section>
1271
+ )}
1272
+ </article>
1273
+ );
1274
+ }
1275
+ `
1276
+ },
1277
+ {
1278
+ path: `${blogDir}/category/[slug]/page.tsx`,
1279
+ content: `import { BlogCard, BlogSearch, Pagination, CategoryList } from 'next-blogpanel/components';
1280
+ import { listPosts, listCategories } from 'next-blogpanel/lib';
1281
+
1282
+ interface Props {
1283
+ params: Promise<{ slug: string }>;
1284
+ searchParams: Promise<{ page?: string }>;
1285
+ }
1286
+
1287
+ export default async function CategoryPage({ params, searchParams }: Props) {
1288
+ const { slug } = await params;
1289
+ const sp = await searchParams;
1290
+ const page = parseInt(sp.page || '1', 10);
1291
+
1292
+ const [{ posts, total }, categories] = await Promise.all([
1293
+ listPosts({
1294
+ page,
1295
+ limit: 10,
1296
+ status: 'published',
1297
+ category: slug,
1298
+ sortBy: 'publishedAt',
1299
+ sortOrder: 'desc',
1300
+ }),
1301
+ listCategories(),
1302
+ ]);
1303
+
1304
+ const totalPages = Math.ceil(total / 10);
1305
+
1306
+ return (
1307
+ <div className="nbp-blog-list">
1308
+ <div className="nbp-blog-header">
1309
+ <BlogSearch apiPath="${apiUrl}" />
1310
+ </div>
1311
+
1312
+ <div className="nbp-blog-layout">
1313
+ <div className="nbp-blog-main">
1314
+ {posts.length === 0 ? (
1315
+ <div className="nbp-empty-state">
1316
+ <p>No posts found in this category.</p>
1317
+ </div>
1318
+ ) : (
1319
+ <div className="nbp-posts-grid">
1320
+ {posts.map((post) => (
1321
+ <BlogCard
1322
+ key={String(post._id || post.slug)}
1323
+ post={JSON.parse(JSON.stringify(post))}
1324
+ basePath="${blogUrl}"
1325
+ />
1326
+ ))}
1327
+ </div>
1328
+ )}
1329
+
1330
+ {totalPages > 1 && (
1331
+ <Pagination
1332
+ currentPage={page}
1333
+ totalPages={totalPages}
1334
+ basePath="${blogUrl}"
1335
+ category={slug}
1336
+ />
1337
+ )}
1338
+ </div>
1339
+
1340
+ {categories.length > 0 && (
1341
+ <aside className="nbp-blog-sidebar">
1342
+ <CategoryList
1343
+ categories={JSON.parse(JSON.stringify(categories))}
1344
+ activeCategory={slug}
1345
+ basePath="${blogUrl}"
1346
+ />
1347
+ </aside>
1348
+ )}
1349
+ </div>
1350
+ </div>
1351
+ );
1352
+ }
1353
+ `
1354
+ },
1355
+ // ---- Admin Pages ----
1356
+ {
1357
+ path: `${adminDir}/layout.tsx`,
1358
+ content: `import { AdminLayout } from 'next-blogpanel/admin';
1359
+ import 'next-blogpanel/styles/admin.css';
1360
+
1361
+ export default function AdminBlogLayout({ children }: { children: React.ReactNode }) {
1362
+ return (
1363
+ <AdminLayout adminPath="${adminUrl}" apiPath="${apiUrl}" basePath="${blogUrl}">
1364
+ {children}
1365
+ </AdminLayout>
1366
+ );
1367
+ }
1368
+ `
1369
+ },
1370
+ {
1371
+ path: `${adminDir}/page.tsx`,
1372
+ content: `import { Dashboard } from 'next-blogpanel/admin';
1373
+
1374
+ export default function AdminDashboard() {
1375
+ return <Dashboard />;
1376
+ }
1377
+ `
1378
+ },
1379
+ {
1380
+ path: `${adminDir}/posts/page.tsx`,
1381
+ content: `import { PostList } from 'next-blogpanel/admin';
1382
+
1383
+ export default function AdminPosts() {
1384
+ return <PostList />;
1385
+ }
1386
+ `
1387
+ },
1388
+ {
1389
+ path: `${adminDir}/new/page.tsx`,
1390
+ content: `import { PostEditor } from 'next-blogpanel/admin';
1391
+
1392
+ export default function NewPost() {
1393
+ return <PostEditor />;
1394
+ }
1395
+ `
1396
+ },
1397
+ {
1398
+ path: `${adminDir}/[id]/edit/page.tsx`,
1399
+ content: `import { PostEditor } from 'next-blogpanel/admin';
1400
+
1401
+ interface Props {
1402
+ params: Promise<{ id: string }>;
1403
+ }
1404
+
1405
+ export default async function EditPost({ params }: Props) {
1406
+ const { id } = await params;
1407
+ return <PostEditor postId={id} />;
1408
+ }
1409
+ `
1410
+ },
1411
+ {
1412
+ path: `${adminDir}/media/page.tsx`,
1413
+ content: `import { MediaLibrary } from 'next-blogpanel/admin';
1414
+
1415
+ export default function AdminMedia() {
1416
+ return <MediaLibrary />;
1417
+ }
1418
+ `
1419
+ },
1420
+ {
1421
+ path: `${adminDir}/categories/page.tsx`,
1422
+ content: `import { CategoryManager } from 'next-blogpanel/admin';
1423
+
1424
+ export default function AdminCategories() {
1425
+ return <CategoryManager />;
1426
+ }
1427
+ `
1428
+ },
1429
+ {
1430
+ path: `${adminDir}/settings/page.tsx`,
1431
+ content: `import { SettingsPage } from 'next-blogpanel/admin';
1432
+
1433
+ export default function AdminSettings() {
1434
+ return <SettingsPage />;
1435
+ }
1436
+ `
1437
+ },
1438
+ // ---- API Routes ----
1439
+ {
1440
+ path: `${apiDir}/posts/route.ts`,
1441
+ content: `export { GET, POST, PUT, DELETE } from 'next-blogpanel/api/posts';
1442
+ `
1443
+ },
1444
+ {
1445
+ path: `${apiDir}/media/route.ts`,
1446
+ content: `export { GET, POST, DELETE } from 'next-blogpanel/api/media';
1447
+ `
1448
+ },
1449
+ {
1450
+ path: `${apiDir}/media/file/[...key]/route.ts`,
1451
+ content: `export { GET } from 'next-blogpanel/api/media-file';
1452
+ `
1453
+ },
1454
+ {
1455
+ path: `${apiDir}/categories/route.ts`,
1456
+ content: `export { GET, POST, PUT, DELETE } from 'next-blogpanel/api/categories';
1457
+ `
1458
+ },
1459
+ {
1460
+ path: `${apiDir}/settings/route.ts`,
1461
+ content: `export { GET, PUT } from 'next-blogpanel/api/settings';
1462
+ `
1463
+ },
1464
+ {
1465
+ path: `${apiDir}/tokens/route.ts`,
1466
+ content: `export { GET, POST, DELETE } from 'next-blogpanel/api/tokens';
1467
+ `
1468
+ },
1469
+ {
1470
+ path: `${apiDir}/sitemap.xml/route.ts`,
1471
+ content: `export { GET } from 'next-blogpanel/api/sitemap';
1472
+ `
1473
+ },
1474
+ {
1475
+ path: `${apiDir}/rss.xml/route.ts`,
1476
+ content: `export { GET } from 'next-blogpanel/api/rss';
1477
+ `
1478
+ }
1479
+ ];
1480
+ return [
1481
+ setup,
1482
+ ...files.map((file) => ({
1483
+ ...file,
1484
+ content: `import '${setupSpecifier(file.path)}';
1485
+ ${file.content}`
1486
+ }))
1487
+ ];
1488
+ }
1489
+
1490
+ // src/cli/init.ts
1491
+ var SETUP_TEMPLATE_PATH = `${SETUP_MODULE}.ts`;
1492
+ async function init(options) {
1493
+ const cwd2 = options.cwd || process.cwd();
1494
+ const packageJsonPath = import_path.default.join(cwd2, "package.json");
1495
+ if (!import_fs.default.existsSync(packageJsonPath)) {
1496
+ console.error("Error: No package.json found. Run this in a Next.js project root.");
1497
+ process.exit(1);
1498
+ }
1499
+ const pkg = JSON.parse(import_fs.default.readFileSync(packageJsonPath, "utf-8"));
1500
+ const hasNext = pkg.dependencies?.next || pkg.devDependencies?.next;
1501
+ if (!hasNext) {
1502
+ console.error("Error: next is not a dependency. This must be a Next.js project.");
1503
+ process.exit(1);
1504
+ }
1505
+ const usesSrcDir = import_fs.default.existsSync(import_path.default.join(cwd2, "src", "app"));
1506
+ const appDir = usesSrcDir ? import_path.default.join(cwd2, "src", "app") : import_path.default.join(cwd2, "app");
1507
+ if (!import_fs.default.existsSync(appDir)) {
1508
+ console.error("Error: No app/ directory found. Next BlogPanel requires the Next.js App Router.");
1509
+ process.exit(1);
1510
+ }
1511
+ const blogDir = toDir(options.blogPath);
1512
+ const adminDir = toDir(options.adminPath);
1513
+ const apiDir = toDir(options.apiPath);
1514
+ const blogUrl = toUrl(options.blogPath);
1515
+ const adminUrl = toUrl(options.adminPath);
1516
+ const apiUrl = toUrl(options.apiPath);
1517
+ const named = [
1518
+ ["--blog-path", blogDir],
1519
+ ["--admin-path", adminDir],
1520
+ ["--api-path", apiDir]
1521
+ ];
1522
+ const rootPaths = named.filter(([, dir]) => dir === "");
1523
+ if (rootPaths.length > 0) {
1524
+ const flags = rootPaths.map(([flag]) => flag).join(" and ");
1525
+ console.error(
1526
+ `Error: ${flags} resolve${rootPaths.length === 1 ? "s" : ""} to the site root ('/'). Choose a non-root path, e.g. '/blog'.`
1527
+ );
1528
+ process.exit(1);
1529
+ }
1530
+ for (let i = 0; i < named.length; i++) {
1531
+ for (let j = i + 1; j < named.length; j++) {
1532
+ if (named[i][1] === named[j][1]) {
1533
+ console.error(
1534
+ `Error: ${named[i][0]} and ${named[j][0]} both resolve to 'app/${named[i][1]}/'. Choose distinct paths.`
1535
+ );
1536
+ process.exit(1);
1537
+ }
1538
+ }
1539
+ }
1540
+ const knownDefaults = ["blog", "admin/blog", "api/blog"];
1541
+ const chosen = [blogDir, adminDir, apiDir];
1542
+ for (const [i, def] of knownDefaults.entries()) {
1543
+ if (chosen[i] !== def && import_fs.default.existsSync(import_path.default.join(appDir, def))) {
1544
+ console.log(` \u26A0 Found an existing app/${def}/ from a previous init.`);
1545
+ console.log(` You are scaffolding to app/${chosen[i]}/ \u2014 remove the old folder when ready.`);
1546
+ }
1547
+ }
1548
+ console.log("");
1549
+ console.log(" Next BlogPanel \u2014 Initializing...");
1550
+ console.log("");
1551
+ const templates = getTemplates({
1552
+ ...options,
1553
+ configImport: usesSrcDir ? "../../next-blogpanel.config" : "../next-blogpanel.config"
1554
+ });
1555
+ let setupModuleWritten = false;
1556
+ const skippedFiles = [];
1557
+ for (const template of templates) {
1558
+ const filePath = import_path.default.join(appDir, template.path);
1559
+ const dir = import_path.default.dirname(filePath);
1560
+ if (!import_fs.default.existsSync(dir)) {
1561
+ import_fs.default.mkdirSync(dir, { recursive: true });
1562
+ }
1563
+ if (import_fs.default.existsSync(filePath)) {
1564
+ console.log(` \u26A0 Skipping ${template.path} (already exists)`);
1565
+ if (template.path !== SETUP_TEMPLATE_PATH) {
1566
+ skippedFiles.push(template.path);
1567
+ }
1568
+ continue;
1569
+ }
1570
+ import_fs.default.writeFileSync(filePath, template.content);
1571
+ console.log(` \u2713 ${template.path}`);
1572
+ if (template.path === SETUP_TEMPLATE_PATH) {
1573
+ setupModuleWritten = true;
1574
+ }
1575
+ }
1576
+ const configPath = import_path.default.join(cwd2, "next-blogpanel.config.ts");
1577
+ if (!import_fs.default.existsSync(configPath)) {
1578
+ import_fs.default.writeFileSync(
1579
+ configPath,
1580
+ `import { defineConfig } from 'next-blogpanel';
1581
+
1582
+ export default defineConfig({
1583
+ basePath: '${blogUrl}',
1584
+ adminPath: '${adminUrl}',
1585
+ apiPath: '${apiUrl}',
1586
+ auth: {
1587
+ strategy: 'api-key',
1588
+ },
1589
+ });
1590
+ `
1591
+ );
1592
+ console.log(" \u2713 next-blogpanel.config.ts");
1593
+ }
1594
+ const envExamplePath = import_path.default.join(cwd2, ".env.local.example");
1595
+ if (!import_fs.default.existsSync(envExamplePath)) {
1596
+ import_fs.default.writeFileSync(envExamplePath, getEnvExample());
1597
+ console.log(" \u2713 .env.local.example");
1598
+ }
1599
+ console.log("");
1600
+ console.log(" Done! Next steps:");
1601
+ console.log("");
1602
+ console.log(" 1. Copy .env.local.example to .env.local and fill in your values");
1603
+ console.log(" 2. Run: npm run dev");
1604
+ console.log(` 3. Visit: http://localhost:3000${adminUrl}`);
1605
+ console.log("");
1606
+ if (setupModuleWritten && skippedFiles.length > 0) {
1607
+ console.log(` \u26A0 IMPORTANT: ${SETUP_TEMPLATE_PATH} was created, but ${skippedFiles.length} existing file${skippedFiles.length === 1 ? "" : "s"} that would import it ${skippedFiles.length === 1 ? "was" : "were"} left untouched:`);
1608
+ console.log("");
1609
+ for (const file of skippedFiles) {
1610
+ console.log(` - ${file}`);
1611
+ }
1612
+ console.log("");
1613
+ console.log(` None of them installs your next-blogpanel.config.ts at runtime, so every`);
1614
+ console.log(` GridFS media upload will fail with CONFIG_NOT_INSTALLED instead of writing`);
1615
+ console.log(` a URL \u2014 add this import to the top of each file above:`);
1616
+ console.log("");
1617
+ console.log(` import '<relative path to app dir>/${SETUP_MODULE}';`);
1618
+ console.log("");
1619
+ console.log(" See docs/migrating-from-nextblogkit.md for the exact import line per file.");
1620
+ console.log("");
1621
+ }
1622
+ }
1623
+
1624
+ // src/cli/seed.ts
1625
+ async function seed() {
1626
+ console.log("");
1627
+ console.log(" Next BlogPanel \u2014 Seeding example content...");
1628
+ console.log("");
1629
+ try {
1630
+ const { ensureIndexes: ensureIndexes2 } = await Promise.resolve().then(() => (init_db(), db_exports));
1631
+ const { createPost: createPost2, createCategory: createCategory2 } = await Promise.resolve().then(() => (init_db(), db_exports));
1632
+ await ensureIndexes2();
1633
+ console.log(" \u2713 Database indexes created");
1634
+ const categories = [
1635
+ { name: "Technology", slug: "technology", description: "Tech tutorials and updates", order: 0 },
1636
+ { name: "Design", slug: "design", description: "UI/UX and design tips", order: 1 },
1637
+ { name: "Tutorials", slug: "tutorials", description: "Step-by-step guides", order: 2 }
1638
+ ];
1639
+ for (const cat of categories) {
1640
+ try {
1641
+ await createCategory2(cat);
1642
+ console.log(` \u2713 Category: ${cat.name}`);
1643
+ } catch (err) {
1644
+ if (err.code === 11e3) {
1645
+ console.log(` \u26A0 Category "${cat.name}" already exists`);
1646
+ } else {
1647
+ throw err;
1648
+ }
1649
+ }
1650
+ }
1651
+ try {
1652
+ await createPost2({
1653
+ title: "Welcome to Next BlogPanel",
1654
+ slug: "welcome-to-next-blogpanel",
1655
+ excerpt: "Your new blog is ready! This is an example post to get you started with Next BlogPanel.",
1656
+ content: [
1657
+ {
1658
+ type: "heading",
1659
+ attrs: { level: 2 },
1660
+ content: [{ type: "text", text: "Getting Started" }]
1661
+ },
1662
+ {
1663
+ type: "paragraph",
1664
+ content: [
1665
+ { type: "text", text: "Congratulations! Your Next BlogPanel blog is up and running. This example post shows you what\u2019s possible." }
1666
+ ]
1667
+ },
1668
+ {
1669
+ type: "heading",
1670
+ attrs: { level: 2 },
1671
+ content: [{ type: "text", text: "Features" }]
1672
+ },
1673
+ {
1674
+ type: "bulletList",
1675
+ content: [
1676
+ { type: "listItem", content: [{ type: "paragraph", content: [{ type: "text", text: "Block editor with slash commands" }] }] },
1677
+ { type: "listItem", content: [{ type: "paragraph", content: [{ type: "text", text: "Full SEO optimization out of the box" }] }] },
1678
+ { type: "listItem", content: [{ type: "paragraph", content: [{ type: "text", text: "Media library with built-in image storage" }] }] },
1679
+ { type: "listItem", content: [{ type: "paragraph", content: [{ type: "text", text: "Dynamic sitemap and RSS feed" }] }] }
1680
+ ]
1681
+ },
1682
+ {
1683
+ type: "callout",
1684
+ attrs: { type: "tip" },
1685
+ content: [
1686
+ { type: "paragraph", content: [{ type: "text", text: "Head to the admin panel to start creating your own posts!" }] }
1687
+ ]
1688
+ }
1689
+ ],
1690
+ contentHTML: "<h2>Getting Started</h2><p>Congratulations! Your Next BlogPanel 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 built-in image storage</li><li>Dynamic sitemap and RSS feed</li></ul>",
1691
+ status: "published",
1692
+ categories: ["technology"],
1693
+ tags: ["nextjs", "blog", "getting-started"]
1694
+ });
1695
+ console.log(' \u2713 Example post: "Welcome to Next BlogPanel"');
1696
+ } catch (err) {
1697
+ if (err.code === 11e3) {
1698
+ console.log(" \u26A0 Example post already exists");
1699
+ } else {
1700
+ throw err;
1701
+ }
1702
+ }
1703
+ console.log("");
1704
+ console.log(" Seeding complete!");
1705
+ console.log("");
1706
+ process.exit(0);
1707
+ } catch (error) {
1708
+ console.error(" \u2717 Seeding failed:", error);
1709
+ process.exit(1);
1710
+ }
1711
+ }
1712
+
1713
+ // src/cli/health.ts
1714
+ function editDistance(a, b) {
1715
+ if (a === b) return 0;
1716
+ if (!a.length) return b.length;
1717
+ if (!b.length) return a.length;
1718
+ let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
1719
+ for (let i = 1; i <= a.length; i++) {
1720
+ const current = [i];
1721
+ for (let j = 1; j <= b.length; j++) {
1722
+ const substitution = previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1);
1723
+ current[j] = Math.min(current[j - 1] + 1, previous[j] + 1, substitution);
1724
+ }
1725
+ previous = current;
1726
+ }
1727
+ return previous[b.length];
1728
+ }
1729
+ function looksLikeTypoOf(name, candidate) {
1730
+ if (name === candidate) return false;
1731
+ const lowerName = name.toLowerCase();
1732
+ const lowerCandidate = candidate.toLowerCase();
1733
+ if (lowerName === lowerCandidate) return true;
1734
+ const threshold = Math.max(name.length, candidate.length) <= 4 ? 1 : 2;
1735
+ return editDistance(lowerName, lowerCandidate) <= threshold;
1736
+ }
1737
+ async function classifyUnresolvedProvider(name) {
1738
+ const { BUILT_IN_PROVIDER_NAMES: BUILT_IN_PROVIDER_NAMES2 } = await Promise.resolve().then(() => (init_storage(), storage_exports));
1739
+ const known = [...BUILT_IN_PROVIDER_NAMES2];
1740
+ try {
1741
+ const { getCollection: getCollection2 } = await Promise.resolve().then(() => (init_db(), db_exports));
1742
+ const col = await getCollection2("nbk_media");
1743
+ const stored = (await col.distinct("provider")).filter(
1744
+ (value) => typeof value === "string" && value.length > 0
1745
+ );
1746
+ if (stored.includes(name)) {
1747
+ return { kind: "custom", storedCount: await col.countDocuments({ provider: name }) };
1748
+ }
1749
+ known.push(...stored);
1750
+ } catch {
1751
+ return { kind: "custom", storedCount: 0 };
1752
+ }
1753
+ for (const candidate of known) {
1754
+ if (looksLikeTypoOf(name, candidate)) return { kind: "typo", suggestion: candidate };
1755
+ }
1756
+ return { kind: "custom", storedCount: 0 };
1757
+ }
1758
+ async function health() {
1759
+ console.log("");
1760
+ console.log(" Next BlogPanel \u2014 Health Check");
1761
+ console.log("");
1762
+ let allPassed = true;
1763
+ try {
1764
+ const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), db_exports));
1765
+ const db2 = await getDb2();
1766
+ await db2.command({ ping: 1 });
1767
+ console.log(" \u2713 MongoDB: Connected");
1768
+ } catch (err) {
1769
+ console.log(` \u2717 MongoDB: ${err.message}`);
1770
+ allPassed = false;
1771
+ }
1772
+ try {
1773
+ const { getStorageProvider: getStorageProvider2 } = await Promise.resolve().then(() => (init_storage(), storage_exports));
1774
+ const storage = getStorageProvider2();
1775
+ await storage.list("media/");
1776
+ const labels = { r2: "Cloudflare R2", gridfs: "GridFS" };
1777
+ console.log(` \u2713 Storage (${labels[storage.name] || storage.name}): Connected`);
1778
+ } catch (err) {
1779
+ if (err?.name === "StorageProviderNotRegisteredError") {
1780
+ const verdict = await classifyUnresolvedProvider(err.providerName);
1781
+ if (verdict.kind === "typo") {
1782
+ console.log(
1783
+ ` \u2717 Storage: BLOGPANEL_STORAGE is set to '${err.providerName}', which resolves to nothing.`
1784
+ );
1785
+ console.log(` Did you mean '${verdict.suggestion}'?`);
1786
+ console.log(
1787
+ ` Storage backend names are case-sensitive and must match exactly. Uploads will fail`
1788
+ );
1789
+ console.log(` with STORAGE_PROVIDER_NOT_REGISTERED until this is corrected.`);
1790
+ allPassed = false;
1791
+ } else {
1792
+ console.log(
1793
+ ` \u26A0 Storage ('${err.providerName}'): not verified \u2014 custom providers cannot be resolved from the CLI.`
1794
+ );
1795
+ if (verdict.storedCount > 0) {
1796
+ console.log(
1797
+ ` ${verdict.storedCount} media document(s) are already stored under this name, so your`
1798
+ );
1799
+ console.log(` app has registered it successfully.`);
1800
+ }
1801
+ console.log(
1802
+ ` This CLI does not load your app's next-blogpanel.setup module, where`
1803
+ );
1804
+ console.log(
1805
+ ` registerStorageProvider() is called, so this is expected. Check the running server.`
1806
+ );
1807
+ console.log(` The exit code is not affected by this warning.`);
1808
+ }
1809
+ } else {
1810
+ console.log(` \u2717 Storage: ${err.message}`);
1811
+ allPassed = false;
1812
+ }
1813
+ }
1814
+ try {
1815
+ const { getEnvConfig: getEnvConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
1816
+ getEnvConfig2();
1817
+ console.log(" \u2713 Environment: Required variables set");
1818
+ } catch (err) {
1819
+ console.log(` \u2717 Environment: ${err.message}`);
1820
+ allPassed = false;
1821
+ }
1822
+ console.log("");
1823
+ if (allPassed) {
1824
+ console.log(" All checks passed!");
1825
+ } else {
1826
+ console.log(" Some checks failed. Please review your configuration.");
1827
+ }
1828
+ console.log("");
1829
+ process.exit(allPassed ? 0 : 1);
1830
+ }
1831
+
1832
+ // src/cli/migrate.ts
1833
+ init_db();
1834
+ async function migrate() {
1835
+ console.log("");
1836
+ console.log(" Next BlogPanel \u2014 Database Migration");
1837
+ console.log("");
1838
+ const db2 = await getDb();
1839
+ const media = db2.collection("nbk_media");
1840
+ const legacy = await media.countDocuments({ r2Key: { $exists: true } });
1841
+ if (legacy > 0) {
1842
+ await media.updateMany({ r2Key: { $exists: true } }, [
1843
+ { $set: { storageKey: "$r2Key", provider: "r2" } },
1844
+ { $unset: "r2Key" }
1845
+ ]);
1846
+ console.log(` \u2713 Migrated ${legacy} media record(s): r2Key \u2192 storageKey`);
1847
+ }
1848
+ await media.dropIndex("r2Key_1").catch(() => {
1849
+ });
1850
+ await ensureIndexes();
1851
+ console.log(" \u2713 Indexes created");
1852
+ console.log("");
1853
+ }
1854
+ async function migrateCommand() {
1855
+ try {
1856
+ await migrate();
1857
+ console.log(" Migration complete!");
1858
+ console.log("");
1859
+ process.exit(0);
1860
+ } catch (error) {
1861
+ console.error(" \u2717 Migration failed:", error);
1862
+ process.exit(1);
1863
+ }
1864
+ }
1865
+
1866
+ // package.json
1867
+ var package_default = {
1868
+ name: "next-blogpanel",
1869
+ version: "1.0.0",
1870
+ description: "A drop-in blog engine for Next.js \u2014 admin panel, block editor, SEO, and media, all backed by your own MongoDB.",
1871
+ license: "MIT",
1872
+ author: "Santosh Patidar",
1873
+ repository: {
1874
+ type: "git",
1875
+ url: "https://github.com/butterblogs/next-blogpanel"
1876
+ },
1877
+ keywords: [
1878
+ "nextjs",
1879
+ "blog",
1880
+ "blog-engine",
1881
+ "cms",
1882
+ "seo",
1883
+ "editor",
1884
+ "tiptap",
1885
+ "mongodb",
1886
+ "gridfs"
1887
+ ],
1888
+ type: "module",
1889
+ main: "./dist/index.cjs",
1890
+ module: "./dist/index.js",
1891
+ types: "./dist/index.d.ts",
1892
+ bin: {
1893
+ "next-blogpanel": "./dist/cli/index.cjs"
1894
+ },
1895
+ exports: {
1896
+ ".": {
1897
+ types: "./dist/index.d.ts",
1898
+ import: "./dist/index.js",
1899
+ require: "./dist/index.cjs"
1900
+ },
1901
+ "./components": {
1902
+ types: "./dist/components/index.d.ts",
1903
+ import: "./dist/components/index.js",
1904
+ require: "./dist/components/index.cjs"
1905
+ },
1906
+ "./admin": {
1907
+ types: "./dist/admin/index.d.ts",
1908
+ import: "./dist/admin/index.js",
1909
+ require: "./dist/admin/index.cjs"
1910
+ },
1911
+ "./api/*": {
1912
+ types: "./dist/api/*.d.ts",
1913
+ import: "./dist/api/*.js",
1914
+ require: "./dist/api/*.cjs"
1915
+ },
1916
+ "./lib": {
1917
+ types: "./dist/lib/index.d.ts",
1918
+ import: "./dist/lib/index.js",
1919
+ require: "./dist/lib/index.cjs"
1920
+ },
1921
+ "./editor": {
1922
+ types: "./dist/editor/index.d.ts",
1923
+ import: "./dist/editor/index.js",
1924
+ require: "./dist/editor/index.cjs"
1925
+ },
1926
+ "./styles/*.css": "./dist/styles/*.css"
1927
+ },
1928
+ files: [
1929
+ "dist",
1930
+ "README.md",
1931
+ "LICENSE"
1932
+ ],
1933
+ scripts: {
1934
+ build: "tsup",
1935
+ dev: "tsup --watch",
1936
+ lint: "eslint src/ --ext .ts,.tsx",
1937
+ typecheck: "tsc --noEmit",
1938
+ test: "vitest run",
1939
+ "test:watch": "vitest",
1940
+ prepublishOnly: "npm run build"
1941
+ },
1942
+ dependencies: {
1943
+ "@aws-sdk/client-s3": "^3.500.0",
1944
+ "@tiptap/core": "^2.6.0",
1945
+ "@tiptap/extension-blockquote": "^2.6.0",
1946
+ "@tiptap/extension-bold": "^2.6.0",
1947
+ "@tiptap/extension-bullet-list": "^2.6.0",
1948
+ "@tiptap/extension-code": "^2.6.0",
1949
+ "@tiptap/extension-code-block": "^2.6.0",
1950
+ "@tiptap/extension-document": "^2.6.0",
1951
+ "@tiptap/extension-dropcursor": "^2.6.0",
1952
+ "@tiptap/extension-gapcursor": "^2.6.0",
1953
+ "@tiptap/extension-hard-break": "^2.6.0",
1954
+ "@tiptap/extension-heading": "^2.6.0",
1955
+ "@tiptap/extension-highlight": "^2.6.0",
1956
+ "@tiptap/extension-history": "^2.6.0",
1957
+ "@tiptap/extension-horizontal-rule": "^2.6.0",
1958
+ "@tiptap/extension-image": "^2.6.0",
1959
+ "@tiptap/extension-italic": "^2.6.0",
1960
+ "@tiptap/extension-link": "^2.6.0",
1961
+ "@tiptap/extension-list-item": "^2.6.0",
1962
+ "@tiptap/extension-ordered-list": "^2.6.0",
1963
+ "@tiptap/extension-paragraph": "^2.6.0",
1964
+ "@tiptap/extension-placeholder": "^2.6.0",
1965
+ "@tiptap/extension-strike": "^2.6.0",
1966
+ "@tiptap/extension-table": "^2.6.0",
1967
+ "@tiptap/extension-table-cell": "^2.6.0",
1968
+ "@tiptap/extension-table-header": "^2.6.0",
1969
+ "@tiptap/extension-table-row": "^2.6.0",
1970
+ "@tiptap/extension-task-item": "^2.6.0",
1971
+ "@tiptap/extension-task-list": "^2.6.0",
1972
+ "@tiptap/extension-text": "^2.6.0",
1973
+ "@tiptap/extension-typography": "^2.6.0",
1974
+ "@tiptap/extension-underline": "^2.6.0",
1975
+ "@tiptap/pm": "^2.6.0",
1976
+ "@tiptap/react": "^2.6.0",
1977
+ "@tiptap/starter-kit": "^2.6.0",
1978
+ commander: "^12.0.0",
1979
+ inquirer: "^9.2.0",
1980
+ mongodb: "^6.5.0",
1981
+ sharp: "^0.33.0",
1982
+ shiki: "^1.1.0",
1983
+ zod: "^3.22.0"
1984
+ },
1985
+ peerDependencies: {
1986
+ next: ">=14.0.0",
1987
+ react: ">=18.0.0",
1988
+ "react-dom": ">=18.0.0"
1989
+ },
1990
+ devDependencies: {
1991
+ "@testing-library/jest-dom": "^6.4.0",
1992
+ "@testing-library/react": "^14.2.0",
1993
+ "@types/node": "^20.11.0",
1994
+ "@types/react": "^18.2.0",
1995
+ "@types/react-dom": "^18.2.0",
1996
+ "@typescript-eslint/eslint-plugin": "^7.18.0",
1997
+ "@typescript-eslint/parser": "^7.18.0",
1998
+ eslint: "^8.56.0",
1999
+ "eslint-plugin-react": "^7.37.5",
2000
+ "eslint-plugin-react-hooks": "^5.2.0",
2001
+ "mongodb-memory-server": "^9.5.0",
2002
+ tsup: "^8.0.0",
2003
+ typescript: "^5.3.0",
2004
+ vitest: "^1.3.0"
2005
+ }
2006
+ };
2007
+
2008
+ // src/cli/index.ts
2009
+ function loadEnvFile(filePath) {
2010
+ if (!(0, import_node_fs.existsSync)(filePath)) return;
2011
+ const content = (0, import_node_fs.readFileSync)(filePath, "utf-8");
2012
+ for (const line of content.split("\n")) {
2013
+ const trimmed = line.trim();
2014
+ if (!trimmed || trimmed.startsWith("#")) continue;
2015
+ const eqIndex = trimmed.indexOf("=");
2016
+ if (eqIndex === -1) continue;
2017
+ const key = trimmed.slice(0, eqIndex).trim();
2018
+ let value = trimmed.slice(eqIndex + 1).trim();
2019
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
2020
+ value = value.slice(1, -1);
2021
+ }
2022
+ if (process.env[key] === void 0) {
2023
+ process.env[key] = value;
2024
+ }
2025
+ }
2026
+ }
2027
+ var cwd = process.cwd();
2028
+ loadEnvFile((0, import_node_path.resolve)(cwd, ".env.local"));
2029
+ loadEnvFile((0, import_node_path.resolve)(cwd, ".env"));
2030
+ var program = new import_commander.Command();
2031
+ program.name("next-blogpanel").description("Next BlogPanel \u2014 Blog engine for Next.js").version(package_default.version);
2032
+ program.command("init").description("Initialize Next BlogPanel 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);
2033
+ program.command("seed").description("Seed example blog content").action(seed);
2034
+ program.command("health").description("Check database and storage connectivity").action(health);
2035
+ program.command("migrate").description("Run database migrations").action(migrateCommand);
2036
+ program.parse();