chalknotes 0.0.30 → 0.0.32
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 +258 -255
- package/bin/cli.js +421 -212
- package/package.json +1 -1
- package/src/index.js +1 -2
- package/src/components/NotionRenderer.jsx +0 -82
package/README.md
CHANGED
@@ -1,255 +1,258 @@
|
|
1
|
-
# ✏️ ChalkNotes
|
2
|
-
|
3
|
-
**Turn your Notion database into a blog with zero config. Works with Next.js Page Router and App Router.**
|
4
|
-
|
5
|
-
---
|
6
|
-
|
7
|
-
## 🚀 Features
|
8
|
-
|
9
|
-
- 📄 Fetch blog posts from Notion
|
10
|
-
- 🪄 Auto-generate routes for App Router or Page Router
|
11
|
-
- ⚙️ Helpers for `getStaticProps` / `getStaticPaths`
|
12
|
-
- 🎨 Clean, responsive themes (light & dark mode)
|
13
|
-
- 🔧 Interactive configuration setup
|
14
|
-
- 📁 Customizable route paths
|
15
|
-
- 🧠 Minimal setup – just run `chalknotes`
|
16
|
-
- 🖼️ **Rich content support** - Images, code blocks, lists, quotes, and more
|
17
|
-
- 🔒 **Secure rendering** - React-based component instead of raw HTML
|
18
|
-
|
19
|
-
---
|
20
|
-
|
21
|
-
## 📦 Installation
|
22
|
-
|
23
|
-
```bash
|
24
|
-
pnpm add chalknotes
|
25
|
-
# or
|
26
|
-
npm install chalknotes
|
27
|
-
```
|
28
|
-
|
29
|
-
---
|
30
|
-
|
31
|
-
## 🧙♂️ Quick Start
|
32
|
-
|
33
|
-
1. **Set up environment variables**
|
34
|
-
```bash
|
35
|
-
# Create .env file
|
36
|
-
NOTION_TOKEN=secret_...
|
37
|
-
NOTION_DATABASE_ID=xxxxxxxxxxxxxxxxxxxxxx
|
38
|
-
```
|
39
|
-
|
40
|
-
2. **Run the CLI**
|
41
|
-
```bash
|
42
|
-
npx chalknotes
|
43
|
-
```
|
44
|
-
|
45
|
-
3. **That's it!** ✅
|
46
|
-
- Automatically detects if you're using **App Router** or **Page Router**
|
47
|
-
- Creates `blog.config.js` with default configuration (if needed)
|
48
|
-
- Generates blog routes with clean, responsive templates
|
49
|
-
- Supports light and dark themes
|
50
|
-
- **Renders rich Notion content** with images, code blocks, and more
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
- **`
|
75
|
-
- **`
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
-
|
84
|
-
-
|
85
|
-
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
-
|
90
|
-
-
|
91
|
-
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
export
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
<
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
```
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
- **
|
211
|
-
- **
|
212
|
-
- **
|
213
|
-
- **
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
-
|
224
|
-
-
|
225
|
-
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
- [ ]
|
241
|
-
- [ ]
|
242
|
-
- [ ]
|
243
|
-
- [ ]
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
1
|
+
# ✏️ ChalkNotes
|
2
|
+
|
3
|
+
**Turn your Notion database into a blog with zero config. Works with Next.js Page Router and App Router.**
|
4
|
+
|
5
|
+
---
|
6
|
+
|
7
|
+
## 🚀 Features
|
8
|
+
|
9
|
+
- 📄 Fetch blog posts from Notion
|
10
|
+
- 🪄 Auto-generate routes for App Router or Page Router
|
11
|
+
- ⚙️ Helpers for `getStaticProps` / `getStaticPaths`
|
12
|
+
- 🎨 Clean, responsive themes (light & dark mode)
|
13
|
+
- 🔧 Interactive configuration setup
|
14
|
+
- 📁 Customizable route paths
|
15
|
+
- 🧠 Minimal setup – just run `chalknotes`
|
16
|
+
- 🖼️ **Rich content support** - Images, code blocks, lists, quotes, and more
|
17
|
+
- 🔒 **Secure rendering** - React-based component instead of raw HTML
|
18
|
+
|
19
|
+
---
|
20
|
+
|
21
|
+
## 📦 Installation
|
22
|
+
|
23
|
+
```bash
|
24
|
+
pnpm add chalknotes
|
25
|
+
# or
|
26
|
+
npm install chalknotes
|
27
|
+
```
|
28
|
+
|
29
|
+
---
|
30
|
+
|
31
|
+
## 🧙♂️ Quick Start
|
32
|
+
|
33
|
+
1. **Set up environment variables**
|
34
|
+
```bash
|
35
|
+
# Create .env file
|
36
|
+
NOTION_TOKEN=secret_...
|
37
|
+
NOTION_DATABASE_ID=xxxxxxxxxxxxxxxxxxxxxx
|
38
|
+
```
|
39
|
+
|
40
|
+
2. **Run the CLI**
|
41
|
+
```bash
|
42
|
+
npx chalknotes
|
43
|
+
```
|
44
|
+
|
45
|
+
3. **That's it!** ✅
|
46
|
+
- Automatically detects if you're using **App Router** or **Page Router**
|
47
|
+
- Creates `blog.config.js` with default configuration (if needed)
|
48
|
+
- Generates blog routes with clean, responsive templates
|
49
|
+
- Supports light and dark themes
|
50
|
+
- **Renders rich Notion content** with images, code blocks, and more
|
51
|
+
- **Configures `next.config.js`** for unoptimized images (works with any Notion domain)
|
52
|
+
|
53
|
+
---
|
54
|
+
|
55
|
+
## 🔧 Configuration
|
56
|
+
|
57
|
+
The CLI creates a `blog.config.js` file in your project root. Customize it to match your needs:
|
58
|
+
|
59
|
+
```javascript
|
60
|
+
module.exports = {
|
61
|
+
// Notion Configuration
|
62
|
+
notionToken: process.env.NOTION_TOKEN,
|
63
|
+
notionDatabaseId: process.env.NOTION_DATABASE_ID,
|
64
|
+
|
65
|
+
// Blog Configuration
|
66
|
+
routeBasePath: '/blog', // Default: '/blog'
|
67
|
+
theme: 'default', // Options: 'default' (light) or 'dark'
|
68
|
+
plugins: [],
|
69
|
+
};
|
70
|
+
```
|
71
|
+
|
72
|
+
### Configuration Options
|
73
|
+
|
74
|
+
- **`routeBasePath`**: Customize your blog route (e.g., `/posts`, `/articles`)
|
75
|
+
- **`theme`**: Choose between `'default'` (light mode) or `'dark'` (dark mode)
|
76
|
+
- **`plugins`**: Array for future plugin support
|
77
|
+
|
78
|
+
---
|
79
|
+
|
80
|
+
## 🎨 Themes
|
81
|
+
|
82
|
+
### Default Theme (Light Mode)
|
83
|
+
- Clean white cards with subtle shadows
|
84
|
+
- Light gray background
|
85
|
+
- Dark text for optimal readability
|
86
|
+
- Responsive design with Tailwind CSS
|
87
|
+
|
88
|
+
### Dark Theme
|
89
|
+
- Dark background with gray cards
|
90
|
+
- White text with proper contrast
|
91
|
+
- Inverted typography for dark mode
|
92
|
+
- Same responsive layout
|
93
|
+
|
94
|
+
---
|
95
|
+
|
96
|
+
## 📚 Usage in Next.js
|
97
|
+
|
98
|
+
### Page Router
|
99
|
+
|
100
|
+
Creates:
|
101
|
+
|
102
|
+
```js
|
103
|
+
// pages/blog/[slug].js (or custom route)
|
104
|
+
import { getStaticPropsForPost, getStaticPathsForPosts } from 'chalknotes';
|
105
|
+
import NotionRenderer from './NotionRenderer';
|
106
|
+
|
107
|
+
export const getStaticProps = getStaticPropsForPost;
|
108
|
+
export const getStaticPaths = getStaticPathsForPosts;
|
109
|
+
|
110
|
+
export default function BlogPost({ post }) {
|
111
|
+
return (
|
112
|
+
<div className="min-h-screen bg-gray-50">
|
113
|
+
<main className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
114
|
+
<article className="bg-white rounded-lg shadow-sm border border-gray-200 p-8">
|
115
|
+
<h1 className="text-4xl font-bold text-gray-900 mb-6 leading-tight">
|
116
|
+
{post.title}
|
117
|
+
</h1>
|
118
|
+
<NotionRenderer blocks={post.blocks} />
|
119
|
+
</article>
|
120
|
+
</main>
|
121
|
+
</div>
|
122
|
+
);
|
123
|
+
}
|
124
|
+
```
|
125
|
+
|
126
|
+
---
|
127
|
+
|
128
|
+
### App Router
|
129
|
+
|
130
|
+
Creates:
|
131
|
+
|
132
|
+
```jsx
|
133
|
+
// app/blog/[slug]/page.jsx (or custom route)
|
134
|
+
import { getPostBySlug } from 'chalknotes';
|
135
|
+
import NotionRenderer from './NotionRenderer';
|
136
|
+
|
137
|
+
export default async function BlogPost({ params }) {
|
138
|
+
const post = await getPostBySlug(params.slug);
|
139
|
+
|
140
|
+
return (
|
141
|
+
<div className="min-h-screen bg-gray-50">
|
142
|
+
<main className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
143
|
+
<article className="bg-white rounded-lg shadow-sm border border-gray-200 p-8">
|
144
|
+
<h1 className="text-4xl font-bold text-gray-900 mb-6 leading-tight">
|
145
|
+
{post.title}
|
146
|
+
</h1>
|
147
|
+
<NotionRenderer blocks={post.blocks} />
|
148
|
+
</article>
|
149
|
+
</main>
|
150
|
+
</div>
|
151
|
+
);
|
152
|
+
}
|
153
|
+
```
|
154
|
+
|
155
|
+
---
|
156
|
+
|
157
|
+
## 🧩 API
|
158
|
+
|
159
|
+
### `getPostBySlug(slug: string)`
|
160
|
+
Fetches a post and returns structured data for rendering.
|
161
|
+
|
162
|
+
```js
|
163
|
+
const post = await getPostBySlug('my-post-title');
|
164
|
+
// Returns: { title, slug, blocks, notionPageId }
|
165
|
+
```
|
166
|
+
|
167
|
+
---
|
168
|
+
|
169
|
+
### `getAllPosts()`
|
170
|
+
Returns all published posts with metadata:
|
171
|
+
|
172
|
+
```js
|
173
|
+
[
|
174
|
+
{
|
175
|
+
title: "My First Post",
|
176
|
+
slug: "my-first-post",
|
177
|
+
notionPageId: "xxxxxxxx"
|
178
|
+
},
|
179
|
+
...
|
180
|
+
]
|
181
|
+
```
|
182
|
+
|
183
|
+
---
|
184
|
+
|
185
|
+
### `getStaticPropsForPost()`
|
186
|
+
For use with `getStaticProps` in Page Router.
|
187
|
+
|
188
|
+
---
|
189
|
+
|
190
|
+
### `getStaticPathsForPosts()`
|
191
|
+
For use with `getStaticPaths` in Page Router.
|
192
|
+
|
193
|
+
---
|
194
|
+
|
195
|
+
### `NotionRenderer`
|
196
|
+
React component for rendering Notion blocks (scaffolded in your project):
|
197
|
+
|
198
|
+
```jsx
|
199
|
+
import NotionRenderer from './NotionRenderer';
|
200
|
+
|
201
|
+
<NotionRenderer blocks={post.blocks} />
|
202
|
+
```
|
203
|
+
|
204
|
+
---
|
205
|
+
|
206
|
+
## 🖼️ Supported Content Types
|
207
|
+
|
208
|
+
The `NotionRenderer` component supports all major Notion block types:
|
209
|
+
|
210
|
+
- **Text blocks**: Paragraphs, headings (H1, H2, H3)
|
211
|
+
- **Lists**: Bulleted and numbered lists
|
212
|
+
- **Code blocks**: With syntax highlighting support
|
213
|
+
- **Images**: With captions and Next.js optimization
|
214
|
+
- **Quotes**: Styled blockquotes
|
215
|
+
- **Dividers**: Horizontal rules
|
216
|
+
- **Rich text**: Bold, italic, strikethrough, code, links
|
217
|
+
|
218
|
+
---
|
219
|
+
|
220
|
+
## 🎨 Styling
|
221
|
+
|
222
|
+
The generated templates use Tailwind CSS with:
|
223
|
+
- Clean, minimal design
|
224
|
+
- Responsive layout
|
225
|
+
- Typography optimized for readability
|
226
|
+
- Proper spacing and hierarchy
|
227
|
+
- Light and dark mode support
|
228
|
+
- **Rich content styling** for all Notion block types
|
229
|
+
|
230
|
+
Make sure you have Tailwind CSS installed in your project:
|
231
|
+
|
232
|
+
```bash
|
233
|
+
npm install -D tailwindcss @tailwindcss/typography
|
234
|
+
```
|
235
|
+
|
236
|
+
---
|
237
|
+
|
238
|
+
## 📅 Roadmap
|
239
|
+
|
240
|
+
- [ ] Plugin system for custom components
|
241
|
+
- [ ] More Notion block support (callouts, bookmarks, toggles)
|
242
|
+
- [ ] RSS feed support
|
243
|
+
- [ ] MDX or Markdown output option
|
244
|
+
- [ ] Custom theme templates
|
245
|
+
- [ ] Search functionality
|
246
|
+
- [ ] Categories and tags support
|
247
|
+
|
248
|
+
---
|
249
|
+
|
250
|
+
## 💡 Inspiration
|
251
|
+
|
252
|
+
Built to scratch an itch while exploring the simplicity of tools like [feather.so](https://feather.so/) and [Notion Blog](https://github.com/ijjk/notion-blog).
|
253
|
+
|
254
|
+
---
|
255
|
+
|
256
|
+
## 🧑💻 Author
|
257
|
+
|
258
|
+
[NepTune](https://github.com/yourhandle) • MIT License
|
package/bin/cli.js
CHANGED
@@ -1,213 +1,422 @@
|
|
1
|
-
#!/usr/bin/env node
|
2
|
-
const fs = require("fs");
|
3
|
-
const path = require("path");
|
4
|
-
const dotenv = require("dotenv");
|
5
|
-
const readline = require("readline");
|
6
|
-
dotenv.config();
|
7
|
-
|
8
|
-
console.log("🚀 Initializing ChalkNotes...");
|
9
|
-
|
10
|
-
// Check environment variables
|
11
|
-
if (!process.env.NOTION_TOKEN) {
|
12
|
-
console.error("❌ NOTION_TOKEN is not set in .env file");
|
13
|
-
console.log("💡 Please create a .env file with your NOTION_TOKEN");
|
14
|
-
process.exit(1);
|
15
|
-
}
|
16
|
-
|
17
|
-
if (!process.env.NOTION_DATABASE_ID) {
|
18
|
-
console.error("❌ NOTION_DATABASE_ID is not set in .env file");
|
19
|
-
console.log("💡 Please create a .env file with your NOTION_DATABASE_ID");
|
20
|
-
process.exit(1);
|
21
|
-
}
|
22
|
-
|
23
|
-
console.log("✅ Environment variables are set");
|
24
|
-
|
25
|
-
const configPath = path.join(process.cwd(), 'blog.config.js');
|
26
|
-
|
27
|
-
// Check if blog.config.js exists
|
28
|
-
if (!fs.existsSync(configPath)) {
|
29
|
-
console.log("\n❌ blog.config.js not found");
|
30
|
-
console.log("This file is required to configure your blog settings.");
|
31
|
-
|
32
|
-
const rl = readline.createInterface({
|
33
|
-
input: process.stdin,
|
34
|
-
output: process.stdout
|
35
|
-
});
|
36
|
-
|
37
|
-
rl.question("Would you like to create a default blog.config.js? (y/n): ", (answer) => {
|
38
|
-
if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
|
39
|
-
console.log("📝 Creating blog.config.js with default configuration...");
|
40
|
-
|
41
|
-
const configTemplate = `module.exports = {
|
42
|
-
notionToken: process.env.NOTION_TOKEN,
|
43
|
-
notionDatabaseId: process.env.NOTION_DATABASE_ID,
|
44
|
-
routeBasePath: '/blog',
|
45
|
-
theme: 'default',
|
46
|
-
plugins: [],
|
47
|
-
};`.trim();
|
48
|
-
|
49
|
-
fs.writeFileSync(configPath, configTemplate);
|
50
|
-
console.log("✅ Created blog.config.js with default configuration");
|
51
|
-
console.log("\n💡 Now you can re-run 'npx chalknotes' to scaffold your blog pages!");
|
52
|
-
} else {
|
53
|
-
console.log("❌ Please create a blog.config.js file and try again.");
|
54
|
-
}
|
55
|
-
rl.close();
|
56
|
-
});
|
57
|
-
return;
|
58
|
-
}
|
59
|
-
|
60
|
-
// Load configuration
|
61
|
-
let config;
|
62
|
-
try {
|
63
|
-
config = require(configPath);
|
64
|
-
} catch (error) {
|
65
|
-
console.error("❌ Error loading blog.config.js:", error.message);
|
66
|
-
process.exit(1);
|
67
|
-
}
|
68
|
-
|
69
|
-
// Set defaults for missing config values
|
70
|
-
config.routeBasePath = config.routeBasePath || '/blog';
|
71
|
-
config.theme = config.theme || 'default';
|
72
|
-
|
73
|
-
console.log("✅ Configuration loaded successfully");
|
74
|
-
console.log(`📁 Route base path: ${config.routeBasePath}`);
|
75
|
-
console.log(`🎨 Theme: ${config.theme}`);
|
76
|
-
|
77
|
-
// Ask to proceed with scaffolding
|
78
|
-
console.log("\n🔨 Ready to scaffold your blog page?");
|
79
|
-
console.log("This will create a clean, responsive blog template using Tailwind CSS.");
|
80
|
-
console.log("Press Enter to continue or Ctrl+C to cancel...");
|
81
|
-
|
82
|
-
// Create blog page templates based on theme and route
|
83
|
-
const pageRouter = path.join(process.cwd(), '/pages')
|
84
|
-
const appRouter = path.join(process.cwd(), '/app')
|
85
|
-
|
86
|
-
// Generate templates based on theme
|
87
|
-
function getTemplates(theme, routeBasePath) {
|
88
|
-
const routePath = routeBasePath.replace(/^\//, ''); // Remove leading slash
|
89
|
-
|
90
|
-
if (theme === 'dark') {
|
91
|
-
return {
|
92
|
-
pageRouter: `
|
93
|
-
import { getStaticPropsForPost, getStaticPathsForPosts
|
94
|
-
|
95
|
-
|
96
|
-
export const
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
export
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
<
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
const
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
1
|
+
#!/usr/bin/env node
|
2
|
+
const fs = require("fs");
|
3
|
+
const path = require("path");
|
4
|
+
const dotenv = require("dotenv");
|
5
|
+
const readline = require("readline");
|
6
|
+
dotenv.config();
|
7
|
+
|
8
|
+
console.log("🚀 Initializing ChalkNotes...");
|
9
|
+
|
10
|
+
// Check environment variables
|
11
|
+
if (!process.env.NOTION_TOKEN) {
|
12
|
+
console.error("❌ NOTION_TOKEN is not set in .env file");
|
13
|
+
console.log("💡 Please create a .env file with your NOTION_TOKEN");
|
14
|
+
process.exit(1);
|
15
|
+
}
|
16
|
+
|
17
|
+
if (!process.env.NOTION_DATABASE_ID) {
|
18
|
+
console.error("❌ NOTION_DATABASE_ID is not set in .env file");
|
19
|
+
console.log("💡 Please create a .env file with your NOTION_DATABASE_ID");
|
20
|
+
process.exit(1);
|
21
|
+
}
|
22
|
+
|
23
|
+
console.log("✅ Environment variables are set");
|
24
|
+
|
25
|
+
const configPath = path.join(process.cwd(), 'blog.config.js');
|
26
|
+
|
27
|
+
// Check if blog.config.js exists
|
28
|
+
if (!fs.existsSync(configPath)) {
|
29
|
+
console.log("\n❌ blog.config.js not found");
|
30
|
+
console.log("This file is required to configure your blog settings.");
|
31
|
+
|
32
|
+
const rl = readline.createInterface({
|
33
|
+
input: process.stdin,
|
34
|
+
output: process.stdout
|
35
|
+
});
|
36
|
+
|
37
|
+
rl.question("Would you like to create a default blog.config.js? (y/n): ", (answer) => {
|
38
|
+
if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
|
39
|
+
console.log("📝 Creating blog.config.js with default configuration...");
|
40
|
+
|
41
|
+
const configTemplate = `module.exports = {
|
42
|
+
notionToken: process.env.NOTION_TOKEN,
|
43
|
+
notionDatabaseId: process.env.NOTION_DATABASE_ID,
|
44
|
+
routeBasePath: '/blog',
|
45
|
+
theme: 'default',
|
46
|
+
plugins: [],
|
47
|
+
};`.trim();
|
48
|
+
|
49
|
+
fs.writeFileSync(configPath, configTemplate);
|
50
|
+
console.log("✅ Created blog.config.js with default configuration");
|
51
|
+
console.log("\n💡 Now you can re-run 'npx chalknotes' to scaffold your blog pages!");
|
52
|
+
} else {
|
53
|
+
console.log("❌ Please create a blog.config.js file and try again.");
|
54
|
+
}
|
55
|
+
rl.close();
|
56
|
+
});
|
57
|
+
return;
|
58
|
+
}
|
59
|
+
|
60
|
+
// Load configuration
|
61
|
+
let config;
|
62
|
+
try {
|
63
|
+
config = require(configPath);
|
64
|
+
} catch (error) {
|
65
|
+
console.error("❌ Error loading blog.config.js:", error.message);
|
66
|
+
process.exit(1);
|
67
|
+
}
|
68
|
+
|
69
|
+
// Set defaults for missing config values
|
70
|
+
config.routeBasePath = config.routeBasePath || '/blog';
|
71
|
+
config.theme = config.theme || 'default';
|
72
|
+
|
73
|
+
console.log("✅ Configuration loaded successfully");
|
74
|
+
console.log(`📁 Route base path: ${config.routeBasePath}`);
|
75
|
+
console.log(`🎨 Theme: ${config.theme}`);
|
76
|
+
|
77
|
+
// Ask to proceed with scaffolding
|
78
|
+
console.log("\n🔨 Ready to scaffold your blog page?");
|
79
|
+
console.log("This will create a clean, responsive blog template using Tailwind CSS.");
|
80
|
+
console.log("Press Enter to continue or Ctrl+C to cancel...");
|
81
|
+
|
82
|
+
// Create blog page templates based on theme and route
|
83
|
+
const pageRouter = path.join(process.cwd(), '/pages')
|
84
|
+
const appRouter = path.join(process.cwd(), '/app')
|
85
|
+
|
86
|
+
// Generate templates based on theme
|
87
|
+
function getTemplates(theme, routeBasePath) {
|
88
|
+
const routePath = routeBasePath.replace(/^\//, ''); // Remove leading slash
|
89
|
+
|
90
|
+
if (theme === 'dark') {
|
91
|
+
return {
|
92
|
+
pageRouter: `
|
93
|
+
import { getStaticPropsForPost, getStaticPathsForPosts } from 'chalknotes';
|
94
|
+
import NotionRenderer from './NotionRenderer';
|
95
|
+
|
96
|
+
export const getStaticProps = getStaticPropsForPost;
|
97
|
+
export const getStaticPaths = getStaticPathsForPosts;
|
98
|
+
|
99
|
+
export default function BlogPost({ post }) {
|
100
|
+
return (
|
101
|
+
<div className="min-h-screen bg-gray-900">
|
102
|
+
<main className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
103
|
+
<article className="bg-gray-800 rounded-lg shadow-lg border border-gray-700 p-8">
|
104
|
+
<h1 className="text-4xl font-bold text-white mb-6 leading-tight">
|
105
|
+
{post.title}
|
106
|
+
</h1>
|
107
|
+
<NotionRenderer blocks={post.blocks} />
|
108
|
+
</article>
|
109
|
+
</main>
|
110
|
+
</div>
|
111
|
+
);
|
112
|
+
}`.trim(),
|
113
|
+
appRouter: `
|
114
|
+
import { getPostBySlug } from 'chalknotes';
|
115
|
+
import NotionRenderer from './NotionRenderer';
|
116
|
+
|
117
|
+
export default async function BlogPost({ params }) {
|
118
|
+
const post = await getPostBySlug(params.slug);
|
119
|
+
|
120
|
+
return (
|
121
|
+
<div className="min-h-screen bg-gray-900">
|
122
|
+
<main className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
123
|
+
<article className="bg-gray-800 rounded-lg shadow-lg border border-gray-700 p-8">
|
124
|
+
<h1 className="text-4xl font-bold text-white mb-6 leading-tight">
|
125
|
+
{post.title}
|
126
|
+
</h1>
|
127
|
+
<NotionRenderer blocks={post.blocks} />
|
128
|
+
</article>
|
129
|
+
</main>
|
130
|
+
</div>
|
131
|
+
);
|
132
|
+
}`.trim()
|
133
|
+
};
|
134
|
+
} else {
|
135
|
+
// Default theme (light mode)
|
136
|
+
return {
|
137
|
+
pageRouter: `
|
138
|
+
import { getStaticPropsForPost, getStaticPathsForPosts } from 'chalknotes';
|
139
|
+
import NotionRenderer from './NotionRenderer';
|
140
|
+
|
141
|
+
export const getStaticProps = getStaticPropsForPost;
|
142
|
+
export const getStaticPaths = getStaticPathsForPosts;
|
143
|
+
|
144
|
+
export default function BlogPost({ post }) {
|
145
|
+
return (
|
146
|
+
<div className="min-h-screen bg-gray-50">
|
147
|
+
<main className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
148
|
+
<article className="bg-white rounded-lg shadow-sm border border-gray-200 p-8">
|
149
|
+
<h1 className="text-4xl font-bold text-gray-900 mb-6 leading-tight">
|
150
|
+
{post.title}
|
151
|
+
</h1>
|
152
|
+
<NotionRenderer blocks={post.blocks} />
|
153
|
+
</article>
|
154
|
+
</main>
|
155
|
+
</div>
|
156
|
+
);
|
157
|
+
}`.trim(),
|
158
|
+
appRouter: `
|
159
|
+
import { getPostBySlug } from 'chalknotes';
|
160
|
+
import NotionRenderer from './NotionRenderer';
|
161
|
+
|
162
|
+
export default async function BlogPost({ params }) {
|
163
|
+
const post = await getPostBySlug(params.slug);
|
164
|
+
|
165
|
+
return (
|
166
|
+
<div className="min-h-screen bg-gray-50">
|
167
|
+
<main className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
168
|
+
<article className="bg-white rounded-lg shadow-sm border border-gray-200 p-8">
|
169
|
+
<h1 className="text-4xl font-bold text-gray-900 mb-6 leading-tight">
|
170
|
+
{post.title}
|
171
|
+
</h1>
|
172
|
+
<NotionRenderer blocks={post.blocks} />
|
173
|
+
</article>
|
174
|
+
</main>
|
175
|
+
</div>
|
176
|
+
);
|
177
|
+
}`.trim()
|
178
|
+
};
|
179
|
+
}
|
180
|
+
}
|
181
|
+
|
182
|
+
// Create blog page templates
|
183
|
+
if (fs.existsSync(pageRouter)) {
|
184
|
+
console.log("✅ Page router found");
|
185
|
+
const routePath = config.routeBasePath.replace(/^\//, ''); // Remove leading slash
|
186
|
+
const slugDir = path.join(pageRouter, routePath, "[slug].js")
|
187
|
+
const dirPath = path.dirname(slugDir);
|
188
|
+
|
189
|
+
const templates = getTemplates(config.theme, config.routeBasePath);
|
190
|
+
|
191
|
+
// Create NotionRenderer component in the same directory as the blog page
|
192
|
+
const notionRendererContent = `import React from "react";
|
193
|
+
import Image from "next/image";
|
194
|
+
|
195
|
+
export default function NotionRenderer({ blocks }) {
|
196
|
+
if (!blocks || blocks.length === 0) return null;
|
197
|
+
|
198
|
+
return (
|
199
|
+
<div className="prose prose-lg max-w-none prose-headings:font-semibold prose-headings:text-gray-900 dark:prose-headings:text-gray-100 prose-p:text-gray-700 dark:prose-p:text-gray-300 prose-li:text-gray-700 dark:prose-li:text-gray-300">
|
200
|
+
{blocks.map((block, i) => {
|
201
|
+
switch (block.type) {
|
202
|
+
case "heading_1":
|
203
|
+
return (
|
204
|
+
<h1 key={i} className="text-3xl font-bold text-gray-900 dark:text-gray-100 mb-6 mt-8 first:mt-0 border-b border-gray-200 dark:border-gray-700 pb-2">
|
205
|
+
{block.text}
|
206
|
+
</h1>
|
207
|
+
);
|
208
|
+
|
209
|
+
case "heading_2":
|
210
|
+
return (
|
211
|
+
<h2 key={i} className="text-2xl font-semibold text-gray-900 dark:text-gray-100 mb-4 mt-6 first:mt-0">
|
212
|
+
{block.text}
|
213
|
+
</h2>
|
214
|
+
);
|
215
|
+
|
216
|
+
case "heading_3":
|
217
|
+
return (
|
218
|
+
<h3 key={i} className="text-xl font-medium text-gray-900 dark:text-gray-100 mb-3 mt-5 first:mt-0">
|
219
|
+
{block.text}
|
220
|
+
</h3>
|
221
|
+
);
|
222
|
+
|
223
|
+
case "paragraph":
|
224
|
+
return (
|
225
|
+
<p key={i} className="text-gray-700 dark:text-gray-300 leading-relaxed mb-4 last:mb-0">
|
226
|
+
{block.text}
|
227
|
+
</p>
|
228
|
+
);
|
229
|
+
|
230
|
+
case "bulleted_list_item":
|
231
|
+
return (
|
232
|
+
<div key={i} className="flex items-start mb-2">
|
233
|
+
<div className="w-2 h-2 bg-gray-400 dark:bg-gray-500 rounded-full mt-2 mr-3 flex-shrink-0"></div>
|
234
|
+
<p className="text-gray-700 dark:text-gray-300 leading-relaxed">{block.text}</p>
|
235
|
+
</div>
|
236
|
+
);
|
237
|
+
|
238
|
+
case "numbered_list_item":
|
239
|
+
return (
|
240
|
+
<div key={i} className="flex items-start mb-2">
|
241
|
+
<span className="text-gray-500 dark:text-gray-400 font-medium mr-3 mt-0.5 flex-shrink-0">{(i + 1)}.</span>
|
242
|
+
<p className="text-gray-700 dark:text-gray-300 leading-relaxed">{block.text}</p>
|
243
|
+
</div>
|
244
|
+
);
|
245
|
+
|
246
|
+
case "quote":
|
247
|
+
return (
|
248
|
+
<blockquote key={i} className="border-l-4 border-blue-500 dark:border-blue-400 pl-6 py-4 my-6 bg-blue-50 dark:bg-blue-900/20 rounded-r-lg shadow-sm">
|
249
|
+
<p className="text-gray-700 dark:text-gray-300 italic text-lg leading-relaxed">
|
250
|
+
{block.text}
|
251
|
+
</p>
|
252
|
+
</blockquote>
|
253
|
+
);
|
254
|
+
|
255
|
+
case "code":
|
256
|
+
return (
|
257
|
+
<div key={i} className="my-6">
|
258
|
+
<pre className="bg-gray-900 dark:bg-gray-800 text-gray-100 p-4 rounded-lg shadow-md overflow-x-auto text-sm border border-gray-700 dark:border-gray-600">
|
259
|
+
<code className={\`language-\${block.language}\`}>{block.code}</code>
|
260
|
+
</pre>
|
261
|
+
</div>
|
262
|
+
);
|
263
|
+
|
264
|
+
case "divider":
|
265
|
+
return (
|
266
|
+
<hr key={i} className="my-8 border-gray-200 dark:border-gray-700 shadow-sm" />
|
267
|
+
);
|
268
|
+
|
269
|
+
case "image":
|
270
|
+
return (
|
271
|
+
<figure key={i} className="my-8">
|
272
|
+
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-4 shadow-sm border border-gray-200 dark:border-gray-700">
|
273
|
+
<Image
|
274
|
+
src={block.imageUrl}
|
275
|
+
alt={block.alt || "Image"}
|
276
|
+
width={400}
|
277
|
+
height={300}
|
278
|
+
className="rounded-lg object-contain w-full h-auto shadow-sm"
|
279
|
+
unoptimized={true}
|
280
|
+
/>
|
281
|
+
{block.caption && (
|
282
|
+
<figcaption className="text-sm text-center text-gray-500 dark:text-gray-400 mt-3 italic">
|
283
|
+
{block.caption}
|
284
|
+
</figcaption>
|
285
|
+
)}
|
286
|
+
</div>
|
287
|
+
</figure>
|
288
|
+
);
|
289
|
+
|
290
|
+
default:
|
291
|
+
return (
|
292
|
+
<div key={i} className="my-4 p-3 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg">
|
293
|
+
<p className="text-sm text-yellow-700 dark:text-yellow-300 italic">
|
294
|
+
⚠️ Unsupported block type: {block.type}
|
295
|
+
</p>
|
296
|
+
</div>
|
297
|
+
);
|
298
|
+
}
|
299
|
+
})}
|
300
|
+
</div>
|
301
|
+
);
|
302
|
+
}`;
|
303
|
+
|
304
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
305
|
+
fs.writeFileSync(path.join(dirPath, 'NotionRenderer.jsx'), notionRendererContent);
|
306
|
+
console.log(`✅ Created ${routePath}/NotionRenderer.jsx`);
|
307
|
+
|
308
|
+
fs.writeFileSync(slugDir, templates.pageRouter);
|
309
|
+
console.log(`✅ Created pages/${routePath}/[slug].js`);
|
310
|
+
|
311
|
+
} else if (fs.existsSync(appRouter)) {
|
312
|
+
console.log("✅ App router found");
|
313
|
+
const routePath = config.routeBasePath.replace(/^\//, ''); // Remove leading slash
|
314
|
+
const slugDir = path.join(appRouter, routePath, "[slug]", "page.jsx")
|
315
|
+
const dirPath = path.dirname(slugDir);
|
316
|
+
|
317
|
+
const templates = getTemplates(config.theme, config.routeBasePath);
|
318
|
+
|
319
|
+
|
320
|
+
// Create NotionRenderer component in the same directory as the blog page
|
321
|
+
const notionRendererContent = `import React from "react";
|
322
|
+
import Image from "next/image";
|
323
|
+
|
324
|
+
export default function NotionRenderer({ blocks }) {
|
325
|
+
if (!blocks || blocks.length === 0) return null;
|
326
|
+
|
327
|
+
return (
|
328
|
+
<div className="prose prose-lg max-w-none text-slate-700 leading-relaxed dark:prose-invert dark:text-slate-300">
|
329
|
+
{blocks.map((block, i) => {
|
330
|
+
switch (block.type) {
|
331
|
+
case "heading_1":
|
332
|
+
return <h1 key={i}>{block.text}</h1>;
|
333
|
+
|
334
|
+
case "heading_2":
|
335
|
+
return <h2 key={i}>{block.text}</h2>;
|
336
|
+
|
337
|
+
case "heading_3":
|
338
|
+
return <h3 key={i}>{block.text}</h3>;
|
339
|
+
|
340
|
+
case "paragraph":
|
341
|
+
return <p key={i}>{block.text}</p>;
|
342
|
+
|
343
|
+
case "bulleted_list_item":
|
344
|
+
return (
|
345
|
+
<ul key={i} className="list-disc ml-6">
|
346
|
+
<li>{block.text}</li>
|
347
|
+
</ul>
|
348
|
+
);
|
349
|
+
|
350
|
+
case "numbered_list_item":
|
351
|
+
return (
|
352
|
+
<ol key={i} className="list-decimal ml-6">
|
353
|
+
<li>{block.text}</li>
|
354
|
+
</ol>
|
355
|
+
);
|
356
|
+
|
357
|
+
case "quote":
|
358
|
+
return (
|
359
|
+
<blockquote key={i} className="border-l-4 pl-4 italic text-slate-600 bg-slate-50 p-4 rounded-r">
|
360
|
+
{block.text}
|
361
|
+
</blockquote>
|
362
|
+
);
|
363
|
+
|
364
|
+
case "code":
|
365
|
+
return (
|
366
|
+
<pre key={i} className="bg-slate-900 text-slate-100 p-4 rounded-xl overflow-x-auto text-sm">
|
367
|
+
<code className={\`language-\${block.language}\`}>{block.code}</code>
|
368
|
+
</pre>
|
369
|
+
);
|
370
|
+
|
371
|
+
case "divider":
|
372
|
+
return <hr key={i} className="my-8 border-slate-300" />;
|
373
|
+
|
374
|
+
case "image":
|
375
|
+
return (
|
376
|
+
<figure key={i} className="max-w-[400px] mx-auto my-6 px-4">
|
377
|
+
<Image
|
378
|
+
src={block.imageUrl}
|
379
|
+
alt={block.alt || "Image"}
|
380
|
+
width={400}
|
381
|
+
height={300}
|
382
|
+
className="rounded-xl object-contain"
|
383
|
+
unoptimized={true}
|
384
|
+
/>
|
385
|
+
{block.caption && (
|
386
|
+
<figcaption className="text-sm text-center text-slate-500 mt-2 italic">
|
387
|
+
{block.caption}
|
388
|
+
</figcaption>
|
389
|
+
)}
|
390
|
+
</figure>
|
391
|
+
);
|
392
|
+
|
393
|
+
default:
|
394
|
+
return (
|
395
|
+
<p key={i} className="text-sm text-red-500 italic">
|
396
|
+
⚠️ Unsupported block: {block.type}
|
397
|
+
</p>
|
398
|
+
);
|
399
|
+
}
|
400
|
+
})}
|
401
|
+
</div>
|
402
|
+
);
|
403
|
+
}`;
|
404
|
+
|
405
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
406
|
+
fs.writeFileSync(path.join(dirPath, 'NotionRenderer.jsx'), notionRendererContent);
|
407
|
+
console.log(`✅ Created ${routePath}/[slug]/NotionRenderer.jsx`);
|
408
|
+
|
409
|
+
fs.writeFileSync(slugDir, templates.appRouter);
|
410
|
+
console.log(`✅ Created app/${routePath}/[slug]/page.jsx`);
|
411
|
+
|
412
|
+
} else {
|
413
|
+
console.log("❌ Neither pages/ nor app/ directory found");
|
414
|
+
console.log("💡 Please make sure you're running this in a Next.js project");
|
415
|
+
process.exit(1);
|
416
|
+
}
|
417
|
+
|
418
|
+
console.log("\n🎉 Blog page scaffolded successfully!");
|
419
|
+
console.log(`\n📝 Next steps:`);
|
420
|
+
console.log("1. Add Tailwind CSS to your project if not already installed");
|
421
|
+
console.log("2. Start your development server");
|
213
422
|
console.log(`3. Visit ${config.routeBasePath}/[your-post-slug] to see your blog`);
|
package/package.json
CHANGED
package/src/index.js
CHANGED
@@ -1,82 +0,0 @@
|
|
1
|
-
import React from "react";
|
2
|
-
import Image from "next/image";
|
3
|
-
|
4
|
-
export default function NotionRenderer({ blocks }) {
|
5
|
-
if (!blocks || blocks.length === 0) return null;
|
6
|
-
|
7
|
-
return (
|
8
|
-
<div className="prose prose-lg max-w-none text-slate-700 leading-relaxed dark:prose-invert dark:text-slate-300">
|
9
|
-
{blocks.map((block, i) => {
|
10
|
-
switch (block.type) {
|
11
|
-
case "heading_1":
|
12
|
-
return <h1 key={i}>{block.text}</h1>;
|
13
|
-
|
14
|
-
case "heading_2":
|
15
|
-
return <h2 key={i}>{block.text}</h2>;
|
16
|
-
|
17
|
-
case "heading_3":
|
18
|
-
return <h3 key={i}>{block.text}</h3>;
|
19
|
-
|
20
|
-
case "paragraph":
|
21
|
-
return <p key={i}>{block.text}</p>;
|
22
|
-
|
23
|
-
case "bulleted_list_item":
|
24
|
-
return (
|
25
|
-
<ul key={i} className="list-disc ml-6">
|
26
|
-
<li>{block.text}</li>
|
27
|
-
</ul>
|
28
|
-
);
|
29
|
-
|
30
|
-
case "numbered_list_item":
|
31
|
-
return (
|
32
|
-
<ol key={i} className="list-decimal ml-6">
|
33
|
-
<li>{block.text}</li>
|
34
|
-
</ol>
|
35
|
-
);
|
36
|
-
|
37
|
-
case "quote":
|
38
|
-
return (
|
39
|
-
<blockquote key={i} className="border-l-4 pl-4 italic text-slate-600 bg-slate-50 p-4 rounded-r">
|
40
|
-
{block.text}
|
41
|
-
</blockquote>
|
42
|
-
);
|
43
|
-
|
44
|
-
case "code":
|
45
|
-
return (
|
46
|
-
<pre key={i} className="bg-slate-900 text-slate-100 p-4 rounded-xl overflow-x-auto text-sm">
|
47
|
-
<code className={`language-${block.language}`}>{block.code}</code>
|
48
|
-
</pre>
|
49
|
-
);
|
50
|
-
|
51
|
-
case "divider":
|
52
|
-
return <hr key={i} className="my-8 border-slate-300" />;
|
53
|
-
|
54
|
-
case "image":
|
55
|
-
return (
|
56
|
-
<figure key={i} className="max-w-[400px] mx-auto my-6 px-4">
|
57
|
-
<Image
|
58
|
-
src={block.imageUrl}
|
59
|
-
alt={block.alt || "Image"}
|
60
|
-
width={400}
|
61
|
-
height={300}
|
62
|
-
className="rounded-xl object-contain"
|
63
|
-
/>
|
64
|
-
{block.caption && (
|
65
|
-
<figcaption className="text-sm text-center text-slate-500 mt-2 italic">
|
66
|
-
{block.caption}
|
67
|
-
</figcaption>
|
68
|
-
)}
|
69
|
-
</figure>
|
70
|
-
);
|
71
|
-
|
72
|
-
default:
|
73
|
-
return (
|
74
|
-
<p key={i} className="text-sm text-red-500 italic">
|
75
|
-
⚠️ Unsupported block: {block.type}
|
76
|
-
</p>
|
77
|
-
);
|
78
|
-
}
|
79
|
-
})}
|
80
|
-
</div>
|
81
|
-
);
|
82
|
-
}
|