@jhits/plugin-blog 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +216 -0
  2. package/package.json +57 -0
  3. package/src/api/README.md +224 -0
  4. package/src/api/categories.ts +43 -0
  5. package/src/api/check-title.ts +60 -0
  6. package/src/api/handler.ts +419 -0
  7. package/src/api/index.ts +33 -0
  8. package/src/api/route.ts +116 -0
  9. package/src/api/router.ts +114 -0
  10. package/src/api-server.ts +11 -0
  11. package/src/config.ts +161 -0
  12. package/src/hooks/README.md +91 -0
  13. package/src/hooks/index.ts +8 -0
  14. package/src/hooks/useBlog.ts +85 -0
  15. package/src/hooks/useBlogs.ts +123 -0
  16. package/src/index.server.ts +12 -0
  17. package/src/index.tsx +354 -0
  18. package/src/init.tsx +72 -0
  19. package/src/lib/blocks/BlockRenderer.tsx +141 -0
  20. package/src/lib/blocks/index.ts +6 -0
  21. package/src/lib/index.ts +9 -0
  22. package/src/lib/layouts/blocks/ColumnsBlock.tsx +134 -0
  23. package/src/lib/layouts/blocks/SectionBlock.tsx +104 -0
  24. package/src/lib/layouts/blocks/index.ts +8 -0
  25. package/src/lib/layouts/index.ts +52 -0
  26. package/src/lib/layouts/registerLayoutBlocks.ts +59 -0
  27. package/src/lib/mappers/apiMapper.ts +223 -0
  28. package/src/lib/migration/index.ts +6 -0
  29. package/src/lib/migration/mapper.ts +140 -0
  30. package/src/lib/rich-text/RichTextEditor.tsx +826 -0
  31. package/src/lib/rich-text/RichTextPreview.tsx +210 -0
  32. package/src/lib/rich-text/index.ts +10 -0
  33. package/src/lib/utils/blockHelpers.ts +72 -0
  34. package/src/lib/utils/configValidation.ts +137 -0
  35. package/src/lib/utils/index.ts +8 -0
  36. package/src/lib/utils/slugify.ts +79 -0
  37. package/src/registry/BlockRegistry.ts +142 -0
  38. package/src/registry/index.ts +11 -0
  39. package/src/state/EditorContext.tsx +277 -0
  40. package/src/state/index.ts +8 -0
  41. package/src/state/reducer.ts +694 -0
  42. package/src/state/types.ts +160 -0
  43. package/src/types/block.ts +269 -0
  44. package/src/types/index.ts +15 -0
  45. package/src/types/post.ts +165 -0
  46. package/src/utils/README.md +75 -0
  47. package/src/utils/client.ts +122 -0
  48. package/src/utils/index.ts +9 -0
  49. package/src/views/CanvasEditor/BlockWrapper.tsx +459 -0
  50. package/src/views/CanvasEditor/CanvasEditorView.tsx +917 -0
  51. package/src/views/CanvasEditor/EditorBody.tsx +475 -0
  52. package/src/views/CanvasEditor/EditorHeader.tsx +179 -0
  53. package/src/views/CanvasEditor/LayoutContainer.tsx +494 -0
  54. package/src/views/CanvasEditor/SaveConfirmationModal.tsx +233 -0
  55. package/src/views/CanvasEditor/components/CustomBlockItem.tsx +92 -0
  56. package/src/views/CanvasEditor/components/FeaturedMediaSection.tsx +130 -0
  57. package/src/views/CanvasEditor/components/LibraryItem.tsx +80 -0
  58. package/src/views/CanvasEditor/components/PrivacySettingsSection.tsx +212 -0
  59. package/src/views/CanvasEditor/components/index.ts +17 -0
  60. package/src/views/CanvasEditor/index.ts +16 -0
  61. package/src/views/PostManager/EmptyState.tsx +42 -0
  62. package/src/views/PostManager/PostActionsMenu.tsx +112 -0
  63. package/src/views/PostManager/PostCards.tsx +192 -0
  64. package/src/views/PostManager/PostFilters.tsx +80 -0
  65. package/src/views/PostManager/PostManagerView.tsx +280 -0
  66. package/src/views/PostManager/PostStats.tsx +81 -0
  67. package/src/views/PostManager/PostTable.tsx +225 -0
  68. package/src/views/PostManager/index.ts +15 -0
  69. package/src/views/Preview/PreviewBridgeView.tsx +64 -0
  70. package/src/views/Preview/index.ts +7 -0
  71. package/src/views/README.md +82 -0
  72. package/src/views/Settings/SettingsView.tsx +298 -0
  73. package/src/views/Settings/index.ts +7 -0
  74. package/src/views/SlugSEO/SlugSEOManagerView.tsx +94 -0
  75. package/src/views/SlugSEO/index.ts +7 -0
package/README.md ADDED
@@ -0,0 +1,216 @@
1
+ # Blog Plugin - Block-Based Architecture
2
+
3
+ A modern, modular Blog Plugin system with a Block-Based Architecture (similar to Notion or Gutenberg) where posts are stored as JSON arrays of "Blocks" rather than HTML blobs.
4
+
5
+ ## 🏗️ Architecture Overview
6
+
7
+ ### Core Principles
8
+
9
+ 1. **Block-Based Storage**: Posts are stored as structured JSON arrays of blocks
10
+ 2. **Headless Ready**: All data is saved as structured JSON, perfect for headless CMS usage
11
+ 3. **Extensible Registry**: Register new block types without refactoring core
12
+ 4. **Separation of Concerns**: Library components are decoupled from editor shell
13
+ 5. **Migration-Friendly**: Easy to convert legacy HTML/data to block format
14
+
15
+ ## 📁 Directory Structure
16
+
17
+ ```
18
+ src/
19
+ ├── types/ # TypeScript definitions
20
+ ├── registry/ # Block Registry system
21
+ ├── state/ # State management (Context + Reducer)
22
+ ├── lib/ # Library components (decoupled)
23
+ │ ├── blocks/ # Block rendering
24
+ │ ├── utils/ # Utilities (slugify, etc.)
25
+ │ └── migration/ # Legacy data mapper
26
+ ├── views/ # Editor views (to be implemented)
27
+ ├── components/ # Shared UI components (to be implemented)
28
+ └── hooks/ # Custom hooks (to be implemented)
29
+ ```
30
+
31
+ See [ARCHITECTURE.md](./ARCHITECTURE.md) for detailed structure.
32
+
33
+ ## 🧩 Block System
34
+
35
+ ### Default Block Types
36
+
37
+ - **Heading** (`heading`) - H1-H6 headings
38
+ - **Paragraph** (`paragraph`) - Text paragraphs
39
+ - **Image** (`image`) - Single images with captions
40
+ - **Gallery** (`gallery`) - Image galleries
41
+ - **CTA** (`cta`) - Call-to-action blocks
42
+
43
+ ### Registering New Blocks
44
+
45
+ ```typescript
46
+ import { blockRegistry } from '@jhits/plugin-blog';
47
+
48
+ blockRegistry.register({
49
+ type: 'custom-block',
50
+ name: 'Custom Block',
51
+ description: 'My custom block',
52
+ defaultData: {
53
+ customField: '',
54
+ },
55
+ category: 'custom',
56
+ });
57
+ ```
58
+
59
+ ## 🔄 State Management
60
+
61
+ The editor uses React Context + Reducer pattern for state management:
62
+
63
+ ```typescript
64
+ import { useEditor } from '@jhits/plugin-blog';
65
+
66
+ function MyComponent() {
67
+ const { state, helpers } = useEditor();
68
+
69
+ // Access state
70
+ const { blocks, title, slug } = state;
71
+
72
+ // Use helpers
73
+ helpers.addBlock('paragraph');
74
+ helpers.updateBlock(blockId, { text: 'New text' });
75
+ helpers.save();
76
+ }
77
+ ```
78
+
79
+ ### State Structure
80
+
81
+ - `blocks`: Array of Block objects
82
+ - `title`: Post title
83
+ - `slug`: URL slug
84
+ - `seo`: SEO metadata
85
+ - `metadata`: Categories, tags, featured image
86
+ - `status`: Publication status
87
+ - `isDirty`: Has unsaved changes
88
+ - `focusMode`: Editor focus mode
89
+
90
+ ## 📦 Library Components
91
+
92
+ ### Block Renderer
93
+
94
+ Headless block rendering (decoupled from editor):
95
+
96
+ ```typescript
97
+ import { BlocksRenderer } from '@jhits/plugin-blog/lib';
98
+
99
+ <BlocksRenderer blocks={post.blocks} />
100
+ ```
101
+
102
+ ### Utilities
103
+
104
+ ```typescript
105
+ import { slugify, generateSlugFromTitle, checkSlugCollision } from '@jhits/plugin-blog/lib';
106
+
107
+ const slug = slugify('My Blog Post');
108
+ const uniqueSlug = generateSlugFromTitle('My Post', existingSlugs);
109
+ const { isCollision } = checkSlugCollision(slug, existingSlugs);
110
+ ```
111
+
112
+ ### Migration
113
+
114
+ Convert legacy HTML to blocks:
115
+
116
+ ```typescript
117
+ import { mapLegacyPostToBlogPost } from '@jhits/plugin-blog/lib/migration';
118
+
119
+ const newPost = mapLegacyPostToBlogPost(legacyPost, {
120
+ defaultStatus: 'draft',
121
+ authorId: 'user-123',
122
+ });
123
+ ```
124
+
125
+ ## 🎯 Core Views (To Be Implemented)
126
+
127
+ 1. **Canvas Editor** - Drag-and-drop block editor
128
+ 2. **Slug & SEO Manager** - URL and SEO management with collision detection
129
+ 3. **Preview Bridge** - Live preview of posts
130
+ 4. **Post Manager** - List view with filters and bulk actions
131
+
132
+ ## 🚀 Usage
133
+
134
+ ### Basic Setup
135
+
136
+ ```typescript
137
+ import { EditorProvider } from '@jhits/plugin-blog';
138
+
139
+ function BlogEditor() {
140
+ return (
141
+ <EditorProvider onSave={async (state) => {
142
+ // Save to API
143
+ await fetch('/api/blog/posts', {
144
+ method: 'POST',
145
+ body: JSON.stringify(state),
146
+ });
147
+ }}>
148
+ <CanvasEditor />
149
+ </EditorProvider>
150
+ );
151
+ }
152
+ ```
153
+
154
+ ## 📝 Data Format
155
+
156
+ ### Block Structure
157
+
158
+ ```json
159
+ {
160
+ "id": "block-123",
161
+ "type": "paragraph",
162
+ "data": {
163
+ "text": "This is a paragraph"
164
+ },
165
+ "meta": {
166
+ "order": 0
167
+ }
168
+ }
169
+ ```
170
+
171
+ ### Post Structure
172
+
173
+ ```json
174
+ {
175
+ "id": "post-123",
176
+ "title": "My Blog Post",
177
+ "slug": "my-blog-post",
178
+ "blocks": [...],
179
+ "seo": {
180
+ "title": "SEO Title",
181
+ "description": "Meta description"
182
+ },
183
+ "publication": {
184
+ "status": "published",
185
+ "date": "2024-01-01T00:00:00Z"
186
+ },
187
+ "metadata": {
188
+ "categories": ["tech"],
189
+ "tags": ["react", "typescript"]
190
+ }
191
+ }
192
+ ```
193
+
194
+ ## 🔧 Extensibility
195
+
196
+ The system is designed to be extended:
197
+
198
+ 1. **New Block Types**: Register via `blockRegistry.register()`
199
+ 2. **Custom Renderers**: Pass to `BlockRenderer` via `customRenderers` prop
200
+ 3. **Custom Actions**: Extend reducer with new action types
201
+ 4. **Migration Mappers**: Extend mapper for custom legacy formats
202
+
203
+ ## 📚 Next Steps
204
+
205
+ 1. Implement Canvas Editor with drag-and-drop
206
+ 2. Implement Slug & SEO Manager
207
+ 3. Implement Preview Bridge
208
+ 4. Implement Post Manager
209
+ 5. Add API routes for CRUD operations
210
+ 6. Add database integration
211
+
212
+ ## 📖 Documentation
213
+
214
+ - [ARCHITECTURE.md](./ARCHITECTURE.md) - Detailed architecture documentation
215
+ - [VIEWS.md](./Markdown/VIEWS.md) - Planned views and features
216
+
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@jhits/plugin-blog",
3
+ "version": "0.0.1",
4
+ "description": "Professional blog management system for the JHITS ecosystem",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "main": "./src/index.tsx",
9
+ "types": "./src/index.tsx",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./src/index.tsx",
13
+ "default": "./src/index.tsx"
14
+ },
15
+ "./server": {
16
+ "types": "./src/index.server.ts",
17
+ "default": "./src/index.server.ts"
18
+ }
19
+ },
20
+ "dependencies": {
21
+ "@jhits/plugin-core": "^0.0.1",
22
+ "@jhits/plugin-content": "^0.0.1",
23
+ "@jhits/plugin-images": "^0.0.1",
24
+ "bcrypt": "^6.0.0",
25
+ "framer-motion": "^12.23.26",
26
+ "lucide-react": "^0.562.0",
27
+ "mongodb": "^7.0.0",
28
+ "next-auth": "^4.24.13"
29
+ },
30
+ "peerDependencies": {
31
+ "next": ">=15.0.0",
32
+ "next-intl": ">=4.0.0",
33
+ "next-themes": ">=0.4.0",
34
+ "react": ">=18.0.0",
35
+ "react-dom": ">=18.0.0"
36
+ },
37
+ "devDependencies": {
38
+ "@tailwindcss/postcss": "^4",
39
+ "@types/bcrypt": "^6.0.0",
40
+ "@types/node": "^20.19.27",
41
+ "@types/react": "^19",
42
+ "@types/react-dom": "^19",
43
+ "eslint": "^9",
44
+ "eslint-config-next": "16.1.1",
45
+ "next": "16.1.1",
46
+ "next-intl": "4.6.1",
47
+ "next-themes": "0.4.6",
48
+ "react": "19.2.3",
49
+ "react-dom": "19.2.3",
50
+ "tailwindcss": "^4",
51
+ "typescript": "^5"
52
+ },
53
+ "files": [
54
+ "src",
55
+ "package.json"
56
+ ]
57
+ }
@@ -0,0 +1,224 @@
1
+ # Blog API
2
+
3
+ RESTful API handlers for blog posts, included in the `@jhits/plugin-blog` package.
4
+
5
+ ## Installation
6
+
7
+ The API handlers are automatically available when you install `@jhits/plugin-blog`.
8
+
9
+ ## Setup
10
+
11
+ ### 1. Create API Route in Your App
12
+
13
+ Create API routes in your Next.js app to mount the blog API handlers:
14
+
15
+ **`src/app/api/blogs/route.ts`** - List and create blogs:
16
+ ```typescript
17
+ import { NextRequest } from 'next/server';
18
+ import { GET, POST, createBlogApiConfig } from '@jhits/plugin-blog/api';
19
+ import clientPromise from '@/lib/mongodb';
20
+ import { cookies } from 'next/headers';
21
+ import jwt from 'jsonwebtoken';
22
+
23
+ const JWT_SECRET = process.env.JWT_SECRET || 'secret';
24
+
25
+ async function getUserId(req: NextRequest): Promise<string | null> {
26
+ try {
27
+ const cookieStore = await cookies();
28
+ const token = cookieStore.get('auth_token')?.value;
29
+ if (!token) return null;
30
+ const decoded = jwt.verify(token, JWT_SECRET) as { id: string };
31
+ return decoded.id;
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ const config = createBlogApiConfig({
38
+ getDb: async () => clientPromise,
39
+ getUserId,
40
+ collectionName: 'blogs', // optional, defaults to 'blogs'
41
+ });
42
+
43
+ export async function GET(req: NextRequest) {
44
+ return GET(req, undefined, config);
45
+ }
46
+
47
+ export async function POST(req: NextRequest) {
48
+ return POST(req, undefined, config);
49
+ }
50
+ ```
51
+
52
+ **`src/app/api/blogs/[slug]/route.ts`** - Get, update, and delete by slug:
53
+ ```typescript
54
+ import { NextRequest } from 'next/server';
55
+ import { GET, PUT, DELETE, createBlogApiConfig } from '@jhits/plugin-blog/api';
56
+ import clientPromise from '@/lib/mongodb';
57
+ import { cookies } from 'next/headers';
58
+ import jwt from 'jsonwebtoken';
59
+
60
+ const JWT_SECRET = process.env.JWT_SECRET || 'secret';
61
+
62
+ async function getUserId(req: NextRequest): Promise<string | null> {
63
+ try {
64
+ const cookieStore = await cookies();
65
+ const token = cookieStore.get('auth_token')?.value;
66
+ if (!token) return null;
67
+ const decoded = jwt.verify(token, JWT_SECRET) as { id: string };
68
+ return decoded.id;
69
+ } catch {
70
+ return null;
71
+ }
72
+ }
73
+
74
+ const config = createBlogApiConfig({
75
+ getDb: async () => clientPromise,
76
+ getUserId,
77
+ collectionName: 'blogs',
78
+ });
79
+
80
+ export async function GET(
81
+ req: NextRequest,
82
+ { params }: { params: Promise<{ slug: string }> }
83
+ ) {
84
+ return GET(req, { params }, config);
85
+ }
86
+
87
+ export async function PUT(
88
+ req: NextRequest,
89
+ { params }: { params: Promise<{ slug: string }> }
90
+ ) {
91
+ return PUT(req, { params }, config);
92
+ }
93
+
94
+ export async function DELETE(
95
+ req: NextRequest,
96
+ { params }: { params: Promise<{ slug: string }> }
97
+ ) {
98
+ return DELETE(req, { params }, config);
99
+ }
100
+ ```
101
+
102
+ ## API Endpoints
103
+
104
+ ### GET /api/blogs
105
+ List all blog posts.
106
+
107
+ **Query Parameters:**
108
+ - `limit` (number, default: 10) - Number of posts to return
109
+ - `skip` (number, default: 0) - Number of posts to skip
110
+ - `status` (string, optional) - Filter by status (`published`, `concept`, `draft`)
111
+ - `admin` (boolean, default: false) - If true, returns all posts for authenticated user (including drafts)
112
+
113
+ **Response:**
114
+ ```json
115
+ {
116
+ "blogs": [...],
117
+ "total": 100
118
+ }
119
+ ```
120
+
121
+ ### POST /api/blogs
122
+ Create a new blog post.
123
+
124
+ **Request Body:**
125
+ ```json
126
+ {
127
+ "title": "My Blog Post",
128
+ "summary": "A brief summary",
129
+ "contentBlocks": [...],
130
+ "image": {
131
+ "src": "/uploads/image.jpg",
132
+ "alt": "Image alt text",
133
+ "brightness": 100,
134
+ "blur": 0
135
+ },
136
+ "categoryTags": {
137
+ "category": "Technology",
138
+ "tags": ["web", "development"]
139
+ },
140
+ "publicationData": {
141
+ "status": "concept" | "published",
142
+ "date": "2024-01-01T00:00:00.000Z"
143
+ },
144
+ "seo": {
145
+ "title": "SEO Title",
146
+ "description": "SEO Description",
147
+ "keywords": ["keyword1", "keyword2"],
148
+ "ogImage": "/uploads/og-image.jpg",
149
+ "canonicalUrl": "https://example.com/blog/my-post"
150
+ }
151
+ }
152
+ ```
153
+
154
+ **Response:**
155
+ ```json
156
+ {
157
+ "message": "Draft saved successfully",
158
+ "blogId": "...",
159
+ "slug": "my-blog-post-draft-1234"
160
+ }
161
+ ```
162
+
163
+ ### GET /api/blogs/[slug]
164
+ Get a single blog post by slug.
165
+
166
+ **Response:**
167
+ ```json
168
+ {
169
+ "_id": "...",
170
+ "title": "My Blog Post",
171
+ "slug": "my-blog-post",
172
+ "contentBlocks": [...],
173
+ ...
174
+ }
175
+ ```
176
+
177
+ ### PUT /api/blogs/[slug]
178
+ Update a blog post by slug.
179
+
180
+ **Request Body:** Same as POST /api/blogs
181
+
182
+ **Response:**
183
+ ```json
184
+ {
185
+ "message": "Blog updated successfully",
186
+ "slug": "updated-slug"
187
+ }
188
+ ```
189
+
190
+ ### DELETE /api/blogs/[slug]
191
+ Delete a blog post by slug.
192
+
193
+ **Response:**
194
+ ```json
195
+ {
196
+ "message": "Blog deleted successfully"
197
+ }
198
+ ```
199
+
200
+ ## Security
201
+
202
+ - All endpoints (except public GET) require authentication
203
+ - Users can only access/modify their own posts
204
+ - Published posts are visible to everyone
205
+ - Drafts/concepts are only visible to the author
206
+
207
+ ## Error Responses
208
+
209
+ All endpoints return standard HTTP status codes:
210
+ - `200` - Success
211
+ - `400` - Bad Request (validation errors)
212
+ - `401` - Unauthorized (not authenticated)
213
+ - `403` - Forbidden (not authorized)
214
+ - `404` - Not Found
215
+ - `500` - Internal Server Error
216
+
217
+ Error response format:
218
+ ```json
219
+ {
220
+ "error": "Error message",
221
+ "detail": "Detailed error information"
222
+ }
223
+ ```
224
+
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Blog Categories API Handler
3
+ * GET /api/plugin-blog/categories - Get all unique categories
4
+ */
5
+
6
+ import { NextRequest, NextResponse } from 'next/server';
7
+ import { BlogApiConfig } from './handler';
8
+
9
+ export async function GET(req: NextRequest, config: BlogApiConfig): Promise<NextResponse> {
10
+ try {
11
+ const dbConnection = await config.getDb();
12
+ const db = dbConnection.db();
13
+ const blogs = db.collection(config.collectionName || 'blogs');
14
+
15
+ // Get all unique categories from blog posts
16
+ const categories = await blogs.distinct('categoryTags.category');
17
+
18
+ // Also get categories from Hero blocks in contentBlocks
19
+ const heroBlocks = await blogs.aggregate([
20
+ { $unwind: '$contentBlocks' },
21
+ { $match: { 'contentBlocks.type': 'hero' } },
22
+ { $project: { category: '$contentBlocks.data.category' } },
23
+ { $match: { category: { $exists: true, $ne: null, $nin: [''] } } },
24
+ { $group: { _id: '$category' } },
25
+ ]).toArray();
26
+
27
+ const heroCategories = heroBlocks.map((block: any) => block._id);
28
+
29
+ // Combine and deduplicate
30
+ const allCategories = Array.from(
31
+ new Set([...categories.filter(Boolean), ...heroCategories.filter(Boolean)])
32
+ ).sort();
33
+
34
+ return NextResponse.json({ categories: allCategories });
35
+ } catch (err: any) {
36
+ console.error('[BlogAPI] Categories error:', err);
37
+ return NextResponse.json(
38
+ { error: 'Failed to fetch categories', detail: err.message },
39
+ { status: 500 }
40
+ );
41
+ }
42
+ }
43
+
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Blog Check Title API Handler
3
+ * GET /api/plugin-blog/check-title - Check if a blog title already exists
4
+ */
5
+
6
+ import { NextRequest, NextResponse } from 'next/server';
7
+ import { BlogApiConfig } from './handler';
8
+
9
+ export async function GET(req: NextRequest, config: BlogApiConfig): Promise<NextResponse> {
10
+ try {
11
+ const url = new URL(req.url);
12
+ const title = url.searchParams.get('title');
13
+ const excludeSlug = url.searchParams.get('excludeSlug');
14
+
15
+ // If title is empty or too short, don't bother the database
16
+ if (!title || title.trim().length < 3) {
17
+ return NextResponse.json({ duplicate: false });
18
+ }
19
+
20
+ const dbConnection = await config.getDb();
21
+ const db = dbConnection.db();
22
+ const blogs = db.collection(config.collectionName || 'blogs');
23
+
24
+ /**
25
+ * We search for a title match using:
26
+ * 1. A case-insensitive regex (^ and $ ensure it's the exact full string)
27
+ * 2. An exclusion of the current slug (so editing an article doesn't flag itself)
28
+ */
29
+ const query: any = {
30
+ title: {
31
+ $regex: new RegExp(`^${title.trim().replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'i')
32
+ }
33
+ };
34
+
35
+ if (excludeSlug) {
36
+ query.slug = { $ne: excludeSlug };
37
+ }
38
+
39
+ const duplicate = await blogs.findOne(query);
40
+
41
+ if (duplicate) {
42
+ return NextResponse.json({
43
+ duplicate: true,
44
+ blog: {
45
+ title: duplicate.title,
46
+ slug: duplicate.slug
47
+ }
48
+ });
49
+ }
50
+
51
+ return NextResponse.json({ duplicate: false });
52
+ } catch (err: any) {
53
+ console.error('[BlogAPI] Check title error:', err);
54
+ return NextResponse.json(
55
+ { error: 'Internal server error', detail: err.message },
56
+ { status: 500 }
57
+ );
58
+ }
59
+ }
60
+