chalknotes 0.0.24 → 0.0.26

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 ADDED
@@ -0,0 +1,231 @@
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
+
17
+ ---
18
+
19
+ ## 📦 Installation
20
+
21
+ ```bash
22
+ pnpm add chalknotes
23
+ # or
24
+ npm install chalknotes
25
+ ```
26
+
27
+ ---
28
+
29
+ ## 🧙‍♂️ Quick Start
30
+
31
+ 1. **Set up environment variables**
32
+ ```bash
33
+ # Create .env file
34
+ NOTION_TOKEN=secret_...
35
+ NOTION_DATABASE_ID=xxxxxxxxxxxxxxxxxxxxxx
36
+ ```
37
+
38
+ 2. **Run the CLI**
39
+ ```bash
40
+ npx chalknotes
41
+ ```
42
+
43
+ 3. **That's it!** ✅
44
+ - Automatically detects if you're using **App Router** or **Page Router**
45
+ - Creates `blog.config.js` with default configuration (if needed)
46
+ - Generates blog routes with clean, responsive templates
47
+ - Supports light and dark themes
48
+
49
+ ---
50
+
51
+ ## 🔧 Configuration
52
+
53
+ The CLI creates a `blog.config.js` file in your project root. Customize it to match your needs:
54
+
55
+ ```javascript
56
+ module.exports = {
57
+ // Notion Configuration
58
+ notionToken: process.env.NOTION_TOKEN,
59
+ notionDatabaseId: process.env.NOTION_DATABASE_ID,
60
+
61
+ // Blog Configuration
62
+ routeBasePath: '/blog', // Default: '/blog'
63
+ theme: 'default', // Options: 'default' (light) or 'dark'
64
+ plugins: [],
65
+ };
66
+ ```
67
+
68
+ ### Configuration Options
69
+
70
+ - **`routeBasePath`**: Customize your blog route (e.g., `/posts`, `/articles`)
71
+ - **`theme`**: Choose between `'default'` (light mode) or `'dark'` (dark mode)
72
+ - **`plugins`**: Array for future plugin support
73
+
74
+ ---
75
+
76
+ ## 🎨 Themes
77
+
78
+ ### Default Theme (Light Mode)
79
+ - Clean white cards with subtle shadows
80
+ - Light gray background
81
+ - Dark text for optimal readability
82
+ - Responsive design with Tailwind CSS
83
+
84
+ ### Dark Theme
85
+ - Dark background with gray cards
86
+ - White text with proper contrast
87
+ - Inverted typography for dark mode
88
+ - Same responsive layout
89
+
90
+ ---
91
+
92
+ ## 📚 Usage in Next.js
93
+
94
+ ### Page Router
95
+
96
+ Creates:
97
+
98
+ ```js
99
+ // pages/blog/[slug].js (or custom route)
100
+ import { getStaticPropsForPost, getStaticPathsForPosts } from 'chalknotes';
101
+
102
+ export const getStaticProps = getStaticPropsForPost;
103
+ export const getStaticPaths = getStaticPathsForPosts;
104
+
105
+ export default function BlogPost({ post }) {
106
+ return (
107
+ <div className="min-h-screen bg-gray-50">
108
+ <main className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
109
+ <article className="bg-white rounded-lg shadow-sm border border-gray-200 p-8">
110
+ <h1 className="text-4xl font-bold text-gray-900 mb-6 leading-tight">
111
+ {post.title}
112
+ </h1>
113
+ <div
114
+ className="prose prose-lg max-w-none text-gray-700 leading-relaxed"
115
+ dangerouslySetInnerHTML={{ __html: post.content }}
116
+ />
117
+ </article>
118
+ </main>
119
+ </div>
120
+ );
121
+ }
122
+ ```
123
+
124
+ ---
125
+
126
+ ### App Router
127
+
128
+ Creates:
129
+
130
+ ```jsx
131
+ // app/blog/[slug]/page.jsx (or custom route)
132
+ import { getPostBySlug } from 'chalknotes';
133
+
134
+ export default async function BlogPost({ params }) {
135
+ const post = await getPostBySlug(params.slug);
136
+
137
+ return (
138
+ <div className="min-h-screen bg-gray-50">
139
+ <main className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
140
+ <article className="bg-white rounded-lg shadow-sm border border-gray-200 p-8">
141
+ <h1 className="text-4xl font-bold text-gray-900 mb-6 leading-tight">
142
+ {post.title}
143
+ </h1>
144
+ <div
145
+ className="prose prose-lg max-w-none text-gray-700 leading-relaxed"
146
+ dangerouslySetInnerHTML={{ __html: post.content }}
147
+ />
148
+ </article>
149
+ </main>
150
+ </div>
151
+ );
152
+ }
153
+ ```
154
+
155
+ ---
156
+
157
+ ## 🧩 API
158
+
159
+ ### `getPostBySlug(slug: string)`
160
+ Fetches a post and renders Notion blocks as HTML.
161
+
162
+ ```js
163
+ const post = await getPostBySlug('my-post-title');
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
+ ## 🎨 Styling
195
+
196
+ The generated templates use Tailwind CSS with:
197
+ - Clean, minimal design
198
+ - Responsive layout
199
+ - Typography optimized for readability
200
+ - Proper spacing and hierarchy
201
+ - Light and dark mode support
202
+
203
+ Make sure you have Tailwind CSS installed in your project:
204
+
205
+ ```bash
206
+ npm install -D tailwindcss @tailwindcss/typography
207
+ ```
208
+
209
+ ---
210
+
211
+ ## 📅 Roadmap
212
+
213
+ - [ ] Plugin system for custom components
214
+ - [ ] More Notion block support (images, lists, code blocks)
215
+ - [ ] RSS feed support
216
+ - [ ] MDX or Markdown output option
217
+ - [ ] Custom theme templates
218
+ - [ ] Search functionality
219
+ - [ ] Categories and tags support
220
+
221
+ ---
222
+
223
+ ## 💡 Inspiration
224
+
225
+ 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).
226
+
227
+ ---
228
+
229
+ ## 🧑‍💻 Author
230
+
231
+ [NepTune](https://github.com/yourhandle) • MIT License
package/bin/cli.js CHANGED
@@ -1,20 +1,95 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  const fs = require("fs");
3
3
  const path = require("path");
4
+ const dotenv = require("dotenv");
5
+ const readline = require("readline");
6
+ dotenv.config();
4
7
 
5
- console.log("Initializing ChalkNotes...");
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");
6
24
 
7
25
  const configPath = path.join(process.cwd(), 'blog.config.js');
8
- const pageRouter = path.join(process.cwd(), '/pages')
9
- const appRouter = path.join(process.cwd(), '/app')
10
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
+ }
11
59
 
12
- if (fs.existsSync(pageRouter)) {
13
- console.log("✅ Page router found");
14
- const slugDir = path.join(pageRouter, "blog", "[slug].js")
15
- const dirPath = path.dirname(slugDir);
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
+ }
16
68
 
17
- const pageRouterTemplate = `
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: `
18
93
  import { getStaticPropsForPost, getStaticPathsForPosts } from 'chalknotes';
19
94
 
20
95
  export const getStaticProps = getStaticPropsForPost;
@@ -22,53 +97,129 @@ export const getStaticPaths = getStaticPathsForPosts;
22
97
 
23
98
  export default function BlogPost({ post }) {
24
99
  return (
25
- <article>
26
- <h1>{post.title}</h1>
27
- <div dangerouslySetInnerHTML={{ __html: post.content }} />
28
- </article>
100
+ <div className="min-h-screen bg-gray-900">
101
+ <main className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
102
+ <article className="bg-gray-800 rounded-lg shadow-lg border border-gray-700 p-8">
103
+ <h1 className="text-4xl font-bold text-white mb-6 leading-tight">
104
+ {post.title}
105
+ </h1>
106
+ <div
107
+ className="prose prose-lg prose-invert max-w-none text-gray-300 leading-relaxed"
108
+ dangerouslySetInnerHTML={{ __html: post.content }}
109
+ />
110
+ </article>
111
+ </main>
112
+ </div>
29
113
  );
30
- }
31
- `.trim();
114
+ }`.trim(),
115
+ appRouter: `
116
+ import { getPostBySlug } from 'chalknotes';
32
117
 
33
- fs.mkdirSync(dirPath, { recursive: true });
34
- fs.writeFileSync(slugDir, pageRouterTemplate)
35
- } else if (fs.existsSync(appRouter)) {
36
- console.log("✅ App router found");
37
- const slugDir = path.join(appRouter, 'blog', "[slug]", "page.jsx")
38
- const dirPath = path.dirname(slugDir);
118
+ export default async function BlogPost({ params }) {
119
+ const post = await getPostBySlug(params.slug);
39
120
 
40
- const appRouterTemplate = `
121
+ return (
122
+ <div className="min-h-screen bg-gray-900">
123
+ <main className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
124
+ <article className="bg-gray-800 rounded-lg shadow-lg border border-gray-700 p-8">
125
+ <h1 className="text-4xl font-bold text-white mb-6 leading-tight">
126
+ {post.title}
127
+ </h1>
128
+ <div
129
+ className="prose prose-lg prose-invert max-w-none text-gray-300 leading-relaxed"
130
+ dangerouslySetInnerHTML={{ __html: post.content }}
131
+ />
132
+ </article>
133
+ </main>
134
+ </div>
135
+ );
136
+ }`.trim()
137
+ };
138
+ } else {
139
+ // Default theme (light mode)
140
+ return {
141
+ pageRouter: `
142
+ import { getStaticPropsForPost, getStaticPathsForPosts } from 'chalknotes';
143
+
144
+ export const getStaticProps = getStaticPropsForPost;
145
+ export const getStaticPaths = getStaticPathsForPosts;
146
+
147
+ export default function BlogPost({ post }) {
148
+ return (
149
+ <div className="min-h-screen bg-gray-50">
150
+ <main className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
151
+ <article className="bg-white rounded-lg shadow-sm border border-gray-200 p-8">
152
+ <h1 className="text-4xl font-bold text-gray-900 mb-6 leading-tight">
153
+ {post.title}
154
+ </h1>
155
+ <div
156
+ className="prose prose-lg max-w-none text-gray-700 leading-relaxed"
157
+ dangerouslySetInnerHTML={{ __html: post.content }}
158
+ />
159
+ </article>
160
+ </main>
161
+ </div>
162
+ );
163
+ }`.trim(),
164
+ appRouter: `
41
165
  import { getPostBySlug } from 'chalknotes';
42
166
 
43
167
  export default async function BlogPost({ params }) {
44
168
  const post = await getPostBySlug(params.slug);
45
169
 
46
170
  return (
47
- <article>
48
- <h1>{post.title}</h1>
49
- <div dangerouslySetInnerHTML={{ __html: post.content }} />
50
- </article>
171
+ <div className="min-h-screen bg-gray-50">
172
+ <main className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
173
+ <article className="bg-white rounded-lg shadow-sm border border-gray-200 p-8">
174
+ <h1 className="text-4xl font-bold text-gray-900 mb-6 leading-tight">
175
+ {post.title}
176
+ </h1>
177
+ <div
178
+ className="prose prose-lg max-w-none text-gray-700 leading-relaxed"
179
+ dangerouslySetInnerHTML={{ __html: post.content }}
180
+ />
181
+ </article>
182
+ </main>
183
+ </div>
51
184
  );
185
+ }`.trim()
186
+ };
187
+ }
52
188
  }
53
- `.trim();
54
189
 
190
+ // Create blog page templates
191
+ if (fs.existsSync(pageRouter)) {
192
+ console.log("✅ Page router found");
193
+ const routePath = config.routeBasePath.replace(/^\//, ''); // Remove leading slash
194
+ const slugDir = path.join(pageRouter, routePath, "[slug].js")
195
+ const dirPath = path.dirname(slugDir);
196
+
197
+ const templates = getTemplates(config.theme, config.routeBasePath);
198
+
199
+ fs.mkdirSync(dirPath, { recursive: true });
200
+ fs.writeFileSync(slugDir, templates.pageRouter);
201
+ console.log(`✅ Created pages/${routePath}/[slug].js`);
202
+
203
+ } else if (fs.existsSync(appRouter)) {
204
+ console.log("✅ App router found");
205
+ const routePath = config.routeBasePath.replace(/^\//, ''); // Remove leading slash
206
+ const slugDir = path.join(appRouter, routePath, "[slug]", "page.jsx")
207
+ const dirPath = path.dirname(slugDir);
208
+
209
+ const templates = getTemplates(config.theme, config.routeBasePath);
210
+
55
211
  fs.mkdirSync(dirPath, { recursive: true });
56
- fs.writeFileSync(slugDir, appRouterTemplate)
212
+ fs.writeFileSync(slugDir, templates.appRouter);
213
+ console.log(`✅ Created app/${routePath}/[slug]/page.jsx`);
214
+
57
215
  } else {
58
- console.log("❌ Page router and app router not found");
216
+ console.log("❌ Neither pages/ nor app/ directory found");
217
+ console.log("💡 Please make sure you're running this in a Next.js project");
218
+ process.exit(1);
59
219
  }
60
220
 
61
- // if (!fs.existsSync(configPath)) {
62
- // const configTemplate = `
63
- // module.exports = {
64
- // notionDatabaseId: '',
65
- // notionToken: process.env.NOTION_TOKEN,
66
- // routeBasePath: '/blog',
67
- // plugins: [],
68
- // };
69
- // `;
70
- // fs.writeFileSync(configPath, configTemplate.trim());
71
- // console.log("✅ Created blog.config.js");
72
- // } else {
73
- // console.log("⚠️ blog.config.js already exists");
74
- // }
221
+ console.log("\n🎉 Blog page scaffolded successfully!");
222
+ console.log(`\n📝 Next steps:`);
223
+ console.log("1. Add Tailwind CSS to your project if not already installed");
224
+ console.log("2. Start your development server");
225
+ console.log(`3. Visit ${config.routeBasePath}/[your-post-slug] to see your blog`);
package/blog.config.js ADDED
@@ -0,0 +1,7 @@
1
+ module.exports = {
2
+ notionToken: process.env.NOTION_TOKEN,
3
+ notionDatabaseId: process.env.NOTION_DATABASE_ID,
4
+ routeBasePath: '/blog',
5
+ theme: 'default',
6
+ plugins: [],
7
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chalknotes",
3
- "version": "0.0.24",
3
+ "version": "0.0.26",
4
4
  "description": "A tool that simplifies blogs.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -12,6 +12,9 @@ const getPostBySlug = async (slug) => {
12
12
  }
13
13
  }
14
14
  })
15
+ if (response.results.length === 0) {
16
+ throw new Error("No posts found");
17
+ }
15
18
 
16
19
  for (const page of response.results) {
17
20
  const titleProperty = page.properties["Name"];
@@ -27,22 +30,21 @@ const getPostBySlug = async (slug) => {
27
30
  switch (block.type) {
28
31
  case "paragraph":
29
32
  const textHTML = block.paragraph.rich_text.map(text => text.plain_text).join("");
30
- content += `<p>${textHTML}</p>`
33
+ content += `<p class="mb-4">${textHTML}</p>`
31
34
  break;
32
35
  case "heading_1":
33
36
  const headingHTML = block.heading_1.rich_text.map(text => text.plain_text).join("");
34
- content += `<h1>${headingHTML}</h1>`
37
+ content += `<h1 class="text-2xl font-bold mb-4">${headingHTML}</h1>`
35
38
  break;
36
39
  case "heading_2":
37
40
  const heading2HTML = block.heading_2.rich_text.map(text => text.plain_text).join("");
38
- content += `<h2>${heading2HTML}</h2>`
41
+ content += `<h2 class="text-xl font-bold mb-4">${heading2HTML}</h2>`
39
42
  break;
40
43
  case "heading_3":
41
44
  const heading3HTML = block.heading_3.rich_text.map(text => text.plain_text).join("");
42
- content += `<h3>${heading3HTML}</h3>`
45
+ content += `<h3 class="text-lg font-bold mb-4">${heading3HTML}</h3>`
43
46
  break;
44
47
  }
45
- console.log(content)
46
48
  }
47
49
  return {
48
50
  title,