@jhits/plugin-blog 0.0.17 → 0.0.19

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 (66) hide show
  1. package/dist/api/categories.d.ts.map +1 -1
  2. package/dist/api/categories.js +43 -12
  3. package/dist/api/handler.d.ts +1 -0
  4. package/dist/api/handler.d.ts.map +1 -1
  5. package/dist/api/handler.js +259 -42
  6. package/dist/hooks/useBlogs.d.ts +2 -0
  7. package/dist/hooks/useBlogs.d.ts.map +1 -1
  8. package/dist/hooks/useBlogs.js +10 -2
  9. package/dist/index.d.ts +1 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +5 -3
  12. package/dist/lib/i18n.d.ts +14 -0
  13. package/dist/lib/i18n.d.ts.map +1 -0
  14. package/dist/lib/i18n.js +58 -0
  15. package/dist/lib/mappers/apiMapper.d.ts +18 -0
  16. package/dist/lib/mappers/apiMapper.d.ts.map +1 -1
  17. package/dist/lib/mappers/apiMapper.js +1 -0
  18. package/dist/state/reducer.d.ts.map +1 -1
  19. package/dist/state/reducer.js +11 -6
  20. package/dist/state/types.d.ts +5 -0
  21. package/dist/state/types.d.ts.map +1 -1
  22. package/dist/state/types.js +1 -0
  23. package/dist/types/post.d.ts +25 -0
  24. package/dist/types/post.d.ts.map +1 -1
  25. package/dist/utils/client.d.ts +2 -0
  26. package/dist/utils/client.d.ts.map +1 -1
  27. package/dist/utils/client.js +3 -1
  28. package/dist/views/CanvasEditor/CanvasEditorView.d.ts.map +1 -1
  29. package/dist/views/CanvasEditor/CanvasEditorView.js +130 -4
  30. package/dist/views/CanvasEditor/EditorHeader.d.ts +5 -1
  31. package/dist/views/CanvasEditor/EditorHeader.d.ts.map +1 -1
  32. package/dist/views/CanvasEditor/EditorHeader.js +23 -5
  33. package/dist/views/CanvasEditor/hooks/usePostLoader.d.ts +1 -1
  34. package/dist/views/CanvasEditor/hooks/usePostLoader.d.ts.map +1 -1
  35. package/dist/views/CanvasEditor/hooks/usePostLoader.js +14 -4
  36. package/dist/views/PostManager/LanguageFlags.d.ts +11 -0
  37. package/dist/views/PostManager/LanguageFlags.d.ts.map +1 -0
  38. package/dist/views/PostManager/LanguageFlags.js +60 -0
  39. package/dist/views/PostManager/PostCards.d.ts.map +1 -1
  40. package/dist/views/PostManager/PostCards.js +4 -1
  41. package/dist/views/PostManager/PostFilters.d.ts +4 -1
  42. package/dist/views/PostManager/PostFilters.d.ts.map +1 -1
  43. package/dist/views/PostManager/PostFilters.js +13 -3
  44. package/dist/views/PostManager/PostManagerView.d.ts.map +1 -1
  45. package/dist/views/PostManager/PostManagerView.js +24 -3
  46. package/dist/views/PostManager/PostTable.d.ts.map +1 -1
  47. package/dist/views/PostManager/PostTable.js +4 -1
  48. package/package.json +4 -4
  49. package/src/api/categories.ts +58 -11
  50. package/src/api/handler.ts +286 -41
  51. package/src/hooks/useBlogs.ts +12 -1
  52. package/src/index.tsx +7 -3
  53. package/src/lib/i18n.ts +78 -0
  54. package/src/lib/mappers/apiMapper.ts +20 -0
  55. package/src/state/reducer.ts +12 -6
  56. package/src/state/types.ts +5 -0
  57. package/src/types/post.ts +28 -0
  58. package/src/utils/client.ts +4 -0
  59. package/src/views/CanvasEditor/CanvasEditorView.tsx +164 -20
  60. package/src/views/CanvasEditor/EditorHeader.tsx +93 -18
  61. package/src/views/CanvasEditor/hooks/usePostLoader.ts +15 -4
  62. package/src/views/PostManager/LanguageFlags.tsx +136 -0
  63. package/src/views/PostManager/PostCards.tsx +22 -12
  64. package/src/views/PostManager/PostFilters.tsx +38 -1
  65. package/src/views/PostManager/PostManagerView.tsx +25 -2
  66. package/src/views/PostManager/PostTable.tsx +12 -1
@@ -1 +1 @@
1
- {"version":3,"file":"categories.d.ts","sourceRoot":"","sources":["../../src/api/categories.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAE1C,wBAAsB,GAAG,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CAiCxF"}
1
+ {"version":3,"file":"categories.d.ts","sourceRoot":"","sources":["../../src/api/categories.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAE1C,wBAAsB,GAAG,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CAgFxF"}
@@ -5,22 +5,53 @@
5
5
  import { NextResponse } from 'next/server';
6
6
  export async function GET(req, config) {
7
7
  try {
8
+ const url = new URL(req.url);
9
+ const language = url.searchParams.get('language');
10
+ const publishedOnly = url.searchParams.get('published') === 'true';
8
11
  const dbConnection = await config.getDb();
9
12
  const db = dbConnection.db();
10
13
  const blogs = db.collection(config.collectionName || 'blogs');
11
- // Get all unique categories from blog posts
12
- const categories = await blogs.distinct('categoryTags.category');
13
- // Also get categories from Hero blocks in contentBlocks
14
- const heroBlocks = await blogs.aggregate([
15
- { $unwind: '$contentBlocks' },
16
- { $match: { 'contentBlocks.type': 'hero' } },
17
- { $project: { category: '$contentBlocks.data.category' } },
18
- { $match: { category: { $exists: true, $ne: null, $nin: [''] } } },
19
- { $group: { _id: '$category' } },
20
- ]).toArray();
21
- const heroCategories = heroBlocks.map((block) => block._id);
14
+ // Build query
15
+ const query = {};
16
+ if (publishedOnly) {
17
+ // If language is specified, check language status, otherwise root status
18
+ if (language) {
19
+ query[`languages.${language}.status`] = 'published';
20
+ }
21
+ else {
22
+ query['publicationData.status'] = 'published';
23
+ }
24
+ }
25
+ if (language) {
26
+ query[`languages.${language}`] = { $exists: true };
27
+ }
28
+ // 1. Get unique categories from language-specific metadata
29
+ let categories = [];
30
+ if (language) {
31
+ const result = await blogs.distinct(`languages.${language}.metadata.categories`, query);
32
+ categories = result.filter(Boolean);
33
+ }
34
+ else {
35
+ const result = await blogs.distinct('categoryTags.category', query);
36
+ categories = result.filter(Boolean);
37
+ }
38
+ // 2. Get categories from Hero blocks in contentBlocks
39
+ let heroCategories = [];
40
+ const pipeline = [];
41
+ if (Object.keys(query).length > 0) {
42
+ pipeline.push({ $match: query });
43
+ }
44
+ if (language) {
45
+ pipeline.push({ $project: { blocks: `$languages.${language}.blocks` } }, { $unwind: '$blocks' }, { $match: { 'blocks.type': 'hero' } }, { $project: { category: '$blocks.data.category' } });
46
+ }
47
+ else {
48
+ pipeline.push({ $unwind: '$contentBlocks' }, { $match: { 'contentBlocks.type': 'hero' } }, { $project: { category: '$contentBlocks.data.category' } });
49
+ }
50
+ pipeline.push({ $match: { category: { $exists: true, $ne: null, $nin: [''] } } }, { $group: { _id: '$category' } });
51
+ const heroBlocks = await blogs.aggregate(pipeline).toArray();
52
+ heroCategories = heroBlocks.map((block) => block._id);
22
53
  // Combine and deduplicate
23
- const allCategories = Array.from(new Set([...categories.filter(Boolean), ...heroCategories.filter(Boolean)])).sort();
54
+ const allCategories = Array.from(new Set([...categories, ...heroCategories])).sort();
24
55
  return NextResponse.json({ categories: allCategories });
25
56
  }
26
57
  catch (err) {
@@ -21,6 +21,7 @@ export interface BlogApiConfig {
21
21
  * GET /api/blogs - List all blog posts
22
22
  * GET /api/blogs?admin=true - List all posts for admin (includes drafts)
23
23
  * GET /api/blogs?status=published - Filter by status
24
+ * GET /api/blogs?language=en - Filter by language (falls back to nl if not found)
24
25
  */
25
26
  export declare function GET(req: NextRequest, config: BlogApiConfig): Promise<NextResponse>;
26
27
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/api/handler.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGxD,MAAM,WAAW,aAAa;IAC1B,yFAAyF;IACzF,KAAK,EAAE,MAAM,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,GAAG,CAAA;KAAE,CAAC,CAAC;IACxC,yDAAyD;IACzD,SAAS,EAAE,CAAC,GAAG,EAAE,WAAW,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACxD,yCAAyC;IACzC,cAAc,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;GAIG;AACH,wBAAsB,GAAG,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CAgExF;AAED;;GAEG;AACH,wBAAsB,IAAI,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CA0GzF;AAED;;GAEG;AACH,wBAAsB,WAAW,CAC7B,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,aAAa,GACtB,OAAO,CAAC,YAAY,CAAC,CAgCvB;AAED;;GAEG;AACH,wBAAsB,WAAW,CAC7B,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,aAAa,GACtB,OAAO,CAAC,YAAY,CAAC,CA8HvB;AAED;;GAEG;AACH,wBAAsB,cAAc,CAChC,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,aAAa,GACtB,OAAO,CAAC,YAAY,CAAC,CA8BvB"}
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/api/handler.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGxD,MAAM,WAAW,aAAa;IAC1B,yFAAyF;IACzF,KAAK,EAAE,MAAM,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,GAAG,CAAA;KAAE,CAAC,CAAC;IACxC,yDAAyD;IACzD,SAAS,EAAE,CAAC,GAAG,EAAE,WAAW,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACxD,yCAAyC;IACzC,cAAc,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;GAKG;AACH,wBAAsB,GAAG,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CAqKxF;AAED;;GAEG;AACH,wBAAsB,IAAI,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CA+HzF;AAED;;GAEG;AACH,wBAAsB,WAAW,CAC7B,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,aAAa,GACtB,OAAO,CAAC,YAAY,CAAC,CAiIvB;AAED;;GAEG;AACH,wBAAsB,WAAW,CAC7B,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,aAAa,GACtB,OAAO,CAAC,YAAY,CAAC,CAuJvB;AAED;;GAEG;AACH,wBAAsB,cAAc,CAChC,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,aAAa,GACtB,OAAO,CAAC,YAAY,CAAC,CA8BvB"}
@@ -12,6 +12,7 @@ import { slugify } from '../lib/utils/slugify';
12
12
  * GET /api/blogs - List all blog posts
13
13
  * GET /api/blogs?admin=true - List all posts for admin (includes drafts)
14
14
  * GET /api/blogs?status=published - Filter by status
15
+ * GET /api/blogs?language=en - Filter by language (falls back to nl if not found)
15
16
  */
16
17
  export async function GET(req, config) {
17
18
  try {
@@ -20,6 +21,7 @@ export async function GET(req, config) {
20
21
  const skip = Number(url.searchParams.get('skip') ?? 0);
21
22
  const statusFilter = url.searchParams.get('status');
22
23
  const isAdminView = url.searchParams.get('admin') === 'true';
24
+ const requestedLanguage = url.searchParams.get('language') || 'nl';
23
25
  const userId = await config.getUserId(req);
24
26
  const dbConnection = await config.getDb();
25
27
  const db = dbConnection.db();
@@ -39,9 +41,10 @@ export async function GET(req, config) {
39
41
  }
40
42
  }
41
43
  else {
42
- // Public view: only published posts
44
+ // Public view: only published posts in the SPECIFIC language
45
+ // This ensures we only fetch blogs that actually have content for this language
43
46
  query = {
44
- 'publicationData.status': 'published',
47
+ [`languages.${requestedLanguage}.status`]: 'published',
45
48
  'publicationData.date': { $lte: new Date() },
46
49
  };
47
50
  if (statusFilter && statusFilter !== 'published') {
@@ -58,13 +61,99 @@ export async function GET(req, config) {
58
61
  .toArray(),
59
62
  blogs.countDocuments(query),
60
63
  ]);
61
- const formatted = data.map((doc) => ({
62
- ...doc,
63
- _id: doc._id.toString(),
64
- }));
64
+ // Supported languages for fallback (in order of preference)
65
+ const fallbackLanguages = [requestedLanguage, 'nl', 'en'];
66
+ const formatted = data.map((doc) => {
67
+ const languages = doc.languages || {};
68
+ // Only use exact language match public views - no fallback for
69
+ // This ensures visitors see content in their language only
70
+ const hasExactLanguage = !!languages[requestedLanguage];
71
+ // Skip this post if no exact language match (for public non-admin views)
72
+ if (!isAdminView && !hasExactLanguage) {
73
+ return null;
74
+ }
75
+ const postPrimaryLang = doc.metadata?.lang || 'nl';
76
+ // Find the best available language for this post
77
+ let bestLanguage = requestedLanguage;
78
+ let isMissingTranslation = false;
79
+ if (!languages[requestedLanguage]) {
80
+ // For admin view, if specific language is requested, show it as missing instead of falling back
81
+ // This ensures the UI is "synced" with the selected language
82
+ if (isAdminView) {
83
+ isMissingTranslation = true;
84
+ }
85
+ else {
86
+ // Public view still uses fallback or filtering
87
+ const fallbackLanguages = [postPrimaryLang, 'nl', 'en'];
88
+ for (const lang of fallbackLanguages) {
89
+ if (languages[lang]) {
90
+ bestLanguage = lang;
91
+ break;
92
+ }
93
+ }
94
+ }
95
+ }
96
+ const langContent = languages[bestLanguage] || {};
97
+ const meta = langContent.metadata || {};
98
+ // Ensure all languages in doc have a status and updatedAt for the dashboard
99
+ const enrichedLanguages = { ...languages };
100
+ Object.keys(enrichedLanguages).forEach(lang => {
101
+ if (!enrichedLanguages[lang].status) {
102
+ enrichedLanguages[lang].status = doc.publicationData?.status === 'concept' ? 'draft' : (doc.publicationData?.status || 'draft');
103
+ }
104
+ if (!enrichedLanguages[lang].updatedAt) {
105
+ enrichedLanguages[lang].updatedAt = doc.updatedAt;
106
+ }
107
+ });
108
+ // Language display names for placeholders
109
+ const langNames = {
110
+ nl: 'Dutch',
111
+ en: 'English',
112
+ sv: 'Swedish',
113
+ de: 'German',
114
+ fr: 'French',
115
+ es: 'Spanish'
116
+ };
117
+ const displayTitle = isMissingTranslation
118
+ ? `(No ${langNames[requestedLanguage] || requestedLanguage.toUpperCase()} translation)`
119
+ : (meta.title || doc.title || '');
120
+ const displayStatus = isMissingTranslation
121
+ ? 'not-translated'
122
+ : (langContent.status || doc.publicationData?.status || 'concept');
123
+ return {
124
+ ...doc,
125
+ _id: doc._id.toString(),
126
+ title: displayTitle,
127
+ summary: isMissingTranslation ? '' : (meta.excerpt || doc.summary || ''),
128
+ contentBlocks: isMissingTranslation ? [] : (langContent.blocks || doc.contentBlocks || doc.blocks || []),
129
+ image: isMissingTranslation ? null : (meta.featuredImage || doc.image),
130
+ categoryTags: isMissingTranslation ? { category: '', tags: [] } : (meta.categories ? {
131
+ category: meta.categories[0] || '',
132
+ tags: meta.tags || doc.categoryTags?.tags || []
133
+ } : (doc.categoryTags || { category: '', tags: [] })),
134
+ publicationData: {
135
+ ...doc.publicationData,
136
+ status: displayStatus,
137
+ },
138
+ seo: isMissingTranslation ? {} : (meta.seo || doc.seo || {}),
139
+ lang: bestLanguage,
140
+ isMissingTranslation,
141
+ requestedLanguage,
142
+ availableLanguages: Object.keys(languages),
143
+ languages: enrichedLanguages,
144
+ updatedAt: isMissingTranslation ? doc.updatedAt : (langContent.updatedAt || doc.updatedAt),
145
+ status: displayStatus,
146
+ };
147
+ }).filter(Boolean); // Remove null entries
148
+ // Sort by updatedAt descending so the UI order makes sense
149
+ formatted.sort((a, b) => {
150
+ const dateA = new Date(a.updatedAt).getTime();
151
+ const dateB = new Date(b.updatedAt).getTime();
152
+ return dateB - dateA;
153
+ });
65
154
  return NextResponse.json({
66
155
  blogs: formatted,
67
- total: totalCount,
156
+ total: formatted.length,
68
157
  });
69
158
  }
70
159
  catch (err) {
@@ -77,6 +166,8 @@ export async function GET(req, config) {
77
166
  */
78
167
  export async function POST(req, config) {
79
168
  try {
169
+ const url = new URL(req.url);
170
+ const language = url.searchParams.get('language') || 'nl';
80
171
  const userId = await config.getUserId(req);
81
172
  if (!userId) {
82
173
  return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
@@ -149,6 +240,24 @@ export async function POST(req, config) {
149
240
  seo: seo || { title: '', description: '' },
150
241
  slug,
151
242
  authorId: userId,
243
+ languages: {
244
+ [language]: {
245
+ blocks: contentBlocks || [],
246
+ metadata: {
247
+ title: title.trim(),
248
+ excerpt: (summary || '').trim(),
249
+ featuredImage: image,
250
+ categories: categoryTags?.category ? [categoryTags.category] : [],
251
+ tags: categoryTags?.tags || [],
252
+ seo: seo || {},
253
+ },
254
+ updatedAt: new Date().toISOString(),
255
+ status: finalStatus,
256
+ },
257
+ },
258
+ metadata: {
259
+ lang: language,
260
+ },
152
261
  createdAt: new Date(),
153
262
  updatedAt: new Date(),
154
263
  };
@@ -169,6 +278,8 @@ export async function POST(req, config) {
169
278
  */
170
279
  export async function GET_BY_SLUG(req, slug, config) {
171
280
  try {
281
+ const url = new URL(req.url);
282
+ const requestedLanguage = url.searchParams.get('language') || 'nl';
172
283
  const userId = await config.getUserId(req);
173
284
  const dbConnection = await config.getDb();
174
285
  const db = dbConnection.db();
@@ -178,14 +289,100 @@ export async function GET_BY_SLUG(req, slug, config) {
178
289
  return NextResponse.json({ error: 'Blog not found' }, { status: 404 });
179
290
  }
180
291
  // Security check
181
- const isPublished = blog.publicationData?.status === 'published';
182
292
  const isAuthor = userId && blog.authorId === userId;
183
- if (!isPublished && !isAuthor) {
184
- return NextResponse.json({ error: 'Access denied' }, { status: 403 });
293
+ const isAdminView = url.searchParams.get('admin') === 'true';
294
+ // For public view, we must have a published version in the requested language
295
+ if (!isAdminView && !isAuthor) {
296
+ const langData = blog.languages?.[requestedLanguage];
297
+ if (!langData || langData.status !== 'published') {
298
+ return NextResponse.json({ error: 'Blog post not available in this language' }, { status: 404 });
299
+ }
300
+ // Also check publication date
301
+ if (blog.publicationData?.date && new Date(blog.publicationData.date) > new Date()) {
302
+ return NextResponse.json({ error: 'Blog post not yet published' }, { status: 404 });
303
+ }
304
+ }
305
+ const languages = blog.languages || {};
306
+ const postPrimaryLang = blog.metadata?.lang || 'nl';
307
+ // Find the best available language for this post
308
+ let bestLanguage = requestedLanguage;
309
+ let isMissingTranslation = false;
310
+ if (!languages[requestedLanguage]) {
311
+ if (isAdminView) {
312
+ isMissingTranslation = true;
313
+ }
314
+ else {
315
+ // Try fallback languages in order
316
+ const fallbackLanguages = [requestedLanguage, 'nl', 'en'];
317
+ for (const lang of fallbackLanguages) {
318
+ if (languages[lang]) {
319
+ bestLanguage = lang;
320
+ break;
321
+ }
322
+ }
323
+ // If still no match, use primary language from metadata
324
+ if (!languages[bestLanguage] && languages[postPrimaryLang]) {
325
+ bestLanguage = postPrimaryLang;
326
+ }
327
+ }
185
328
  }
329
+ const langContent = languages[bestLanguage] || {};
330
+ const meta = langContent.metadata || {};
331
+ // Language display names for placeholders
332
+ const langNames = {
333
+ nl: 'Dutch',
334
+ en: 'English',
335
+ sv: 'Swedish',
336
+ de: 'German',
337
+ fr: 'French',
338
+ es: 'Spanish'
339
+ };
340
+ let title = isMissingTranslation
341
+ ? `(No ${langNames[requestedLanguage] || requestedLanguage.toUpperCase()} translation)`
342
+ : (meta.title || blog.title || '');
343
+ let summary = isMissingTranslation ? '' : (meta.excerpt || blog.summary || '');
344
+ let contentBlocks = isMissingTranslation ? [] : (langContent.blocks || blog.contentBlocks || blog.blocks || []);
345
+ let image = isMissingTranslation ? null : (meta.featuredImage || blog.image);
346
+ let categoryTags = isMissingTranslation ? { category: '', tags: [] } : (meta.categories ? {
347
+ category: meta.categories[0] || '',
348
+ tags: meta.tags || blog.categoryTags?.tags || []
349
+ } : (blog.categoryTags || { category: '', tags: [] }));
350
+ let seo = isMissingTranslation ? {} : (meta.seo || blog.seo || {});
351
+ let metadata = { ...blog.metadata, ...meta, lang: bestLanguage };
352
+ // Ensure all languages in doc have a status and updatedAt for the dashboard
353
+ const enrichedLanguages = { ...languages };
354
+ Object.keys(enrichedLanguages).forEach(lang => {
355
+ if (!enrichedLanguages[lang].status) {
356
+ enrichedLanguages[lang].status = blog.publicationData?.status === 'concept' ? 'draft' : (blog.publicationData?.status || 'draft');
357
+ }
358
+ if (!enrichedLanguages[lang].updatedAt) {
359
+ enrichedLanguages[lang].updatedAt = blog.updatedAt;
360
+ }
361
+ });
362
+ const displayStatus = isMissingTranslation
363
+ ? 'not-translated'
364
+ : (langContent.status || blog.publicationData?.status || 'concept');
186
365
  return NextResponse.json({
187
366
  ...blog,
188
367
  _id: blog._id.toString(),
368
+ title,
369
+ summary,
370
+ contentBlocks,
371
+ image,
372
+ categoryTags,
373
+ publicationData: {
374
+ ...blog.publicationData,
375
+ status: displayStatus,
376
+ },
377
+ seo,
378
+ metadata,
379
+ languages: enrichedLanguages,
380
+ availableLanguages: Object.keys(languages),
381
+ lang: bestLanguage,
382
+ isMissingTranslation,
383
+ requestedLanguage,
384
+ updatedAt: isMissingTranslation ? blog.updatedAt : (langContent.updatedAt || blog.updatedAt),
385
+ status: displayStatus,
189
386
  });
190
387
  }
191
388
  catch (err) {
@@ -198,6 +395,8 @@ export async function GET_BY_SLUG(req, slug, config) {
198
395
  */
199
396
  export async function PUT_BY_SLUG(req, slug, config) {
200
397
  try {
398
+ const url = new URL(req.url);
399
+ const language = url.searchParams.get('language') || 'nl';
201
400
  const userId = await config.getUserId(req);
202
401
  if (!userId) {
203
402
  return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
@@ -237,31 +436,15 @@ export async function PUT_BY_SLUG(req, slug, config) {
237
436
  if (!hasContent)
238
437
  missingFields.push('content');
239
438
  if (missingFields.length > 0) {
240
- console.log('[BlogAPI] PUT_BY_SLUG validation failed:', {
241
- isPublishing,
242
- missingFields,
243
- summary: summary?.trim() || 'missing',
244
- imageId: image?.id?.trim() || 'missing',
245
- category: categoryTags?.category?.trim() || 'missing',
246
- hasContent,
247
- contentBlocksLength: contentBlocks?.length || 0,
248
- contentLength: content?.length || 0,
249
- });
250
439
  return NextResponse.json({
251
440
  message: `Missing required fields for publishing: ${missingFields.join(', ')}`,
252
441
  missingFields
253
442
  }, { status: 400 });
254
443
  }
255
444
  }
256
- // Slug logic
257
- let finalSlug = slug;
258
- if (isPublishing) {
259
- finalSlug = slugify(title);
260
- }
261
- else if (publicationData?.status === 'concept' && !slug.includes('-draft-')) {
262
- finalSlug = `${slugify(title)}-draft-${Date.now().toString().slice(-4)}`;
263
- }
264
- // Update data
445
+ // Slug logic - DON'T change slug for existing posts, keep original
446
+ // The slug should remain constant across all language versions
447
+ const finalSlug = slug;
265
448
  // Determine the final status: if publishing, set to 'published', otherwise preserve or convert draft to concept
266
449
  let finalStatus = publicationData?.status;
267
450
  if (isPublishing) {
@@ -270,30 +453,64 @@ export async function PUT_BY_SLUG(req, slug, config) {
270
453
  else if (publicationData?.status === 'draft') {
271
454
  finalStatus = 'concept';
272
455
  }
273
- const updateData = {
274
- title: title.trim(),
275
- summary: (summary || '').trim(),
276
- contentBlocks: contentBlocks || [],
277
- content: content || [],
278
- image: image || {},
279
- categoryTags: {
280
- category: categoryTags?.category?.trim() || '',
281
- tags: categoryTags?.tags || [],
456
+ // Preserve existing languages or initialize
457
+ const existingLanguages = existingBlog.languages || {};
458
+ const primaryLanguage = existingBlog.metadata?.lang || 'nl';
459
+ // Update the specific language content
460
+ // Only the language being saved gets a new updatedAt timestamp
461
+ const now = new Date();
462
+ const updatedLanguages = {
463
+ ...existingLanguages,
464
+ [language]: {
465
+ blocks: contentBlocks || [],
466
+ metadata: {
467
+ title: title.trim(),
468
+ excerpt: (summary || '').trim(),
469
+ featuredImage: image,
470
+ categories: categoryTags?.category ? [categoryTags.category] : [],
471
+ tags: categoryTags?.tags || [],
472
+ seo: seo || {},
473
+ },
474
+ updatedAt: now.toISOString(),
475
+ status: finalStatus,
282
476
  },
477
+ };
478
+ // For root-level fields, only update if this is the primary language
479
+ // Otherwise preserve existing root values to maintain backward compatibility
480
+ const isPrimaryLanguage = language === primaryLanguage || !existingLanguages[primaryLanguage];
481
+ const updateData = {
482
+ // Only update root-level title/summary if saving in primary language
483
+ // This maintains backward compatibility
484
+ ...(isPrimaryLanguage ? {
485
+ title: title.trim(),
486
+ summary: (summary || '').trim(),
487
+ contentBlocks: contentBlocks || [],
488
+ content: content || [],
489
+ image: image || {},
490
+ categoryTags: {
491
+ category: categoryTags?.category?.trim() || '',
492
+ tags: categoryTags?.tags || [],
493
+ },
494
+ seo: seo || {},
495
+ } : {}),
283
496
  publicationData: {
497
+ ...existingBlog.publicationData,
284
498
  ...publicationData,
285
499
  status: finalStatus,
286
- date: publicationData?.date ? new Date(publicationData.date) : new Date(),
500
+ date: publicationData?.date ? new Date(publicationData.date) : existingBlog.publicationData?.date || new Date(),
287
501
  },
288
- seo: seo || {},
289
- slug: finalSlug,
290
502
  authorId: userId,
503
+ languages: updatedLanguages,
504
+ metadata: {
505
+ ...existingBlog.metadata,
506
+ lang: primaryLanguage,
507
+ },
291
508
  updatedAt: new Date(),
292
509
  };
293
510
  await blogs.updateOne({ slug }, { $set: updateData });
294
511
  return NextResponse.json({
295
512
  message: 'Blog updated successfully',
296
- slug: finalSlug,
513
+ slug: slug, // Return original slug, not finalSlug
297
514
  });
298
515
  }
299
516
  catch (err) {
@@ -10,6 +10,8 @@ export interface UseBlogsOptions {
10
10
  skip?: number;
11
11
  /** Filter by status (published, draft, concept) */
12
12
  status?: string;
13
+ /** Filter by language */
14
+ language?: string;
13
15
  /** Whether to fetch all posts for admin (includes drafts) */
14
16
  admin?: boolean;
15
17
  /** API base URL (default: '/api/blogs') */
@@ -1 +1 @@
1
- {"version":3,"file":"useBlogs.d.ts","sourceRoot":"","sources":["../../src/hooks/useBlogs.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAE7C,MAAM,WAAW,eAAe;IAC5B,qDAAqD;IACrD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2CAA2C;IAC3C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mDAAmD;IACnD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6DAA6D;IAC7D,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,2CAA2C;IAC3C,UAAU,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,cAAc;IAC3B,0BAA0B;IAC1B,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,wCAAwC;IACxC,OAAO,EAAE,OAAO,CAAC;IACjB,oCAAoC;IACpC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,sCAAsC;IACtC,KAAK,EAAE,MAAM,CAAC;IACd,gCAAgC;IAChC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAChC;AAED;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,OAAO,GAAE,eAAoB,GAAG,cAAc,CA8EtE"}
1
+ {"version":3,"file":"useBlogs.d.ts","sourceRoot":"","sources":["../../src/hooks/useBlogs.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAE7C,MAAM,WAAW,eAAe;IAC5B,qDAAqD;IACrD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2CAA2C;IAC3C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mDAAmD;IACnD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yBAAyB;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6DAA6D;IAC7D,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,2CAA2C;IAC3C,UAAU,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,cAAc;IAC3B,0BAA0B;IAC1B,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,wCAAwC;IACxC,OAAO,EAAE,OAAO,CAAC;IACjB,oCAAoC;IACpC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,sCAAsC;IACtC,KAAK,EAAE,MAAM,CAAC;IACd,gCAAgC;IAChC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAChC;AAED;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,OAAO,GAAE,eAAoB,GAAG,cAAc,CAuFtE"}
@@ -13,12 +13,18 @@ import { apiToBlogPost } from '../lib/mappers/apiMapper';
13
13
  * ```
14
14
  */
15
15
  export function useBlogs(options = {}) {
16
- const { limit = 10, skip = 0, status, admin = false, apiBaseUrl = '/api/plugin-blog', } = options;
16
+ const { limit = 10, skip = 0, status, language, admin = false, apiBaseUrl = '/api/plugin-blog', } = options;
17
17
  const [blogs, setBlogs] = useState([]);
18
18
  const [loading, setLoading] = useState(true);
19
19
  const [error, setError] = useState(null);
20
20
  const [total, setTotal] = useState(0);
21
21
  const fetchBlogs = async () => {
22
+ // If language is expected but not provided, wait for it to be ready
23
+ // (Prevents fetching all languages if the hook is called with a language but it's not yet loaded)
24
+ if (!language && !admin) {
25
+ // In public view, we always want a specific language
26
+ return;
27
+ }
22
28
  try {
23
29
  setLoading(true);
24
30
  setError(null);
@@ -29,6 +35,8 @@ export function useBlogs(options = {}) {
29
35
  params.set('skip', skip.toString());
30
36
  if (status)
31
37
  params.set('status', status);
38
+ if (language)
39
+ params.set('language', language);
32
40
  if (admin)
33
41
  params.set('admin', 'true');
34
42
  const url = `${apiBaseUrl}?${params.toString()}`;
@@ -71,7 +79,7 @@ export function useBlogs(options = {}) {
71
79
  };
72
80
  useEffect(() => {
73
81
  fetchBlogs();
74
- }, [limit, skip, status, admin, apiBaseUrl]);
82
+ }, [limit, skip, status, language, admin, apiBaseUrl]);
75
83
  return {
76
84
  blogs,
77
85
  loading,
package/dist/index.d.ts CHANGED
@@ -36,7 +36,7 @@ export interface PluginProps {
36
36
  export default function BlogPlugin(props: PluginProps): import("react/jsx-runtime").JSX.Element;
37
37
  export { BlogPlugin as Index };
38
38
  export type { Block, BlockTypeDefinition, ClientBlockDefinition, RichTextFormattingConfig, BlockEditProps, BlockPreviewProps, IBlockComponent, } from './types';
39
- export type { SEOMetadata, PublicationData, PostStatus, PostMetadata, BlogPost, PostListItem, PostFilterOptions, } from './types/post';
39
+ export type { SEOMetadata, PublicationData, PostStatus, PostMetadata, BlogPost, PostListItem, PostFilterOptions, BlogPostLanguageContent, BlogPostLanguages, } from './types/post';
40
40
  export { initBlogPlugin } from './init';
41
41
  export type { BlogPluginConfig } from './init';
42
42
  export { RichTextEditor, RichTextPreview } from './lib/rich-text';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAStD;;;GAGG;AACH,MAAM,WAAW,WAAW;IACxB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,yGAAyG;IACzG,YAAY,CAAC,EAAE,qBAAqB,EAAE,CAAC;IACvC,qEAAqE;IACrE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,uCAAuC;IACvC,gBAAgB,CAAC,EAAE;QACf,iDAAiD;QACjD,KAAK,EAAE,MAAM,CAAC;QACd,gDAAgD;QAChD,IAAI,CAAC,EAAE,MAAM,CAAC;KACjB,CAAC;CACL;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,OAAO,UAAU,UAAU,CAAC,KAAK,EAAE,WAAW,2CA4NpD;AAID,OAAO,EAAE,UAAU,IAAI,KAAK,EAAE,CAAC;AAG/B,YAAY,EACR,KAAK,EACL,mBAAmB,EACnB,qBAAqB,EACrB,wBAAwB,EACxB,cAAc,EACd,iBAAiB,EACjB,eAAe,GAClB,MAAM,SAAS,CAAC;AAGjB,YAAY,EACR,WAAW,EACX,eAAe,EACf,UAAU,EACV,YAAY,EACZ,QAAQ,EACR,YAAY,EACZ,iBAAiB,GACpB,MAAM,cAAc,CAAC;AAGtB,OAAO,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAC;AACxC,YAAY,EAAE,gBAAgB,EAAE,MAAM,QAAQ,CAAC;AAG/C,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClE,YAAY,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAGjF,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAC5C,YAAY,EAAE,eAAe,EAAE,cAAc,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAG9F,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AACvD,YAAY,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAG5F,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAG3E,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAG3C,OAAO,EAAE,oBAAoB,EAAE,MAAM,oCAAoC,CAAC;AAG1E,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAGzE,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAClE,YAAY,EAAE,mBAAmB,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAGpF,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAStD;;;GAGG;AACH,MAAM,WAAW,WAAW;IACxB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,yGAAyG;IACzG,YAAY,CAAC,EAAE,qBAAqB,EAAE,CAAC;IACvC,qEAAqE;IACrE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,uCAAuC;IACvC,gBAAgB,CAAC,EAAE;QACf,iDAAiD;QACjD,KAAK,EAAE,MAAM,CAAC;QACd,gDAAgD;QAChD,IAAI,CAAC,EAAE,MAAM,CAAC;KACjB,CAAC;CACL;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,OAAO,UAAU,UAAU,CAAC,KAAK,EAAE,WAAW,2CA8NpD;AAID,OAAO,EAAE,UAAU,IAAI,KAAK,EAAE,CAAC;AAG/B,YAAY,EACR,KAAK,EACL,mBAAmB,EACnB,qBAAqB,EACrB,wBAAwB,EACxB,cAAc,EACd,iBAAiB,EACjB,eAAe,GAClB,MAAM,SAAS,CAAC;AAGjB,YAAY,EACR,WAAW,EACX,eAAe,EACf,UAAU,EACV,YAAY,EACZ,QAAQ,EACR,YAAY,EACZ,iBAAiB,EACjB,uBAAuB,EACvB,iBAAiB,GACpB,MAAM,cAAc,CAAC;AAGtB,OAAO,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAC;AACxC,YAAY,EAAE,gBAAgB,EAAE,MAAM,QAAQ,CAAC;AAG/C,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClE,YAAY,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAGjF,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAC5C,YAAY,EAAE,eAAe,EAAE,cAAc,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAG9F,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AACvD,YAAY,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAG5F,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAG3E,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAG3C,OAAO,EAAE,oBAAoB,EAAE,MAAM,oCAAoC,CAAC;AAG1E,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAGzE,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAClE,YAAY,EAAE,mBAAmB,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAGpF,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC"}
package/dist/index.js CHANGED
@@ -140,9 +140,10 @@ export default function BlogPlugin(props) {
140
140
  if (!originalSlug) {
141
141
  throw new Error('Cannot save: no post identifier available. Please reload the page.');
142
142
  }
143
- console.log('[BlogPlugin] Saving post with slug:', originalSlug);
143
+ const language = state.currentLanguage || locale;
144
+ console.log('[BlogPlugin] Saving post with slug:', originalSlug, 'language:', language);
144
145
  const apiData = editorStateToAPI(state, undefined, heroBlock);
145
- const response = await fetch(`/api/plugin-blog/${originalSlug}`, {
146
+ const response = await fetch(`/api/plugin-blog/${originalSlug}?language=${language}`, {
146
147
  method: 'PUT',
147
148
  headers: { 'Content-Type': 'application/json' },
148
149
  credentials: 'include', // Include cookies for authentication
@@ -173,8 +174,9 @@ export default function BlogPlugin(props) {
173
174
  case 'new':
174
175
  return (_jsx(EditorProvider, { customBlocks: customBlocks, darkMode: darkMode, backgroundColors: backgroundColors, onSave: async (state) => {
175
176
  // Save to API - create new post
177
+ const language = state.currentLanguage || locale;
176
178
  const apiData = editorStateToAPI(state);
177
- const response = await fetch('/api/plugin-blog/new', {
179
+ const response = await fetch(`/api/plugin-blog/new?language=${language}`, {
178
180
  method: 'POST',
179
181
  headers: { 'Content-Type': 'application/json' },
180
182
  credentials: 'include', // Include cookies for authentication
@@ -0,0 +1,14 @@
1
+ /**
2
+ * i18n Utility
3
+ * Simple translation loader for the blog plugin
4
+ */
5
+ export declare function getTranslations(locale: string): any;
6
+ export declare function getEditorTranslations(language: string): {
7
+ language: string;
8
+ selectLanguage: string;
9
+ addLanguage: string;
10
+ switchLanguage: string;
11
+ availableLanguages: string;
12
+ primaryLanguage: string;
13
+ };
14
+ //# sourceMappingURL=i18n.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../../src/lib/i18n.ts"],"names":[],"mappings":"AAAA;;;GAGG;AA4CH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,GAAG,CASnD;AAED,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG;IACrD,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,eAAe,EAAE,MAAM,CAAC;CAC3B,CAYA"}