chalknotes 0.0.25 β 0.0.27
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 +104 -27
- package/bin/cli.js +193 -42
- package/blog.config.js +7 -0
- package/package.json +1 -1
- package/src/lib/getPostBySlug.js +218 -39
package/README.md
CHANGED
@@ -9,6 +9,9 @@
|
|
9
9
|
- π Fetch blog posts from Notion
|
10
10
|
- πͺ Auto-generate routes for App Router or Page Router
|
11
11
|
- βοΈ Helpers for `getStaticProps` / `getStaticPaths`
|
12
|
+
- π¨ Clean, responsive themes (light & dark mode)
|
13
|
+
- π§ Interactive configuration setup
|
14
|
+
- π Customizable route paths
|
12
15
|
- π§ Minimal setup β just run `chalknotes`
|
13
16
|
|
14
17
|
---
|
@@ -25,28 +28,64 @@ npm install chalknotes
|
|
25
28
|
|
26
29
|
## π§ββοΈ Quick Start
|
27
30
|
|
28
|
-
|
29
|
-
|
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
|
+
```
|
31
42
|
|
32
|
-
|
33
|
-
-
|
34
|
-
-
|
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
|
35
48
|
|
36
49
|
---
|
37
50
|
|
38
|
-
## π§
|
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
|
+
```
|
39
67
|
|
40
|
-
|
68
|
+
### Configuration Options
|
41
69
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
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
|
46
83
|
|
47
|
-
|
48
|
-
-
|
49
|
-
-
|
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
|
50
89
|
|
51
90
|
---
|
52
91
|
|
@@ -57,7 +96,7 @@ Your Notion database should have:
|
|
57
96
|
Creates:
|
58
97
|
|
59
98
|
```js
|
60
|
-
// pages/blog/[slug].js
|
99
|
+
// pages/blog/[slug].js (or custom route)
|
61
100
|
import { getStaticPropsForPost, getStaticPathsForPosts } from 'chalknotes';
|
62
101
|
|
63
102
|
export const getStaticProps = getStaticPropsForPost;
|
@@ -65,10 +104,19 @@ export const getStaticPaths = getStaticPathsForPosts;
|
|
65
104
|
|
66
105
|
export default function BlogPost({ post }) {
|
67
106
|
return (
|
68
|
-
<
|
69
|
-
<
|
70
|
-
|
71
|
-
|
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>
|
72
120
|
);
|
73
121
|
}
|
74
122
|
```
|
@@ -80,17 +128,26 @@ export default function BlogPost({ post }) {
|
|
80
128
|
Creates:
|
81
129
|
|
82
130
|
```jsx
|
83
|
-
// app/blog/[slug]/page.jsx
|
131
|
+
// app/blog/[slug]/page.jsx (or custom route)
|
84
132
|
import { getPostBySlug } from 'chalknotes';
|
85
133
|
|
86
134
|
export default async function BlogPost({ params }) {
|
87
135
|
const post = await getPostBySlug(params.slug);
|
88
136
|
|
89
137
|
return (
|
90
|
-
<
|
91
|
-
<
|
92
|
-
|
93
|
-
|
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>
|
94
151
|
);
|
95
152
|
}
|
96
153
|
```
|
@@ -134,12 +191,32 @@ For use with `getStaticPaths` in Page Router.
|
|
134
191
|
|
135
192
|
---
|
136
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
|
+
|
137
211
|
## π
Roadmap
|
138
212
|
|
139
|
-
- [ ]
|
213
|
+
- [ ] Plugin system for custom components
|
214
|
+
- [ ] More Notion block support (images, lists, code blocks)
|
140
215
|
- [ ] RSS feed support
|
141
216
|
- [ ] MDX or Markdown output option
|
142
|
-
- [ ] Custom
|
217
|
+
- [ ] Custom theme templates
|
218
|
+
- [ ] Search functionality
|
219
|
+
- [ ] Categories and tags support
|
143
220
|
|
144
221
|
---
|
145
222
|
|
package/bin/cli.js
CHANGED
@@ -1,20 +1,95 @@
|
|
1
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
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
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
|
-
<
|
26
|
-
<
|
27
|
-
|
28
|
-
|
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
|
-
|
114
|
+
}`.trim(),
|
115
|
+
appRouter: `
|
116
|
+
import { getPostBySlug } from 'chalknotes';
|
32
117
|
|
33
|
-
|
34
|
-
|
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
|
-
|
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
|
-
<
|
48
|
-
<
|
49
|
-
|
50
|
-
|
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,
|
212
|
+
fs.writeFileSync(slugDir, templates.appRouter);
|
213
|
+
console.log(`β
Created app/${routePath}/[slug]/page.jsx`);
|
214
|
+
|
57
215
|
} else {
|
58
|
-
console.log("β
|
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
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
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
package/package.json
CHANGED
package/src/lib/getPostBySlug.js
CHANGED
@@ -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"];
|
@@ -24,24 +27,7 @@ const getPostBySlug = async (slug) => {
|
|
24
27
|
})
|
25
28
|
let content = ""
|
26
29
|
for (const block of response.results) {
|
27
|
-
|
28
|
-
case "paragraph":
|
29
|
-
const textHTML = block.paragraph.rich_text.map(text => text.plain_text).join("");
|
30
|
-
content += `<p class="mb-4">${textHTML}</p>`
|
31
|
-
break;
|
32
|
-
case "heading_1":
|
33
|
-
const headingHTML = block.heading_1.rich_text.map(text => text.plain_text).join("");
|
34
|
-
content += `<h1 class="text-2xl font-bold mb-4">${headingHTML}</h1>`
|
35
|
-
break;
|
36
|
-
case "heading_2":
|
37
|
-
const heading2HTML = block.heading_2.rich_text.map(text => text.plain_text).join("");
|
38
|
-
content += `<h2 class="text-xl font-bold mb-4">${heading2HTML}</h2>`
|
39
|
-
break;
|
40
|
-
case "heading_3":
|
41
|
-
const heading3HTML = block.heading_3.rich_text.map(text => text.plain_text).join("");
|
42
|
-
content += `<h3 class="text-lg font-bold mb-4">${heading3HTML}</h3>`
|
43
|
-
break;
|
44
|
-
}
|
30
|
+
content += processBlock(block);
|
45
31
|
}
|
46
32
|
return {
|
47
33
|
title,
|
@@ -59,29 +45,222 @@ const getPostBySlug = async (slug) => {
|
|
59
45
|
}
|
60
46
|
}
|
61
47
|
|
62
|
-
|
63
|
-
|
48
|
+
/**
|
49
|
+
* Process individual Notion blocks and convert to HTML
|
50
|
+
* @param {Object} block - Notion block object
|
51
|
+
* @returns {string} HTML string
|
52
|
+
*/
|
53
|
+
function processBlock(block) {
|
54
|
+
switch (block.type) {
|
55
|
+
case "paragraph":
|
56
|
+
return processRichText(block.paragraph.rich_text, "p", "mb-4");
|
57
|
+
|
58
|
+
case "heading_1":
|
59
|
+
return processRichText(block.heading_1.rich_text, "h1", "text-3xl font-bold mb-6");
|
60
|
+
|
61
|
+
case "heading_2":
|
62
|
+
return processRichText(block.heading_2.rich_text, "h2", "text-2xl font-bold mb-5");
|
63
|
+
|
64
|
+
case "heading_3":
|
65
|
+
return processRichText(block.heading_3.rich_text, "h3", "text-xl font-bold mb-4");
|
66
|
+
|
67
|
+
case "bulleted_list_item":
|
68
|
+
return processRichText(block.bulleted_list_item.rich_text, "li", "mb-2");
|
69
|
+
|
70
|
+
case "numbered_list_item":
|
71
|
+
return processRichText(block.numbered_list_item.rich_text, "li", "mb-2");
|
72
|
+
|
73
|
+
case "quote":
|
74
|
+
return processRichText(block.quote.rich_text, "blockquote", "border-l-4 border-gray-300 pl-4 italic text-gray-600 mb-4");
|
75
|
+
|
76
|
+
case "code":
|
77
|
+
const codeContent = block.code.rich_text.map(text => text.plain_text).join("");
|
78
|
+
const language = block.code.language || 'text';
|
79
|
+
return `<pre class="bg-gray-100 p-4 rounded-lg overflow-x-auto mb-4"><code class="language-${language}">${escapeHtml(codeContent)}</code></pre>`;
|
80
|
+
|
81
|
+
case "image":
|
82
|
+
return processImage(block.image);
|
83
|
+
|
84
|
+
case "divider":
|
85
|
+
return '<hr class="my-8 border-gray-300" />';
|
86
|
+
|
87
|
+
case "callout":
|
88
|
+
return processCallout(block.callout);
|
89
|
+
|
90
|
+
case "toggle":
|
91
|
+
return processToggle(block.toggle);
|
92
|
+
|
93
|
+
case "table_of_contents":
|
94
|
+
return '<div class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-4"><p class="text-blue-800 font-medium">π Table of Contents</p><p class="text-blue-600 text-sm">(Generated automatically)</p></div>';
|
95
|
+
|
96
|
+
case "bookmark":
|
97
|
+
return processBookmark(block.bookmark);
|
98
|
+
|
99
|
+
case "equation":
|
100
|
+
return `<div class="bg-gray-50 p-4 rounded-lg mb-4 text-center"><p class="text-gray-600">π Mathematical equation</p><p class="font-mono text-sm">${block.equation.expression}</p></div>`;
|
101
|
+
|
102
|
+
default:
|
103
|
+
// For unsupported blocks, try to extract plain text
|
104
|
+
if (block[block.type]?.rich_text) {
|
105
|
+
return processRichText(block[block.type].rich_text, "p", "mb-4 text-gray-500");
|
106
|
+
}
|
107
|
+
return "";
|
108
|
+
}
|
64
109
|
}
|
65
|
-
// const slugify = (title) =>
|
66
|
-
// title
|
67
|
-
// .toLowerCase()
|
68
|
-
// .replace(/[^a-z0-9]+/g, "-")
|
69
|
-
// .replace(/(^-|-$)/g, "");
|
70
110
|
|
71
|
-
|
72
|
-
|
73
|
-
|
111
|
+
/**
|
112
|
+
* Process image block with size, alignment, and alt text
|
113
|
+
* @param {Object} image - Notion image block
|
114
|
+
* @returns {string} HTML string
|
115
|
+
*/
|
116
|
+
function processImage(image) {
|
117
|
+
const imageUrl = image.type === 'external' ? image.external.url : image.file.url;
|
118
|
+
const caption = image.caption?.map(text => text.plain_text).join("") || "";
|
119
|
+
const altText = caption || "Image";
|
120
|
+
|
121
|
+
// Get image size and alignment from Notion properties
|
122
|
+
let sizeClass = "w-full"; // Default full width
|
123
|
+
let alignmentClass = "text-center"; // Default center alignment
|
124
|
+
|
125
|
+
// You can extend this based on Notion's image properties
|
126
|
+
// For now, we'll use responsive sizing
|
127
|
+
const responsiveClasses = "max-w-full h-auto rounded-lg shadow-sm";
|
128
|
+
|
129
|
+
return `
|
130
|
+
<figure class="my-8 ${alignmentClass}">
|
131
|
+
<img
|
132
|
+
src="${imageUrl}"
|
133
|
+
alt="${escapeHtml(altText)}"
|
134
|
+
class="${sizeClass} ${responsiveClasses}"
|
135
|
+
loading="lazy"
|
136
|
+
/>
|
137
|
+
${caption ? `<figcaption class="text-center text-gray-600 mt-2 text-sm">${escapeHtml(caption)}</figcaption>` : ''}
|
138
|
+
</figure>
|
139
|
+
`.trim();
|
140
|
+
}
|
74
141
|
|
75
|
-
|
142
|
+
/**
|
143
|
+
* Process callout block
|
144
|
+
* @param {Object} callout - Notion callout block
|
145
|
+
* @returns {string} HTML string
|
146
|
+
*/
|
147
|
+
function processCallout(callout) {
|
148
|
+
const content = processRichText(callout.rich_text, "div", "");
|
149
|
+
const icon = callout.icon?.emoji || "π‘";
|
150
|
+
const bgColor = callout.color || "blue";
|
151
|
+
|
152
|
+
const colorClasses = {
|
153
|
+
blue: "bg-blue-50 border-blue-200 text-blue-800",
|
154
|
+
gray: "bg-gray-50 border-gray-200 text-gray-800",
|
155
|
+
yellow: "bg-yellow-50 border-yellow-200 text-yellow-800",
|
156
|
+
red: "bg-red-50 border-red-200 text-red-800",
|
157
|
+
green: "bg-green-50 border-green-200 text-green-800",
|
158
|
+
purple: "bg-purple-50 border-purple-200 text-purple-800",
|
159
|
+
pink: "bg-pink-50 border-pink-200 text-pink-800"
|
160
|
+
};
|
161
|
+
|
162
|
+
const colorClass = colorClasses[bgColor] || colorClasses.blue;
|
163
|
+
|
164
|
+
return `
|
165
|
+
<div class="${colorClass} border-l-4 p-4 my-4 rounded-r-lg">
|
166
|
+
<div class="flex items-start">
|
167
|
+
<span class="mr-3 text-lg">${icon}</span>
|
168
|
+
<div class="flex-1">${content}</div>
|
169
|
+
</div>
|
170
|
+
</div>
|
171
|
+
`.trim();
|
172
|
+
}
|
76
173
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
174
|
+
/**
|
175
|
+
* Process toggle block
|
176
|
+
* @param {Object} toggle - Notion toggle block
|
177
|
+
* @returns {string} HTML string
|
178
|
+
*/
|
179
|
+
function processToggle(toggle) {
|
180
|
+
const content = processRichText(toggle.rich_text, "div", "");
|
181
|
+
return `
|
182
|
+
<details class="my-4">
|
183
|
+
<summary class="cursor-pointer font-medium text-gray-700 hover:text-gray-900">
|
184
|
+
${content}
|
185
|
+
</summary>
|
186
|
+
<div class="mt-2 pl-4 border-l-2 border-gray-200">
|
187
|
+
<!-- Toggle content would go here if Notion API provided it -->
|
188
|
+
<p class="text-gray-600 text-sm">Toggle content not available in current API</p>
|
189
|
+
</div>
|
190
|
+
</details>
|
191
|
+
`.trim();
|
192
|
+
}
|
86
193
|
|
87
|
-
|
194
|
+
/**
|
195
|
+
* Process bookmark block
|
196
|
+
* @param {Object} bookmark - Notion bookmark block
|
197
|
+
* @returns {string} HTML string
|
198
|
+
*/
|
199
|
+
function processBookmark(bookmark) {
|
200
|
+
const url = bookmark.url;
|
201
|
+
const title = bookmark.caption?.[0]?.plain_text || "Bookmark";
|
202
|
+
|
203
|
+
return `
|
204
|
+
<div class="my-4">
|
205
|
+
<a href="${url}" target="_blank" rel="noopener noreferrer" class="block border border-gray-200 rounded-lg p-4 hover:border-gray-300 transition-colors">
|
206
|
+
<div class="flex items-center">
|
207
|
+
<div class="flex-1">
|
208
|
+
<p class="font-medium text-gray-900">${escapeHtml(title)}</p>
|
209
|
+
<p class="text-sm text-gray-500 truncate">${url}</p>
|
210
|
+
</div>
|
211
|
+
<svg class="w-5 h-5 text-gray-400 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
212
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path>
|
213
|
+
</svg>
|
214
|
+
</div>
|
215
|
+
</a>
|
216
|
+
</div>
|
217
|
+
`.trim();
|
218
|
+
}
|
219
|
+
|
220
|
+
/**
|
221
|
+
* Process rich text and apply formatting
|
222
|
+
* @param {Array} richText - Array of rich text objects
|
223
|
+
* @param {string} tag - HTML tag to wrap content
|
224
|
+
* @param {string} className - CSS classes
|
225
|
+
* @returns {string} HTML string
|
226
|
+
*/
|
227
|
+
function processRichText(richText, tag, className) {
|
228
|
+
if (!richText || richText.length === 0) return "";
|
229
|
+
|
230
|
+
const content = richText.map(text => {
|
231
|
+
let result = text.plain_text;
|
232
|
+
|
233
|
+
// Apply annotations
|
234
|
+
if (text.annotations.bold) result = `<strong>${result}</strong>`;
|
235
|
+
if (text.annotations.italic) result = `<em>${result}</em>`;
|
236
|
+
if (text.annotations.strikethrough) result = `<del>${result}</del>`;
|
237
|
+
if (text.annotations.code) result = `<code class="bg-gray-100 px-1 py-0.5 rounded text-sm font-mono">${result}</code>`;
|
238
|
+
|
239
|
+
// Apply links
|
240
|
+
if (text.href) result = `<a href="${text.href}" class="text-blue-600 hover:text-blue-800 underline" target="_blank" rel="noopener noreferrer">${result}</a>`;
|
241
|
+
|
242
|
+
return result;
|
243
|
+
}).join("");
|
244
|
+
|
245
|
+
return `<${tag} class="${className}">${content}</${tag}>`;
|
246
|
+
}
|
247
|
+
|
248
|
+
/**
|
249
|
+
* Escape HTML special characters
|
250
|
+
* @param {string} text - Text to escape
|
251
|
+
* @returns {string} Escaped text
|
252
|
+
*/
|
253
|
+
function escapeHtml(text) {
|
254
|
+
const map = {
|
255
|
+
'&': '&',
|
256
|
+
'<': '<',
|
257
|
+
'>': '>',
|
258
|
+
'"': '"',
|
259
|
+
"'": '''
|
260
|
+
};
|
261
|
+
return text.replace(/[&<>"']/g, m => map[m]);
|
262
|
+
}
|
263
|
+
|
264
|
+
module.exports = {
|
265
|
+
getPostBySlug
|
266
|
+
}
|