@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.
Files changed (291) hide show
  1. package/dist/api/categories.d.ts.map +1 -1
  2. package/dist/api/categories.js +42 -38
  3. package/dist/api/handler.d.ts +1 -26
  4. package/dist/api/handler.d.ts.map +1 -1
  5. package/dist/api/handler.js +81 -500
  6. package/dist/api/router.d.ts +0 -5
  7. package/dist/api/router.d.ts.map +1 -1
  8. package/dist/api/router.js +8 -35
  9. package/dist/api/service.d.ts +80 -0
  10. package/dist/api/service.d.ts.map +1 -0
  11. package/dist/api/service.js +219 -0
  12. package/dist/hooks/useAutoSave.d.ts +10 -0
  13. package/dist/hooks/useAutoSave.d.ts.map +1 -0
  14. package/dist/hooks/useAutoSave.js +57 -0
  15. package/dist/hooks/useCategories.d.ts +1 -1
  16. package/dist/hooks/useCategories.d.ts.map +1 -1
  17. package/dist/hooks/useCategories.js +15 -46
  18. package/dist/index.d.ts +24 -31
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +44 -201
  21. package/dist/init.d.ts +20 -7
  22. package/dist/init.d.ts.map +1 -1
  23. package/dist/init.js +8 -7
  24. package/dist/lib/blocks/BlockRenderer.d.ts.map +1 -1
  25. package/dist/lib/layouts/blocks/ColumnsBlock.d.ts.map +1 -1
  26. package/dist/lib/layouts/blocks/ColumnsBlock.js +30 -113
  27. package/dist/lib/layouts/blocks/SectionBlock.d.ts.map +1 -1
  28. package/dist/lib/layouts/blocks/SectionBlock.js +9 -21
  29. package/dist/lib/layouts/index.d.ts +3 -3
  30. package/dist/lib/layouts/index.js +4 -4
  31. package/dist/lib/mappers/apiMapper.d.ts +10 -0
  32. package/dist/lib/mappers/apiMapper.d.ts.map +1 -1
  33. package/dist/lib/mappers/apiMapper.js +47 -32
  34. package/dist/lib/rich-text/RichTextEditor.d.ts +4 -2
  35. package/dist/lib/rich-text/RichTextEditor.d.ts.map +1 -1
  36. package/dist/lib/rich-text/RichTextEditor.js +12 -9
  37. package/dist/lib/utils/config-resolver.d.ts +28 -0
  38. package/dist/lib/utils/config-resolver.d.ts.map +1 -0
  39. package/dist/lib/utils/config-resolver.js +46 -0
  40. package/dist/lib/utils/tree.d.ts +29 -0
  41. package/dist/lib/utils/tree.d.ts.map +1 -0
  42. package/dist/lib/utils/tree.js +129 -0
  43. package/dist/state/EditorContext.d.ts +3 -25
  44. package/dist/state/EditorContext.d.ts.map +1 -1
  45. package/dist/state/EditorContext.js +124 -174
  46. package/dist/state/reducer.d.ts +1 -5
  47. package/dist/state/reducer.d.ts.map +1 -1
  48. package/dist/state/reducer.js +128 -521
  49. package/dist/state/types.d.ts +12 -1
  50. package/dist/state/types.d.ts.map +1 -1
  51. package/dist/types/block.d.ts +9 -0
  52. package/dist/types/block.d.ts.map +1 -1
  53. package/dist/types/post.d.ts +17 -1
  54. package/dist/types/post.d.ts.map +1 -1
  55. package/dist/views/CanvasEditor/BlockWrapper.d.ts +5 -6
  56. package/dist/views/CanvasEditor/BlockWrapper.d.ts.map +1 -1
  57. package/dist/views/CanvasEditor/BlockWrapper.js +56 -264
  58. package/dist/views/CanvasEditor/CanvasEditorView.d.ts +5 -3
  59. package/dist/views/CanvasEditor/CanvasEditorView.d.ts.map +1 -1
  60. package/dist/views/CanvasEditor/CanvasEditorView.js +55 -315
  61. package/dist/views/CanvasEditor/EditorBody.d.ts +6 -8
  62. package/dist/views/CanvasEditor/EditorBody.d.ts.map +1 -1
  63. package/dist/views/CanvasEditor/EditorBody.js +34 -482
  64. package/dist/views/CanvasEditor/EditorHeader.d.ts.map +1 -1
  65. package/dist/views/CanvasEditor/EditorHeader.js +27 -63
  66. package/dist/views/CanvasEditor/LayoutContainer.d.ts.map +1 -1
  67. package/dist/views/CanvasEditor/LayoutContainer.js +49 -70
  68. package/dist/views/CanvasEditor/components/CustomBlockItem.js +1 -1
  69. package/dist/views/CanvasEditor/components/EditorCanvas.d.ts +15 -3
  70. package/dist/views/CanvasEditor/components/EditorCanvas.d.ts.map +1 -1
  71. package/dist/views/CanvasEditor/components/EditorCanvas.js +40 -18
  72. package/dist/views/CanvasEditor/components/EditorLibrary.d.ts +5 -1
  73. package/dist/views/CanvasEditor/components/EditorLibrary.d.ts.map +1 -1
  74. package/dist/views/CanvasEditor/components/EditorLibrary.js +11 -7
  75. package/dist/views/CanvasEditor/components/EditorSidebar.d.ts.map +1 -1
  76. package/dist/views/CanvasEditor/components/EditorSidebar.js +32 -14
  77. package/dist/views/CanvasEditor/components/FeaturedMediaSection.d.ts +0 -6
  78. package/dist/views/CanvasEditor/components/FeaturedMediaSection.d.ts.map +1 -1
  79. package/dist/views/CanvasEditor/components/FeaturedMediaSection.js +17 -128
  80. package/dist/views/CanvasEditor/components/JSONInspector.d.ts +9 -0
  81. package/dist/views/CanvasEditor/components/JSONInspector.d.ts.map +1 -0
  82. package/dist/views/CanvasEditor/components/JSONInspector.js +56 -0
  83. package/dist/views/CanvasEditor/components/LibraryItem.js +2 -2
  84. package/dist/views/CanvasEditor/components/PrivacySettingsSection.d.ts +0 -4
  85. package/dist/views/CanvasEditor/components/PrivacySettingsSection.d.ts.map +1 -1
  86. package/dist/views/CanvasEditor/components/PrivacySettingsSection.js +6 -28
  87. package/dist/views/CanvasEditor/components/index.d.ts +2 -0
  88. package/dist/views/CanvasEditor/components/index.d.ts.map +1 -1
  89. package/dist/views/CanvasEditor/components/index.js +1 -0
  90. package/dist/views/CanvasEditor/hooks/useHeroBlock.d.ts.map +1 -1
  91. package/dist/views/CanvasEditor/hooks/useHeroBlock.js +15 -18
  92. package/dist/views/CanvasEditor/hooks/usePostLoader.d.ts +3 -0
  93. package/dist/views/CanvasEditor/hooks/usePostLoader.d.ts.map +1 -1
  94. package/dist/views/CanvasEditor/hooks/usePostLoader.js +12 -13
  95. package/dist/views/CanvasEditor/hooks/useUnsavedChanges.js +0 -4
  96. package/dist/views/PostManager/EmptyState.d.ts +1 -1
  97. package/dist/views/PostManager/EmptyState.js +4 -4
  98. package/dist/views/PostManager/FilterDropdown.d.ts +21 -0
  99. package/dist/views/PostManager/FilterDropdown.d.ts.map +1 -0
  100. package/dist/views/PostManager/FilterDropdown.js +28 -0
  101. package/dist/views/PostManager/LanguageFlags.d.ts.map +1 -1
  102. package/dist/views/PostManager/LanguageFlags.js +4 -1
  103. package/dist/views/PostManager/PostCards.d.ts.map +1 -1
  104. package/dist/views/PostManager/PostCards.js +23 -40
  105. package/dist/views/PostManager/PostFilters.d.ts.map +1 -1
  106. package/dist/views/PostManager/PostFilters.js +34 -3
  107. package/dist/views/PostManager/PostManagerView.d.ts +1 -2
  108. package/dist/views/PostManager/PostManagerView.d.ts.map +1 -1
  109. package/dist/views/PostManager/PostManagerView.js +30 -96
  110. package/dist/views/PostManager/PostStats.d.ts.map +1 -1
  111. package/dist/views/PostManager/PostStats.js +10 -10
  112. package/dist/views/PostManager/PostTable.d.ts.map +1 -1
  113. package/dist/views/PostManager/PostTable.js +23 -40
  114. package/dist/views/Settings/SettingsView.d.ts +1 -1
  115. package/dist/views/Settings/SettingsView.d.ts.map +1 -1
  116. package/dist/views/Settings/SettingsView.js +12 -39
  117. package/dist/views/SlugSEO/SlugSEOManagerView.d.ts.map +1 -1
  118. package/dist/views/SlugSEO/SlugSEOManagerView.js +2 -2
  119. package/package.json +42 -6
  120. package/src/api/categories.ts +48 -52
  121. package/src/api/handler.ts +87 -604
  122. package/src/api/router.ts +15 -65
  123. package/src/api/service.ts +241 -0
  124. package/src/hooks/useAutoSave.ts +64 -0
  125. package/src/hooks/useCategories.ts +19 -47
  126. package/src/index.tsx +79 -293
  127. package/src/init.tsx +24 -11
  128. package/src/lib/blocks/BlockRenderer.tsx +1 -0
  129. package/src/lib/layouts/blocks/ColumnsBlock.tsx +60 -173
  130. package/src/lib/layouts/blocks/SectionBlock.tsx +22 -26
  131. package/src/lib/layouts/index.ts +4 -4
  132. package/src/lib/mappers/apiMapper.ts +63 -32
  133. package/src/lib/rich-text/RichTextEditor.tsx +16 -9
  134. package/src/lib/utils/config-resolver.ts +64 -0
  135. package/src/lib/utils/tree.ts +150 -0
  136. package/src/state/EditorContext.tsx +153 -232
  137. package/src/state/reducer.ts +141 -606
  138. package/src/state/types.ts +14 -1
  139. package/src/types/block.ts +10 -0
  140. package/src/types/post.ts +19 -1
  141. package/src/views/CanvasEditor/BlockWrapper.tsx +130 -460
  142. package/src/views/CanvasEditor/CanvasEditorView.tsx +145 -420
  143. package/src/views/CanvasEditor/EditorBody.tsx +98 -610
  144. package/src/views/CanvasEditor/EditorHeader.tsx +176 -196
  145. package/src/views/CanvasEditor/LayoutContainer.tsx +74 -89
  146. package/src/views/CanvasEditor/components/CustomBlockItem.tsx +7 -8
  147. package/src/views/CanvasEditor/components/EditorCanvas.tsx +139 -84
  148. package/src/views/CanvasEditor/components/EditorLibrary.tsx +25 -10
  149. package/src/views/CanvasEditor/components/EditorSidebar.tsx +196 -127
  150. package/src/views/CanvasEditor/components/FeaturedMediaSection.tsx +78 -210
  151. package/src/views/CanvasEditor/components/JSONInspector.tsx +125 -0
  152. package/src/views/CanvasEditor/components/LibraryItem.tsx +5 -6
  153. package/src/views/CanvasEditor/components/PrivacySettingsSection.tsx +73 -124
  154. package/src/views/CanvasEditor/components/index.ts +2 -1
  155. package/src/views/CanvasEditor/hooks/useHeroBlock.ts +15 -18
  156. package/src/views/CanvasEditor/hooks/usePostLoader.ts +21 -13
  157. package/src/views/CanvasEditor/hooks/useUnsavedChanges.ts +4 -4
  158. package/src/views/PostManager/EmptyState.tsx +9 -10
  159. package/src/views/PostManager/FilterDropdown.tsx +95 -0
  160. package/src/views/PostManager/LanguageFlags.tsx +6 -2
  161. package/src/views/PostManager/PostCards.tsx +127 -133
  162. package/src/views/PostManager/PostFilters.tsx +73 -68
  163. package/src/views/PostManager/PostManagerView.tsx +132 -179
  164. package/src/views/PostManager/PostStats.tsx +21 -20
  165. package/src/views/PostManager/PostTable.tsx +137 -165
  166. package/src/views/Settings/SettingsView.tsx +64 -180
  167. package/src/views/SlugSEO/SlugSEOManagerView.tsx +59 -44
  168. package/src/hooks/index.d.ts +0 -8
  169. package/src/hooks/index.d.ts.map +0 -1
  170. package/src/hooks/useBlog.d.ts +0 -31
  171. package/src/hooks/useBlog.d.ts.map +0 -1
  172. package/src/hooks/useBlogs.d.ts +0 -39
  173. package/src/hooks/useBlogs.d.ts.map +0 -1
  174. package/src/hooks/useCategories.d.ts +0 -9
  175. package/src/hooks/useCategories.d.ts.map +0 -1
  176. package/src/lib/blocks/BlockRenderer.d.ts +0 -54
  177. package/src/lib/blocks/BlockRenderer.d.ts.map +0 -1
  178. package/src/lib/config-storage.d.ts +0 -30
  179. package/src/lib/config-storage.d.ts.map +0 -1
  180. package/src/lib/layouts/blocks/ColumnsBlock.d.ts +0 -25
  181. package/src/lib/layouts/blocks/ColumnsBlock.d.ts.map +0 -1
  182. package/src/lib/layouts/blocks/SectionBlock.d.ts +0 -25
  183. package/src/lib/layouts/blocks/SectionBlock.d.ts.map +0 -1
  184. package/src/lib/layouts/index.d.ts +0 -23
  185. package/src/lib/layouts/index.d.ts.map +0 -1
  186. package/src/lib/layouts/registerLayoutBlocks.d.ts +0 -9
  187. package/src/lib/layouts/registerLayoutBlocks.d.ts.map +0 -1
  188. package/src/lib/mappers/apiMapper.d.ts +0 -66
  189. package/src/lib/mappers/apiMapper.d.ts.map +0 -1
  190. package/src/lib/rich-text/RichTextEditor.d.ts +0 -45
  191. package/src/lib/rich-text/RichTextEditor.d.ts.map +0 -1
  192. package/src/lib/rich-text/RichTextPreview.d.ts +0 -16
  193. package/src/lib/rich-text/RichTextPreview.d.ts.map +0 -1
  194. package/src/lib/rich-text/index.d.ts +0 -9
  195. package/src/lib/rich-text/index.d.ts.map +0 -1
  196. package/src/lib/utils/blockHelpers.d.ts +0 -23
  197. package/src/lib/utils/blockHelpers.d.ts.map +0 -1
  198. package/src/lib/utils/configValidation.d.ts +0 -23
  199. package/src/lib/utils/configValidation.d.ts.map +0 -1
  200. package/src/registry/BlockRegistry.d.ts +0 -62
  201. package/src/registry/BlockRegistry.d.ts.map +0 -1
  202. package/src/registry/index.d.ts +0 -6
  203. package/src/registry/index.d.ts.map +0 -1
  204. package/src/state/EditorContext.d.ts +0 -45
  205. package/src/state/EditorContext.d.ts.map +0 -1
  206. package/src/state/index.d.ts +0 -7
  207. package/src/state/index.d.ts.map +0 -1
  208. package/src/state/reducer.d.ts +0 -11
  209. package/src/state/reducer.d.ts.map +0 -1
  210. package/src/state/types.d.ts +0 -162
  211. package/src/state/types.d.ts.map +0 -1
  212. package/src/types/block.d.ts +0 -221
  213. package/src/types/block.d.ts.map +0 -1
  214. package/src/types/index.d.ts +0 -8
  215. package/src/types/index.d.ts.map +0 -1
  216. package/src/types/post.d.ts +0 -136
  217. package/src/types/post.d.ts.map +0 -1
  218. package/src/utils/client.d.ts +0 -48
  219. package/src/utils/client.d.ts.map +0 -1
  220. package/src/views/CanvasEditor/BlockWrapper.d.ts +0 -16
  221. package/src/views/CanvasEditor/BlockWrapper.d.ts.map +0 -1
  222. package/src/views/CanvasEditor/CanvasEditorView.d.ts +0 -14
  223. package/src/views/CanvasEditor/CanvasEditorView.d.ts.map +0 -1
  224. package/src/views/CanvasEditor/EditorBody.d.ts +0 -22
  225. package/src/views/CanvasEditor/EditorBody.d.ts.map +0 -1
  226. package/src/views/CanvasEditor/EditorHeader.d.ts +0 -18
  227. package/src/views/CanvasEditor/EditorHeader.d.ts.map +0 -1
  228. package/src/views/CanvasEditor/LayoutContainer.d.ts +0 -17
  229. package/src/views/CanvasEditor/LayoutContainer.d.ts.map +0 -1
  230. package/src/views/CanvasEditor/SaveConfirmationModal.d.ts +0 -13
  231. package/src/views/CanvasEditor/SaveConfirmationModal.d.ts.map +0 -1
  232. package/src/views/CanvasEditor/components/CustomBlockItem.d.ts +0 -14
  233. package/src/views/CanvasEditor/components/CustomBlockItem.d.ts.map +0 -1
  234. package/src/views/CanvasEditor/components/EditorCanvas.d.ts +0 -29
  235. package/src/views/CanvasEditor/components/EditorCanvas.d.ts.map +0 -1
  236. package/src/views/CanvasEditor/components/EditorLibrary.d.ts +0 -7
  237. package/src/views/CanvasEditor/components/EditorLibrary.d.ts.map +0 -1
  238. package/src/views/CanvasEditor/components/EditorSidebar.d.ts +0 -13
  239. package/src/views/CanvasEditor/components/EditorSidebar.d.ts.map +0 -1
  240. package/src/views/CanvasEditor/components/ErrorBanner.d.ts +0 -6
  241. package/src/views/CanvasEditor/components/ErrorBanner.d.ts.map +0 -1
  242. package/src/views/CanvasEditor/components/FeaturedMediaSection.d.ts +0 -25
  243. package/src/views/CanvasEditor/components/FeaturedMediaSection.d.ts.map +0 -1
  244. package/src/views/CanvasEditor/components/LibraryItem.d.ts +0 -14
  245. package/src/views/CanvasEditor/components/LibraryItem.d.ts.map +0 -1
  246. package/src/views/CanvasEditor/components/PrivacySettingsSection.d.ts +0 -15
  247. package/src/views/CanvasEditor/components/PrivacySettingsSection.d.ts.map +0 -1
  248. package/src/views/CanvasEditor/components/index.d.ts +0 -21
  249. package/src/views/CanvasEditor/components/index.d.ts.map +0 -1
  250. package/src/views/CanvasEditor/hooks/index.d.ts +0 -10
  251. package/src/views/CanvasEditor/hooks/index.d.ts.map +0 -1
  252. package/src/views/CanvasEditor/hooks/useHeroBlock.d.ts +0 -8
  253. package/src/views/CanvasEditor/hooks/useHeroBlock.d.ts.map +0 -1
  254. package/src/views/CanvasEditor/hooks/useKeyboardShortcuts.d.ts +0 -3
  255. package/src/views/CanvasEditor/hooks/useKeyboardShortcuts.d.ts.map +0 -1
  256. package/src/views/CanvasEditor/hooks/usePostLoader.d.ts +0 -5
  257. package/src/views/CanvasEditor/hooks/usePostLoader.d.ts.map +0 -1
  258. package/src/views/CanvasEditor/hooks/useRegisteredBlocks.d.ts +0 -2
  259. package/src/views/CanvasEditor/hooks/useRegisteredBlocks.d.ts.map +0 -1
  260. package/src/views/CanvasEditor/hooks/useUnsavedChanges.d.ts +0 -25
  261. package/src/views/CanvasEditor/hooks/useUnsavedChanges.d.ts.map +0 -1
  262. package/src/views/CanvasEditor/index.d.ts +0 -16
  263. package/src/views/CanvasEditor/index.d.ts.map +0 -1
  264. package/src/views/PostManager/EmptyState.d.ts +0 -10
  265. package/src/views/PostManager/EmptyState.d.ts.map +0 -1
  266. package/src/views/PostManager/PostActionsMenu.d.ts +0 -12
  267. package/src/views/PostManager/PostActionsMenu.d.ts.map +0 -1
  268. package/src/views/PostManager/PostCards.d.ts +0 -15
  269. package/src/views/PostManager/PostCards.d.ts.map +0 -1
  270. package/src/views/PostManager/PostFilters.d.ts +0 -16
  271. package/src/views/PostManager/PostFilters.d.ts.map +0 -1
  272. package/src/views/PostManager/PostManagerView.d.ts +0 -11
  273. package/src/views/PostManager/PostManagerView.d.ts.map +0 -1
  274. package/src/views/PostManager/PostStats.d.ts +0 -11
  275. package/src/views/PostManager/PostStats.d.ts.map +0 -1
  276. package/src/views/PostManager/PostTable.d.ts +0 -15
  277. package/src/views/PostManager/PostTable.d.ts.map +0 -1
  278. package/src/views/PostManager/index.d.ts +0 -12
  279. package/src/views/PostManager/index.d.ts.map +0 -1
  280. package/src/views/Preview/PreviewBridgeView.d.ts +0 -12
  281. package/src/views/Preview/PreviewBridgeView.d.ts.map +0 -1
  282. package/src/views/Preview/index.d.ts +0 -6
  283. package/src/views/Preview/index.d.ts.map +0 -1
  284. package/src/views/Settings/SettingsView.d.ts +0 -10
  285. package/src/views/Settings/SettingsView.d.ts.map +0 -1
  286. package/src/views/Settings/index.d.ts +0 -6
  287. package/src/views/Settings/index.d.ts.map +0 -1
  288. package/src/views/SlugSEO/SlugSEOManagerView.d.ts +0 -12
  289. package/src/views/SlugSEO/SlugSEOManagerView.d.ts.map +0 -1
  290. package/src/views/SlugSEO/index.d.ts +0 -6
  291. package/src/views/SlugSEO/index.d.ts.map +0 -1
@@ -1,673 +1,156 @@
1
1
  /**
2
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.
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
- * GET /api/blogs - List all blog posts
24
- * GET /api/blogs?admin=true - List all posts for admin (includes drafts)
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 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
- const requestedLanguage = url.searchParams.get('language') || 'nl';
36
-
37
- const userId = await config.getUserId(req);
38
- const dbConnection = await config.getDb();
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
- console.error('[BlogAPI] GET error:', err);
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
- const {
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 blogDocument = {
272
- title: title.trim(),
273
- summary: (summary || '').trim(),
274
- contentBlocks: contentBlocks || [],
275
- content: content || [],
276
- image: image || { id: '', alt: '' }, // Only store id and alt - plugin-images handles transforms
277
- categoryTags: {
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
- languages: {
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 result = await blogs.insertOne(blogDocument);
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
- console.error('[BlogAPI] POST error:', err);
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 requestedLanguage = url.searchParams.get('language') || 'nl';
92
+ const service = new BlogService(config);
93
+ const lang = url.searchParams.get('language') || 'nl';
338
94
 
339
- const userId = await config.getUserId(req);
340
- const dbConnection = await config.getDb();
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
- // For public view, we must have a published version in the requested language
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
- // Find the best available language for this post
371
- let bestLanguage = requestedLanguage;
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
- return NextResponse.json({
435
- ...blog,
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
- // Slug logic - DON'T change slug for existing posts, keep original
551
- // The slug should remain constant across all language versions
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
- // Update the specific language content
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
- const updateData: any = {
591
- // Only update root-level title/summary if saving in primary language
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
- return NextResponse.json({
623
- message: 'Blog updated successfully',
624
- slug: slug, // Return original slug, not finalSlug
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
- console.error('[BlogAPI] PUT_BY_SLUG error:', err);
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
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
647
- }
648
-
649
- const dbConnection = await config.getDb();
650
- const db = dbConnection.db();
651
- const blogs = db.collection(config.collectionName || 'blogs');
652
-
653
- // Verify ownership
654
- const blog = await blogs.findOne({ slug });
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
- console.error('[BlogAPI] DELETE_BY_SLUG error:', err);
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
-