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.
- package/LICENSE +21 -0
- package/README.md +211 -0
- package/dist/admin/index.cjs +3062 -0
- package/dist/admin/index.cjs.map +1 -0
- package/dist/admin/index.d.cts +51 -0
- package/dist/admin/index.d.ts +51 -0
- package/dist/admin/index.js +3029 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/api/categories.cjs +82 -0
- package/dist/api/categories.cjs.map +1 -0
- package/dist/api/categories.d.cts +27 -0
- package/dist/api/categories.d.ts +27 -0
- package/dist/api/categories.js +77 -0
- package/dist/api/categories.js.map +1 -0
- package/dist/api/media-file.cjs +46 -0
- package/dist/api/media-file.cjs.map +1 -0
- package/dist/api/media-file.d.cts +7 -0
- package/dist/api/media-file.d.ts +7 -0
- package/dist/api/media-file.js +44 -0
- package/dist/api/media-file.js.map +1 -0
- package/dist/api/media.cjs +180 -0
- package/dist/api/media.cjs.map +1 -0
- package/dist/api/media.d.cts +22 -0
- package/dist/api/media.d.ts +22 -0
- package/dist/api/media.js +176 -0
- package/dist/api/media.js.map +1 -0
- package/dist/api/posts.cjs +103 -0
- package/dist/api/posts.cjs.map +1 -0
- package/dist/api/posts.d.cts +27 -0
- package/dist/api/posts.d.ts +27 -0
- package/dist/api/posts.js +98 -0
- package/dist/api/posts.js.map +1 -0
- package/dist/api/rss.cjs +25 -0
- package/dist/api/rss.cjs.map +1 -0
- package/dist/api/rss.d.cts +5 -0
- package/dist/api/rss.d.ts +5 -0
- package/dist/api/rss.js +23 -0
- package/dist/api/rss.js.map +1 -0
- package/dist/api/settings.cjs +40 -0
- package/dist/api/settings.cjs.map +1 -0
- package/dist/api/settings.d.cts +17 -0
- package/dist/api/settings.d.ts +17 -0
- package/dist/api/settings.js +37 -0
- package/dist/api/settings.js.map +1 -0
- package/dist/api/sitemap.cjs +25 -0
- package/dist/api/sitemap.cjs.map +1 -0
- package/dist/api/sitemap.d.cts +5 -0
- package/dist/api/sitemap.d.ts +5 -0
- package/dist/api/sitemap.js +23 -0
- package/dist/api/sitemap.js.map +1 -0
- package/dist/api/tokens.cjs +56 -0
- package/dist/api/tokens.cjs.map +1 -0
- package/dist/api/tokens.d.cts +22 -0
- package/dist/api/tokens.d.ts +22 -0
- package/dist/api/tokens.js +52 -0
- package/dist/api/tokens.js.map +1 -0
- package/dist/chunk-2M46MMWN.cjs +76 -0
- package/dist/chunk-2M46MMWN.cjs.map +1 -0
- package/dist/chunk-4GK22Q3R.cjs +229 -0
- package/dist/chunk-4GK22Q3R.cjs.map +1 -0
- package/dist/chunk-6PZ772NQ.js +595 -0
- package/dist/chunk-6PZ772NQ.js.map +1 -0
- package/dist/chunk-BZXPRGLN.cjs +351 -0
- package/dist/chunk-BZXPRGLN.cjs.map +1 -0
- package/dist/chunk-DAX5C6SC.cjs +86 -0
- package/dist/chunk-DAX5C6SC.cjs.map +1 -0
- package/dist/chunk-DWNMHQ4I.cjs +57 -0
- package/dist/chunk-DWNMHQ4I.cjs.map +1 -0
- package/dist/chunk-FPGVR2BX.cjs +121 -0
- package/dist/chunk-FPGVR2BX.cjs.map +1 -0
- package/dist/chunk-JBP3G4QJ.js +50 -0
- package/dist/chunk-JBP3G4QJ.js.map +1 -0
- package/dist/chunk-JDP4DIHI.js +215 -0
- package/dist/chunk-JDP4DIHI.js.map +1 -0
- package/dist/chunk-LIZOVA4W.js +118 -0
- package/dist/chunk-LIZOVA4W.js.map +1 -0
- package/dist/chunk-QLBTOCT3.js +118 -0
- package/dist/chunk-QLBTOCT3.js.map +1 -0
- package/dist/chunk-UI2KDZ5H.js +344 -0
- package/dist/chunk-UI2KDZ5H.js.map +1 -0
- package/dist/chunk-V7QPRX4A.cjs +125 -0
- package/dist/chunk-V7QPRX4A.cjs.map +1 -0
- package/dist/chunk-VEJFN65Y.js +82 -0
- package/dist/chunk-VEJFN65Y.js.map +1 -0
- package/dist/chunk-WYZFAI6O.js +74 -0
- package/dist/chunk-WYZFAI6O.js.map +1 -0
- package/dist/chunk-XLYTATRQ.cjs +637 -0
- package/dist/chunk-XLYTATRQ.cjs.map +1 -0
- package/dist/cli/index.cjs +2036 -0
- package/dist/components/index.cjs +647 -0
- package/dist/components/index.cjs.map +1 -0
- package/dist/components/index.d.cts +187 -0
- package/dist/components/index.d.ts +187 -0
- package/dist/components/index.js +633 -0
- package/dist/components/index.js.map +1 -0
- package/dist/db-M3KXLBUP.js +3 -0
- package/dist/db-M3KXLBUP.js.map +1 -0
- package/dist/db-TDCZNY3Q.cjs +112 -0
- package/dist/db-TDCZNY3Q.cjs.map +1 -0
- package/dist/editor/index.cjs +1126 -0
- package/dist/editor/index.cjs.map +1 -0
- package/dist/editor/index.d.cts +137 -0
- package/dist/editor/index.d.ts +137 -0
- package/dist/editor/index.js +1093 -0
- package/dist/editor/index.js.map +1 -0
- package/dist/index-BcNka7gK.d.cts +475 -0
- package/dist/index-DPRgaNEE.d.ts +475 -0
- package/dist/index.cjs +433 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +27 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +209 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/index.cjs +193 -0
- package/dist/lib/index.cjs.map +1 -0
- package/dist/lib/index.d.cts +4 -0
- package/dist/lib/index.d.ts +4 -0
- package/dist/lib/index.js +8 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/styles/admin.css +667 -0
- package/dist/styles/blog.css +851 -0
- package/dist/styles/editor.css +452 -0
- package/dist/styles/globals.css +270 -0
- package/dist/styles/prose.css +299 -0
- package/dist/types-CgrBuig-.d.cts +778 -0
- package/dist/types-CgrBuig-.d.ts +778 -0
- package/package.json +140 -0
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
import { MongoClient, ObjectId } from 'mongodb';
|
|
2
|
+
import { randomBytes, createHash } from 'crypto';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
// src/lib/db.ts
|
|
6
|
+
|
|
7
|
+
// src/lib/limits.ts
|
|
8
|
+
var DEFAULT_MAX_IMAGE_SIZE = 2 * 1024 * 1024;
|
|
9
|
+
var DEFAULT_MAX_UPLOAD_SIZE = 10 * 1024 * 1024;
|
|
10
|
+
|
|
11
|
+
// src/lib/config.ts
|
|
12
|
+
var envSchema = z.object({
|
|
13
|
+
// Required
|
|
14
|
+
BLOGPANEL_MONGODB_URI: z.string().min(1, "MongoDB URI is required"),
|
|
15
|
+
BLOGPANEL_API_KEY: z.string().min(32, "API key must be at least 32 characters"),
|
|
16
|
+
// Optional — Database name (defaults to the database in your connection URI)
|
|
17
|
+
BLOGPANEL_MONGODB_DB: z.string().optional(),
|
|
18
|
+
// Optional — Cloudflare R2 (image storage)
|
|
19
|
+
BLOGPANEL_R2_ACCOUNT_ID: z.string().optional(),
|
|
20
|
+
BLOGPANEL_R2_ACCESS_KEY: z.string().optional(),
|
|
21
|
+
BLOGPANEL_R2_SECRET_KEY: z.string().optional(),
|
|
22
|
+
BLOGPANEL_R2_BUCKET: z.string().optional(),
|
|
23
|
+
BLOGPANEL_R2_PUBLIC_URL: z.string().optional(),
|
|
24
|
+
// Optional — force a storage backend regardless of what is configured.
|
|
25
|
+
//
|
|
26
|
+
// Not a z.enum(['gridfs','r2']) any more: a custom provider registered via
|
|
27
|
+
// registerStorageProvider() is selectable here by its own name, and an enum
|
|
28
|
+
// could not know those names — they are supplied by application code at
|
|
29
|
+
// runtime, long after this schema is evaluated.
|
|
30
|
+
//
|
|
31
|
+
// The enum did buy loud detection of a typo, and that is not given up, only
|
|
32
|
+
// moved: getStorageProviderByName() throws StorageProviderNotRegisteredError
|
|
33
|
+
// and names every resolvable provider. That is a strictly better failure
|
|
34
|
+
// than the enum's, which rejected the whole environment — taking down
|
|
35
|
+
// routes that never touch storage — and could only say "expected 'gridfs' |
|
|
36
|
+
// 'r2'". Left as a plain optional string rather than .min(1) so that an
|
|
37
|
+
// empty `BLOGPANEL_STORAGE=` line, which reads as "unset" in every .env
|
|
38
|
+
// file, does not fail config parsing process-wide; getStorageProvider()
|
|
39
|
+
// treats blank as absent.
|
|
40
|
+
BLOGPANEL_STORAGE: z.string().optional(),
|
|
41
|
+
// Optional — Site info (defaults provided)
|
|
42
|
+
BLOGPANEL_SITE_URL: z.string().optional().default(""),
|
|
43
|
+
BLOGPANEL_SITE_NAME: z.string().optional().default("Blog")
|
|
44
|
+
});
|
|
45
|
+
var cachedEnv = null;
|
|
46
|
+
function getEnvConfig() {
|
|
47
|
+
if (cachedEnv) return cachedEnv;
|
|
48
|
+
const result = envSchema.safeParse(process.env);
|
|
49
|
+
if (!result.success) {
|
|
50
|
+
const missing = result.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`);
|
|
51
|
+
throw new Error(`Next BlogPanel configuration error:
|
|
52
|
+
${missing.join("\n")}`);
|
|
53
|
+
}
|
|
54
|
+
cachedEnv = result.data;
|
|
55
|
+
return cachedEnv;
|
|
56
|
+
}
|
|
57
|
+
function isR2Configured() {
|
|
58
|
+
const env = getEnvConfig();
|
|
59
|
+
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);
|
|
60
|
+
}
|
|
61
|
+
var defaultConfig = {
|
|
62
|
+
basePath: "/blog",
|
|
63
|
+
adminPath: "/admin/blog",
|
|
64
|
+
apiPath: "/api/blog",
|
|
65
|
+
postsPerPage: 10,
|
|
66
|
+
excerptLength: 160,
|
|
67
|
+
codeHighlighter: "shiki",
|
|
68
|
+
editor: {
|
|
69
|
+
blocks: [
|
|
70
|
+
"paragraph",
|
|
71
|
+
"heading",
|
|
72
|
+
"image",
|
|
73
|
+
"codeBlock",
|
|
74
|
+
"blockquote",
|
|
75
|
+
"bulletList",
|
|
76
|
+
"orderedList",
|
|
77
|
+
"taskList",
|
|
78
|
+
"table",
|
|
79
|
+
"embed",
|
|
80
|
+
"horizontalRule",
|
|
81
|
+
"callout",
|
|
82
|
+
"tableOfContents",
|
|
83
|
+
"faq",
|
|
84
|
+
"html"
|
|
85
|
+
],
|
|
86
|
+
maxImageSize: DEFAULT_MAX_IMAGE_SIZE,
|
|
87
|
+
// stored result ceiling — compression target
|
|
88
|
+
maxUploadSize: DEFAULT_MAX_UPLOAD_SIZE,
|
|
89
|
+
// raw ingest ceiling — rejection threshold
|
|
90
|
+
imageFormats: ["jpg", "jpeg", "png", "gif", "webp", "svg"],
|
|
91
|
+
autosaveInterval: 3e4
|
|
92
|
+
},
|
|
93
|
+
seo: {
|
|
94
|
+
titleTemplate: "%s | %siteName%",
|
|
95
|
+
generateRSS: true,
|
|
96
|
+
generateSitemap: true,
|
|
97
|
+
structuredData: true,
|
|
98
|
+
minContentLength: 300
|
|
99
|
+
},
|
|
100
|
+
auth: {
|
|
101
|
+
strategy: "api-key"
|
|
102
|
+
},
|
|
103
|
+
features: {
|
|
104
|
+
search: true,
|
|
105
|
+
relatedPosts: true,
|
|
106
|
+
readingProgress: true,
|
|
107
|
+
tableOfContents: true,
|
|
108
|
+
shareButtons: true,
|
|
109
|
+
darkMode: true,
|
|
110
|
+
scheduling: true,
|
|
111
|
+
revisionHistory: true,
|
|
112
|
+
imageOptimization: true
|
|
113
|
+
},
|
|
114
|
+
theme: {
|
|
115
|
+
darkMode: true,
|
|
116
|
+
variables: {
|
|
117
|
+
"--nbp-primary": "#2563eb",
|
|
118
|
+
"--nbp-primary-hover": "#1d4ed8",
|
|
119
|
+
"--nbp-text": "#1f2937",
|
|
120
|
+
"--nbp-text-muted": "#6b7280",
|
|
121
|
+
"--nbp-bg": "#ffffff",
|
|
122
|
+
"--nbp-bg-secondary": "#f9fafb",
|
|
123
|
+
"--nbp-card-bg": "#ffffff",
|
|
124
|
+
"--nbp-border": "#e5e7eb",
|
|
125
|
+
"--nbp-radius": "0.5rem",
|
|
126
|
+
"--nbp-font-heading": '"Inter", system-ui, sans-serif',
|
|
127
|
+
"--nbp-font-body": '"Inter", system-ui, sans-serif',
|
|
128
|
+
"--nbp-font-code": '"JetBrains Mono", "Fira Code", monospace'
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
hooks: {}
|
|
132
|
+
};
|
|
133
|
+
function mergeConfig(base, userConfig) {
|
|
134
|
+
return {
|
|
135
|
+
...base,
|
|
136
|
+
...userConfig,
|
|
137
|
+
editor: { ...base.editor, ...userConfig?.editor },
|
|
138
|
+
seo: { ...base.seo, ...userConfig?.seo },
|
|
139
|
+
auth: { ...base.auth, ...userConfig?.auth },
|
|
140
|
+
features: { ...base.features, ...userConfig?.features },
|
|
141
|
+
theme: {
|
|
142
|
+
...base.theme,
|
|
143
|
+
...userConfig?.theme,
|
|
144
|
+
variables: { ...base.theme.variables, ...userConfig?.theme?.variables }
|
|
145
|
+
},
|
|
146
|
+
hooks: { ...base.hooks, ...userConfig?.hooks }
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
var CONFIG_SLOT = /* @__PURE__ */ Symbol.for("next-blogpanel.config.v1");
|
|
150
|
+
function slot() {
|
|
151
|
+
const store = globalThis;
|
|
152
|
+
let current = store[CONFIG_SLOT];
|
|
153
|
+
if (!current) {
|
|
154
|
+
current = { installed: false, config: mergeConfig(defaultConfig) };
|
|
155
|
+
store[CONFIG_SLOT] = current;
|
|
156
|
+
}
|
|
157
|
+
return current;
|
|
158
|
+
}
|
|
159
|
+
var CONFIG_WARNED = /* @__PURE__ */ Symbol.for("next-blogpanel.config.warned");
|
|
160
|
+
function warnApiPathConflict(previous, next) {
|
|
161
|
+
const store = globalThis;
|
|
162
|
+
const seen = store[CONFIG_WARNED] || (store[CONFIG_WARNED] = /* @__PURE__ */ new Set());
|
|
163
|
+
const key = `${previous}=>${next}`;
|
|
164
|
+
if (seen.has(key)) return;
|
|
165
|
+
seen.add(key);
|
|
166
|
+
console.warn(
|
|
167
|
+
`[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).`
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
function defineConfig(config) {
|
|
171
|
+
return config;
|
|
172
|
+
}
|
|
173
|
+
function setConfig(userConfig) {
|
|
174
|
+
const current = slot();
|
|
175
|
+
const base = current.installed ? current.config : defaultConfig;
|
|
176
|
+
if (current.installed && userConfig.apiPath !== void 0 && userConfig.apiPath !== current.config.apiPath) {
|
|
177
|
+
warnApiPathConflict(current.config.apiPath, userConfig.apiPath);
|
|
178
|
+
}
|
|
179
|
+
const merged = mergeConfig(base, userConfig);
|
|
180
|
+
current.config = merged;
|
|
181
|
+
current.installed = true;
|
|
182
|
+
return merged;
|
|
183
|
+
}
|
|
184
|
+
function isConfigInstalled() {
|
|
185
|
+
return slot().installed;
|
|
186
|
+
}
|
|
187
|
+
function getConfig(userConfig) {
|
|
188
|
+
if (userConfig) return mergeConfig(defaultConfig, userConfig);
|
|
189
|
+
return slot().config;
|
|
190
|
+
}
|
|
191
|
+
var ConfigNotInstalledError = class extends Error {
|
|
192
|
+
constructor(context) {
|
|
193
|
+
super(
|
|
194
|
+
`Next BlogPanel: no configuration has been installed in this server runtime, so ${context} cannot be resolved.
|
|
195
|
+
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.
|
|
196
|
+
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.
|
|
197
|
+
See docs/configuration.md \u2014 "Getting the config to the server".`
|
|
198
|
+
);
|
|
199
|
+
this.name = "ConfigNotInstalledError";
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
function requireApiPath(context) {
|
|
203
|
+
const current = slot();
|
|
204
|
+
if (!current.installed) {
|
|
205
|
+
throw new ConfigNotInstalledError(context);
|
|
206
|
+
}
|
|
207
|
+
return current.config.apiPath;
|
|
208
|
+
}
|
|
209
|
+
function getBlogConfig() {
|
|
210
|
+
const env = getEnvConfig();
|
|
211
|
+
const config = getConfig();
|
|
212
|
+
return {
|
|
213
|
+
...config,
|
|
214
|
+
siteUrl: env.BLOGPANEL_SITE_URL,
|
|
215
|
+
siteName: env.BLOGPANEL_SITE_NAME,
|
|
216
|
+
metadata: {
|
|
217
|
+
title: `Blog | ${env.BLOGPANEL_SITE_NAME}`,
|
|
218
|
+
description: `Latest posts from ${env.BLOGPANEL_SITE_NAME}`,
|
|
219
|
+
openGraph: {
|
|
220
|
+
title: `Blog | ${env.BLOGPANEL_SITE_NAME}`,
|
|
221
|
+
description: `Latest posts from ${env.BLOGPANEL_SITE_NAME}`,
|
|
222
|
+
url: `${env.BLOGPANEL_SITE_URL}${config.basePath}`,
|
|
223
|
+
siteName: env.BLOGPANEL_SITE_NAME,
|
|
224
|
+
type: "website"
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// src/lib/slug.ts
|
|
231
|
+
function generateSlug(text) {
|
|
232
|
+
return text.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
|
|
233
|
+
}
|
|
234
|
+
async function ensureUniqueSlug(slug, collection, excludeId) {
|
|
235
|
+
let candidate = slug;
|
|
236
|
+
let counter = 1;
|
|
237
|
+
while (true) {
|
|
238
|
+
const query = { slug: candidate };
|
|
239
|
+
if (excludeId) {
|
|
240
|
+
query._id = { $ne: excludeId };
|
|
241
|
+
}
|
|
242
|
+
const existing = await collection.findOne(query);
|
|
243
|
+
if (!existing) return candidate;
|
|
244
|
+
candidate = `${slug}-${counter}`;
|
|
245
|
+
counter++;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// src/lib/reading-time.ts
|
|
250
|
+
var WORDS_PER_MINUTE = 200;
|
|
251
|
+
function calculateReadingTime(text) {
|
|
252
|
+
const words = countWords(text);
|
|
253
|
+
return Math.max(1, Math.ceil(words / WORDS_PER_MINUTE));
|
|
254
|
+
}
|
|
255
|
+
function countWords(text) {
|
|
256
|
+
if (!text || !text.trim()) return 0;
|
|
257
|
+
return text.trim().split(/\s+/).length;
|
|
258
|
+
}
|
|
259
|
+
function extractTextFromHTML(html) {
|
|
260
|
+
return html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "").replace(/<[^>]+>/g, " ").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/\s+/g, " ").trim();
|
|
261
|
+
}
|
|
262
|
+
function extractTextFromBlocks(blocks) {
|
|
263
|
+
if (!Array.isArray(blocks)) return "";
|
|
264
|
+
const parts = [];
|
|
265
|
+
function walk(node) {
|
|
266
|
+
if (!node || typeof node !== "object") return;
|
|
267
|
+
const n = node;
|
|
268
|
+
if (typeof n.text === "string") {
|
|
269
|
+
parts.push(n.text);
|
|
270
|
+
}
|
|
271
|
+
if (Array.isArray(n.content)) {
|
|
272
|
+
for (const child of n.content) {
|
|
273
|
+
walk(child);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
for (const block of blocks) {
|
|
278
|
+
walk(block);
|
|
279
|
+
}
|
|
280
|
+
return parts.join(" ");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// src/lib/db.ts
|
|
284
|
+
var client = null;
|
|
285
|
+
var db = null;
|
|
286
|
+
async function getDb() {
|
|
287
|
+
if (db) return db;
|
|
288
|
+
const env = getEnvConfig();
|
|
289
|
+
client = new MongoClient(env.BLOGPANEL_MONGODB_URI);
|
|
290
|
+
await client.connect();
|
|
291
|
+
db = client.db(env.BLOGPANEL_MONGODB_DB || void 0);
|
|
292
|
+
return db;
|
|
293
|
+
}
|
|
294
|
+
async function getCollection(name) {
|
|
295
|
+
const database = await getDb();
|
|
296
|
+
return database.collection(name);
|
|
297
|
+
}
|
|
298
|
+
async function ensureIndexes() {
|
|
299
|
+
const database = await getDb();
|
|
300
|
+
const posts = database.collection("nbk_posts");
|
|
301
|
+
await posts.createIndex({ slug: 1 }, { unique: true });
|
|
302
|
+
await posts.createIndex({ status: 1, publishedAt: -1 });
|
|
303
|
+
await posts.createIndex({ categories: 1, status: 1 });
|
|
304
|
+
await posts.createIndex({ tags: 1, status: 1 });
|
|
305
|
+
await posts.createIndex({ "seo.focusKeyword": 1 });
|
|
306
|
+
await posts.createIndex({ contentText: "text", title: "text", excerpt: "text" });
|
|
307
|
+
const categories = database.collection("nbk_categories");
|
|
308
|
+
await categories.createIndex({ slug: 1 }, { unique: true });
|
|
309
|
+
await categories.createIndex({ order: 1 });
|
|
310
|
+
const media = database.collection("nbk_media");
|
|
311
|
+
await media.createIndex({ createdAt: -1 });
|
|
312
|
+
await media.createIndex({ storageKey: 1 }, { unique: true });
|
|
313
|
+
const tokens = database.collection("nbk_api_tokens");
|
|
314
|
+
await tokens.createIndex({ tokenHash: 1 }, { unique: true });
|
|
315
|
+
await tokens.createIndex({ createdAt: -1 });
|
|
316
|
+
}
|
|
317
|
+
async function createPost(input, defaultAuthor) {
|
|
318
|
+
const col = await getCollection("nbk_posts");
|
|
319
|
+
const slug = await ensureUniqueSlug(
|
|
320
|
+
input.slug || generateSlug(input.title),
|
|
321
|
+
col
|
|
322
|
+
);
|
|
323
|
+
const contentText = input.contentText || extractTextFromBlocks(input.content || []);
|
|
324
|
+
const wordCount = countWords(contentText);
|
|
325
|
+
const readingTime = calculateReadingTime(contentText);
|
|
326
|
+
const excerpt = input.excerpt || contentText.slice(0, 160).replace(/\s+\S*$/, "") + "...";
|
|
327
|
+
const now = /* @__PURE__ */ new Date();
|
|
328
|
+
const doc = {
|
|
329
|
+
title: input.title,
|
|
330
|
+
slug,
|
|
331
|
+
excerpt,
|
|
332
|
+
content: input.content || [],
|
|
333
|
+
contentHTML: input.contentHTML || "",
|
|
334
|
+
contentText,
|
|
335
|
+
coverImage: input.coverImage,
|
|
336
|
+
categories: input.categories || [],
|
|
337
|
+
tags: input.tags || [],
|
|
338
|
+
author: input.author || defaultAuthor || { name: "Admin" },
|
|
339
|
+
seo: {
|
|
340
|
+
ogType: "article",
|
|
341
|
+
noIndex: false,
|
|
342
|
+
...input.seo
|
|
343
|
+
},
|
|
344
|
+
status: input.status || "draft",
|
|
345
|
+
publishedAt: input.status === "published" ? now : input.publishedAt,
|
|
346
|
+
scheduledAt: input.scheduledAt,
|
|
347
|
+
readingTime,
|
|
348
|
+
wordCount,
|
|
349
|
+
version: 1,
|
|
350
|
+
revisions: [],
|
|
351
|
+
createdAt: now,
|
|
352
|
+
updatedAt: now
|
|
353
|
+
};
|
|
354
|
+
const result = await col.insertOne(doc);
|
|
355
|
+
return { _id: result.insertedId, ...doc };
|
|
356
|
+
}
|
|
357
|
+
async function updatePost(id, input) {
|
|
358
|
+
const col = await getCollection("nbk_posts");
|
|
359
|
+
const objectId = new ObjectId(id);
|
|
360
|
+
const existing = await col.findOne({ _id: objectId });
|
|
361
|
+
if (!existing) return null;
|
|
362
|
+
const updates = { ...input, updatedAt: /* @__PURE__ */ new Date() };
|
|
363
|
+
if (input.slug && input.slug !== existing.slug) {
|
|
364
|
+
updates.slug = await ensureUniqueSlug(input.slug, col, id);
|
|
365
|
+
}
|
|
366
|
+
if (input.content) {
|
|
367
|
+
const contentText = input.contentText || extractTextFromBlocks(input.content);
|
|
368
|
+
updates.contentText = contentText;
|
|
369
|
+
updates.wordCount = countWords(contentText);
|
|
370
|
+
updates.readingTime = calculateReadingTime(contentText);
|
|
371
|
+
if (!input.excerpt) {
|
|
372
|
+
updates.excerpt = contentText.slice(0, 160).replace(/\s+\S*$/, "") + "...";
|
|
373
|
+
}
|
|
374
|
+
const revision = {
|
|
375
|
+
version: existing.version || 1,
|
|
376
|
+
title: existing.title,
|
|
377
|
+
content: existing.content,
|
|
378
|
+
contentHTML: existing.contentHTML,
|
|
379
|
+
savedAt: /* @__PURE__ */ new Date()
|
|
380
|
+
};
|
|
381
|
+
const revisions = [...existing.revisions || [], revision].slice(-10);
|
|
382
|
+
updates.revisions = revisions;
|
|
383
|
+
updates.version = (existing.version || 1) + 1;
|
|
384
|
+
}
|
|
385
|
+
if (input.status === "published" && existing.status !== "published") {
|
|
386
|
+
updates.publishedAt = /* @__PURE__ */ new Date();
|
|
387
|
+
}
|
|
388
|
+
await col.updateOne({ _id: objectId }, { $set: updates });
|
|
389
|
+
return await col.findOne({ _id: objectId });
|
|
390
|
+
}
|
|
391
|
+
async function deletePost(id) {
|
|
392
|
+
const col = await getCollection("nbk_posts");
|
|
393
|
+
const result = await col.updateOne(
|
|
394
|
+
{ _id: new ObjectId(id) },
|
|
395
|
+
{ $set: { status: "archived", updatedAt: /* @__PURE__ */ new Date() } }
|
|
396
|
+
);
|
|
397
|
+
return result.modifiedCount > 0;
|
|
398
|
+
}
|
|
399
|
+
async function hardDeletePost(id) {
|
|
400
|
+
const col = await getCollection("nbk_posts");
|
|
401
|
+
const result = await col.deleteOne({ _id: new ObjectId(id) });
|
|
402
|
+
return result.deletedCount > 0;
|
|
403
|
+
}
|
|
404
|
+
async function getPostBySlug(slug) {
|
|
405
|
+
const col = await getCollection("nbk_posts");
|
|
406
|
+
return await col.findOne({ slug });
|
|
407
|
+
}
|
|
408
|
+
async function getPostById(id) {
|
|
409
|
+
const col = await getCollection("nbk_posts");
|
|
410
|
+
return await col.findOne({ _id: new ObjectId(id) });
|
|
411
|
+
}
|
|
412
|
+
async function listPosts(query = {}) {
|
|
413
|
+
const col = await getCollection("nbk_posts");
|
|
414
|
+
const {
|
|
415
|
+
page = 1,
|
|
416
|
+
limit = 10,
|
|
417
|
+
category,
|
|
418
|
+
tag,
|
|
419
|
+
status,
|
|
420
|
+
search,
|
|
421
|
+
sortBy = "publishedAt",
|
|
422
|
+
sortOrder = "desc"
|
|
423
|
+
} = query;
|
|
424
|
+
const filter = {};
|
|
425
|
+
if (status) {
|
|
426
|
+
filter.status = status;
|
|
427
|
+
} else {
|
|
428
|
+
filter.status = { $ne: "archived" };
|
|
429
|
+
}
|
|
430
|
+
if (category) filter.categories = category;
|
|
431
|
+
if (tag) filter.tags = tag;
|
|
432
|
+
if (search) {
|
|
433
|
+
filter.$text = { $search: search };
|
|
434
|
+
}
|
|
435
|
+
const sort = {
|
|
436
|
+
[sortBy]: sortOrder === "asc" ? 1 : -1
|
|
437
|
+
};
|
|
438
|
+
const skip = (page - 1) * limit;
|
|
439
|
+
const [posts, total] = await Promise.all([
|
|
440
|
+
col.find(filter).sort(sort).skip(skip).limit(limit).toArray(),
|
|
441
|
+
col.countDocuments(filter)
|
|
442
|
+
]);
|
|
443
|
+
return {
|
|
444
|
+
posts,
|
|
445
|
+
total
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
async function createCategory(input) {
|
|
449
|
+
const col = await getCollection("nbk_categories");
|
|
450
|
+
const slug = await ensureUniqueSlug(
|
|
451
|
+
input.slug || generateSlug(input.name),
|
|
452
|
+
col
|
|
453
|
+
);
|
|
454
|
+
const doc = {
|
|
455
|
+
name: input.name,
|
|
456
|
+
slug,
|
|
457
|
+
description: input.description,
|
|
458
|
+
seo: input.seo,
|
|
459
|
+
order: input.order ?? 0,
|
|
460
|
+
parentId: input.parentId ? new ObjectId(input.parentId) : void 0,
|
|
461
|
+
postCount: 0
|
|
462
|
+
};
|
|
463
|
+
const result = await col.insertOne(doc);
|
|
464
|
+
return { _id: result.insertedId, ...doc };
|
|
465
|
+
}
|
|
466
|
+
async function updateCategory(id, input) {
|
|
467
|
+
const col = await getCollection("nbk_categories");
|
|
468
|
+
const objectId = new ObjectId(id);
|
|
469
|
+
const updates = { ...input };
|
|
470
|
+
if (input.slug) {
|
|
471
|
+
updates.slug = await ensureUniqueSlug(input.slug, col, id);
|
|
472
|
+
}
|
|
473
|
+
if (input.parentId) {
|
|
474
|
+
updates.parentId = new ObjectId(input.parentId);
|
|
475
|
+
}
|
|
476
|
+
await col.updateOne({ _id: objectId }, { $set: updates });
|
|
477
|
+
return await col.findOne({ _id: objectId });
|
|
478
|
+
}
|
|
479
|
+
async function deleteCategory(id) {
|
|
480
|
+
const col = await getCollection("nbk_categories");
|
|
481
|
+
const result = await col.deleteOne({ _id: new ObjectId(id) });
|
|
482
|
+
return result.deletedCount > 0;
|
|
483
|
+
}
|
|
484
|
+
async function listCategories() {
|
|
485
|
+
const col = await getCollection("nbk_categories");
|
|
486
|
+
return await col.find({}).sort({ order: 1 }).toArray();
|
|
487
|
+
}
|
|
488
|
+
async function getCategoryBySlug(slug) {
|
|
489
|
+
const col = await getCollection("nbk_categories");
|
|
490
|
+
return await col.findOne({ slug });
|
|
491
|
+
}
|
|
492
|
+
async function updateCategoryPostCount(categorySlug) {
|
|
493
|
+
const posts = await getCollection("nbk_posts");
|
|
494
|
+
const categories = await getCollection("nbk_categories");
|
|
495
|
+
const count = await posts.countDocuments({
|
|
496
|
+
categories: categorySlug,
|
|
497
|
+
status: "published"
|
|
498
|
+
});
|
|
499
|
+
await categories.updateOne({ slug: categorySlug }, { $set: { postCount: count } });
|
|
500
|
+
}
|
|
501
|
+
async function createMedia(data) {
|
|
502
|
+
const col = await getCollection("nbk_media");
|
|
503
|
+
const result = await col.insertOne(data);
|
|
504
|
+
return { _id: result.insertedId, ...data };
|
|
505
|
+
}
|
|
506
|
+
async function getMediaById(id) {
|
|
507
|
+
const col = await getCollection("nbk_media");
|
|
508
|
+
const media = await col.findOne({ _id: new ObjectId(id) });
|
|
509
|
+
return media || null;
|
|
510
|
+
}
|
|
511
|
+
async function deleteMedia(id) {
|
|
512
|
+
const col = await getCollection("nbk_media");
|
|
513
|
+
const media = await col.findOne({ _id: new ObjectId(id) });
|
|
514
|
+
if (!media) return null;
|
|
515
|
+
await col.deleteOne({ _id: new ObjectId(id) });
|
|
516
|
+
return media;
|
|
517
|
+
}
|
|
518
|
+
async function listMedia(query = {}) {
|
|
519
|
+
const col = await getCollection("nbk_media");
|
|
520
|
+
const { page = 1, limit = 20, mimeType } = query;
|
|
521
|
+
const filter = {};
|
|
522
|
+
if (mimeType) filter.mimeType = { $regex: mimeType };
|
|
523
|
+
const skip = (page - 1) * limit;
|
|
524
|
+
const [media, total] = await Promise.all([
|
|
525
|
+
col.find(filter).sort({ createdAt: -1 }).skip(skip).limit(limit).toArray(),
|
|
526
|
+
col.countDocuments(filter)
|
|
527
|
+
]);
|
|
528
|
+
return { media, total };
|
|
529
|
+
}
|
|
530
|
+
async function getSettings() {
|
|
531
|
+
const col = await getCollection("nbk_settings");
|
|
532
|
+
const settings = await col.findOne({ _id: "global" });
|
|
533
|
+
if (!settings) {
|
|
534
|
+
const defaults = {
|
|
535
|
+
_id: "global",
|
|
536
|
+
postsPerPage: 10,
|
|
537
|
+
commentSystem: "none"
|
|
538
|
+
};
|
|
539
|
+
await col.insertOne(defaults);
|
|
540
|
+
return defaults;
|
|
541
|
+
}
|
|
542
|
+
return settings;
|
|
543
|
+
}
|
|
544
|
+
async function updateSettings(data) {
|
|
545
|
+
const col = await getCollection("nbk_settings");
|
|
546
|
+
const { _id, ...updates } = data;
|
|
547
|
+
await col.updateOne(
|
|
548
|
+
{ _id: "global" },
|
|
549
|
+
{ $set: updates },
|
|
550
|
+
{ upsert: true }
|
|
551
|
+
);
|
|
552
|
+
return getSettings();
|
|
553
|
+
}
|
|
554
|
+
function hashToken(plain) {
|
|
555
|
+
return createHash("sha256").update(plain).digest("hex");
|
|
556
|
+
}
|
|
557
|
+
async function createApiToken(name) {
|
|
558
|
+
const col = await getCollection("nbk_api_tokens");
|
|
559
|
+
const plainToken = "nbk_" + randomBytes(24).toString("hex");
|
|
560
|
+
const tokenHash = hashToken(plainToken);
|
|
561
|
+
const prefix = plainToken.slice(0, 8);
|
|
562
|
+
const now = /* @__PURE__ */ new Date();
|
|
563
|
+
const doc = {
|
|
564
|
+
name,
|
|
565
|
+
tokenHash,
|
|
566
|
+
prefix,
|
|
567
|
+
createdAt: now
|
|
568
|
+
};
|
|
569
|
+
const result = await col.insertOne(doc);
|
|
570
|
+
const token = { _id: result.insertedId, ...doc };
|
|
571
|
+
return { token, plainToken };
|
|
572
|
+
}
|
|
573
|
+
async function listApiTokens() {
|
|
574
|
+
const col = await getCollection("nbk_api_tokens");
|
|
575
|
+
return await col.find({}, { projection: { tokenHash: 0 } }).sort({ createdAt: -1 }).toArray();
|
|
576
|
+
}
|
|
577
|
+
async function deleteApiToken(id) {
|
|
578
|
+
const col = await getCollection("nbk_api_tokens");
|
|
579
|
+
const result = await col.deleteOne({ _id: new ObjectId(id) });
|
|
580
|
+
return result.deletedCount > 0;
|
|
581
|
+
}
|
|
582
|
+
async function verifyApiToken(plainToken) {
|
|
583
|
+
const col = await getCollection("nbk_api_tokens");
|
|
584
|
+
const tokenHash = hashToken(plainToken);
|
|
585
|
+
const token = await col.findOneAndUpdate(
|
|
586
|
+
{ tokenHash },
|
|
587
|
+
{ $set: { lastUsedAt: /* @__PURE__ */ new Date() } },
|
|
588
|
+
{ returnDocument: "after" }
|
|
589
|
+
);
|
|
590
|
+
return token;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
export { ConfigNotInstalledError, calculateReadingTime, countWords, createApiToken, createCategory, createMedia, createPost, defineConfig, deleteApiToken, deleteCategory, deleteMedia, deletePost, ensureIndexes, ensureUniqueSlug, extractTextFromBlocks, extractTextFromHTML, generateSlug, getBlogConfig, getCategoryBySlug, getCollection, getConfig, getDb, getEnvConfig, getMediaById, getPostById, getPostBySlug, getSettings, hardDeletePost, isConfigInstalled, isR2Configured, listApiTokens, listCategories, listMedia, listPosts, requireApiPath, setConfig, updateCategory, updateCategoryPostCount, updatePost, updateSettings, verifyApiToken };
|
|
594
|
+
//# sourceMappingURL=chunk-6PZ772NQ.js.map
|
|
595
|
+
//# sourceMappingURL=chunk-6PZ772NQ.js.map
|