@jhits/plugin-blog 0.0.18 → 0.0.20
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/dist/api/categories.d.ts.map +1 -1
- package/dist/api/categories.js +42 -38
- package/dist/api/handler.d.ts +1 -26
- package/dist/api/handler.d.ts.map +1 -1
- package/dist/api/handler.js +81 -500
- package/dist/api/router.d.ts +0 -5
- package/dist/api/router.d.ts.map +1 -1
- package/dist/api/router.js +8 -35
- package/dist/api/service.d.ts +80 -0
- package/dist/api/service.d.ts.map +1 -0
- package/dist/api/service.js +219 -0
- package/dist/hooks/useAutoSave.d.ts +10 -0
- package/dist/hooks/useAutoSave.d.ts.map +1 -0
- package/dist/hooks/useAutoSave.js +57 -0
- package/dist/hooks/useCategories.d.ts +1 -1
- package/dist/hooks/useCategories.d.ts.map +1 -1
- package/dist/hooks/useCategories.js +15 -46
- package/dist/index.d.ts +24 -31
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +44 -201
- package/dist/init.d.ts +20 -7
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +8 -7
- package/dist/lib/blocks/BlockRenderer.d.ts.map +1 -1
- package/dist/lib/layouts/blocks/ColumnsBlock.d.ts.map +1 -1
- package/dist/lib/layouts/blocks/ColumnsBlock.js +30 -113
- package/dist/lib/layouts/blocks/SectionBlock.d.ts.map +1 -1
- package/dist/lib/layouts/blocks/SectionBlock.js +9 -21
- package/dist/lib/layouts/index.d.ts +3 -3
- package/dist/lib/layouts/index.js +4 -4
- package/dist/lib/mappers/apiMapper.d.ts +10 -0
- package/dist/lib/mappers/apiMapper.d.ts.map +1 -1
- package/dist/lib/mappers/apiMapper.js +47 -32
- package/dist/lib/rich-text/RichTextEditor.d.ts +4 -2
- package/dist/lib/rich-text/RichTextEditor.d.ts.map +1 -1
- package/dist/lib/rich-text/RichTextEditor.js +12 -9
- package/dist/lib/utils/config-resolver.d.ts +28 -0
- package/dist/lib/utils/config-resolver.d.ts.map +1 -0
- package/dist/lib/utils/config-resolver.js +46 -0
- package/dist/lib/utils/tree.d.ts +29 -0
- package/dist/lib/utils/tree.d.ts.map +1 -0
- package/dist/lib/utils/tree.js +129 -0
- package/dist/state/EditorContext.d.ts +3 -25
- package/dist/state/EditorContext.d.ts.map +1 -1
- package/dist/state/EditorContext.js +124 -174
- package/dist/state/reducer.d.ts +1 -5
- package/dist/state/reducer.d.ts.map +1 -1
- package/dist/state/reducer.js +128 -521
- package/dist/state/types.d.ts +12 -1
- package/dist/state/types.d.ts.map +1 -1
- package/dist/types/block.d.ts +9 -0
- package/dist/types/block.d.ts.map +1 -1
- package/dist/types/post.d.ts +17 -1
- package/dist/types/post.d.ts.map +1 -1
- package/dist/views/CanvasEditor/BlockWrapper.d.ts +5 -6
- package/dist/views/CanvasEditor/BlockWrapper.d.ts.map +1 -1
- package/dist/views/CanvasEditor/BlockWrapper.js +56 -264
- package/dist/views/CanvasEditor/CanvasEditorView.d.ts +5 -3
- package/dist/views/CanvasEditor/CanvasEditorView.d.ts.map +1 -1
- package/dist/views/CanvasEditor/CanvasEditorView.js +55 -315
- package/dist/views/CanvasEditor/EditorBody.d.ts +6 -8
- package/dist/views/CanvasEditor/EditorBody.d.ts.map +1 -1
- package/dist/views/CanvasEditor/EditorBody.js +34 -482
- package/dist/views/CanvasEditor/EditorHeader.d.ts.map +1 -1
- package/dist/views/CanvasEditor/EditorHeader.js +27 -63
- package/dist/views/CanvasEditor/LayoutContainer.d.ts.map +1 -1
- package/dist/views/CanvasEditor/LayoutContainer.js +49 -70
- package/dist/views/CanvasEditor/components/CustomBlockItem.js +1 -1
- package/dist/views/CanvasEditor/components/EditorCanvas.d.ts +15 -3
- package/dist/views/CanvasEditor/components/EditorCanvas.d.ts.map +1 -1
- package/dist/views/CanvasEditor/components/EditorCanvas.js +40 -18
- package/dist/views/CanvasEditor/components/EditorLibrary.d.ts +5 -1
- package/dist/views/CanvasEditor/components/EditorLibrary.d.ts.map +1 -1
- package/dist/views/CanvasEditor/components/EditorLibrary.js +11 -7
- package/dist/views/CanvasEditor/components/EditorSidebar.d.ts.map +1 -1
- package/dist/views/CanvasEditor/components/EditorSidebar.js +32 -14
- package/dist/views/CanvasEditor/components/FeaturedMediaSection.d.ts +0 -6
- package/dist/views/CanvasEditor/components/FeaturedMediaSection.d.ts.map +1 -1
- package/dist/views/CanvasEditor/components/FeaturedMediaSection.js +17 -128
- package/dist/views/CanvasEditor/components/JSONInspector.d.ts +9 -0
- package/dist/views/CanvasEditor/components/JSONInspector.d.ts.map +1 -0
- package/dist/views/CanvasEditor/components/JSONInspector.js +56 -0
- package/dist/views/CanvasEditor/components/LibraryItem.js +2 -2
- package/dist/views/CanvasEditor/components/PrivacySettingsSection.d.ts +0 -4
- package/dist/views/CanvasEditor/components/PrivacySettingsSection.d.ts.map +1 -1
- package/dist/views/CanvasEditor/components/PrivacySettingsSection.js +6 -28
- package/dist/views/CanvasEditor/components/index.d.ts +2 -0
- package/dist/views/CanvasEditor/components/index.d.ts.map +1 -1
- package/dist/views/CanvasEditor/components/index.js +1 -0
- package/dist/views/CanvasEditor/hooks/useHeroBlock.d.ts.map +1 -1
- package/dist/views/CanvasEditor/hooks/useHeroBlock.js +15 -18
- package/dist/views/CanvasEditor/hooks/usePostLoader.d.ts +3 -0
- package/dist/views/CanvasEditor/hooks/usePostLoader.d.ts.map +1 -1
- package/dist/views/CanvasEditor/hooks/usePostLoader.js +12 -13
- package/dist/views/CanvasEditor/hooks/useUnsavedChanges.js +0 -4
- package/dist/views/PostManager/EmptyState.d.ts +1 -1
- package/dist/views/PostManager/EmptyState.js +4 -4
- package/dist/views/PostManager/FilterDropdown.d.ts +21 -0
- package/dist/views/PostManager/FilterDropdown.d.ts.map +1 -0
- package/dist/views/PostManager/FilterDropdown.js +28 -0
- package/dist/views/PostManager/LanguageFlags.d.ts.map +1 -1
- package/dist/views/PostManager/LanguageFlags.js +4 -1
- package/dist/views/PostManager/PostCards.d.ts.map +1 -1
- package/dist/views/PostManager/PostCards.js +23 -40
- package/dist/views/PostManager/PostFilters.d.ts.map +1 -1
- package/dist/views/PostManager/PostFilters.js +34 -3
- package/dist/views/PostManager/PostManagerView.d.ts +1 -2
- package/dist/views/PostManager/PostManagerView.d.ts.map +1 -1
- package/dist/views/PostManager/PostManagerView.js +30 -96
- package/dist/views/PostManager/PostStats.d.ts.map +1 -1
- package/dist/views/PostManager/PostStats.js +10 -10
- package/dist/views/PostManager/PostTable.d.ts.map +1 -1
- package/dist/views/PostManager/PostTable.js +23 -40
- package/dist/views/Settings/SettingsView.d.ts +1 -1
- package/dist/views/Settings/SettingsView.d.ts.map +1 -1
- package/dist/views/Settings/SettingsView.js +12 -39
- package/dist/views/SlugSEO/SlugSEOManagerView.d.ts.map +1 -1
- package/dist/views/SlugSEO/SlugSEOManagerView.js +2 -2
- package/package.json +42 -6
- package/src/api/categories.ts +48 -52
- package/src/api/handler.ts +87 -604
- package/src/api/router.ts +15 -65
- package/src/api/service.ts +241 -0
- package/src/hooks/useAutoSave.ts +64 -0
- package/src/hooks/useCategories.ts +19 -47
- package/src/index.tsx +79 -293
- package/src/init.tsx +24 -11
- package/src/lib/blocks/BlockRenderer.tsx +1 -0
- package/src/lib/layouts/blocks/ColumnsBlock.tsx +60 -173
- package/src/lib/layouts/blocks/SectionBlock.tsx +22 -26
- package/src/lib/layouts/index.ts +4 -4
- package/src/lib/mappers/apiMapper.ts +63 -32
- package/src/lib/rich-text/RichTextEditor.tsx +16 -9
- package/src/lib/utils/config-resolver.ts +64 -0
- package/src/lib/utils/tree.ts +150 -0
- package/src/state/EditorContext.tsx +153 -232
- package/src/state/reducer.ts +141 -606
- package/src/state/types.ts +14 -1
- package/src/types/block.ts +10 -0
- package/src/types/post.ts +19 -1
- package/src/views/CanvasEditor/BlockWrapper.tsx +130 -460
- package/src/views/CanvasEditor/CanvasEditorView.tsx +145 -420
- package/src/views/CanvasEditor/EditorBody.tsx +98 -610
- package/src/views/CanvasEditor/EditorHeader.tsx +176 -196
- package/src/views/CanvasEditor/LayoutContainer.tsx +74 -89
- package/src/views/CanvasEditor/components/CustomBlockItem.tsx +7 -8
- package/src/views/CanvasEditor/components/EditorCanvas.tsx +139 -84
- package/src/views/CanvasEditor/components/EditorLibrary.tsx +25 -10
- package/src/views/CanvasEditor/components/EditorSidebar.tsx +196 -127
- package/src/views/CanvasEditor/components/FeaturedMediaSection.tsx +78 -210
- package/src/views/CanvasEditor/components/JSONInspector.tsx +125 -0
- package/src/views/CanvasEditor/components/LibraryItem.tsx +5 -6
- package/src/views/CanvasEditor/components/PrivacySettingsSection.tsx +73 -124
- package/src/views/CanvasEditor/components/index.ts +2 -1
- package/src/views/CanvasEditor/hooks/useHeroBlock.ts +15 -18
- package/src/views/CanvasEditor/hooks/usePostLoader.ts +21 -13
- package/src/views/CanvasEditor/hooks/useUnsavedChanges.ts +4 -4
- package/src/views/PostManager/EmptyState.tsx +9 -10
- package/src/views/PostManager/FilterDropdown.tsx +95 -0
- package/src/views/PostManager/LanguageFlags.tsx +6 -2
- package/src/views/PostManager/PostCards.tsx +127 -133
- package/src/views/PostManager/PostFilters.tsx +73 -68
- package/src/views/PostManager/PostManagerView.tsx +132 -179
- package/src/views/PostManager/PostStats.tsx +21 -20
- package/src/views/PostManager/PostTable.tsx +137 -165
- package/src/views/Settings/SettingsView.tsx +64 -180
- package/src/views/SlugSEO/SlugSEOManagerView.tsx +59 -44
- package/src/hooks/index.d.ts +0 -8
- package/src/hooks/index.d.ts.map +0 -1
- package/src/hooks/useBlog.d.ts +0 -31
- package/src/hooks/useBlog.d.ts.map +0 -1
- package/src/hooks/useBlogs.d.ts +0 -39
- package/src/hooks/useBlogs.d.ts.map +0 -1
- package/src/hooks/useCategories.d.ts +0 -9
- package/src/hooks/useCategories.d.ts.map +0 -1
- package/src/lib/blocks/BlockRenderer.d.ts +0 -54
- package/src/lib/blocks/BlockRenderer.d.ts.map +0 -1
- package/src/lib/config-storage.d.ts +0 -30
- package/src/lib/config-storage.d.ts.map +0 -1
- package/src/lib/layouts/blocks/ColumnsBlock.d.ts +0 -25
- package/src/lib/layouts/blocks/ColumnsBlock.d.ts.map +0 -1
- package/src/lib/layouts/blocks/SectionBlock.d.ts +0 -25
- package/src/lib/layouts/blocks/SectionBlock.d.ts.map +0 -1
- package/src/lib/layouts/index.d.ts +0 -23
- package/src/lib/layouts/index.d.ts.map +0 -1
- package/src/lib/layouts/registerLayoutBlocks.d.ts +0 -9
- package/src/lib/layouts/registerLayoutBlocks.d.ts.map +0 -1
- package/src/lib/mappers/apiMapper.d.ts +0 -66
- package/src/lib/mappers/apiMapper.d.ts.map +0 -1
- package/src/lib/rich-text/RichTextEditor.d.ts +0 -45
- package/src/lib/rich-text/RichTextEditor.d.ts.map +0 -1
- package/src/lib/rich-text/RichTextPreview.d.ts +0 -16
- package/src/lib/rich-text/RichTextPreview.d.ts.map +0 -1
- package/src/lib/rich-text/index.d.ts +0 -9
- package/src/lib/rich-text/index.d.ts.map +0 -1
- package/src/lib/utils/blockHelpers.d.ts +0 -23
- package/src/lib/utils/blockHelpers.d.ts.map +0 -1
- package/src/lib/utils/configValidation.d.ts +0 -23
- package/src/lib/utils/configValidation.d.ts.map +0 -1
- package/src/registry/BlockRegistry.d.ts +0 -62
- package/src/registry/BlockRegistry.d.ts.map +0 -1
- package/src/registry/index.d.ts +0 -6
- package/src/registry/index.d.ts.map +0 -1
- package/src/state/EditorContext.d.ts +0 -45
- package/src/state/EditorContext.d.ts.map +0 -1
- package/src/state/index.d.ts +0 -7
- package/src/state/index.d.ts.map +0 -1
- package/src/state/reducer.d.ts +0 -11
- package/src/state/reducer.d.ts.map +0 -1
- package/src/state/types.d.ts +0 -162
- package/src/state/types.d.ts.map +0 -1
- package/src/types/block.d.ts +0 -221
- package/src/types/block.d.ts.map +0 -1
- package/src/types/index.d.ts +0 -8
- package/src/types/index.d.ts.map +0 -1
- package/src/types/post.d.ts +0 -136
- package/src/types/post.d.ts.map +0 -1
- package/src/utils/client.d.ts +0 -48
- package/src/utils/client.d.ts.map +0 -1
- package/src/views/CanvasEditor/BlockWrapper.d.ts +0 -16
- package/src/views/CanvasEditor/BlockWrapper.d.ts.map +0 -1
- package/src/views/CanvasEditor/CanvasEditorView.d.ts +0 -14
- package/src/views/CanvasEditor/CanvasEditorView.d.ts.map +0 -1
- package/src/views/CanvasEditor/EditorBody.d.ts +0 -22
- package/src/views/CanvasEditor/EditorBody.d.ts.map +0 -1
- package/src/views/CanvasEditor/EditorHeader.d.ts +0 -18
- package/src/views/CanvasEditor/EditorHeader.d.ts.map +0 -1
- package/src/views/CanvasEditor/LayoutContainer.d.ts +0 -17
- package/src/views/CanvasEditor/LayoutContainer.d.ts.map +0 -1
- package/src/views/CanvasEditor/SaveConfirmationModal.d.ts +0 -13
- package/src/views/CanvasEditor/SaveConfirmationModal.d.ts.map +0 -1
- package/src/views/CanvasEditor/components/CustomBlockItem.d.ts +0 -14
- package/src/views/CanvasEditor/components/CustomBlockItem.d.ts.map +0 -1
- package/src/views/CanvasEditor/components/EditorCanvas.d.ts +0 -29
- package/src/views/CanvasEditor/components/EditorCanvas.d.ts.map +0 -1
- package/src/views/CanvasEditor/components/EditorLibrary.d.ts +0 -7
- package/src/views/CanvasEditor/components/EditorLibrary.d.ts.map +0 -1
- package/src/views/CanvasEditor/components/EditorSidebar.d.ts +0 -13
- package/src/views/CanvasEditor/components/EditorSidebar.d.ts.map +0 -1
- package/src/views/CanvasEditor/components/ErrorBanner.d.ts +0 -6
- package/src/views/CanvasEditor/components/ErrorBanner.d.ts.map +0 -1
- package/src/views/CanvasEditor/components/FeaturedMediaSection.d.ts +0 -25
- package/src/views/CanvasEditor/components/FeaturedMediaSection.d.ts.map +0 -1
- package/src/views/CanvasEditor/components/LibraryItem.d.ts +0 -14
- package/src/views/CanvasEditor/components/LibraryItem.d.ts.map +0 -1
- package/src/views/CanvasEditor/components/PrivacySettingsSection.d.ts +0 -15
- package/src/views/CanvasEditor/components/PrivacySettingsSection.d.ts.map +0 -1
- package/src/views/CanvasEditor/components/index.d.ts +0 -21
- package/src/views/CanvasEditor/components/index.d.ts.map +0 -1
- package/src/views/CanvasEditor/hooks/index.d.ts +0 -10
- package/src/views/CanvasEditor/hooks/index.d.ts.map +0 -1
- package/src/views/CanvasEditor/hooks/useHeroBlock.d.ts +0 -8
- package/src/views/CanvasEditor/hooks/useHeroBlock.d.ts.map +0 -1
- package/src/views/CanvasEditor/hooks/useKeyboardShortcuts.d.ts +0 -3
- package/src/views/CanvasEditor/hooks/useKeyboardShortcuts.d.ts.map +0 -1
- package/src/views/CanvasEditor/hooks/usePostLoader.d.ts +0 -5
- package/src/views/CanvasEditor/hooks/usePostLoader.d.ts.map +0 -1
- package/src/views/CanvasEditor/hooks/useRegisteredBlocks.d.ts +0 -2
- package/src/views/CanvasEditor/hooks/useRegisteredBlocks.d.ts.map +0 -1
- package/src/views/CanvasEditor/hooks/useUnsavedChanges.d.ts +0 -25
- package/src/views/CanvasEditor/hooks/useUnsavedChanges.d.ts.map +0 -1
- package/src/views/CanvasEditor/index.d.ts +0 -16
- package/src/views/CanvasEditor/index.d.ts.map +0 -1
- package/src/views/PostManager/EmptyState.d.ts +0 -10
- package/src/views/PostManager/EmptyState.d.ts.map +0 -1
- package/src/views/PostManager/PostActionsMenu.d.ts +0 -12
- package/src/views/PostManager/PostActionsMenu.d.ts.map +0 -1
- package/src/views/PostManager/PostCards.d.ts +0 -15
- package/src/views/PostManager/PostCards.d.ts.map +0 -1
- package/src/views/PostManager/PostFilters.d.ts +0 -16
- package/src/views/PostManager/PostFilters.d.ts.map +0 -1
- package/src/views/PostManager/PostManagerView.d.ts +0 -11
- package/src/views/PostManager/PostManagerView.d.ts.map +0 -1
- package/src/views/PostManager/PostStats.d.ts +0 -11
- package/src/views/PostManager/PostStats.d.ts.map +0 -1
- package/src/views/PostManager/PostTable.d.ts +0 -15
- package/src/views/PostManager/PostTable.d.ts.map +0 -1
- package/src/views/PostManager/index.d.ts +0 -12
- package/src/views/PostManager/index.d.ts.map +0 -1
- package/src/views/Preview/PreviewBridgeView.d.ts +0 -12
- package/src/views/Preview/PreviewBridgeView.d.ts.map +0 -1
- package/src/views/Preview/index.d.ts +0 -6
- package/src/views/Preview/index.d.ts.map +0 -1
- package/src/views/Settings/SettingsView.d.ts +0 -10
- package/src/views/Settings/SettingsView.d.ts.map +0 -1
- package/src/views/Settings/index.d.ts +0 -6
- package/src/views/Settings/index.d.ts.map +0 -1
- package/src/views/SlugSEO/SlugSEOManagerView.d.ts +0 -12
- package/src/views/SlugSEO/SlugSEOManagerView.d.ts.map +0 -1
- package/src/views/SlugSEO/index.d.ts +0 -6
- package/src/views/SlugSEO/index.d.ts.map +0 -1
package/src/api/handler.ts
CHANGED
|
@@ -1,673 +1,156 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Blog API Handler
|
|
3
|
-
* RESTful API handler
|
|
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.
|
|
3
|
+
* Simplified RESTful API handler using BlogService
|
|
8
4
|
*/
|
|
9
5
|
|
|
10
6
|
import { NextRequest, NextResponse } from 'next/server';
|
|
7
|
+
import { BlogService } from './service';
|
|
11
8
|
import { slugify } from '../lib/utils/slugify';
|
|
12
9
|
|
|
13
10
|
export interface BlogApiConfig {
|
|
14
|
-
/** MongoDB client promise (from clientPromise) - should return { db: () => Database } */
|
|
15
11
|
getDb: () => Promise<{ db: () => any }>;
|
|
16
|
-
/** Function to get authenticated user ID from request */
|
|
17
12
|
getUserId: (req: NextRequest) => Promise<string | null>;
|
|
18
|
-
/** Collection name (default: 'blogs') */
|
|
19
13
|
collectionName?: string;
|
|
20
14
|
}
|
|
21
15
|
|
|
22
16
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* GET /api/blogs?status=published - Filter by status
|
|
26
|
-
* GET /api/blogs?language=en - Filter by language (falls back to nl if not found)
|
|
17
|
+
* Generic helper to find a blog post by slug across all languages
|
|
18
|
+
* Avoids hardcoding language codes
|
|
27
19
|
*/
|
|
20
|
+
async function findPostBySlug(collection: any, slug: string) {
|
|
21
|
+
// 1. Try root slug (fastest, indexed)
|
|
22
|
+
const rootMatch = await collection.findOne({ slug });
|
|
23
|
+
if (rootMatch) return rootMatch;
|
|
24
|
+
|
|
25
|
+
// 2. Generic search across all localized slugs using aggregation
|
|
26
|
+
// This works for ANY language key (nl, en, sv, de, fr, es, etc.)
|
|
27
|
+
const results = await collection.aggregate([
|
|
28
|
+
// Convert the 'languages' object into an array of { k, v } entries
|
|
29
|
+
{ $addFields: { _langEntries: { $objectToArray: { $ifNull: ["$languages", {}] } } } },
|
|
30
|
+
// Match if any entry's slug matches our target
|
|
31
|
+
{ $match: { "_langEntries.v.metadata.slug": slug } },
|
|
32
|
+
// Remove the temporary field
|
|
33
|
+
{ $project: { _langEntries: 0 } },
|
|
34
|
+
{ $limit: 1 }
|
|
35
|
+
]).toArray();
|
|
36
|
+
|
|
37
|
+
return results[0] || null;
|
|
38
|
+
}
|
|
39
|
+
|
|
28
40
|
export async function GET(req: NextRequest, config: BlogApiConfig): Promise<NextResponse> {
|
|
29
41
|
try {
|
|
30
42
|
const url = new URL(req.url);
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const db = dbConnection.db();
|
|
40
|
-
const blogs = db.collection(config.collectionName || 'blogs');
|
|
41
|
-
|
|
42
|
-
// Build query
|
|
43
|
-
let query: any = {};
|
|
44
|
-
|
|
45
|
-
if (isAdminView && userId) {
|
|
46
|
-
// Admin view: show all posts owned by user
|
|
47
|
-
if (statusFilter) {
|
|
48
|
-
query = {
|
|
49
|
-
'publicationData.status': statusFilter,
|
|
50
|
-
authorId: userId,
|
|
51
|
-
};
|
|
52
|
-
} else {
|
|
53
|
-
query = { authorId: userId };
|
|
54
|
-
}
|
|
55
|
-
} else {
|
|
56
|
-
// Public view: only published posts in the SPECIFIC language
|
|
57
|
-
// This ensures we only fetch blogs that actually have content for this language
|
|
58
|
-
query = {
|
|
59
|
-
[`languages.${requestedLanguage}.status`]: 'published',
|
|
60
|
-
'publicationData.date': { $lte: new Date() },
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
if (statusFilter && statusFilter !== 'published') {
|
|
64
|
-
// Non-admin can't filter by non-published status
|
|
65
|
-
return NextResponse.json({ error: 'Invalid status filter' }, { status: 400 });
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const [data, totalCount] = await Promise.all([
|
|
70
|
-
blogs
|
|
71
|
-
.find(query)
|
|
72
|
-
.sort({ 'publicationData.date': -1 })
|
|
73
|
-
.skip(skip)
|
|
74
|
-
.limit(isAdminView ? 0 : limit)
|
|
75
|
-
.toArray(),
|
|
76
|
-
blogs.countDocuments(query),
|
|
77
|
-
]);
|
|
78
|
-
|
|
79
|
-
// Supported languages for fallback (in order of preference)
|
|
80
|
-
const fallbackLanguages = [requestedLanguage, 'nl', 'en'];
|
|
81
|
-
|
|
82
|
-
const formatted = data.map((doc: any) => {
|
|
83
|
-
const languages = doc.languages || {};
|
|
84
|
-
|
|
85
|
-
// Only use exact language match public views - no fallback for
|
|
86
|
-
// This ensures visitors see content in their language only
|
|
87
|
-
const hasExactLanguage = !!languages[requestedLanguage];
|
|
88
|
-
|
|
89
|
-
// Skip this post if no exact language match (for public non-admin views)
|
|
90
|
-
if (!isAdminView && !hasExactLanguage) {
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const postPrimaryLang = doc.metadata?.lang || 'nl';
|
|
95
|
-
|
|
96
|
-
// Find the best available language for this post
|
|
97
|
-
let bestLanguage = requestedLanguage;
|
|
98
|
-
let isMissingTranslation = false;
|
|
99
|
-
|
|
100
|
-
if (!languages[requestedLanguage]) {
|
|
101
|
-
// For admin view, if specific language is requested, show it as missing instead of falling back
|
|
102
|
-
// This ensures the UI is "synced" with the selected language
|
|
103
|
-
if (isAdminView) {
|
|
104
|
-
isMissingTranslation = true;
|
|
105
|
-
} else {
|
|
106
|
-
// Public view still uses fallback or filtering
|
|
107
|
-
const fallbackLanguages = [postPrimaryLang, 'nl', 'en'];
|
|
108
|
-
for (const lang of fallbackLanguages) {
|
|
109
|
-
if (languages[lang]) {
|
|
110
|
-
bestLanguage = lang;
|
|
111
|
-
break;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const langContent = languages[bestLanguage] || {};
|
|
118
|
-
const meta = langContent.metadata || {};
|
|
119
|
-
|
|
120
|
-
// Ensure all languages in doc have a status and updatedAt for the dashboard
|
|
121
|
-
const enrichedLanguages = { ...languages };
|
|
122
|
-
Object.keys(enrichedLanguages).forEach(lang => {
|
|
123
|
-
if (!enrichedLanguages[lang].status) {
|
|
124
|
-
enrichedLanguages[lang].status = doc.publicationData?.status === 'concept' ? 'draft' : (doc.publicationData?.status || 'draft');
|
|
125
|
-
}
|
|
126
|
-
if (!enrichedLanguages[lang].updatedAt) {
|
|
127
|
-
enrichedLanguages[lang].updatedAt = doc.updatedAt;
|
|
128
|
-
}
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
// Language display names for placeholders
|
|
132
|
-
const langNames: Record<string, string> = {
|
|
133
|
-
nl: 'Dutch',
|
|
134
|
-
en: 'English',
|
|
135
|
-
sv: 'Swedish',
|
|
136
|
-
de: 'German',
|
|
137
|
-
fr: 'French',
|
|
138
|
-
es: 'Spanish'
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
const displayTitle = isMissingTranslation
|
|
142
|
-
? `(No ${langNames[requestedLanguage] || requestedLanguage.toUpperCase()} translation)`
|
|
143
|
-
: (meta.title || doc.title || '');
|
|
144
|
-
|
|
145
|
-
const displayStatus = isMissingTranslation
|
|
146
|
-
? 'not-translated'
|
|
147
|
-
: (langContent.status || doc.publicationData?.status || 'concept');
|
|
148
|
-
|
|
149
|
-
return {
|
|
150
|
-
...doc,
|
|
151
|
-
_id: doc._id.toString(),
|
|
152
|
-
title: displayTitle,
|
|
153
|
-
summary: isMissingTranslation ? '' : (meta.excerpt || doc.summary || ''),
|
|
154
|
-
contentBlocks: isMissingTranslation ? [] : (langContent.blocks || doc.contentBlocks || doc.blocks || []),
|
|
155
|
-
image: isMissingTranslation ? null : (meta.featuredImage || doc.image),
|
|
156
|
-
categoryTags: isMissingTranslation ? { category: '', tags: [] } : (meta.categories ? {
|
|
157
|
-
category: meta.categories[0] || '',
|
|
158
|
-
tags: meta.tags || doc.categoryTags?.tags || []
|
|
159
|
-
} : (doc.categoryTags || { category: '', tags: [] })),
|
|
160
|
-
publicationData: {
|
|
161
|
-
...doc.publicationData,
|
|
162
|
-
status: displayStatus,
|
|
163
|
-
},
|
|
164
|
-
seo: isMissingTranslation ? {} : (meta.seo || doc.seo || {}),
|
|
165
|
-
lang: bestLanguage,
|
|
166
|
-
isMissingTranslation,
|
|
167
|
-
requestedLanguage,
|
|
168
|
-
availableLanguages: Object.keys(languages),
|
|
169
|
-
languages: enrichedLanguages,
|
|
170
|
-
updatedAt: isMissingTranslation ? doc.updatedAt : (langContent.updatedAt || doc.updatedAt),
|
|
171
|
-
status: displayStatus,
|
|
172
|
-
};
|
|
173
|
-
}).filter(Boolean); // Remove null entries
|
|
174
|
-
|
|
175
|
-
// Sort by updatedAt descending so the UI order makes sense
|
|
176
|
-
formatted.sort((a: any, b: any) => {
|
|
177
|
-
const dateA = new Date(a.updatedAt).getTime();
|
|
178
|
-
const dateB = new Date(b.updatedAt).getTime();
|
|
179
|
-
return dateB - dateA;
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
return NextResponse.json({
|
|
183
|
-
blogs: formatted,
|
|
184
|
-
total: formatted.length,
|
|
43
|
+
const service = new BlogService(config);
|
|
44
|
+
const results = await service.listBlogs({
|
|
45
|
+
limit: Number(url.searchParams.get('limit') ?? 10),
|
|
46
|
+
skip: Number(url.searchParams.get('skip') ?? 0),
|
|
47
|
+
status: url.searchParams.get('status') || undefined,
|
|
48
|
+
isAdmin: url.searchParams.get('admin') === 'true',
|
|
49
|
+
userId: await config.getUserId(req),
|
|
50
|
+
requestedLanguage: url.searchParams.get('language') || 'nl',
|
|
185
51
|
});
|
|
52
|
+
return NextResponse.json(results);
|
|
186
53
|
} catch (err: any) {
|
|
187
|
-
|
|
188
|
-
return NextResponse.json(
|
|
189
|
-
{ error: 'Failed to fetch blogs', detail: err.message },
|
|
190
|
-
{ status: 500 }
|
|
191
|
-
);
|
|
54
|
+
return NextResponse.json({ error: 'Fetch failed', detail: err.message }, { status: 500 });
|
|
192
55
|
}
|
|
193
56
|
}
|
|
194
57
|
|
|
195
|
-
/**
|
|
196
|
-
* POST /api/blogs - Create new blog post
|
|
197
|
-
*/
|
|
198
58
|
export async function POST(req: NextRequest, config: BlogApiConfig): Promise<NextResponse> {
|
|
199
59
|
try {
|
|
200
60
|
const url = new URL(req.url);
|
|
201
61
|
const language = url.searchParams.get('language') || 'nl';
|
|
202
|
-
|
|
203
62
|
const userId = await config.getUserId(req);
|
|
204
|
-
if (!userId) {
|
|
205
|
-
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
206
|
-
}
|
|
63
|
+
if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
207
64
|
|
|
208
65
|
const body = await req.json();
|
|
209
|
-
|
|
210
|
-
title,
|
|
211
|
-
summary,
|
|
212
|
-
content,
|
|
213
|
-
contentBlocks,
|
|
214
|
-
image,
|
|
215
|
-
categoryTags,
|
|
216
|
-
publicationData,
|
|
217
|
-
seo,
|
|
218
|
-
} = body;
|
|
219
|
-
|
|
220
|
-
const isPublishing = publicationData?.status === 'published';
|
|
221
|
-
const isConcept = publicationData?.status === 'concept' || publicationData?.status === 'draft';
|
|
222
|
-
|
|
223
|
-
// Validation
|
|
224
|
-
const errors: string[] = [];
|
|
225
|
-
if (!title?.trim()) {
|
|
226
|
-
errors.push('Title is required');
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
if (isPublishing) {
|
|
230
|
-
// Publishing requires all fields
|
|
231
|
-
if (!summary?.trim()) errors.push('Summary is required for publishing');
|
|
232
|
-
if (!image?.id?.trim()) errors.push('Featured image is required for publishing');
|
|
233
|
-
// Only require category if it's explicitly provided and empty
|
|
234
|
-
// If categoryTags is undefined or category is undefined, that's also missing
|
|
235
|
-
if (!categoryTags || !categoryTags.category || !categoryTags.category.trim()) {
|
|
236
|
-
errors.push('Category is required for publishing');
|
|
237
|
-
}
|
|
238
|
-
const hasContent =
|
|
239
|
-
(contentBlocks && Array.isArray(contentBlocks) && contentBlocks.length > 0) ||
|
|
240
|
-
(content && Array.isArray(content) && content.length > 0);
|
|
241
|
-
if (!hasContent) {
|
|
242
|
-
errors.push('Content is required for publishing');
|
|
243
|
-
}
|
|
244
|
-
if (!publicationData?.date) errors.push('Publication date is required');
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
if (errors.length > 0) {
|
|
248
|
-
return NextResponse.json({ message: errors[0], allErrors: errors }, { status: 400 });
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Create slug
|
|
252
|
-
let baseSlug = slugify(title);
|
|
253
|
-
const slug = isPublishing
|
|
254
|
-
? baseSlug
|
|
255
|
-
: `${baseSlug}-draft-${Date.now().toString().slice(-4)}`;
|
|
256
|
-
|
|
257
|
-
const dbConnection = await config.getDb();
|
|
258
|
-
const db = dbConnection.db();
|
|
259
|
-
const blogs = db.collection(config.collectionName || 'blogs');
|
|
260
|
-
|
|
261
|
-
// Determine the final status: if publishing, set to 'published', otherwise convert draft to concept
|
|
262
|
-
let finalStatus = publicationData?.status;
|
|
263
|
-
if (isPublishing) {
|
|
264
|
-
finalStatus = 'published';
|
|
265
|
-
} else if (publicationData?.status === 'draft') {
|
|
266
|
-
finalStatus = 'concept';
|
|
267
|
-
} else {
|
|
268
|
-
finalStatus = publicationData?.status || 'concept';
|
|
269
|
-
}
|
|
66
|
+
if (!body.title?.trim()) return NextResponse.json({ message: 'Title is required' }, { status: 400 });
|
|
270
67
|
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
category: categoryTags?.category?.trim() || '',
|
|
279
|
-
tags: categoryTags?.tags || [],
|
|
280
|
-
},
|
|
281
|
-
publicationData: {
|
|
282
|
-
...publicationData,
|
|
283
|
-
status: finalStatus,
|
|
284
|
-
date: publicationData?.date ? new Date(publicationData.date) : new Date(),
|
|
285
|
-
},
|
|
286
|
-
seo: seo || { title: '', description: '' },
|
|
68
|
+
const service = new BlogService(config);
|
|
69
|
+
const isPublishing = body.publicationData?.status === 'published';
|
|
70
|
+
const slug = isPublishing ? slugify(body.title) : `${slugify(body.title)}-draft-${Date.now().toString().slice(-4)}`;
|
|
71
|
+
|
|
72
|
+
const updateData = service.prepareUpdateData(body, null, language);
|
|
73
|
+
const finalDoc = {
|
|
74
|
+
...updateData,
|
|
287
75
|
slug,
|
|
288
76
|
authorId: userId,
|
|
289
|
-
|
|
290
|
-
[language]: {
|
|
291
|
-
blocks: contentBlocks || [],
|
|
292
|
-
metadata: {
|
|
293
|
-
title: title.trim(),
|
|
294
|
-
excerpt: (summary || '').trim(),
|
|
295
|
-
featuredImage: image,
|
|
296
|
-
categories: categoryTags?.category ? [categoryTags.category] : [],
|
|
297
|
-
tags: categoryTags?.tags || [],
|
|
298
|
-
seo: seo || {},
|
|
299
|
-
},
|
|
300
|
-
updatedAt: new Date().toISOString(),
|
|
301
|
-
status: finalStatus,
|
|
302
|
-
},
|
|
303
|
-
},
|
|
304
|
-
metadata: {
|
|
305
|
-
lang: language,
|
|
306
|
-
},
|
|
77
|
+
metadata: { lang: language },
|
|
307
78
|
createdAt: new Date(),
|
|
308
|
-
updatedAt: new Date(),
|
|
309
79
|
};
|
|
310
80
|
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
return NextResponse.json({
|
|
314
|
-
message: isPublishing ? 'Blog published successfully' : 'Draft saved successfully',
|
|
315
|
-
blogId: result.insertedId,
|
|
316
|
-
slug,
|
|
317
|
-
});
|
|
81
|
+
const dbConn = await config.getDb();
|
|
82
|
+
const result = await dbConn.db().collection(config.collectionName || 'blogs').insertOne(finalDoc);
|
|
83
|
+
return NextResponse.json({ message: 'Success', blogId: result.insertedId, slug });
|
|
318
84
|
} catch (err: any) {
|
|
319
|
-
|
|
320
|
-
return NextResponse.json(
|
|
321
|
-
{ error: 'Failed to create blog', detail: err.message },
|
|
322
|
-
{ status: 500 }
|
|
323
|
-
);
|
|
85
|
+
return NextResponse.json({ error: 'Create failed', detail: err.message }, { status: 500 });
|
|
324
86
|
}
|
|
325
87
|
}
|
|
326
88
|
|
|
327
|
-
|
|
328
|
-
* GET /api/blogs/[slug] - Get single blog post by slug
|
|
329
|
-
*/
|
|
330
|
-
export async function GET_BY_SLUG(
|
|
331
|
-
req: NextRequest,
|
|
332
|
-
slug: string,
|
|
333
|
-
config: BlogApiConfig
|
|
334
|
-
): Promise<NextResponse> {
|
|
89
|
+
export async function GET_BY_SLUG(req: NextRequest, slug: string, config: BlogApiConfig): Promise<NextResponse> {
|
|
335
90
|
try {
|
|
336
91
|
const url = new URL(req.url);
|
|
337
|
-
const
|
|
92
|
+
const service = new BlogService(config);
|
|
93
|
+
const lang = url.searchParams.get('language') || 'nl';
|
|
338
94
|
|
|
339
|
-
const
|
|
340
|
-
const
|
|
341
|
-
const db = dbConnection.db();
|
|
342
|
-
const blogs = db.collection(config.collectionName || 'blogs');
|
|
343
|
-
|
|
344
|
-
const blog = await blogs.findOne({ slug });
|
|
345
|
-
|
|
346
|
-
if (!blog) {
|
|
347
|
-
return NextResponse.json({ error: 'Blog not found' }, { status: 404 });
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// Security check
|
|
351
|
-
const isAuthor = userId && blog.authorId === userId;
|
|
352
|
-
const isAdminView = url.searchParams.get('admin') === 'true';
|
|
95
|
+
const dbConn = await config.getDb();
|
|
96
|
+
const collection = dbConn.db().collection(config.collectionName || 'blogs');
|
|
353
97
|
|
|
354
|
-
|
|
355
|
-
if (!isAdminView && !isAuthor) {
|
|
356
|
-
const langData = blog.languages?.[requestedLanguage];
|
|
357
|
-
if (!langData || langData.status !== 'published') {
|
|
358
|
-
return NextResponse.json({ error: 'Blog post not available in this language' }, { status: 404 });
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// Also check publication date
|
|
362
|
-
if (blog.publicationData?.date && new Date(blog.publicationData.date) > new Date()) {
|
|
363
|
-
return NextResponse.json({ error: 'Blog post not yet published' }, { status: 404 });
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
const languages = blog.languages || {};
|
|
368
|
-
const postPrimaryLang = blog.metadata?.lang || 'nl';
|
|
98
|
+
const blogDoc = await findPostBySlug(collection, slug);
|
|
369
99
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
let isMissingTranslation = false;
|
|
373
|
-
|
|
374
|
-
if (!languages[requestedLanguage]) {
|
|
375
|
-
if (isAdminView) {
|
|
376
|
-
isMissingTranslation = true;
|
|
377
|
-
} else {
|
|
378
|
-
// Try fallback languages in order
|
|
379
|
-
const fallbackLanguages = [requestedLanguage, 'nl', 'en'];
|
|
380
|
-
for (const lang of fallbackLanguages) {
|
|
381
|
-
if (languages[lang]) {
|
|
382
|
-
bestLanguage = lang;
|
|
383
|
-
break;
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
// If still no match, use primary language from metadata
|
|
387
|
-
if (!languages[bestLanguage] && languages[postPrimaryLang]) {
|
|
388
|
-
bestLanguage = postPrimaryLang;
|
|
389
|
-
}
|
|
390
|
-
}
|
|
100
|
+
if (!blogDoc) {
|
|
101
|
+
return NextResponse.json({ error: 'Blog not found' }, { status: 404 });
|
|
391
102
|
}
|
|
392
|
-
|
|
393
|
-
const langContent = languages[bestLanguage] || {};
|
|
394
|
-
const meta = langContent.metadata || {};
|
|
395
|
-
|
|
396
|
-
// Language display names for placeholders
|
|
397
|
-
const langNames: Record<string, string> = {
|
|
398
|
-
nl: 'Dutch',
|
|
399
|
-
en: 'English',
|
|
400
|
-
sv: 'Swedish',
|
|
401
|
-
de: 'German',
|
|
402
|
-
fr: 'French',
|
|
403
|
-
es: 'Spanish'
|
|
404
|
-
};
|
|
405
|
-
|
|
406
|
-
let title = isMissingTranslation
|
|
407
|
-
? `(No ${langNames[requestedLanguage] || requestedLanguage.toUpperCase()} translation)`
|
|
408
|
-
: (meta.title || blog.title || '');
|
|
409
|
-
let summary = isMissingTranslation ? '' : (meta.excerpt || blog.summary || '');
|
|
410
|
-
let contentBlocks = isMissingTranslation ? [] : (langContent.blocks || blog.contentBlocks || blog.blocks || []);
|
|
411
|
-
let image = isMissingTranslation ? null : (meta.featuredImage || blog.image);
|
|
412
|
-
let categoryTags = isMissingTranslation ? { category: '', tags: [] } : (meta.categories ? {
|
|
413
|
-
category: meta.categories[0] || '',
|
|
414
|
-
tags: meta.tags || blog.categoryTags?.tags || []
|
|
415
|
-
} : (blog.categoryTags || { category: '', tags: [] }));
|
|
416
|
-
let seo = isMissingTranslation ? {} : (meta.seo || blog.seo || {});
|
|
417
|
-
let metadata = { ...blog.metadata, ...meta, lang: bestLanguage };
|
|
418
|
-
|
|
419
|
-
// Ensure all languages in doc have a status and updatedAt for the dashboard
|
|
420
|
-
const enrichedLanguages = { ...languages };
|
|
421
|
-
Object.keys(enrichedLanguages).forEach(lang => {
|
|
422
|
-
if (!enrichedLanguages[lang].status) {
|
|
423
|
-
enrichedLanguages[lang].status = blog.publicationData?.status === 'concept' ? 'draft' : (blog.publicationData?.status || 'draft');
|
|
424
|
-
}
|
|
425
|
-
if (!enrichedLanguages[lang].updatedAt) {
|
|
426
|
-
enrichedLanguages[lang].updatedAt = blog.updatedAt;
|
|
427
|
-
}
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
const displayStatus = isMissingTranslation
|
|
431
|
-
? 'not-translated'
|
|
432
|
-
: (langContent.status || blog.publicationData?.status || 'concept');
|
|
433
103
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
_id: blog._id.toString(),
|
|
437
|
-
title,
|
|
438
|
-
summary,
|
|
439
|
-
contentBlocks,
|
|
440
|
-
image,
|
|
441
|
-
categoryTags,
|
|
442
|
-
publicationData: {
|
|
443
|
-
...blog.publicationData,
|
|
444
|
-
status: displayStatus,
|
|
445
|
-
},
|
|
446
|
-
seo,
|
|
447
|
-
metadata,
|
|
448
|
-
languages: enrichedLanguages,
|
|
449
|
-
availableLanguages: Object.keys(languages),
|
|
450
|
-
lang: bestLanguage,
|
|
451
|
-
isMissingTranslation,
|
|
452
|
-
requestedLanguage,
|
|
453
|
-
updatedAt: isMissingTranslation ? blog.updatedAt : (langContent.updatedAt || blog.updatedAt),
|
|
454
|
-
status: displayStatus,
|
|
455
|
-
});
|
|
104
|
+
const formatted = (service as any).formatWithLanguage(blogDoc, lang, url.searchParams.get('admin') === 'true');
|
|
105
|
+
return NextResponse.json(formatted);
|
|
456
106
|
} catch (err: any) {
|
|
457
107
|
console.error('[BlogAPI] GET_BY_SLUG error:', err);
|
|
458
|
-
return NextResponse.json(
|
|
459
|
-
{ error: 'Failed to fetch blog', detail: err.message },
|
|
460
|
-
{ status: 500 }
|
|
461
|
-
);
|
|
108
|
+
return NextResponse.json({ error: 'Fetch failed', detail: err.message }, { status: 500 });
|
|
462
109
|
}
|
|
463
110
|
}
|
|
464
111
|
|
|
465
|
-
|
|
466
|
-
* PUT /api/blogs/[slug] - Update blog post by slug
|
|
467
|
-
*/
|
|
468
|
-
export async function PUT_BY_SLUG(
|
|
469
|
-
req: NextRequest,
|
|
470
|
-
slug: string,
|
|
471
|
-
config: BlogApiConfig
|
|
472
|
-
): Promise<NextResponse> {
|
|
112
|
+
export async function PUT_BY_SLUG(req: NextRequest, slug: string, config: BlogApiConfig): Promise<NextResponse> {
|
|
473
113
|
try {
|
|
474
114
|
const url = new URL(req.url);
|
|
475
115
|
const language = url.searchParams.get('language') || 'nl';
|
|
476
|
-
|
|
477
116
|
const userId = await config.getUserId(req);
|
|
478
|
-
if (!userId) {
|
|
479
|
-
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
const body = await req.json();
|
|
483
|
-
const {
|
|
484
|
-
title,
|
|
485
|
-
summary,
|
|
486
|
-
content,
|
|
487
|
-
contentBlocks,
|
|
488
|
-
image,
|
|
489
|
-
categoryTags,
|
|
490
|
-
publicationData,
|
|
491
|
-
seo,
|
|
492
|
-
} = body;
|
|
493
|
-
|
|
494
|
-
const dbConnection = await config.getDb();
|
|
495
|
-
const db = dbConnection.db();
|
|
496
|
-
const blogs = db.collection(config.collectionName || 'blogs');
|
|
497
|
-
|
|
498
|
-
// Check if blog exists and user is author
|
|
499
|
-
const existingBlog = await blogs.findOne({ slug });
|
|
500
|
-
if (!existingBlog) {
|
|
501
|
-
return NextResponse.json({ error: 'Blog not found' }, { status: 404 });
|
|
502
|
-
}
|
|
503
|
-
if (existingBlog.authorId !== userId) {
|
|
504
|
-
return NextResponse.json({ error: 'Forbidden: Not the author' }, { status: 403 });
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
// Validation
|
|
508
|
-
const isPublishing = publicationData?.status === 'published';
|
|
509
|
-
if (!title?.trim()) {
|
|
510
|
-
return NextResponse.json({ message: 'Title is required' }, { status: 400 });
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
if (isPublishing) {
|
|
514
|
-
const hasContent =
|
|
515
|
-
(contentBlocks && Array.isArray(contentBlocks) && contentBlocks.length > 0) ||
|
|
516
|
-
(content && Array.isArray(content) && content.length > 0);
|
|
517
|
-
|
|
518
|
-
// Collect all missing fields for better error messages
|
|
519
|
-
const missingFields: string[] = [];
|
|
520
|
-
if (!summary?.trim()) missingFields.push('summary');
|
|
521
|
-
if (!image?.id?.trim()) missingFields.push('featured image');
|
|
522
|
-
// Only require category if it's explicitly provided and empty
|
|
523
|
-
// If categoryTags is undefined or category is undefined, that's also missing
|
|
524
|
-
if (!categoryTags || !categoryTags.category || !categoryTags.category.trim()) {
|
|
525
|
-
missingFields.push('category');
|
|
526
|
-
}
|
|
527
|
-
if (!hasContent) missingFields.push('content');
|
|
528
|
-
|
|
529
|
-
if (missingFields.length > 0) {
|
|
530
|
-
console.log('[BlogAPI] PUT_BY_SLUG validation failed:', {
|
|
531
|
-
isPublishing,
|
|
532
|
-
missingFields,
|
|
533
|
-
summary: summary?.trim() || 'missing',
|
|
534
|
-
imageId: image?.id?.trim() || 'missing',
|
|
535
|
-
category: categoryTags?.category?.trim() || 'missing',
|
|
536
|
-
hasContent,
|
|
537
|
-
contentBlocksLength: contentBlocks?.length || 0,
|
|
538
|
-
contentLength: content?.length || 0,
|
|
539
|
-
});
|
|
540
|
-
return NextResponse.json(
|
|
541
|
-
{
|
|
542
|
-
message: `Missing required fields for publishing: ${missingFields.join(', ')}`,
|
|
543
|
-
missingFields
|
|
544
|
-
},
|
|
545
|
-
{ status: 400 }
|
|
546
|
-
);
|
|
547
|
-
}
|
|
548
|
-
}
|
|
117
|
+
if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
549
118
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
const finalSlug = slug;
|
|
553
|
-
|
|
554
|
-
// Determine the final status: if publishing, set to 'published', otherwise preserve or convert draft to concept
|
|
555
|
-
let finalStatus = publicationData?.status;
|
|
556
|
-
if (isPublishing) {
|
|
557
|
-
finalStatus = 'published';
|
|
558
|
-
} else if (publicationData?.status === 'draft') {
|
|
559
|
-
finalStatus = 'concept';
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
// Preserve existing languages or initialize
|
|
563
|
-
const existingLanguages = existingBlog.languages || {};
|
|
564
|
-
const primaryLanguage = existingBlog.metadata?.lang || 'nl';
|
|
119
|
+
const dbConn = await config.getDb();
|
|
120
|
+
const collection = dbConn.db().collection(config.collectionName || 'blogs');
|
|
565
121
|
|
|
566
|
-
|
|
567
|
-
// Only the language being saved gets a new updatedAt timestamp
|
|
568
|
-
const now = new Date();
|
|
569
|
-
const updatedLanguages = {
|
|
570
|
-
...existingLanguages,
|
|
571
|
-
[language]: {
|
|
572
|
-
blocks: contentBlocks || [],
|
|
573
|
-
metadata: {
|
|
574
|
-
title: title.trim(),
|
|
575
|
-
excerpt: (summary || '').trim(),
|
|
576
|
-
featuredImage: image,
|
|
577
|
-
categories: categoryTags?.category ? [categoryTags.category] : [],
|
|
578
|
-
tags: categoryTags?.tags || [],
|
|
579
|
-
seo: seo || {},
|
|
580
|
-
},
|
|
581
|
-
updatedAt: now.toISOString(),
|
|
582
|
-
status: finalStatus,
|
|
583
|
-
},
|
|
584
|
-
};
|
|
585
|
-
|
|
586
|
-
// For root-level fields, only update if this is the primary language
|
|
587
|
-
// Otherwise preserve existing root values to maintain backward compatibility
|
|
588
|
-
const isPrimaryLanguage = language === primaryLanguage || !existingLanguages[primaryLanguage];
|
|
122
|
+
const existing = await findPostBySlug(collection, slug);
|
|
589
123
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
// This maintains backward compatibility
|
|
593
|
-
...(isPrimaryLanguage ? {
|
|
594
|
-
title: title.trim(),
|
|
595
|
-
summary: (summary || '').trim(),
|
|
596
|
-
contentBlocks: contentBlocks || [],
|
|
597
|
-
content: content || [],
|
|
598
|
-
image: image || {},
|
|
599
|
-
categoryTags: {
|
|
600
|
-
category: categoryTags?.category?.trim() || '',
|
|
601
|
-
tags: categoryTags?.tags || [],
|
|
602
|
-
},
|
|
603
|
-
seo: seo || {},
|
|
604
|
-
} : {}),
|
|
605
|
-
publicationData: {
|
|
606
|
-
...existingBlog.publicationData,
|
|
607
|
-
...publicationData,
|
|
608
|
-
status: finalStatus,
|
|
609
|
-
date: publicationData?.date ? new Date(publicationData.date) : existingBlog.publicationData?.date || new Date(),
|
|
610
|
-
},
|
|
611
|
-
authorId: userId,
|
|
612
|
-
languages: updatedLanguages,
|
|
613
|
-
metadata: {
|
|
614
|
-
...existingBlog.metadata,
|
|
615
|
-
lang: primaryLanguage,
|
|
616
|
-
},
|
|
617
|
-
updatedAt: new Date(),
|
|
618
|
-
};
|
|
619
|
-
|
|
620
|
-
await blogs.updateOne({ slug }, { $set: updateData });
|
|
124
|
+
if (!existing) return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
|
125
|
+
if (existing.authorId !== userId) return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
|
621
126
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
});
|
|
127
|
+
const service = new BlogService(config);
|
|
128
|
+
const updateData = service.prepareUpdateData(await req.json(), existing, language);
|
|
129
|
+
|
|
130
|
+
await collection.updateOne({ _id: existing._id }, { $set: updateData });
|
|
131
|
+
|
|
132
|
+
// Return the potentially NEW localized slug so the editor can redirect if needed
|
|
133
|
+
const newSlug = updateData.languages[language].metadata.slug || existing.slug;
|
|
134
|
+
return NextResponse.json({ message: 'Updated', slug: newSlug });
|
|
626
135
|
} catch (err: any) {
|
|
627
|
-
|
|
628
|
-
return NextResponse.json(
|
|
629
|
-
{ error: 'Failed to update blog', detail: err.message },
|
|
630
|
-
{ status: 500 }
|
|
631
|
-
);
|
|
136
|
+
return NextResponse.json({ error: 'Update failed', detail: err.message }, { status: 500 });
|
|
632
137
|
}
|
|
633
138
|
}
|
|
634
139
|
|
|
635
|
-
|
|
636
|
-
* DELETE /api/blogs/[slug] - Delete blog post by slug
|
|
637
|
-
*/
|
|
638
|
-
export async function DELETE_BY_SLUG(
|
|
639
|
-
req: NextRequest,
|
|
640
|
-
slug: string,
|
|
641
|
-
config: BlogApiConfig
|
|
642
|
-
): Promise<NextResponse> {
|
|
140
|
+
export async function DELETE_BY_SLUG(req: NextRequest, slug: string, config: BlogApiConfig): Promise<NextResponse> {
|
|
643
141
|
try {
|
|
644
142
|
const userId = await config.getUserId(req);
|
|
645
|
-
if (!userId) {
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
const
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
if (!blog) {
|
|
656
|
-
return NextResponse.json({ error: 'Blog not found' }, { status: 404 });
|
|
657
|
-
}
|
|
658
|
-
if (blog.authorId !== userId) {
|
|
659
|
-
return NextResponse.json({ error: 'Forbidden: Not the author' }, { status: 403 });
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
await blogs.deleteOne({ slug });
|
|
663
|
-
|
|
664
|
-
return NextResponse.json({ message: 'Blog deleted successfully' });
|
|
143
|
+
if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
144
|
+
const dbConn = await config.getDb();
|
|
145
|
+
const collection = dbConn.db().collection(config.collectionName || 'blogs');
|
|
146
|
+
|
|
147
|
+
const blog = await findPostBySlug(collection, slug);
|
|
148
|
+
|
|
149
|
+
if (!blog) return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
|
150
|
+
if (blog.authorId !== userId) return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
|
151
|
+
await collection.deleteOne({ _id: blog._id });
|
|
152
|
+
return NextResponse.json({ message: 'Deleted' });
|
|
665
153
|
} catch (err: any) {
|
|
666
|
-
|
|
667
|
-
return NextResponse.json(
|
|
668
|
-
{ error: 'Failed to delete blog', detail: err.message },
|
|
669
|
-
{ status: 500 }
|
|
670
|
-
);
|
|
154
|
+
return NextResponse.json({ error: 'Delete failed', detail: err.message }, { status: 500 });
|
|
671
155
|
}
|
|
672
156
|
}
|
|
673
|
-
|