nextblogkit 0.6.0
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/LICENSE +21 -0
- package/README.md +951 -0
- package/dist/admin/index.cjs +2465 -0
- package/dist/admin/index.cjs.map +1 -0
- package/dist/admin/index.d.cts +44 -0
- package/dist/admin/index.d.ts +44 -0
- package/dist/admin/index.js +2438 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/api/categories.cjs +82 -0
- package/dist/api/categories.cjs.map +1 -0
- package/dist/api/categories.d.cts +27 -0
- package/dist/api/categories.d.ts +27 -0
- package/dist/api/categories.js +77 -0
- package/dist/api/categories.js.map +1 -0
- package/dist/api/media.cjs +113 -0
- package/dist/api/media.cjs.map +1 -0
- package/dist/api/media.d.cts +22 -0
- package/dist/api/media.d.ts +22 -0
- package/dist/api/media.js +109 -0
- package/dist/api/media.js.map +1 -0
- package/dist/api/posts.cjs +103 -0
- package/dist/api/posts.cjs.map +1 -0
- package/dist/api/posts.d.cts +27 -0
- package/dist/api/posts.d.ts +27 -0
- package/dist/api/posts.js +98 -0
- package/dist/api/posts.js.map +1 -0
- package/dist/api/rss.cjs +25 -0
- package/dist/api/rss.cjs.map +1 -0
- package/dist/api/rss.d.cts +5 -0
- package/dist/api/rss.d.ts +5 -0
- package/dist/api/rss.js +23 -0
- package/dist/api/rss.js.map +1 -0
- package/dist/api/settings.cjs +40 -0
- package/dist/api/settings.cjs.map +1 -0
- package/dist/api/settings.d.cts +17 -0
- package/dist/api/settings.d.ts +17 -0
- package/dist/api/settings.js +37 -0
- package/dist/api/settings.js.map +1 -0
- package/dist/api/sitemap.cjs +25 -0
- package/dist/api/sitemap.cjs.map +1 -0
- package/dist/api/sitemap.d.cts +5 -0
- package/dist/api/sitemap.d.ts +5 -0
- package/dist/api/sitemap.js +23 -0
- package/dist/api/sitemap.js.map +1 -0
- package/dist/chunk-4NKOJYWJ.js +68 -0
- package/dist/chunk-4NKOJYWJ.js.map +1 -0
- package/dist/chunk-4PY224XM.js +103 -0
- package/dist/chunk-4PY224XM.js.map +1 -0
- package/dist/chunk-64HUVJOZ.js +446 -0
- package/dist/chunk-64HUVJOZ.js.map +1 -0
- package/dist/chunk-6HKMZOI4.cjs +48 -0
- package/dist/chunk-6HKMZOI4.cjs.map +1 -0
- package/dist/chunk-A2S32RZN.js +138 -0
- package/dist/chunk-A2S32RZN.js.map +1 -0
- package/dist/chunk-E2QLTHKN.cjs +70 -0
- package/dist/chunk-E2QLTHKN.cjs.map +1 -0
- package/dist/chunk-JLPJKNRZ.js +37 -0
- package/dist/chunk-JLPJKNRZ.js.map +1 -0
- package/dist/chunk-JM7QRXXK.js +330 -0
- package/dist/chunk-JM7QRXXK.js.map +1 -0
- package/dist/chunk-KDZER3PU.cjs +43 -0
- package/dist/chunk-KDZER3PU.cjs.map +1 -0
- package/dist/chunk-N5MKAD7J.cjs +109 -0
- package/dist/chunk-N5MKAD7J.cjs.map +1 -0
- package/dist/chunk-QE4VLQYN.cjs +337 -0
- package/dist/chunk-QE4VLQYN.cjs.map +1 -0
- package/dist/chunk-R6MO3QIP.js +46 -0
- package/dist/chunk-R6MO3QIP.js.map +1 -0
- package/dist/chunk-U2ROR6AY.cjs +476 -0
- package/dist/chunk-U2ROR6AY.cjs.map +1 -0
- package/dist/chunk-ZP5XRVVH.cjs +141 -0
- package/dist/chunk-ZP5XRVVH.cjs.map +1 -0
- package/dist/cli/index.cjs +1308 -0
- package/dist/components/index.cjs +541 -0
- package/dist/components/index.cjs.map +1 -0
- package/dist/components/index.d.cts +165 -0
- package/dist/components/index.d.ts +165 -0
- package/dist/components/index.js +527 -0
- package/dist/components/index.js.map +1 -0
- package/dist/editor/index.cjs +1083 -0
- package/dist/editor/index.cjs.map +1 -0
- package/dist/editor/index.d.cts +133 -0
- package/dist/editor/index.d.ts +133 -0
- package/dist/editor/index.js +1051 -0
- package/dist/editor/index.js.map +1 -0
- package/dist/index-Cgzphklp.d.ts +266 -0
- package/dist/index-vjlZDWNr.d.cts +266 -0
- package/dist/index.cjs +368 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +27 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +208 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/index.cjs +120 -0
- package/dist/lib/index.cjs.map +1 -0
- package/dist/lib/index.d.cts +4 -0
- package/dist/lib/index.d.ts +4 -0
- package/dist/lib/index.js +7 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/styles/admin.css +657 -0
- package/dist/styles/blog.css +851 -0
- package/dist/styles/editor.css +452 -0
- package/dist/styles/globals.css +270 -0
- package/dist/styles/prose.css +299 -0
- package/dist/types-CBEEBR4A.d.cts +732 -0
- package/dist/types-CBEEBR4A.d.ts +732 -0
- package/package.json +134 -0
package/README.md
ADDED
|
@@ -0,0 +1,951 @@
|
|
|
1
|
+
# NextBlogKit
|
|
2
|
+
|
|
3
|
+
A complete blog engine for Next.js — admin panel, block editor, SEO, media storage, and more. Drop it into any Next.js 14+ app and get a fully featured blog in minutes.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Block Editor** — TipTap-based with slash commands, image upload, code blocks (Shiki), callouts, FAQ schema, tables, task lists
|
|
8
|
+
- **Admin Panel** — Dashboard, post management, media library, category manager, settings — with configurable paths
|
|
9
|
+
- **SEO Engine** — Meta tags, Open Graph, Twitter Cards, JSON-LD structured data, sitemap.xml, RSS feed, Yoast-like SEO scorer
|
|
10
|
+
- **Image Pipeline** — Cloudflare R2 storage, automatic WebP conversion, responsive sizes, thumbnails
|
|
11
|
+
- **MongoDB Backend** — Full CRUD, slug generation, revision history, full-text search
|
|
12
|
+
- **Composable Components** — Page-level components with slot injection, or use individual atomic components to build your own layout
|
|
13
|
+
- **Table of Contents** — Sticky sidebar TOC with IntersectionObserver-based active heading tracking
|
|
14
|
+
- **Theming** — CSS variables for full color/font/spacing customization, dark mode support
|
|
15
|
+
- **CLI** — One command scaffolding for your Next.js app
|
|
16
|
+
|
|
17
|
+
## Prerequisites
|
|
18
|
+
|
|
19
|
+
- **Next.js 14+** with App Router
|
|
20
|
+
- **MongoDB** (Atlas or self-hosted)
|
|
21
|
+
- **Cloudflare R2** bucket (for media storage)
|
|
22
|
+
- **Node.js 18+**
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
### 1. Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pnpm add nextblogkit
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Or with npm/yarn:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install nextblogkit
|
|
36
|
+
# or
|
|
37
|
+
yarn add nextblogkit
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 2. Scaffold into your Next.js project
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npx nextblogkit init
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
This creates:
|
|
47
|
+
- `app/blog/` — Blog pages (list, post, category)
|
|
48
|
+
- `app/admin/blog/` — Admin panel pages (dashboard, posts, media, categories, settings)
|
|
49
|
+
- `app/api/blog/` — API routes (posts, media, categories, settings, sitemap, RSS)
|
|
50
|
+
- `nextblogkit.config.ts` — Configuration file
|
|
51
|
+
- `.env.local.example` — Environment variable template
|
|
52
|
+
|
|
53
|
+
### 3. Configure environment variables
|
|
54
|
+
|
|
55
|
+
Copy the generated `.env.local.example` to `.env.local`:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
cp .env.local.example .env.local
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Fill in your values:
|
|
62
|
+
|
|
63
|
+
```env
|
|
64
|
+
# MongoDB Connection
|
|
65
|
+
NEXTBLOGKIT_MONGODB_URI=mongodb+srv://user:pass@cluster.mongodb.net/mydb
|
|
66
|
+
|
|
67
|
+
# Cloudflare R2 Storage
|
|
68
|
+
NEXTBLOGKIT_R2_ACCOUNT_ID=your-account-id
|
|
69
|
+
NEXTBLOGKIT_R2_ACCESS_KEY=your-access-key
|
|
70
|
+
NEXTBLOGKIT_R2_SECRET_KEY=your-secret-key
|
|
71
|
+
NEXTBLOGKIT_R2_BUCKET=blog-media
|
|
72
|
+
NEXTBLOGKIT_R2_PUBLIC_URL=https://media.yourdomain.com
|
|
73
|
+
|
|
74
|
+
# Authentication
|
|
75
|
+
NEXTBLOGKIT_API_KEY=your-secure-api-key-must-be-at-least-32-characters-long
|
|
76
|
+
|
|
77
|
+
# Site Info
|
|
78
|
+
NEXTBLOGKIT_SITE_URL=https://yourdomain.com
|
|
79
|
+
NEXTBLOGKIT_SITE_NAME="Your Site Name"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### 4. Run database migrations
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
npx nextblogkit migrate
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
This creates the required MongoDB indexes for posts, categories, and media.
|
|
89
|
+
|
|
90
|
+
### 5. Start your app
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
pnpm dev
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
- Blog: [http://localhost:3000/blog](http://localhost:3000/blog)
|
|
97
|
+
- Admin: [http://localhost:3000/admin/blog](http://localhost:3000/admin/blog)
|
|
98
|
+
|
|
99
|
+
The admin panel will prompt you for the API key on first visit (the value of `NEXTBLOGKIT_API_KEY`).
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Configuration
|
|
104
|
+
|
|
105
|
+
The `nextblogkit.config.ts` file controls all behavior:
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
import { defineConfig } from 'nextblogkit';
|
|
109
|
+
|
|
110
|
+
export default defineConfig({
|
|
111
|
+
// URL paths — customize to match your site structure
|
|
112
|
+
basePath: '/blog', // Public blog URL prefix
|
|
113
|
+
adminPath: '/admin/blog', // Admin panel URL prefix
|
|
114
|
+
apiPath: '/api/blog', // API routes URL prefix
|
|
115
|
+
|
|
116
|
+
// Pagination
|
|
117
|
+
postsPerPage: 10,
|
|
118
|
+
excerptLength: 160,
|
|
119
|
+
|
|
120
|
+
// Editor settings
|
|
121
|
+
editor: {
|
|
122
|
+
blocks: ['paragraph', 'heading', 'image', 'codeBlock', 'blockquote',
|
|
123
|
+
'bulletList', 'orderedList', 'taskList', 'table', 'callout',
|
|
124
|
+
'tableOfContents', 'faq', 'horizontalRule'],
|
|
125
|
+
maxImageSize: 10 * 1024 * 1024, // 10MB
|
|
126
|
+
autosaveInterval: 30000, // 30s
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
// SEO
|
|
130
|
+
seo: {
|
|
131
|
+
titleTemplate: '%s | %siteName%',
|
|
132
|
+
generateRSS: true,
|
|
133
|
+
generateSitemap: true,
|
|
134
|
+
structuredData: true,
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
// Feature toggles
|
|
138
|
+
features: {
|
|
139
|
+
search: true,
|
|
140
|
+
relatedPosts: true,
|
|
141
|
+
readingProgress: true,
|
|
142
|
+
tableOfContents: true,
|
|
143
|
+
shareButtons: true,
|
|
144
|
+
darkMode: true,
|
|
145
|
+
scheduling: true,
|
|
146
|
+
revisionHistory: true,
|
|
147
|
+
imageOptimization: true,
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
// Authentication
|
|
151
|
+
auth: {
|
|
152
|
+
strategy: 'api-key',
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Custom URL Paths
|
|
158
|
+
|
|
159
|
+
You can change the blog URL from `/blog` to anything — `/articles`, `/posts`, `/blogs`, etc. Make sure the folder structure in your `app/` directory matches and pass the paths through to components:
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
export default defineConfig({
|
|
163
|
+
basePath: '/articles',
|
|
164
|
+
adminPath: '/admin/articles',
|
|
165
|
+
apiPath: '/api/articles',
|
|
166
|
+
});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Then in your admin layout, pass the paths:
|
|
170
|
+
|
|
171
|
+
```tsx
|
|
172
|
+
// app/admin/articles/layout.tsx
|
|
173
|
+
import { AdminLayout } from 'nextblogkit/admin';
|
|
174
|
+
import 'nextblogkit/styles/admin.css';
|
|
175
|
+
|
|
176
|
+
export default function AdminBlogLayout({ children }: { children: React.ReactNode }) {
|
|
177
|
+
return (
|
|
178
|
+
<AdminLayout apiPath="/api/articles" adminPath="/admin/articles">
|
|
179
|
+
{children}
|
|
180
|
+
</AdminLayout>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Theming
|
|
188
|
+
|
|
189
|
+
NextBlogKit uses CSS variables for theming. Every color, font, radius, and shadow is a `--nbk-*` variable that you can override.
|
|
190
|
+
|
|
191
|
+
### All CSS Variables
|
|
192
|
+
|
|
193
|
+
| Variable | Default | Description |
|
|
194
|
+
|----------|---------|-------------|
|
|
195
|
+
| `--nbk-primary` | `#2563eb` | Primary brand color (links, buttons, active states) |
|
|
196
|
+
| `--nbk-primary-hover` | `#1d4ed8` | Primary hover state |
|
|
197
|
+
| `--nbk-primary-light` | `#dbeafe` | Light primary (tag backgrounds, highlights) |
|
|
198
|
+
| `--nbk-text` | `#1f2937` | Main text color |
|
|
199
|
+
| `--nbk-text-muted` | `#6b7280` | Secondary/muted text |
|
|
200
|
+
| `--nbk-bg` | `#ffffff` | Page background |
|
|
201
|
+
| `--nbk-bg-secondary` | `#f9fafb` | Secondary background (cards, sidebar) |
|
|
202
|
+
| `--nbk-card-bg` | `#ffffff` | Card background |
|
|
203
|
+
| `--nbk-border` | `#e5e7eb` | Border color |
|
|
204
|
+
| `--nbk-border-focus` | `#2563eb` | Input focus border |
|
|
205
|
+
| `--nbk-radius` | `0.5rem` | Border radius |
|
|
206
|
+
| `--nbk-shadow` | `0 1px 3px ...` | Default box shadow |
|
|
207
|
+
| `--nbk-shadow-sm` | `0 1px 2px ...` | Small shadow |
|
|
208
|
+
| `--nbk-shadow-lg` | `0 10px 15px ...` | Large shadow |
|
|
209
|
+
| `--nbk-shadow-xl` | `0 20px 25px ...` | Extra large shadow |
|
|
210
|
+
| `--nbk-font-heading` | `"Inter", system-ui, sans-serif` | Heading font family |
|
|
211
|
+
| `--nbk-font-body` | `"Inter", system-ui, sans-serif` | Body font family |
|
|
212
|
+
| `--nbk-font-code` | `"JetBrains Mono", monospace` | Code font family |
|
|
213
|
+
| `--nbk-success` | `#10b981` | Success color |
|
|
214
|
+
| `--nbk-warning` | `#f59e0b` | Warning color |
|
|
215
|
+
| `--nbk-danger` | `#ef4444` | Danger/error color |
|
|
216
|
+
| `--nbk-info` | `#3b82f6` | Info color |
|
|
217
|
+
| `--nbk-focus-ring` | `0 0 0 3px rgba(...)` | Focus ring box-shadow |
|
|
218
|
+
|
|
219
|
+
### Override via CSS
|
|
220
|
+
|
|
221
|
+
Add a `<style>` block in your blog layout or override in your global CSS:
|
|
222
|
+
|
|
223
|
+
```tsx
|
|
224
|
+
// app/blog/layout.tsx
|
|
225
|
+
import 'nextblogkit/styles/blog.css';
|
|
226
|
+
|
|
227
|
+
export default function BlogLayout({ children }: { children: React.ReactNode }) {
|
|
228
|
+
return (
|
|
229
|
+
<>
|
|
230
|
+
<style dangerouslySetInnerHTML={{ __html: `
|
|
231
|
+
:root {
|
|
232
|
+
--nbk-primary: #DC2626;
|
|
233
|
+
--nbk-primary-hover: #B91C1C;
|
|
234
|
+
--nbk-primary-light: #FEE2E2;
|
|
235
|
+
--nbk-bg-secondary: #FEF2F2;
|
|
236
|
+
--nbk-border-focus: #DC2626;
|
|
237
|
+
--nbk-font-heading: "Poppins", system-ui, sans-serif;
|
|
238
|
+
--nbk-font-body: "Inter", system-ui, sans-serif;
|
|
239
|
+
}
|
|
240
|
+
`}} />
|
|
241
|
+
{children}
|
|
242
|
+
</>
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Color Presets
|
|
248
|
+
|
|
249
|
+
Here are some ready-to-use brand palettes:
|
|
250
|
+
|
|
251
|
+
**Red**
|
|
252
|
+
```css
|
|
253
|
+
--nbk-primary: #DC2626;
|
|
254
|
+
--nbk-primary-hover: #B91C1C;
|
|
255
|
+
--nbk-primary-light: #FEE2E2;
|
|
256
|
+
--nbk-bg-secondary: #FEF2F2;
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
**Teal**
|
|
260
|
+
```css
|
|
261
|
+
--nbk-primary: #0891B2;
|
|
262
|
+
--nbk-primary-hover: #0E7490;
|
|
263
|
+
--nbk-primary-light: #CCFBF1;
|
|
264
|
+
--nbk-bg-secondary: #F0FDFA;
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**Purple**
|
|
268
|
+
```css
|
|
269
|
+
--nbk-primary: #7C3AED;
|
|
270
|
+
--nbk-primary-hover: #6D28D9;
|
|
271
|
+
--nbk-primary-light: #EDE9FE;
|
|
272
|
+
--nbk-bg-secondary: #F5F3FF;
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
**Green**
|
|
276
|
+
```css
|
|
277
|
+
--nbk-primary: #059669;
|
|
278
|
+
--nbk-primary-hover: #047857;
|
|
279
|
+
--nbk-primary-light: #D1FAE5;
|
|
280
|
+
--nbk-bg-secondary: #ECFDF5;
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
**Orange**
|
|
284
|
+
```css
|
|
285
|
+
--nbk-primary: #EA580C;
|
|
286
|
+
--nbk-primary-hover: #C2410C;
|
|
287
|
+
--nbk-primary-light: #FFEDD5;
|
|
288
|
+
--nbk-bg-secondary: #FFF7ED;
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### Dark Mode
|
|
292
|
+
|
|
293
|
+
Dark mode variables are automatically applied via `prefers-color-scheme: dark`. Override them in a media query:
|
|
294
|
+
|
|
295
|
+
```css
|
|
296
|
+
@media (prefers-color-scheme: dark) {
|
|
297
|
+
:root {
|
|
298
|
+
--nbk-bg: #0f172a;
|
|
299
|
+
--nbk-bg-secondary: #1e293b;
|
|
300
|
+
--nbk-text: #f1f5f9;
|
|
301
|
+
--nbk-text-muted: #94a3b8;
|
|
302
|
+
--nbk-border: #334155;
|
|
303
|
+
--nbk-card-bg: #1e293b;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## Components
|
|
311
|
+
|
|
312
|
+
### Page-Level Components
|
|
313
|
+
|
|
314
|
+
These are full-page components with built-in layouts, designed to be used directly in your route files. They accept `slots` for injecting custom content without rebuilding the entire page.
|
|
315
|
+
|
|
316
|
+
#### `BlogPostPage`
|
|
317
|
+
|
|
318
|
+
Renders a complete blog post with header, content, TOC, author card, related posts, and share buttons.
|
|
319
|
+
|
|
320
|
+
```tsx
|
|
321
|
+
import { BlogPostPage } from 'nextblogkit/components';
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
**Props:**
|
|
325
|
+
|
|
326
|
+
| Prop | Type | Default | Description |
|
|
327
|
+
|------|------|---------|-------------|
|
|
328
|
+
| `post` | `object` | *required* | The blog post object (from `getPostBySlug`) |
|
|
329
|
+
| `relatedPosts` | `object[]` | `[]` | Related posts to show at the bottom |
|
|
330
|
+
| `showTOC` | `boolean` | `true` | Show table of contents |
|
|
331
|
+
| `tocPosition` | `'sidebar' \| 'top' \| 'none'` | `'sidebar'` | Where to render the TOC |
|
|
332
|
+
| `showAuthor` | `boolean` | `true` | Show author card |
|
|
333
|
+
| `showRelatedPosts` | `boolean` | `true` | Show related posts section |
|
|
334
|
+
| `showShareButtons` | `boolean` | `true` | Show share buttons |
|
|
335
|
+
| `showReadingProgress` | `boolean` | `true` | Show reading progress bar at top |
|
|
336
|
+
| `basePath` | `string` | `'/blog'` | Blog URL prefix (for links) |
|
|
337
|
+
| `className` | `string` | `''` | Additional CSS class |
|
|
338
|
+
| `slots` | `BlogPostSlots` | — | Custom content injection (see below) |
|
|
339
|
+
|
|
340
|
+
**TOC Positions:**
|
|
341
|
+
|
|
342
|
+
- `'sidebar'` — Sticky sidebar on the left, visible on screens >= 1024px. Best for long-form content.
|
|
343
|
+
- `'top'` — Inline TOC above the post content. Good for simpler layouts.
|
|
344
|
+
- `'none'` — No TOC rendered at all.
|
|
345
|
+
|
|
346
|
+
The TOC only renders if the post has more than 2 headings (H2/H3/H4).
|
|
347
|
+
|
|
348
|
+
**Slots:**
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
interface BlogPostSlots {
|
|
352
|
+
header?: React.ReactNode; // Above the article
|
|
353
|
+
footer?: React.ReactNode; // Below the article
|
|
354
|
+
beforeContent?: React.ReactNode; // Between cover image and post body
|
|
355
|
+
afterContent?: React.ReactNode; // After post body, before tags
|
|
356
|
+
}
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
**Example with slots:**
|
|
360
|
+
|
|
361
|
+
```tsx
|
|
362
|
+
<BlogPostPage
|
|
363
|
+
post={post}
|
|
364
|
+
relatedPosts={relatedPosts}
|
|
365
|
+
showTOC
|
|
366
|
+
tocPosition="sidebar"
|
|
367
|
+
basePath="/blogs"
|
|
368
|
+
slots={{
|
|
369
|
+
header: <SiteHeader />,
|
|
370
|
+
footer: <SiteFooter />,
|
|
371
|
+
beforeContent: <div className="ad-banner">Sponsored</div>,
|
|
372
|
+
afterContent: <NewsletterSignup />,
|
|
373
|
+
}}
|
|
374
|
+
/>
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
**Full page example:**
|
|
378
|
+
|
|
379
|
+
```tsx
|
|
380
|
+
// app/blog/[slug]/page.tsx
|
|
381
|
+
import { BlogPostPage } from 'nextblogkit/components';
|
|
382
|
+
import { getPostBySlug, listPosts, generateMetaTags } from 'nextblogkit/lib';
|
|
383
|
+
import type { Metadata } from 'next';
|
|
384
|
+
|
|
385
|
+
interface Props {
|
|
386
|
+
params: Promise<{ slug: string }>;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|
390
|
+
const { slug } = await params;
|
|
391
|
+
const post = await getPostBySlug(slug);
|
|
392
|
+
if (!post) return { title: 'Post Not Found' };
|
|
393
|
+
const meta = generateMetaTags(post);
|
|
394
|
+
return {
|
|
395
|
+
title: meta.title,
|
|
396
|
+
description: meta.description,
|
|
397
|
+
openGraph: meta.openGraph,
|
|
398
|
+
twitter: meta.twitter,
|
|
399
|
+
alternates: { canonical: meta.canonical },
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
export default async function BlogPost({ params }: Props) {
|
|
404
|
+
const { slug } = await params;
|
|
405
|
+
const post = await getPostBySlug(slug);
|
|
406
|
+
|
|
407
|
+
if (!post) {
|
|
408
|
+
return <div className="nbk-not-found">Post not found</div>;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
let relatedPosts: any[] = [];
|
|
412
|
+
if (post.categories?.length) {
|
|
413
|
+
const { posts } = await listPosts({
|
|
414
|
+
status: 'published',
|
|
415
|
+
category: post.categories[0],
|
|
416
|
+
limit: 4,
|
|
417
|
+
});
|
|
418
|
+
relatedPosts = posts
|
|
419
|
+
.filter((p) => String(p._id) !== String(post._id))
|
|
420
|
+
.slice(0, 3);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return (
|
|
424
|
+
<BlogPostPage
|
|
425
|
+
post={JSON.parse(JSON.stringify(post))}
|
|
426
|
+
relatedPosts={JSON.parse(JSON.stringify(relatedPosts))}
|
|
427
|
+
showTOC
|
|
428
|
+
tocPosition="sidebar"
|
|
429
|
+
basePath="/blog"
|
|
430
|
+
/>
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
---
|
|
436
|
+
|
|
437
|
+
#### `BlogListPage`
|
|
438
|
+
|
|
439
|
+
Renders a blog listing with search, categories, pagination, and grid/list/magazine layouts.
|
|
440
|
+
|
|
441
|
+
```tsx
|
|
442
|
+
import { BlogListPage } from 'nextblogkit/components';
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
**Props:**
|
|
446
|
+
|
|
447
|
+
| Prop | Type | Default | Description |
|
|
448
|
+
|------|------|---------|-------------|
|
|
449
|
+
| `posts` | `object[]` | *required* | Array of post objects |
|
|
450
|
+
| `total` | `number` | *required* | Total number of posts (for pagination) |
|
|
451
|
+
| `page` | `number` | `1` | Current page number |
|
|
452
|
+
| `postsPerPage` | `number` | `10` | Posts per page |
|
|
453
|
+
| `categories` | `object[]` | `[]` | List of categories |
|
|
454
|
+
| `activeCategory` | `string` | — | Currently filtered category slug |
|
|
455
|
+
| `showCategories` | `boolean` | `true` | Show category sidebar |
|
|
456
|
+
| `showSearch` | `boolean` | `true` | Show search bar |
|
|
457
|
+
| `layout` | `'grid' \| 'list' \| 'magazine'` | `'grid'` | Post card layout style |
|
|
458
|
+
| `basePath` | `string` | `'/blog'` | Blog URL prefix |
|
|
459
|
+
| `apiPath` | `string` | `'/api/blog'` | API URL prefix (for search) |
|
|
460
|
+
| `className` | `string` | `''` | Additional CSS class |
|
|
461
|
+
| `slots` | `BlogListSlots` | — | Custom content injection (see below) |
|
|
462
|
+
|
|
463
|
+
**Slots:**
|
|
464
|
+
|
|
465
|
+
```typescript
|
|
466
|
+
interface BlogListSlots {
|
|
467
|
+
header?: React.ReactNode; // Above the blog list
|
|
468
|
+
footer?: React.ReactNode; // Below the blog list
|
|
469
|
+
beforePosts?: React.ReactNode; // Between search bar and posts grid
|
|
470
|
+
afterPosts?: React.ReactNode; // After posts grid, before footer
|
|
471
|
+
sidebar?: React.ReactNode; // Replaces the default category sidebar
|
|
472
|
+
renderCard?: (post: any) => React.ReactNode; // Custom card renderer
|
|
473
|
+
}
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
**Example with custom card:**
|
|
477
|
+
|
|
478
|
+
```tsx
|
|
479
|
+
<BlogListPage
|
|
480
|
+
posts={posts}
|
|
481
|
+
total={total}
|
|
482
|
+
basePath="/blogs"
|
|
483
|
+
apiPath="/api/blogs"
|
|
484
|
+
layout="grid"
|
|
485
|
+
slots={{
|
|
486
|
+
header: <h1 className="text-3xl font-bold">Our Blog</h1>,
|
|
487
|
+
renderCard: (post) => (
|
|
488
|
+
<div key={post.slug} className="my-custom-card">
|
|
489
|
+
<h3>{post.title}</h3>
|
|
490
|
+
<p>{post.excerpt}</p>
|
|
491
|
+
</div>
|
|
492
|
+
),
|
|
493
|
+
sidebar: (
|
|
494
|
+
<div>
|
|
495
|
+
<h3>Newsletter</h3>
|
|
496
|
+
<NewsletterSignup />
|
|
497
|
+
</div>
|
|
498
|
+
),
|
|
499
|
+
}}
|
|
500
|
+
/>
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
---
|
|
504
|
+
|
|
505
|
+
### Atomic Components
|
|
506
|
+
|
|
507
|
+
All individual components are exported from `nextblogkit/components` and can be used to build custom layouts:
|
|
508
|
+
|
|
509
|
+
```tsx
|
|
510
|
+
import {
|
|
511
|
+
BlogCard,
|
|
512
|
+
BlogSearch,
|
|
513
|
+
TableOfContents,
|
|
514
|
+
ShareButtons,
|
|
515
|
+
ReadingProgressBar,
|
|
516
|
+
Pagination,
|
|
517
|
+
AuthorCard,
|
|
518
|
+
BreadcrumbNav,
|
|
519
|
+
CategoryList,
|
|
520
|
+
TagCloud,
|
|
521
|
+
CodeBlock,
|
|
522
|
+
} from 'nextblogkit/components';
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
| Component | Description |
|
|
526
|
+
|-----------|-------------|
|
|
527
|
+
| `BlogCard` | Post preview card (vertical or horizontal layout) |
|
|
528
|
+
| `BlogSearch` | Search input with instant results dropdown |
|
|
529
|
+
| `TableOfContents` | Heading list with scroll-to and active heading tracking |
|
|
530
|
+
| `ShareButtons` | Social share buttons (Twitter, Facebook, LinkedIn, copy link) |
|
|
531
|
+
| `ReadingProgressBar` | Top-of-page progress bar tied to scroll position |
|
|
532
|
+
| `Pagination` | Page navigation with prev/next and page numbers |
|
|
533
|
+
| `AuthorCard` | Author info card with avatar and bio |
|
|
534
|
+
| `BreadcrumbNav` | Breadcrumb trail navigation |
|
|
535
|
+
| `CategoryList` | Category list with post counts |
|
|
536
|
+
| `TagCloud` | Tag cloud with weighted sizes |
|
|
537
|
+
| `CodeBlock` | Syntax-highlighted code with Shiki |
|
|
538
|
+
|
|
539
|
+
---
|
|
540
|
+
|
|
541
|
+
### Admin Components
|
|
542
|
+
|
|
543
|
+
```tsx
|
|
544
|
+
import {
|
|
545
|
+
AdminLayout,
|
|
546
|
+
Dashboard,
|
|
547
|
+
PostList,
|
|
548
|
+
PostEditor,
|
|
549
|
+
MediaLibrary,
|
|
550
|
+
CategoryManager,
|
|
551
|
+
SettingsPage,
|
|
552
|
+
SEOPanel,
|
|
553
|
+
useAdminApi,
|
|
554
|
+
setApiBase,
|
|
555
|
+
} from 'nextblogkit/admin';
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
#### `AdminLayout`
|
|
559
|
+
|
|
560
|
+
Wraps all admin pages with sidebar navigation and authentication.
|
|
561
|
+
|
|
562
|
+
| Prop | Type | Default | Description |
|
|
563
|
+
|------|------|---------|-------------|
|
|
564
|
+
| `children` | `ReactNode` | *required* | Page content |
|
|
565
|
+
| `apiKey` | `string` | — | Pre-set API key (bypasses login prompt) |
|
|
566
|
+
| `apiPath` | `string` | `'/api/blog'` | API route prefix for all admin API calls |
|
|
567
|
+
| `adminPath` | `string` | `'/admin/blog'` | Admin route prefix for sidebar nav links |
|
|
568
|
+
|
|
569
|
+
**Important:** If you change `apiPath` in your config, you **must** pass it to `AdminLayout`:
|
|
570
|
+
|
|
571
|
+
```tsx
|
|
572
|
+
// app/admin/blog/layout.tsx
|
|
573
|
+
import { AdminLayout } from 'nextblogkit/admin';
|
|
574
|
+
import 'nextblogkit/styles/admin.css';
|
|
575
|
+
|
|
576
|
+
export default function AdminBlogLayout({ children }: { children: React.ReactNode }) {
|
|
577
|
+
return (
|
|
578
|
+
<AdminLayout apiPath="/api/blogs" adminPath="/admin/blog">
|
|
579
|
+
{children}
|
|
580
|
+
</AdminLayout>
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
---
|
|
586
|
+
|
|
587
|
+
## Styles
|
|
588
|
+
|
|
589
|
+
Import the CSS files you need in your layout files:
|
|
590
|
+
|
|
591
|
+
```tsx
|
|
592
|
+
// Blog pages — import in app/blog/layout.tsx
|
|
593
|
+
import 'nextblogkit/styles/blog.css';
|
|
594
|
+
|
|
595
|
+
// Admin pages — import in app/admin/blog/layout.tsx
|
|
596
|
+
import 'nextblogkit/styles/admin.css';
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
| Stylesheet | When to import | Description |
|
|
600
|
+
|-----------|----------------|-------------|
|
|
601
|
+
| `nextblogkit/styles/blog.css` | Blog layout | Blog list, post page, TOC, cards, pagination |
|
|
602
|
+
| `nextblogkit/styles/admin.css` | Admin layout | Admin panel, dashboard, forms, tables |
|
|
603
|
+
| `nextblogkit/styles/editor.css` | Admin layout (auto) | TipTap editor styles |
|
|
604
|
+
| `nextblogkit/styles/globals.css` | Root layout (optional) | CSS variables, utility classes, buttons, forms |
|
|
605
|
+
| `nextblogkit/styles/prose.css` | Blog layout (optional) | Post body typography |
|
|
606
|
+
|
|
607
|
+
The `blog.css` and `admin.css` files already include the global variables. You only need `globals.css` if using atomic components outside of the page-level components.
|
|
608
|
+
|
|
609
|
+
---
|
|
610
|
+
|
|
611
|
+
## Package Exports
|
|
612
|
+
|
|
613
|
+
```typescript
|
|
614
|
+
// Main exports — config, types, defineConfig
|
|
615
|
+
import { defineConfig } from 'nextblogkit';
|
|
616
|
+
import type { BlogPost, Category, NextBlogKitConfig } from 'nextblogkit';
|
|
617
|
+
|
|
618
|
+
// Library — server-side utilities
|
|
619
|
+
import { getDb, generateMetaTags, searchPosts, calculateSEOScore } from 'nextblogkit/lib';
|
|
620
|
+
|
|
621
|
+
// Blog components — client-side React components
|
|
622
|
+
import { BlogListPage, BlogPostPage, BlogCard, BlogSearch } from 'nextblogkit/components';
|
|
623
|
+
import { TableOfContents, ShareButtons, ReadingProgressBar } from 'nextblogkit/components';
|
|
624
|
+
import { Pagination, AuthorCard, BreadcrumbNav, CategoryList } from 'nextblogkit/components';
|
|
625
|
+
import type { BlogListSlots, BlogPostSlots } from 'nextblogkit/components';
|
|
626
|
+
|
|
627
|
+
// Admin panel — client-side React components
|
|
628
|
+
import { AdminLayout, Dashboard, PostList, PostEditor } from 'nextblogkit/admin';
|
|
629
|
+
import { MediaLibrary, CategoryManager, SettingsPage, SEOPanel } from 'nextblogkit/admin';
|
|
630
|
+
|
|
631
|
+
// Editor — TipTap block editor
|
|
632
|
+
import { BlogEditor } from 'nextblogkit/editor';
|
|
633
|
+
|
|
634
|
+
// API route handlers — re-export in your route.ts files
|
|
635
|
+
export { GET, POST, PUT, DELETE } from 'nextblogkit/api/posts';
|
|
636
|
+
export { GET, POST, DELETE } from 'nextblogkit/api/media';
|
|
637
|
+
export { GET, POST, PUT, DELETE } from 'nextblogkit/api/categories';
|
|
638
|
+
export { GET, PUT } from 'nextblogkit/api/settings';
|
|
639
|
+
export { GET } from 'nextblogkit/api/sitemap';
|
|
640
|
+
export { GET } from 'nextblogkit/api/rss';
|
|
641
|
+
|
|
642
|
+
// Styles
|
|
643
|
+
import 'nextblogkit/styles/blog.css';
|
|
644
|
+
import 'nextblogkit/styles/admin.css';
|
|
645
|
+
import 'nextblogkit/styles/editor.css';
|
|
646
|
+
import 'nextblogkit/styles/prose.css';
|
|
647
|
+
import 'nextblogkit/styles/globals.css';
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
---
|
|
651
|
+
|
|
652
|
+
## Integrating with Your Site
|
|
653
|
+
|
|
654
|
+
A common pattern is wrapping the blog in your site's header, footer, and theme. Here's a full example:
|
|
655
|
+
|
|
656
|
+
```tsx
|
|
657
|
+
// app/blogs/layout.tsx
|
|
658
|
+
import 'nextblogkit/styles/blog.css';
|
|
659
|
+
import SiteHeader from '@/components/SiteHeader';
|
|
660
|
+
import SiteFooter from '@/components/SiteFooter';
|
|
661
|
+
|
|
662
|
+
export default function BlogLayout({ children }: { children: React.ReactNode }) {
|
|
663
|
+
return (
|
|
664
|
+
<>
|
|
665
|
+
{/* Override nextblogkit colors to match your brand */}
|
|
666
|
+
<style dangerouslySetInnerHTML={{ __html: `
|
|
667
|
+
:root {
|
|
668
|
+
--nbk-primary: #DC2626;
|
|
669
|
+
--nbk-primary-hover: #B91C1C;
|
|
670
|
+
--nbk-primary-light: #FEE2E2;
|
|
671
|
+
--nbk-bg-secondary: #FEF2F2;
|
|
672
|
+
--nbk-border-focus: #DC2626;
|
|
673
|
+
--nbk-font-heading: var(--font-geist-sans), system-ui, sans-serif;
|
|
674
|
+
--nbk-font-body: var(--font-geist-sans), system-ui, sans-serif;
|
|
675
|
+
}
|
|
676
|
+
`}} />
|
|
677
|
+
<SiteHeader />
|
|
678
|
+
<div style={{ paddingTop: '4rem' }}>
|
|
679
|
+
{children}
|
|
680
|
+
</div>
|
|
681
|
+
<SiteFooter />
|
|
682
|
+
</>
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
---
|
|
688
|
+
|
|
689
|
+
## API Routes
|
|
690
|
+
|
|
691
|
+
All API routes require a Bearer token (`NEXTBLOGKIT_API_KEY`) for write operations. GET requests for public data (published posts, categories) are unauthenticated.
|
|
692
|
+
|
|
693
|
+
### Posts
|
|
694
|
+
|
|
695
|
+
| Method | Endpoint | Description |
|
|
696
|
+
|--------|----------|-------------|
|
|
697
|
+
| GET | `/api/blog/posts` | List posts (supports `?status=`, `?category=`, `?search=`, `?page=`, `?limit=`) |
|
|
698
|
+
| GET | `/api/blog/posts?slug=my-post` | Get single post by slug |
|
|
699
|
+
| GET | `/api/blog/posts?id=abc123` | Get single post by ID |
|
|
700
|
+
| POST | `/api/blog/posts` | Create post |
|
|
701
|
+
| PUT | `/api/blog/posts` | Update post (requires `?id=`) |
|
|
702
|
+
| DELETE | `/api/blog/posts` | Delete/archive post (requires `?id=`) |
|
|
703
|
+
|
|
704
|
+
### Media
|
|
705
|
+
|
|
706
|
+
| Method | Endpoint | Description |
|
|
707
|
+
|--------|----------|-------------|
|
|
708
|
+
| GET | `/api/blog/media` | List media files |
|
|
709
|
+
| POST | `/api/blog/media` | Upload file (multipart/form-data) |
|
|
710
|
+
| DELETE | `/api/blog/media?id=abc123` | Delete media file |
|
|
711
|
+
|
|
712
|
+
### Categories
|
|
713
|
+
|
|
714
|
+
| Method | Endpoint | Description |
|
|
715
|
+
|--------|----------|-------------|
|
|
716
|
+
| GET | `/api/blog/categories` | List categories |
|
|
717
|
+
| POST | `/api/blog/categories` | Create category |
|
|
718
|
+
| PUT | `/api/blog/categories?id=abc123` | Update category |
|
|
719
|
+
| DELETE | `/api/blog/categories?id=abc123` | Delete category |
|
|
720
|
+
|
|
721
|
+
### Settings
|
|
722
|
+
|
|
723
|
+
| Method | Endpoint | Description |
|
|
724
|
+
|--------|----------|-------------|
|
|
725
|
+
| GET | `/api/blog/settings` | Get settings |
|
|
726
|
+
| PUT | `/api/blog/settings` | Update settings |
|
|
727
|
+
|
|
728
|
+
> **Note:** These endpoints use the default `/api/blog` prefix. If you changed `apiPath` in your config, replace `/api/blog` with your custom path.
|
|
729
|
+
|
|
730
|
+
### Authentication
|
|
731
|
+
|
|
732
|
+
Include the API key as a Bearer token:
|
|
733
|
+
|
|
734
|
+
```bash
|
|
735
|
+
curl -X POST http://localhost:3000/api/blog/posts \
|
|
736
|
+
-H "Authorization: Bearer your-api-key-here" \
|
|
737
|
+
-H "Content-Type: application/json" \
|
|
738
|
+
-d '{"title": "My Post", "content": {...}}'
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
---
|
|
742
|
+
|
|
743
|
+
## Editor Slash Commands
|
|
744
|
+
|
|
745
|
+
Type `/` in the editor to open the command menu:
|
|
746
|
+
|
|
747
|
+
| Command | Description |
|
|
748
|
+
|---------|-------------|
|
|
749
|
+
| Heading 2 | Section heading |
|
|
750
|
+
| Heading 3 | Subsection heading |
|
|
751
|
+
| Bullet List | Unordered list |
|
|
752
|
+
| Numbered List | Ordered list |
|
|
753
|
+
| Task List | Checklist with checkboxes |
|
|
754
|
+
| Blockquote | Quote block |
|
|
755
|
+
| Code Block | Syntax-highlighted code |
|
|
756
|
+
| Image | Upload an image |
|
|
757
|
+
| Table | Insert a 3x3 table |
|
|
758
|
+
| Divider | Horizontal rule |
|
|
759
|
+
| Callout | Info/warning/tip/danger box |
|
|
760
|
+
| FAQ | FAQ section with schema markup |
|
|
761
|
+
| Table of Contents | Auto-generated TOC |
|
|
762
|
+
|
|
763
|
+
---
|
|
764
|
+
|
|
765
|
+
## Custom Image Upload
|
|
766
|
+
|
|
767
|
+
Pass a custom upload handler to the editor:
|
|
768
|
+
|
|
769
|
+
```tsx
|
|
770
|
+
import { BlogEditor } from 'nextblogkit/editor';
|
|
771
|
+
|
|
772
|
+
function MyEditor() {
|
|
773
|
+
const handleUpload = async (file: File) => {
|
|
774
|
+
const formData = new FormData();
|
|
775
|
+
formData.append('file', file);
|
|
776
|
+
|
|
777
|
+
const res = await fetch('/api/blog/media', {
|
|
778
|
+
method: 'POST',
|
|
779
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
780
|
+
body: formData,
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
const data = await res.json();
|
|
784
|
+
return { url: data.url, alt: file.name };
|
|
785
|
+
};
|
|
786
|
+
|
|
787
|
+
return (
|
|
788
|
+
<BlogEditor
|
|
789
|
+
uploadImage={handleUpload}
|
|
790
|
+
onChange={(content) => console.log(content)}
|
|
791
|
+
placeholder="Start writing..."
|
|
792
|
+
/>
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
```
|
|
796
|
+
|
|
797
|
+
---
|
|
798
|
+
|
|
799
|
+
## SEO
|
|
800
|
+
|
|
801
|
+
NextBlogKit automatically generates:
|
|
802
|
+
|
|
803
|
+
- **Meta tags** — Title, description, canonical URL
|
|
804
|
+
- **Open Graph** — og:title, og:description, og:image, og:type
|
|
805
|
+
- **Twitter Cards** — twitter:card, twitter:title, twitter:description
|
|
806
|
+
- **JSON-LD** — BlogPosting and FAQPage structured data
|
|
807
|
+
- **Sitemap** — Dynamic XML sitemap at `/api/blog/sitemap.xml`
|
|
808
|
+
- **RSS** — RSS 2.0 feed at `/api/blog/rss.xml`
|
|
809
|
+
|
|
810
|
+
The built-in SEO scorer checks 17 factors including keyword density, title length, heading hierarchy, image alt text, and readability.
|
|
811
|
+
|
|
812
|
+
---
|
|
813
|
+
|
|
814
|
+
## CLI Commands
|
|
815
|
+
|
|
816
|
+
```bash
|
|
817
|
+
npx nextblogkit init # Scaffold blog into your Next.js project
|
|
818
|
+
npx nextblogkit seed # Insert example blog content
|
|
819
|
+
npx nextblogkit health # Check MongoDB and R2 connectivity
|
|
820
|
+
npx nextblogkit migrate # Create database indexes
|
|
821
|
+
```
|
|
822
|
+
|
|
823
|
+
### Init options
|
|
824
|
+
|
|
825
|
+
```bash
|
|
826
|
+
npx nextblogkit init --blog-path /articles --admin-path /admin/articles --api-path /api/articles
|
|
827
|
+
```
|
|
828
|
+
|
|
829
|
+
---
|
|
830
|
+
|
|
831
|
+
## What Gets Scaffolded
|
|
832
|
+
|
|
833
|
+
Running `npx nextblogkit init` creates thin wrapper files inside your `app/` directory. Each file is a one-liner that re-exports from the package:
|
|
834
|
+
|
|
835
|
+
```
|
|
836
|
+
app/
|
|
837
|
+
├── blog/
|
|
838
|
+
│ ├── page.tsx # Blog list page
|
|
839
|
+
│ ├── [slug]/page.tsx # Individual post page (with SEO metadata)
|
|
840
|
+
│ └── category/[slug]/page.tsx # Category filter page
|
|
841
|
+
├── admin/blog/
|
|
842
|
+
│ ├── layout.tsx # Admin layout with sidebar
|
|
843
|
+
│ ├── page.tsx # Dashboard
|
|
844
|
+
│ ├── posts/page.tsx # Post list
|
|
845
|
+
│ ├── new/page.tsx # New post editor
|
|
846
|
+
│ ├── [id]/edit/page.tsx # Edit post
|
|
847
|
+
│ ├── media/page.tsx # Media library
|
|
848
|
+
│ ├── categories/page.tsx # Category manager
|
|
849
|
+
│ └── settings/page.tsx # Settings
|
|
850
|
+
└── api/blog/
|
|
851
|
+
├── posts/route.ts # Posts CRUD
|
|
852
|
+
├── media/route.ts # Media upload/list/delete
|
|
853
|
+
├── categories/route.ts # Categories CRUD
|
|
854
|
+
├── settings/route.ts # Settings read/update
|
|
855
|
+
├── sitemap.xml/route.ts # Dynamic sitemap
|
|
856
|
+
└── rss.xml/route.ts # RSS feed
|
|
857
|
+
```
|
|
858
|
+
|
|
859
|
+
Because these are thin wrappers, you can customize any page by editing the generated file directly.
|
|
860
|
+
|
|
861
|
+
---
|
|
862
|
+
|
|
863
|
+
## Local Development (without R2)
|
|
864
|
+
|
|
865
|
+
If you want to try NextBlogKit locally without Cloudflare R2, images will fall back to `URL.createObjectURL` (browser-only, non-persistent). For a fully working local setup:
|
|
866
|
+
|
|
867
|
+
1. **MongoDB** — Use a free [MongoDB Atlas](https://www.mongodb.com/atlas) cluster, or run locally:
|
|
868
|
+
```bash
|
|
869
|
+
# Using Docker
|
|
870
|
+
docker run -d -p 27017:27017 --name mongo mongo:7
|
|
871
|
+
# Then use: NEXTBLOGKIT_MONGODB_URI=mongodb://localhost:27017/nextblogkit
|
|
872
|
+
```
|
|
873
|
+
|
|
874
|
+
2. **R2** — Create a free Cloudflare R2 bucket at [dash.cloudflare.com](https://dash.cloudflare.com). The free tier includes 10 GB storage and 10 million reads/month.
|
|
875
|
+
|
|
876
|
+
3. **API Key** — Generate a secure key:
|
|
877
|
+
```bash
|
|
878
|
+
openssl rand -hex 32
|
|
879
|
+
```
|
|
880
|
+
|
|
881
|
+
---
|
|
882
|
+
|
|
883
|
+
## Project Structure (package internals)
|
|
884
|
+
|
|
885
|
+
```
|
|
886
|
+
src/
|
|
887
|
+
├── lib/ # Server-side: DB, storage, config, SEO, search, utils
|
|
888
|
+
├── api/ # Next.js API route handlers
|
|
889
|
+
├── editor/ # TipTap editor + extensions (client-side)
|
|
890
|
+
├── admin/ # Admin panel components (client-side)
|
|
891
|
+
├── components/ # Public blog components (client-side)
|
|
892
|
+
├── styles/ # CSS files
|
|
893
|
+
├── cli/ # CLI tool (init, seed, health, migrate)
|
|
894
|
+
└── index.ts # Main entry point
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
---
|
|
898
|
+
|
|
899
|
+
## Developing NextBlogKit Itself
|
|
900
|
+
|
|
901
|
+
If you want to contribute or develop locally:
|
|
902
|
+
|
|
903
|
+
```bash
|
|
904
|
+
git clone https://github.com/patidar-santosh/nextblogkit.git
|
|
905
|
+
cd nextblogkit
|
|
906
|
+
pnpm install
|
|
907
|
+
pnpm run build # Build the package
|
|
908
|
+
pnpm run dev # Watch mode for development
|
|
909
|
+
```
|
|
910
|
+
|
|
911
|
+
### Using a local build in a Next.js project
|
|
912
|
+
|
|
913
|
+
```bash
|
|
914
|
+
# Build, pack, and install
|
|
915
|
+
cd /path/to/nextblogkit
|
|
916
|
+
npm run build
|
|
917
|
+
cp -r src/styles dist/styles
|
|
918
|
+
npm pack
|
|
919
|
+
|
|
920
|
+
# In your Next.js project
|
|
921
|
+
cd /path/to/my-nextjs-app
|
|
922
|
+
pnpm install /path/to/nextblogkit/nextblogkit-0.6.0.tgz
|
|
923
|
+
```
|
|
924
|
+
|
|
925
|
+
After installing, clear the Next.js cache:
|
|
926
|
+
|
|
927
|
+
```bash
|
|
928
|
+
rm -rf .next
|
|
929
|
+
pnpm dev
|
|
930
|
+
```
|
|
931
|
+
|
|
932
|
+
---
|
|
933
|
+
|
|
934
|
+
## Tech Stack
|
|
935
|
+
|
|
936
|
+
| Layer | Technology |
|
|
937
|
+
|-------|-----------|
|
|
938
|
+
| Framework | Next.js 14+ (App Router) |
|
|
939
|
+
| Editor | TipTap 2.x |
|
|
940
|
+
| Database | MongoDB (native driver) |
|
|
941
|
+
| Storage | Cloudflare R2 (S3-compatible) |
|
|
942
|
+
| Image Processing | Sharp |
|
|
943
|
+
| Syntax Highlighting | Shiki |
|
|
944
|
+
| Validation | Zod |
|
|
945
|
+
| CLI | Commander.js |
|
|
946
|
+
| Build | tsup (ESM + CJS) |
|
|
947
|
+
| Styling | CSS Variables + Tailwind-compatible |
|
|
948
|
+
|
|
949
|
+
## License
|
|
950
|
+
|
|
951
|
+
MIT
|