@jhits/plugin-blog 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +216 -0
- package/package.json +57 -0
- package/src/api/README.md +224 -0
- package/src/api/categories.ts +43 -0
- package/src/api/check-title.ts +60 -0
- package/src/api/handler.ts +419 -0
- package/src/api/index.ts +33 -0
- package/src/api/route.ts +116 -0
- package/src/api/router.ts +114 -0
- package/src/api-server.ts +11 -0
- package/src/config.ts +161 -0
- package/src/hooks/README.md +91 -0
- package/src/hooks/index.ts +8 -0
- package/src/hooks/useBlog.ts +85 -0
- package/src/hooks/useBlogs.ts +123 -0
- package/src/index.server.ts +12 -0
- package/src/index.tsx +354 -0
- package/src/init.tsx +72 -0
- package/src/lib/blocks/BlockRenderer.tsx +141 -0
- package/src/lib/blocks/index.ts +6 -0
- package/src/lib/index.ts +9 -0
- package/src/lib/layouts/blocks/ColumnsBlock.tsx +134 -0
- package/src/lib/layouts/blocks/SectionBlock.tsx +104 -0
- package/src/lib/layouts/blocks/index.ts +8 -0
- package/src/lib/layouts/index.ts +52 -0
- package/src/lib/layouts/registerLayoutBlocks.ts +59 -0
- package/src/lib/mappers/apiMapper.ts +223 -0
- package/src/lib/migration/index.ts +6 -0
- package/src/lib/migration/mapper.ts +140 -0
- package/src/lib/rich-text/RichTextEditor.tsx +826 -0
- package/src/lib/rich-text/RichTextPreview.tsx +210 -0
- package/src/lib/rich-text/index.ts +10 -0
- package/src/lib/utils/blockHelpers.ts +72 -0
- package/src/lib/utils/configValidation.ts +137 -0
- package/src/lib/utils/index.ts +8 -0
- package/src/lib/utils/slugify.ts +79 -0
- package/src/registry/BlockRegistry.ts +142 -0
- package/src/registry/index.ts +11 -0
- package/src/state/EditorContext.tsx +277 -0
- package/src/state/index.ts +8 -0
- package/src/state/reducer.ts +694 -0
- package/src/state/types.ts +160 -0
- package/src/types/block.ts +269 -0
- package/src/types/index.ts +15 -0
- package/src/types/post.ts +165 -0
- package/src/utils/README.md +75 -0
- package/src/utils/client.ts +122 -0
- package/src/utils/index.ts +9 -0
- package/src/views/CanvasEditor/BlockWrapper.tsx +459 -0
- package/src/views/CanvasEditor/CanvasEditorView.tsx +917 -0
- package/src/views/CanvasEditor/EditorBody.tsx +475 -0
- package/src/views/CanvasEditor/EditorHeader.tsx +179 -0
- package/src/views/CanvasEditor/LayoutContainer.tsx +494 -0
- package/src/views/CanvasEditor/SaveConfirmationModal.tsx +233 -0
- package/src/views/CanvasEditor/components/CustomBlockItem.tsx +92 -0
- package/src/views/CanvasEditor/components/FeaturedMediaSection.tsx +130 -0
- package/src/views/CanvasEditor/components/LibraryItem.tsx +80 -0
- package/src/views/CanvasEditor/components/PrivacySettingsSection.tsx +212 -0
- package/src/views/CanvasEditor/components/index.ts +17 -0
- package/src/views/CanvasEditor/index.ts +16 -0
- package/src/views/PostManager/EmptyState.tsx +42 -0
- package/src/views/PostManager/PostActionsMenu.tsx +112 -0
- package/src/views/PostManager/PostCards.tsx +192 -0
- package/src/views/PostManager/PostFilters.tsx +80 -0
- package/src/views/PostManager/PostManagerView.tsx +280 -0
- package/src/views/PostManager/PostStats.tsx +81 -0
- package/src/views/PostManager/PostTable.tsx +225 -0
- package/src/views/PostManager/index.ts +15 -0
- package/src/views/Preview/PreviewBridgeView.tsx +64 -0
- package/src/views/Preview/index.ts +7 -0
- package/src/views/README.md +82 -0
- package/src/views/Settings/SettingsView.tsx +298 -0
- package/src/views/Settings/index.ts +7 -0
- package/src/views/SlugSEO/SlugSEOManagerView.tsx +94 -0
- package/src/views/SlugSEO/index.ts +7 -0
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
|
+
|