chalknotes 0.0.31 → 0.0.33

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 CHANGED
@@ -1,257 +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
- ## 🔧 Configuration
55
-
56
- The CLI creates a `blog.config.js` file in your project root. Customize it to match your needs:
57
-
58
- ```javascript
59
- module.exports = {
60
- // Notion Configuration
61
- notionToken: process.env.NOTION_TOKEN,
62
- notionDatabaseId: process.env.NOTION_DATABASE_ID,
63
-
64
- // Blog Configuration
65
- routeBasePath: '/blog', // Default: '/blog'
66
- theme: 'default', // Options: 'default' (light) or 'dark'
67
- plugins: [],
68
- };
69
- ```
70
-
71
- ### Configuration Options
72
-
73
- - **`routeBasePath`**: Customize your blog route (e.g., `/posts`, `/articles`)
74
- - **`theme`**: Choose between `'default'` (light mode) or `'dark'` (dark mode)
75
- - **`plugins`**: Array for future plugin support
76
-
77
- ---
78
-
79
- ## 🎨 Themes
80
-
81
- ### Default Theme (Light Mode)
82
- - Clean white cards with subtle shadows
83
- - Light gray background
84
- - Dark text for optimal readability
85
- - Responsive design with Tailwind CSS
86
-
87
- ### Dark Theme
88
- - Dark background with gray cards
89
- - White text with proper contrast
90
- - Inverted typography for dark mode
91
- - Same responsive layout
92
-
93
- ---
94
-
95
- ## 📚 Usage in Next.js
96
-
97
- ### Page Router
98
-
99
- Creates:
100
-
101
- ```js
102
- // pages/blog/[slug].js (or custom route)
103
- import { getStaticPropsForPost, getStaticPathsForPosts } from 'chalknotes';
104
- import NotionRenderer from './NotionRenderer';
105
-
106
- export const getStaticProps = getStaticPropsForPost;
107
- export const getStaticPaths = getStaticPathsForPosts;
108
-
109
- export default function BlogPost({ post }) {
110
- return (
111
- <div className="min-h-screen bg-gray-50">
112
- <main className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
113
- <article className="bg-white rounded-lg shadow-sm border border-gray-200 p-8">
114
- <h1 className="text-4xl font-bold text-gray-900 mb-6 leading-tight">
115
- {post.title}
116
- </h1>
117
- <NotionRenderer blocks={post.blocks} />
118
- </article>
119
- </main>
120
- </div>
121
- );
122
- }
123
- ```
124
-
125
- ---
126
-
127
- ### App Router
128
-
129
- Creates:
130
-
131
- ```jsx
132
- // app/blog/[slug]/page.jsx (or custom route)
133
- import { getPostBySlug } from 'chalknotes';
134
- import NotionRenderer from './NotionRenderer';
135
-
136
- export default async function BlogPost({ params }) {
137
- const post = await getPostBySlug(params.slug);
138
-
139
- return (
140
- <div className="min-h-screen bg-gray-50">
141
- <main className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
142
- <article className="bg-white rounded-lg shadow-sm border border-gray-200 p-8">
143
- <h1 className="text-4xl font-bold text-gray-900 mb-6 leading-tight">
144
- {post.title}
145
- </h1>
146
- <NotionRenderer blocks={post.blocks} />
147
- </article>
148
- </main>
149
- </div>
150
- );
151
- }
152
- ```
153
-
154
- ---
155
-
156
- ## 🧩 API
157
-
158
- ### `getPostBySlug(slug: string)`
159
- Fetches a post and returns structured data for rendering.
160
-
161
- ```js
162
- const post = await getPostBySlug('my-post-title');
163
- // Returns: { title, slug, blocks, notionPageId }
164
- ```
165
-
166
- ---
167
-
168
- ### `getAllPosts()`
169
- Returns all published posts with metadata:
170
-
171
- ```js
172
- [
173
- {
174
- title: "My First Post",
175
- slug: "my-first-post",
176
- notionPageId: "xxxxxxxx"
177
- },
178
- ...
179
- ]
180
- ```
181
-
182
- ---
183
-
184
- ### `getStaticPropsForPost()`
185
- For use with `getStaticProps` in Page Router.
186
-
187
- ---
188
-
189
- ### `getStaticPathsForPosts()`
190
- For use with `getStaticPaths` in Page Router.
191
-
192
- ---
193
-
194
- ### `NotionRenderer`
195
- React component for rendering Notion blocks (scaffolded in your project):
196
-
197
- ```jsx
198
- import NotionRenderer from './NotionRenderer';
199
-
200
- <NotionRenderer blocks={post.blocks} />
201
- ```
202
-
203
- ---
204
-
205
- ## 🖼️ Supported Content Types
206
-
207
- The `NotionRenderer` component supports all major Notion block types:
208
-
209
- - **Text blocks**: Paragraphs, headings (H1, H2, H3)
210
- - **Lists**: Bulleted and numbered lists
211
- - **Code blocks**: With syntax highlighting support
212
- - **Images**: With captions and Next.js optimization
213
- - **Quotes**: Styled blockquotes
214
- - **Dividers**: Horizontal rules
215
- - **Rich text**: Bold, italic, strikethrough, code, links
216
-
217
- ---
218
-
219
- ## 🎨 Styling
220
-
221
- The generated templates use Tailwind CSS with:
222
- - Clean, minimal design
223
- - Responsive layout
224
- - Typography optimized for readability
225
- - Proper spacing and hierarchy
226
- - Light and dark mode support
227
- - **Rich content styling** for all Notion block types
228
-
229
- Make sure you have Tailwind CSS installed in your project:
230
-
231
- ```bash
232
- npm install -D tailwindcss @tailwindcss/typography
233
- ```
234
-
235
- ---
236
-
237
- ## 📅 Roadmap
238
-
239
- - [ ] Plugin system for custom components
240
- - [ ] More Notion block support (callouts, bookmarks, toggles)
241
- - [ ] RSS feed support
242
- - [ ] MDX or Markdown output option
243
- - [ ] Custom theme templates
244
- - [ ] Search functionality
245
- - [ ] Categories and tags support
246
-
247
- ---
248
-
249
- ## 💡 Inspiration
250
-
251
- 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).
252
-
253
- ---
254
-
255
- ## 🧑‍💻 Author
256
-
257
- [NepTune](https://github.com/yourhandle) • MIT License
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,391 +1,338 @@
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 text-slate-700 leading-relaxed dark:prose-invert dark:text-slate-300">
200
- {blocks.map((block, i) => {
201
- switch (block.type) {
202
- case "heading_1":
203
- return <h1 key={i}>{block.text}</h1>;
204
-
205
- case "heading_2":
206
- return <h2 key={i}>{block.text}</h2>;
207
-
208
- case "heading_3":
209
- return <h3 key={i}>{block.text}</h3>;
210
-
211
- case "paragraph":
212
- return <p key={i}>{block.text}</p>;
213
-
214
- case "bulleted_list_item":
215
- return (
216
- <ul key={i} className="list-disc ml-6">
217
- <li>{block.text}</li>
218
- </ul>
219
- );
220
-
221
- case "numbered_list_item":
222
- return (
223
- <ol key={i} className="list-decimal ml-6">
224
- <li>{block.text}</li>
225
- </ol>
226
- );
227
-
228
- case "quote":
229
- return (
230
- <blockquote key={i} className="border-l-4 pl-4 italic text-slate-600 bg-slate-50 p-4 rounded-r">
231
- {block.text}
232
- </blockquote>
233
- );
234
-
235
- case "code":
236
- return (
237
- <pre key={i} className="bg-slate-900 text-slate-100 p-4 rounded-xl overflow-x-auto text-sm">
238
- <code className={\`language-\${block.language}\`}>{block.code}</code>
239
- </pre>
240
- );
241
-
242
- case "divider":
243
- return <hr key={i} className="my-8 border-slate-300" />;
244
-
245
- case "image":
246
- return (
247
- <figure key={i} className="max-w-[400px] mx-auto my-6 px-4">
248
- <Image
249
- src={block.imageUrl}
250
- alt={block.alt || "Image"}
251
- width={400}
252
- height={300}
253
- className="rounded-xl object-contain"
254
- />
255
- {block.caption && (
256
- <figcaption className="text-sm text-center text-slate-500 mt-2 italic">
257
- {block.caption}
258
- </figcaption>
259
- )}
260
- </figure>
261
- );
262
-
263
- default:
264
- return (
265
- <p key={i} className="text-sm text-red-500 italic">
266
- ⚠️ Unsupported block: {block.type}
267
- </p>
268
- );
269
- }
270
- })}
271
- </div>
272
- );
273
- }`;
274
-
275
- fs.mkdirSync(dirPath, { recursive: true });
276
- fs.writeFileSync(path.join(dirPath, 'NotionRenderer.jsx'), notionRendererContent);
277
- console.log(`✅ Created ${routePath}/NotionRenderer.jsx`);
278
-
279
- fs.writeFileSync(slugDir, templates.pageRouter);
280
- console.log(`✅ Created pages/${routePath}/[slug].js`);
281
-
282
- } else if (fs.existsSync(appRouter)) {
283
- console.log(" App router found");
284
- const routePath = config.routeBasePath.replace(/^\//, ''); // Remove leading slash
285
- const slugDir = path.join(appRouter, routePath, "[slug]", "page.jsx")
286
- const dirPath = path.dirname(slugDir);
287
-
288
- const templates = getTemplates(config.theme, config.routeBasePath);
289
-
290
- // Create NotionRenderer component in the same directory as the blog page
291
- const notionRendererContent = `import React from "react";
292
- import Image from "next/image";
293
-
294
- export default function NotionRenderer({ blocks }) {
295
- if (!blocks || blocks.length === 0) return null;
296
-
297
- return (
298
- <div className="prose prose-lg max-w-none text-slate-700 leading-relaxed dark:prose-invert dark:text-slate-300">
299
- {blocks.map((block, i) => {
300
- switch (block.type) {
301
- case "heading_1":
302
- return <h1 key={i}>{block.text}</h1>;
303
-
304
- case "heading_2":
305
- return <h2 key={i}>{block.text}</h2>;
306
-
307
- case "heading_3":
308
- return <h3 key={i}>{block.text}</h3>;
309
-
310
- case "paragraph":
311
- return <p key={i}>{block.text}</p>;
312
-
313
- case "bulleted_list_item":
314
- return (
315
- <ul key={i} className="list-disc ml-6">
316
- <li>{block.text}</li>
317
- </ul>
318
- );
319
-
320
- case "numbered_list_item":
321
- return (
322
- <ol key={i} className="list-decimal ml-6">
323
- <li>{block.text}</li>
324
- </ol>
325
- );
326
-
327
- case "quote":
328
- return (
329
- <blockquote key={i} className="border-l-4 pl-4 italic text-slate-600 bg-slate-50 p-4 rounded-r">
330
- {block.text}
331
- </blockquote>
332
- );
333
-
334
- case "code":
335
- return (
336
- <pre key={i} className="bg-slate-900 text-slate-100 p-4 rounded-xl overflow-x-auto text-sm">
337
- <code className={\`language-\${block.language}\`}>{block.code}</code>
338
- </pre>
339
- );
340
-
341
- case "divider":
342
- return <hr key={i} className="my-8 border-slate-300" />;
343
-
344
- case "image":
345
- return (
346
- <figure key={i} className="max-w-[400px] mx-auto my-6 px-4">
347
- <Image
348
- src={block.imageUrl}
349
- alt={block.alt || "Image"}
350
- width={400}
351
- height={300}
352
- className="rounded-xl object-contain"
353
- />
354
- {block.caption && (
355
- <figcaption className="text-sm text-center text-slate-500 mt-2 italic">
356
- {block.caption}
357
- </figcaption>
358
- )}
359
- </figure>
360
- );
361
-
362
- default:
363
- return (
364
- <p key={i} className="text-sm text-red-500 italic">
365
- ⚠️ Unsupported block: {block.type}
366
- </p>
367
- );
368
- }
369
- })}
370
- </div>
371
- );
372
- }`;
373
-
374
- fs.mkdirSync(dirPath, { recursive: true });
375
- fs.writeFileSync(path.join(dirPath, 'NotionRenderer.jsx'), notionRendererContent);
376
- console.log(`✅ Created ${routePath}/[slug]/NotionRenderer.jsx`);
377
-
378
- fs.writeFileSync(slugDir, templates.appRouter);
379
- console.log(`✅ Created app/${routePath}/[slug]/page.jsx`);
380
-
381
- } else {
382
- console.log("❌ Neither pages/ nor app/ directory found");
383
- console.log("💡 Please make sure you're running this in a Next.js project");
384
- process.exit(1);
385
- }
386
-
387
- console.log("\n🎉 Blog page scaffolded successfully!");
388
- console.log(`\n📝 Next steps:`);
389
- console.log("1. Add Tailwind CSS to your project if not already installed");
390
- console.log("2. Start your development server");
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
+ // NotionRenderer component template
183
+ const notionRendererTemplate = `import React from "react";
184
+ import Image from "next/image";
185
+
186
+ export default function NotionRenderer({ blocks }) {
187
+ if (!blocks || blocks.length === 0) return null;
188
+
189
+ return (
190
+ <div className="max-w-none">
191
+ {blocks.map((block, i) => {
192
+ switch (block.type) {
193
+ case "heading_1":
194
+ return (
195
+ <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">
196
+ {block.text}
197
+ </h1>
198
+ );
199
+
200
+ case "heading_2":
201
+ return (
202
+ <h2 key={i} className="text-2xl font-semibold text-gray-900 dark:text-gray-100 mb-4 mt-6 first:mt-0">
203
+ {block.text}
204
+ </h2>
205
+ );
206
+
207
+ case "heading_3":
208
+ return (
209
+ <h3 key={i} className="text-xl font-medium text-gray-900 dark:text-gray-100 mb-3 mt-5 first:mt-0">
210
+ {block.text}
211
+ </h3>
212
+ );
213
+
214
+ case "paragraph":
215
+ return (
216
+ <p key={i} className="text-gray-700 dark:text-gray-300 leading-relaxed mb-4 last:mb-0">
217
+ {block.text}
218
+ </p>
219
+ );
220
+
221
+ case "bulleted_list_item":
222
+ return (
223
+ <div key={i} className="flex items-start mb-2">
224
+ <div className="w-2 h-2 bg-gray-400 dark:bg-gray-500 rounded-full mt-2 mr-3 flex-shrink-0"></div>
225
+ <p className="text-gray-700 dark:text-gray-300 leading-relaxed">{block.text}</p>
226
+ </div>
227
+ );
228
+
229
+ case "numbered_list_item":
230
+ return (
231
+ <div key={i} className="flex items-start mb-2">
232
+ <span className="text-gray-500 dark:text-gray-400 font-medium mr-3 mt-0.5 flex-shrink-0">{(i + 1)}.</span>
233
+ <p className="text-gray-700 dark:text-gray-300 leading-relaxed">{block.text}</p>
234
+ </div>
235
+ );
236
+
237
+ case "quote":
238
+ return (
239
+ <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">
240
+ <p className="text-gray-700 dark:text-gray-300 italic text-lg leading-relaxed">
241
+ {block.text}
242
+ </p>
243
+ </blockquote>
244
+ );
245
+
246
+ case "code":
247
+ return (
248
+ <div key={i} className="my-6">
249
+ <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">
250
+ <code className={\`language-\${block.language}\`}>{block.code}</code>
251
+ </pre>
252
+ </div>
253
+ );
254
+
255
+ case "divider":
256
+ return (
257
+ <hr key={i} className="my-8 border-gray-200 dark:border-gray-700 shadow-sm" />
258
+ );
259
+
260
+ case "image":
261
+ return (
262
+ <figure key={i} className="my-8">
263
+ <div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-4 shadow-sm border border-gray-200 dark:border-gray-700">
264
+ <Image
265
+ src={block.imageUrl}
266
+ alt={block.alt || "Image"}
267
+ width={400}
268
+ height={300}
269
+ className="rounded-lg object-contain w-full h-auto shadow-sm"
270
+ unoptimized={true}
271
+ />
272
+ {block.caption && (
273
+ <figcaption className="text-sm text-center text-gray-500 dark:text-gray-400 mt-3 italic">
274
+ {block.caption}
275
+ </figcaption>
276
+ )}
277
+ </div>
278
+ </figure>
279
+ );
280
+
281
+ default:
282
+ return (
283
+ <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">
284
+ <p className="text-sm text-yellow-700 dark:text-yellow-300 italic">
285
+ ⚠️ Unsupported block type: {block.type}
286
+ </p>
287
+ </div>
288
+ );
289
+ }
290
+ })}
291
+ </div>
292
+ );
293
+ }`;
294
+
295
+ // Create blog page templates
296
+ if (fs.existsSync(pageRouter)) {
297
+ console.log("✅ Page router found");
298
+ const routePath = config.routeBasePath.replace(/^\//, ''); // Remove leading slash
299
+ const slugDir = path.join(pageRouter, routePath, "[slug].js")
300
+ const dirPath = path.dirname(slugDir);
301
+
302
+ const templates = getTemplates(config.theme, config.routeBasePath);
303
+
304
+ // Create NotionRenderer component in the same directory as the blog page
305
+ fs.mkdirSync(dirPath, { recursive: true });
306
+ fs.writeFileSync(path.join(dirPath, 'NotionRenderer.jsx'), notionRendererTemplate);
307
+ console.log(`✅ Created ${routePath}/NotionRenderer.jsx`);
308
+
309
+ fs.writeFileSync(slugDir, templates.pageRouter);
310
+ console.log(`✅ Created pages/${routePath}/[slug].js`);
311
+
312
+ } else if (fs.existsSync(appRouter)) {
313
+ console.log("✅ App router found");
314
+ const routePath = config.routeBasePath.replace(/^\//, ''); // Remove leading slash
315
+ const slugDir = path.join(appRouter, routePath, "[slug]", "page.jsx")
316
+ const dirPath = path.dirname(slugDir);
317
+
318
+ const templates = getTemplates(config.theme, config.routeBasePath);
319
+
320
+ // Create NotionRenderer component in the same directory as the blog page
321
+ fs.mkdirSync(dirPath, { recursive: true });
322
+ fs.writeFileSync(path.join(dirPath, 'NotionRenderer.jsx'), notionRendererTemplate);
323
+ console.log(`✅ Created ${routePath}/[slug]/NotionRenderer.jsx`);
324
+
325
+ fs.writeFileSync(slugDir, templates.appRouter);
326
+ console.log(`✅ Created app/${routePath}/[slug]/page.jsx`);
327
+
328
+ } else {
329
+ console.log(" Neither pages/ nor app/ directory found");
330
+ console.log("💡 Please make sure you're running this in a Next.js project");
331
+ process.exit(1);
332
+ }
333
+
334
+ console.log("\n🎉 Blog page scaffolded successfully!");
335
+ console.log(`\n📝 Next steps:`);
336
+ console.log("1. Add Tailwind CSS to your project if not already installed");
337
+ console.log("2. Start your development server");
391
338
  console.log(`3. Visit ${config.routeBasePath}/[your-post-slug] to see your blog`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chalknotes",
3
- "version": "0.0.31",
3
+ "version": "0.0.33",
4
4
  "description": "A tool that simplifies blogs.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -105,6 +105,23 @@ function convertBlockToStructuredJSON(block) {
105
105
  case "divider":
106
106
  return { ...base };
107
107
 
108
+ case "callout":
109
+ return { ...base };
110
+
111
+ case "toggle":
112
+ return { ...base };
113
+
114
+ case "table_of_contents":
115
+ return { ...base };
116
+ case "bookmark":
117
+ return { ...base };
118
+
119
+ case "equation":
120
+ return { ...base };
121
+
122
+ case "table":
123
+ return { ...base };
124
+
108
125
  default:
109
126
  return {
110
127
  ...base,