@jhits/plugin-blog 0.0.1
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/README.md +216 -0
- package/package.json +57 -0
- package/src/api/README.md +224 -0
- package/src/api/categories.ts +43 -0
- package/src/api/check-title.ts +60 -0
- package/src/api/handler.ts +419 -0
- package/src/api/index.ts +33 -0
- package/src/api/route.ts +116 -0
- package/src/api/router.ts +114 -0
- package/src/api-server.ts +11 -0
- package/src/config.ts +161 -0
- package/src/hooks/README.md +91 -0
- package/src/hooks/index.ts +8 -0
- package/src/hooks/useBlog.ts +85 -0
- package/src/hooks/useBlogs.ts +123 -0
- package/src/index.server.ts +12 -0
- package/src/index.tsx +354 -0
- package/src/init.tsx +72 -0
- package/src/lib/blocks/BlockRenderer.tsx +141 -0
- package/src/lib/blocks/index.ts +6 -0
- package/src/lib/index.ts +9 -0
- package/src/lib/layouts/blocks/ColumnsBlock.tsx +134 -0
- package/src/lib/layouts/blocks/SectionBlock.tsx +104 -0
- package/src/lib/layouts/blocks/index.ts +8 -0
- package/src/lib/layouts/index.ts +52 -0
- package/src/lib/layouts/registerLayoutBlocks.ts +59 -0
- package/src/lib/mappers/apiMapper.ts +223 -0
- package/src/lib/migration/index.ts +6 -0
- package/src/lib/migration/mapper.ts +140 -0
- package/src/lib/rich-text/RichTextEditor.tsx +826 -0
- package/src/lib/rich-text/RichTextPreview.tsx +210 -0
- package/src/lib/rich-text/index.ts +10 -0
- package/src/lib/utils/blockHelpers.ts +72 -0
- package/src/lib/utils/configValidation.ts +137 -0
- package/src/lib/utils/index.ts +8 -0
- package/src/lib/utils/slugify.ts +79 -0
- package/src/registry/BlockRegistry.ts +142 -0
- package/src/registry/index.ts +11 -0
- package/src/state/EditorContext.tsx +277 -0
- package/src/state/index.ts +8 -0
- package/src/state/reducer.ts +694 -0
- package/src/state/types.ts +160 -0
- package/src/types/block.ts +269 -0
- package/src/types/index.ts +15 -0
- package/src/types/post.ts +165 -0
- package/src/utils/README.md +75 -0
- package/src/utils/client.ts +122 -0
- package/src/utils/index.ts +9 -0
- package/src/views/CanvasEditor/BlockWrapper.tsx +459 -0
- package/src/views/CanvasEditor/CanvasEditorView.tsx +917 -0
- package/src/views/CanvasEditor/EditorBody.tsx +475 -0
- package/src/views/CanvasEditor/EditorHeader.tsx +179 -0
- package/src/views/CanvasEditor/LayoutContainer.tsx +494 -0
- package/src/views/CanvasEditor/SaveConfirmationModal.tsx +233 -0
- package/src/views/CanvasEditor/components/CustomBlockItem.tsx +92 -0
- package/src/views/CanvasEditor/components/FeaturedMediaSection.tsx +130 -0
- package/src/views/CanvasEditor/components/LibraryItem.tsx +80 -0
- package/src/views/CanvasEditor/components/PrivacySettingsSection.tsx +212 -0
- package/src/views/CanvasEditor/components/index.ts +17 -0
- package/src/views/CanvasEditor/index.ts +16 -0
- package/src/views/PostManager/EmptyState.tsx +42 -0
- package/src/views/PostManager/PostActionsMenu.tsx +112 -0
- package/src/views/PostManager/PostCards.tsx +192 -0
- package/src/views/PostManager/PostFilters.tsx +80 -0
- package/src/views/PostManager/PostManagerView.tsx +280 -0
- package/src/views/PostManager/PostStats.tsx +81 -0
- package/src/views/PostManager/PostTable.tsx +225 -0
- package/src/views/PostManager/index.ts +15 -0
- package/src/views/Preview/PreviewBridgeView.tsx +64 -0
- package/src/views/Preview/index.ts +7 -0
- package/src/views/README.md +82 -0
- package/src/views/Settings/SettingsView.tsx +298 -0
- package/src/views/Settings/index.ts +7 -0
- package/src/views/SlugSEO/SlugSEOManagerView.tsx +94 -0
- package/src/views/SlugSEO/index.ts +7 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blog API Handler
|
|
3
|
+
* RESTful API handler for blog posts
|
|
4
|
+
* Compatible with Next.js API routes
|
|
5
|
+
*
|
|
6
|
+
* IMPORTANT: This file should ONLY be imported in server-side API routes.
|
|
7
|
+
* Do NOT import this in client-side code.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
11
|
+
import { APIBlogDocument, apiToBlogPost, editorStateToAPI } from '../lib/mappers/apiMapper';
|
|
12
|
+
import { slugify } from '../lib/utils/slugify';
|
|
13
|
+
|
|
14
|
+
export interface BlogApiConfig {
|
|
15
|
+
/** MongoDB client promise (from clientPromise) - should return { db: () => Database } */
|
|
16
|
+
getDb: () => Promise<{ db: () => any }>;
|
|
17
|
+
/** Function to get authenticated user ID from request */
|
|
18
|
+
getUserId: (req: NextRequest) => Promise<string | null>;
|
|
19
|
+
/** Collection name (default: 'blogs') */
|
|
20
|
+
collectionName?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* GET /api/blogs - List all blog posts
|
|
25
|
+
* GET /api/blogs?admin=true - List all posts for admin (includes drafts)
|
|
26
|
+
* GET /api/blogs?status=published - Filter by status
|
|
27
|
+
*/
|
|
28
|
+
export async function GET(req: NextRequest, config: BlogApiConfig): Promise<NextResponse> {
|
|
29
|
+
try {
|
|
30
|
+
const url = new URL(req.url);
|
|
31
|
+
const limit = Number(url.searchParams.get('limit') ?? 10);
|
|
32
|
+
const skip = Number(url.searchParams.get('skip') ?? 0);
|
|
33
|
+
const statusFilter = url.searchParams.get('status');
|
|
34
|
+
const isAdminView = url.searchParams.get('admin') === 'true';
|
|
35
|
+
|
|
36
|
+
const userId = await config.getUserId(req);
|
|
37
|
+
const dbConnection = await config.getDb();
|
|
38
|
+
const db = dbConnection.db();
|
|
39
|
+
const blogs = db.collection(config.collectionName || 'blogs');
|
|
40
|
+
|
|
41
|
+
// Build query
|
|
42
|
+
let query: any = {};
|
|
43
|
+
|
|
44
|
+
if (isAdminView && userId) {
|
|
45
|
+
// Admin view: show all posts owned by user
|
|
46
|
+
if (statusFilter) {
|
|
47
|
+
query = {
|
|
48
|
+
'publicationData.status': statusFilter,
|
|
49
|
+
authorId: userId,
|
|
50
|
+
};
|
|
51
|
+
} else {
|
|
52
|
+
query = { authorId: userId };
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
// Public view: only published posts
|
|
56
|
+
query = {
|
|
57
|
+
'publicationData.status': 'published',
|
|
58
|
+
'publicationData.date': { $lte: new Date() },
|
|
59
|
+
};
|
|
60
|
+
if (statusFilter && statusFilter !== 'published') {
|
|
61
|
+
// Non-admin can't filter by non-published status
|
|
62
|
+
return NextResponse.json({ error: 'Invalid status filter' }, { status: 400 });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const [data, totalCount] = await Promise.all([
|
|
67
|
+
blogs
|
|
68
|
+
.find(query)
|
|
69
|
+
.sort({ 'publicationData.date': -1 })
|
|
70
|
+
.skip(skip)
|
|
71
|
+
.limit(limit)
|
|
72
|
+
.toArray(),
|
|
73
|
+
blogs.countDocuments(query),
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
const formatted = data.map((doc: any) => ({
|
|
77
|
+
...doc,
|
|
78
|
+
_id: doc._id.toString(),
|
|
79
|
+
}));
|
|
80
|
+
|
|
81
|
+
return NextResponse.json({
|
|
82
|
+
blogs: formatted,
|
|
83
|
+
total: totalCount,
|
|
84
|
+
});
|
|
85
|
+
} catch (err: any) {
|
|
86
|
+
console.error('[BlogAPI] GET error:', err);
|
|
87
|
+
return NextResponse.json(
|
|
88
|
+
{ error: 'Failed to fetch blogs', detail: err.message },
|
|
89
|
+
{ status: 500 }
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* POST /api/blogs - Create new blog post
|
|
96
|
+
*/
|
|
97
|
+
export async function POST(req: NextRequest, config: BlogApiConfig): Promise<NextResponse> {
|
|
98
|
+
try {
|
|
99
|
+
const userId = await config.getUserId(req);
|
|
100
|
+
if (!userId) {
|
|
101
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const body = await req.json();
|
|
105
|
+
const {
|
|
106
|
+
title,
|
|
107
|
+
summary,
|
|
108
|
+
content,
|
|
109
|
+
contentBlocks,
|
|
110
|
+
image,
|
|
111
|
+
categoryTags,
|
|
112
|
+
publicationData,
|
|
113
|
+
seo,
|
|
114
|
+
} = body;
|
|
115
|
+
|
|
116
|
+
const isPublishing = publicationData?.status === 'published';
|
|
117
|
+
const isConcept = publicationData?.status === 'concept' || publicationData?.status === 'draft';
|
|
118
|
+
|
|
119
|
+
// Validation
|
|
120
|
+
const errors: string[] = [];
|
|
121
|
+
if (!title?.trim()) {
|
|
122
|
+
errors.push('Title is required');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (isPublishing) {
|
|
126
|
+
// Publishing requires all fields
|
|
127
|
+
if (!summary?.trim()) errors.push('Summary is required for publishing');
|
|
128
|
+
if (!image?.src?.trim()) errors.push('Featured image is required for publishing');
|
|
129
|
+
// Only require category if it's explicitly provided and empty
|
|
130
|
+
// If categoryTags is undefined or category is undefined, that's also missing
|
|
131
|
+
if (!categoryTags || !categoryTags.category || !categoryTags.category.trim()) {
|
|
132
|
+
errors.push('Category is required for publishing');
|
|
133
|
+
}
|
|
134
|
+
const hasContent =
|
|
135
|
+
(contentBlocks && Array.isArray(contentBlocks) && contentBlocks.length > 0) ||
|
|
136
|
+
(content && Array.isArray(content) && content.length > 0);
|
|
137
|
+
if (!hasContent) {
|
|
138
|
+
errors.push('Content is required for publishing');
|
|
139
|
+
}
|
|
140
|
+
if (!publicationData?.date) errors.push('Publication date is required');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (errors.length > 0) {
|
|
144
|
+
return NextResponse.json({ message: errors[0], allErrors: errors }, { status: 400 });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Create slug
|
|
148
|
+
let baseSlug = slugify(title);
|
|
149
|
+
const slug = isPublishing
|
|
150
|
+
? baseSlug
|
|
151
|
+
: `${baseSlug}-draft-${Date.now().toString().slice(-4)}`;
|
|
152
|
+
|
|
153
|
+
const dbConnection = await config.getDb();
|
|
154
|
+
const db = dbConnection.db();
|
|
155
|
+
const blogs = db.collection(config.collectionName || 'blogs');
|
|
156
|
+
|
|
157
|
+
// Determine the final status: if publishing, set to 'published', otherwise convert draft to concept
|
|
158
|
+
let finalStatus = publicationData?.status;
|
|
159
|
+
if (isPublishing) {
|
|
160
|
+
finalStatus = 'published';
|
|
161
|
+
} else if (publicationData?.status === 'draft') {
|
|
162
|
+
finalStatus = 'concept';
|
|
163
|
+
} else {
|
|
164
|
+
finalStatus = publicationData?.status || 'concept';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const blogDocument = {
|
|
168
|
+
title: title.trim(),
|
|
169
|
+
summary: (summary || '').trim(),
|
|
170
|
+
contentBlocks: contentBlocks || [],
|
|
171
|
+
content: content || [],
|
|
172
|
+
image: image || { src: '', alt: '', brightness: 100, blur: 0 },
|
|
173
|
+
categoryTags: {
|
|
174
|
+
category: categoryTags?.category?.trim() || '',
|
|
175
|
+
tags: categoryTags?.tags || [],
|
|
176
|
+
},
|
|
177
|
+
publicationData: {
|
|
178
|
+
...publicationData,
|
|
179
|
+
status: finalStatus,
|
|
180
|
+
date: publicationData?.date ? new Date(publicationData.date) : new Date(),
|
|
181
|
+
},
|
|
182
|
+
seo: seo || { title: '', description: '' },
|
|
183
|
+
slug,
|
|
184
|
+
authorId: userId,
|
|
185
|
+
createdAt: new Date(),
|
|
186
|
+
updatedAt: new Date(),
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const result = await blogs.insertOne(blogDocument);
|
|
190
|
+
|
|
191
|
+
return NextResponse.json({
|
|
192
|
+
message: isPublishing ? 'Blog published successfully' : 'Draft saved successfully',
|
|
193
|
+
blogId: result.insertedId,
|
|
194
|
+
slug,
|
|
195
|
+
});
|
|
196
|
+
} catch (err: any) {
|
|
197
|
+
console.error('[BlogAPI] POST error:', err);
|
|
198
|
+
return NextResponse.json(
|
|
199
|
+
{ error: 'Failed to create blog', detail: err.message },
|
|
200
|
+
{ status: 500 }
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* GET /api/blogs/[slug] - Get single blog post by slug
|
|
207
|
+
*/
|
|
208
|
+
export async function GET_BY_SLUG(
|
|
209
|
+
req: NextRequest,
|
|
210
|
+
slug: string,
|
|
211
|
+
config: BlogApiConfig
|
|
212
|
+
): Promise<NextResponse> {
|
|
213
|
+
try {
|
|
214
|
+
const userId = await config.getUserId(req);
|
|
215
|
+
const dbConnection = await config.getDb();
|
|
216
|
+
const db = dbConnection.db();
|
|
217
|
+
const blogs = db.collection(config.collectionName || 'blogs');
|
|
218
|
+
|
|
219
|
+
const blog = await blogs.findOne({ slug });
|
|
220
|
+
|
|
221
|
+
if (!blog) {
|
|
222
|
+
return NextResponse.json({ error: 'Blog not found' }, { status: 404 });
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Security check
|
|
226
|
+
const isPublished = blog.publicationData?.status === 'published';
|
|
227
|
+
const isAuthor = userId && blog.authorId === userId;
|
|
228
|
+
|
|
229
|
+
if (!isPublished && !isAuthor) {
|
|
230
|
+
return NextResponse.json({ error: 'Access denied' }, { status: 403 });
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return NextResponse.json({
|
|
234
|
+
...blog,
|
|
235
|
+
_id: blog._id.toString(),
|
|
236
|
+
});
|
|
237
|
+
} catch (err: any) {
|
|
238
|
+
console.error('[BlogAPI] GET_BY_SLUG error:', err);
|
|
239
|
+
return NextResponse.json(
|
|
240
|
+
{ error: 'Failed to fetch blog', detail: err.message },
|
|
241
|
+
{ status: 500 }
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* PUT /api/blogs/[slug] - Update blog post by slug
|
|
248
|
+
*/
|
|
249
|
+
export async function PUT_BY_SLUG(
|
|
250
|
+
req: NextRequest,
|
|
251
|
+
slug: string,
|
|
252
|
+
config: BlogApiConfig
|
|
253
|
+
): Promise<NextResponse> {
|
|
254
|
+
try {
|
|
255
|
+
const userId = await config.getUserId(req);
|
|
256
|
+
if (!userId) {
|
|
257
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const body = await req.json();
|
|
261
|
+
const {
|
|
262
|
+
title,
|
|
263
|
+
summary,
|
|
264
|
+
content,
|
|
265
|
+
contentBlocks,
|
|
266
|
+
image,
|
|
267
|
+
categoryTags,
|
|
268
|
+
publicationData,
|
|
269
|
+
seo,
|
|
270
|
+
} = body;
|
|
271
|
+
|
|
272
|
+
const dbConnection = await config.getDb();
|
|
273
|
+
const db = dbConnection.db();
|
|
274
|
+
const blogs = db.collection(config.collectionName || 'blogs');
|
|
275
|
+
|
|
276
|
+
// Check if blog exists and user is author
|
|
277
|
+
const existingBlog = await blogs.findOne({ slug });
|
|
278
|
+
if (!existingBlog) {
|
|
279
|
+
return NextResponse.json({ error: 'Blog not found' }, { status: 404 });
|
|
280
|
+
}
|
|
281
|
+
if (existingBlog.authorId !== userId) {
|
|
282
|
+
return NextResponse.json({ error: 'Forbidden: Not the author' }, { status: 403 });
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Validation
|
|
286
|
+
const isPublishing = publicationData?.status === 'published';
|
|
287
|
+
if (!title?.trim()) {
|
|
288
|
+
return NextResponse.json({ message: 'Title is required' }, { status: 400 });
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (isPublishing) {
|
|
292
|
+
const hasContent =
|
|
293
|
+
(contentBlocks && Array.isArray(contentBlocks) && contentBlocks.length > 0) ||
|
|
294
|
+
(content && Array.isArray(content) && content.length > 0);
|
|
295
|
+
|
|
296
|
+
// Collect all missing fields for better error messages
|
|
297
|
+
const missingFields: string[] = [];
|
|
298
|
+
if (!summary?.trim()) missingFields.push('summary');
|
|
299
|
+
if (!image?.src?.trim()) missingFields.push('featured image');
|
|
300
|
+
// Only require category if it's explicitly provided and empty
|
|
301
|
+
// If categoryTags is undefined or category is undefined, that's also missing
|
|
302
|
+
if (!categoryTags || !categoryTags.category || !categoryTags.category.trim()) {
|
|
303
|
+
missingFields.push('category');
|
|
304
|
+
}
|
|
305
|
+
if (!hasContent) missingFields.push('content');
|
|
306
|
+
|
|
307
|
+
if (missingFields.length > 0) {
|
|
308
|
+
console.log('[BlogAPI] PUT_BY_SLUG validation failed:', {
|
|
309
|
+
isPublishing,
|
|
310
|
+
missingFields,
|
|
311
|
+
summary: summary?.trim() || 'missing',
|
|
312
|
+
imageSrc: image?.src?.trim() || 'missing',
|
|
313
|
+
category: categoryTags?.category?.trim() || 'missing',
|
|
314
|
+
hasContent,
|
|
315
|
+
contentBlocksLength: contentBlocks?.length || 0,
|
|
316
|
+
contentLength: content?.length || 0,
|
|
317
|
+
});
|
|
318
|
+
return NextResponse.json(
|
|
319
|
+
{
|
|
320
|
+
message: `Missing required fields for publishing: ${missingFields.join(', ')}`,
|
|
321
|
+
missingFields
|
|
322
|
+
},
|
|
323
|
+
{ status: 400 }
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Slug logic
|
|
329
|
+
let finalSlug = slug;
|
|
330
|
+
if (isPublishing) {
|
|
331
|
+
finalSlug = slugify(title);
|
|
332
|
+
} else if (publicationData?.status === 'concept' && !slug.includes('-draft-')) {
|
|
333
|
+
finalSlug = `${slugify(title)}-draft-${Date.now().toString().slice(-4)}`;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Update data
|
|
337
|
+
// Determine the final status: if publishing, set to 'published', otherwise preserve or convert draft to concept
|
|
338
|
+
let finalStatus = publicationData?.status;
|
|
339
|
+
if (isPublishing) {
|
|
340
|
+
finalStatus = 'published';
|
|
341
|
+
} else if (publicationData?.status === 'draft') {
|
|
342
|
+
finalStatus = 'concept';
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const updateData = {
|
|
346
|
+
title: title.trim(),
|
|
347
|
+
summary: (summary || '').trim(),
|
|
348
|
+
contentBlocks: contentBlocks || [],
|
|
349
|
+
content: content || [],
|
|
350
|
+
image: image || {},
|
|
351
|
+
categoryTags: {
|
|
352
|
+
category: categoryTags?.category?.trim() || '',
|
|
353
|
+
tags: categoryTags?.tags || [],
|
|
354
|
+
},
|
|
355
|
+
publicationData: {
|
|
356
|
+
...publicationData,
|
|
357
|
+
status: finalStatus,
|
|
358
|
+
date: publicationData?.date ? new Date(publicationData.date) : new Date(),
|
|
359
|
+
},
|
|
360
|
+
seo: seo || {},
|
|
361
|
+
slug: finalSlug,
|
|
362
|
+
authorId: userId,
|
|
363
|
+
updatedAt: new Date(),
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
await blogs.updateOne({ slug }, { $set: updateData });
|
|
367
|
+
|
|
368
|
+
return NextResponse.json({
|
|
369
|
+
message: 'Blog updated successfully',
|
|
370
|
+
slug: finalSlug,
|
|
371
|
+
});
|
|
372
|
+
} catch (err: any) {
|
|
373
|
+
console.error('[BlogAPI] PUT_BY_SLUG error:', err);
|
|
374
|
+
return NextResponse.json(
|
|
375
|
+
{ error: 'Failed to update blog', detail: err.message },
|
|
376
|
+
{ status: 500 }
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* DELETE /api/blogs/[slug] - Delete blog post by slug
|
|
383
|
+
*/
|
|
384
|
+
export async function DELETE_BY_SLUG(
|
|
385
|
+
req: NextRequest,
|
|
386
|
+
slug: string,
|
|
387
|
+
config: BlogApiConfig
|
|
388
|
+
): Promise<NextResponse> {
|
|
389
|
+
try {
|
|
390
|
+
const userId = await config.getUserId(req);
|
|
391
|
+
if (!userId) {
|
|
392
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const dbConnection = await config.getDb();
|
|
396
|
+
const db = dbConnection.db();
|
|
397
|
+
const blogs = db.collection(config.collectionName || 'blogs');
|
|
398
|
+
|
|
399
|
+
// Verify ownership
|
|
400
|
+
const blog = await blogs.findOne({ slug });
|
|
401
|
+
if (!blog) {
|
|
402
|
+
return NextResponse.json({ error: 'Blog not found' }, { status: 404 });
|
|
403
|
+
}
|
|
404
|
+
if (blog.authorId !== userId) {
|
|
405
|
+
return NextResponse.json({ error: 'Forbidden: Not the author' }, { status: 403 });
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
await blogs.deleteOne({ slug });
|
|
409
|
+
|
|
410
|
+
return NextResponse.json({ message: 'Blog deleted successfully' });
|
|
411
|
+
} catch (err: any) {
|
|
412
|
+
console.error('[BlogAPI] DELETE_BY_SLUG error:', err);
|
|
413
|
+
return NextResponse.json(
|
|
414
|
+
{ error: 'Failed to delete blog', detail: err.message },
|
|
415
|
+
{ status: 500 }
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
package/src/api/index.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blog API Exports
|
|
3
|
+
* RESTful API handlers for blog posts
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export {
|
|
7
|
+
GET,
|
|
8
|
+
POST,
|
|
9
|
+
PUT,
|
|
10
|
+
DELETE,
|
|
11
|
+
createBlogApiConfig,
|
|
12
|
+
} from './route';
|
|
13
|
+
|
|
14
|
+
export type { BlogApiConfig } from './handler';
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
GET as GET_HANDLER,
|
|
18
|
+
POST as POST_HANDLER,
|
|
19
|
+
GET_BY_SLUG,
|
|
20
|
+
PUT_BY_SLUG,
|
|
21
|
+
DELETE_BY_SLUG,
|
|
22
|
+
} from './handler';
|
|
23
|
+
|
|
24
|
+
// Router exports
|
|
25
|
+
export { handleBlogApi } from './router';
|
|
26
|
+
export type { BlogApiRouterConfig } from './router';
|
|
27
|
+
|
|
28
|
+
// Categories handler
|
|
29
|
+
export { GET as GET_CATEGORIES } from './categories';
|
|
30
|
+
|
|
31
|
+
// Check title handler
|
|
32
|
+
export { GET as GET_CHECK_TITLE } from './check-title';
|
|
33
|
+
|
package/src/api/route.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blog API Route Handler
|
|
3
|
+
* Server-only wrapper for the blog API handlers
|
|
4
|
+
* This file should ONLY be used in Next.js API routes
|
|
5
|
+
*
|
|
6
|
+
* IMPORTANT: This file should ONLY be imported in server-side API routes.
|
|
7
|
+
* Do NOT import this in client-side code.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
11
|
+
import {
|
|
12
|
+
GET as handlerGET,
|
|
13
|
+
POST as handlerPOST,
|
|
14
|
+
GET_BY_SLUG,
|
|
15
|
+
PUT_BY_SLUG,
|
|
16
|
+
DELETE_BY_SLUG,
|
|
17
|
+
BlogApiConfig,
|
|
18
|
+
} from './handler';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Default configuration factory
|
|
22
|
+
* Client apps should provide their own config with MongoDB connection and auth
|
|
23
|
+
*/
|
|
24
|
+
export function createBlogApiConfig(config: BlogApiConfig): BlogApiConfig {
|
|
25
|
+
return config;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* GET handler - List all blogs or get by slug
|
|
30
|
+
* Usage:
|
|
31
|
+
* GET /api/blogs - List all
|
|
32
|
+
* GET /api/blogs/[slug] - Get by slug
|
|
33
|
+
*/
|
|
34
|
+
export async function GET(
|
|
35
|
+
req: NextRequest,
|
|
36
|
+
context?: { params?: Promise<{ slug?: string }> },
|
|
37
|
+
config?: BlogApiConfig
|
|
38
|
+
): Promise<NextResponse> {
|
|
39
|
+
if (!config) {
|
|
40
|
+
return NextResponse.json(
|
|
41
|
+
{ error: 'Blog API config not provided' },
|
|
42
|
+
{ status: 500 }
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Check if we have a slug parameter (single blog request)
|
|
47
|
+
if (context?.params) {
|
|
48
|
+
const { slug } = await context.params;
|
|
49
|
+
if (slug) {
|
|
50
|
+
return GET_BY_SLUG(req, slug, config);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Otherwise, list all blogs
|
|
55
|
+
return handlerGET(req, config);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* POST handler - Create new blog
|
|
60
|
+
* Usage: POST /api/blogs
|
|
61
|
+
*/
|
|
62
|
+
export async function POST(
|
|
63
|
+
req: NextRequest,
|
|
64
|
+
context?: any,
|
|
65
|
+
config?: BlogApiConfig
|
|
66
|
+
): Promise<NextResponse> {
|
|
67
|
+
if (!config) {
|
|
68
|
+
return NextResponse.json(
|
|
69
|
+
{ error: 'Blog API config not provided' },
|
|
70
|
+
{ status: 500 }
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return handlerPOST(req, config);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* PUT handler - Update blog by slug
|
|
79
|
+
* Usage: PUT /api/blogs/[slug]
|
|
80
|
+
*/
|
|
81
|
+
export async function PUT(
|
|
82
|
+
req: NextRequest,
|
|
83
|
+
context: { params: Promise<{ slug: string }> },
|
|
84
|
+
config?: BlogApiConfig
|
|
85
|
+
): Promise<NextResponse> {
|
|
86
|
+
if (!config) {
|
|
87
|
+
return NextResponse.json(
|
|
88
|
+
{ error: 'Blog API config not provided' },
|
|
89
|
+
{ status: 500 }
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const { slug } = await context.params;
|
|
94
|
+
return PUT_BY_SLUG(req, slug, config);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* DELETE handler - Delete blog by slug
|
|
99
|
+
* Usage: DELETE /api/blogs/[slug]
|
|
100
|
+
*/
|
|
101
|
+
export async function DELETE(
|
|
102
|
+
req: NextRequest,
|
|
103
|
+
context: { params: Promise<{ slug: string }> },
|
|
104
|
+
config?: BlogApiConfig
|
|
105
|
+
): Promise<NextResponse> {
|
|
106
|
+
if (!config) {
|
|
107
|
+
return NextResponse.json(
|
|
108
|
+
{ error: 'Blog API config not provided' },
|
|
109
|
+
{ status: 500 }
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const { slug } = await context.params;
|
|
114
|
+
return DELETE_BY_SLUG(req, slug, config);
|
|
115
|
+
}
|
|
116
|
+
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
'use server';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Plugin Blog API Router
|
|
5
|
+
* Centralized API handler for all blog plugin routes
|
|
6
|
+
*
|
|
7
|
+
* This router handles requests to /api/plugin-blog/*
|
|
8
|
+
* and routes them to the appropriate handler
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
12
|
+
import { GET as BlogListHandler, POST as BlogCreateHandler } from './handler';
|
|
13
|
+
import { GET as BlogGetHandler, PUT as BlogUpdateHandler, DELETE as BlogDeleteHandler, createBlogApiConfig } from './route';
|
|
14
|
+
|
|
15
|
+
export interface BlogApiRouterConfig {
|
|
16
|
+
/** MongoDB client promise - should return { db: () => Database } */
|
|
17
|
+
getDb: () => Promise<{ db: () => any }>;
|
|
18
|
+
/** Function to get authenticated user ID from request */
|
|
19
|
+
getUserId: (req: NextRequest) => Promise<string | null>;
|
|
20
|
+
/** Collection name (default: 'blogs') */
|
|
21
|
+
collectionName?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Handle blog API requests
|
|
26
|
+
* Routes requests to appropriate handlers based on path
|
|
27
|
+
* Similar to plugin-dep, accepts config directly instead of requiring initialization
|
|
28
|
+
*/
|
|
29
|
+
export async function handleBlogApi(
|
|
30
|
+
req: NextRequest,
|
|
31
|
+
path: string[],
|
|
32
|
+
config: BlogApiRouterConfig
|
|
33
|
+
): Promise<NextResponse> {
|
|
34
|
+
// Create the blog API config from the router config
|
|
35
|
+
const blogApiConfig = createBlogApiConfig({
|
|
36
|
+
getDb: config.getDb,
|
|
37
|
+
getUserId: config.getUserId,
|
|
38
|
+
collectionName: config.collectionName || 'blogs',
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const method = req.method;
|
|
42
|
+
// Handle empty path array - means we're at /api/plugin-blog
|
|
43
|
+
// Ensure path is always an array
|
|
44
|
+
const safePath = Array.isArray(path) ? path : [];
|
|
45
|
+
const route = safePath.length > 0 ? safePath[0] : '';
|
|
46
|
+
|
|
47
|
+
console.log(`[BlogApiRouter] method=${method}, path=${JSON.stringify(safePath)}, route=${route}, url=${req.url}`);
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
// Route: /api/plugin-blog (list/create) - empty path or 'list'
|
|
51
|
+
// This handles both /api/plugin-blog and /api/plugin-blog?limit=3
|
|
52
|
+
if (!route || route === 'list') {
|
|
53
|
+
if (method === 'GET') {
|
|
54
|
+
console.log('[BlogApiRouter] Routing to BlogListHandler');
|
|
55
|
+
return await BlogListHandler(req, blogApiConfig);
|
|
56
|
+
}
|
|
57
|
+
if (method === 'POST') {
|
|
58
|
+
return await BlogCreateHandler(req, blogApiConfig);
|
|
59
|
+
}
|
|
60
|
+
// Method not allowed for root route
|
|
61
|
+
return NextResponse.json(
|
|
62
|
+
{ error: `Method ${method} not allowed for route: /` },
|
|
63
|
+
{ status: 405 }
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
// Route: /api/plugin-blog/new (create new)
|
|
67
|
+
else if (route === 'new') {
|
|
68
|
+
if (method === 'POST') {
|
|
69
|
+
return await BlogCreateHandler(req, blogApiConfig);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Route: /api/plugin-blog/categories (get categories)
|
|
73
|
+
else if (route === 'categories') {
|
|
74
|
+
if (method === 'GET') {
|
|
75
|
+
// Import categories handler
|
|
76
|
+
const categoriesModule = await import('./categories');
|
|
77
|
+
return await categoriesModule.GET(req, blogApiConfig);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Route: /api/plugin-blog/check-title (check title duplicate)
|
|
81
|
+
else if (route === 'check-title') {
|
|
82
|
+
if (method === 'GET') {
|
|
83
|
+
const checkTitleModule = await import('./check-title');
|
|
84
|
+
return await checkTitleModule.GET(req, blogApiConfig);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Route: /api/plugin-blog/[slug] (get/update/delete by slug)
|
|
88
|
+
else {
|
|
89
|
+
const slug = route;
|
|
90
|
+
if (method === 'GET') {
|
|
91
|
+
return await BlogGetHandler(req, { params: Promise.resolve({ slug }) }, blogApiConfig);
|
|
92
|
+
}
|
|
93
|
+
if (method === 'PUT') {
|
|
94
|
+
return await BlogUpdateHandler(req, { params: Promise.resolve({ slug }) }, blogApiConfig);
|
|
95
|
+
}
|
|
96
|
+
if (method === 'DELETE') {
|
|
97
|
+
return await BlogDeleteHandler(req, { params: Promise.resolve({ slug }) }, blogApiConfig);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Method not allowed
|
|
102
|
+
return NextResponse.json(
|
|
103
|
+
{ error: `Method ${method} not allowed for route: ${route || '/'}` },
|
|
104
|
+
{ status: 405 }
|
|
105
|
+
);
|
|
106
|
+
} catch (error: any) {
|
|
107
|
+
console.error('[BlogApiRouter] Error:', error);
|
|
108
|
+
return NextResponse.json(
|
|
109
|
+
{ error: error.message || 'Internal server error' },
|
|
110
|
+
{ status: 500 }
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin Blog - Server-Only API Exports
|
|
3
|
+
* This file is only imported in server-side code (API routes)
|
|
4
|
+
*
|
|
5
|
+
* IMPORTANT: This file uses Node.js modules (fs, path, etc.) and should NEVER
|
|
6
|
+
* be imported in client-side code. Only use in server-side API routes.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Re-export everything from the API index
|
|
10
|
+
export * from './api';
|
|
11
|
+
|