chalknotes 0.0.29 → 0.0.31

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
@@ -13,6 +13,8 @@
13
13
  - šŸ”§ Interactive configuration setup
14
14
  - šŸ“ Customizable route paths
15
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
16
18
 
17
19
  ---
18
20
 
@@ -45,6 +47,7 @@ npm install chalknotes
45
47
  - Creates `blog.config.js` with default configuration (if needed)
46
48
  - Generates blog routes with clean, responsive templates
47
49
  - Supports light and dark themes
50
+ - **Renders rich Notion content** with images, code blocks, and more
48
51
 
49
52
  ---
50
53
 
@@ -98,6 +101,7 @@ Creates:
98
101
  ```js
99
102
  // pages/blog/[slug].js (or custom route)
100
103
  import { getStaticPropsForPost, getStaticPathsForPosts } from 'chalknotes';
104
+ import NotionRenderer from './NotionRenderer';
101
105
 
102
106
  export const getStaticProps = getStaticPropsForPost;
103
107
  export const getStaticPaths = getStaticPathsForPosts;
@@ -110,10 +114,7 @@ export default function BlogPost({ post }) {
110
114
  <h1 className="text-4xl font-bold text-gray-900 mb-6 leading-tight">
111
115
  {post.title}
112
116
  </h1>
113
- <div
114
- className="prose prose-lg max-w-none text-gray-700 leading-relaxed"
115
- dangerouslySetInnerHTML={{ __html: post.content }}
116
- />
117
+ <NotionRenderer blocks={post.blocks} />
117
118
  </article>
118
119
  </main>
119
120
  </div>
@@ -130,6 +131,7 @@ Creates:
130
131
  ```jsx
131
132
  // app/blog/[slug]/page.jsx (or custom route)
132
133
  import { getPostBySlug } from 'chalknotes';
134
+ import NotionRenderer from './NotionRenderer';
133
135
 
134
136
  export default async function BlogPost({ params }) {
135
137
  const post = await getPostBySlug(params.slug);
@@ -141,10 +143,7 @@ export default async function BlogPost({ params }) {
141
143
  <h1 className="text-4xl font-bold text-gray-900 mb-6 leading-tight">
142
144
  {post.title}
143
145
  </h1>
144
- <div
145
- className="prose prose-lg max-w-none text-gray-700 leading-relaxed"
146
- dangerouslySetInnerHTML={{ __html: post.content }}
147
- />
146
+ <NotionRenderer blocks={post.blocks} />
148
147
  </article>
149
148
  </main>
150
149
  </div>
@@ -157,10 +156,11 @@ export default async function BlogPost({ params }) {
157
156
  ## 🧩 API
158
157
 
159
158
  ### `getPostBySlug(slug: string)`
160
- Fetches a post and renders Notion blocks as HTML.
159
+ Fetches a post and returns structured data for rendering.
161
160
 
162
161
  ```js
163
162
  const post = await getPostBySlug('my-post-title');
163
+ // Returns: { title, slug, blocks, notionPageId }
164
164
  ```
165
165
 
166
166
  ---
@@ -191,6 +191,31 @@ For use with `getStaticPaths` in Page Router.
191
191
 
192
192
  ---
193
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
+
194
219
  ## šŸŽØ Styling
195
220
 
196
221
  The generated templates use Tailwind CSS with:
@@ -199,6 +224,7 @@ The generated templates use Tailwind CSS with:
199
224
  - Typography optimized for readability
200
225
  - Proper spacing and hierarchy
201
226
  - Light and dark mode support
227
+ - **Rich content styling** for all Notion block types
202
228
 
203
229
  Make sure you have Tailwind CSS installed in your project:
204
230
 
@@ -211,7 +237,7 @@ npm install -D tailwindcss @tailwindcss/typography
211
237
  ## šŸ“… Roadmap
212
238
 
213
239
  - [ ] Plugin system for custom components
214
- - [ ] More Notion block support (images, lists, code blocks)
240
+ - [ ] More Notion block support (callouts, bookmarks, toggles)
215
241
  - [ ] RSS feed support
216
242
  - [ ] MDX or Markdown output option
217
243
  - [ ] Custom theme templates
package/bin/cli.js CHANGED
@@ -28,16 +28,16 @@ const configPath = path.join(process.cwd(), 'blog.config.js');
28
28
  if (!fs.existsSync(configPath)) {
29
29
  console.log("\nāŒ blog.config.js not found");
30
30
  console.log("This file is required to configure your blog settings.");
31
-
31
+
32
32
  const rl = readline.createInterface({
33
33
  input: process.stdin,
34
34
  output: process.stdout
35
35
  });
36
-
36
+
37
37
  rl.question("Would you like to create a default blog.config.js? (y/n): ", (answer) => {
38
38
  if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
39
39
  console.log("šŸ“ Creating blog.config.js with default configuration...");
40
-
40
+
41
41
  const configTemplate = `module.exports = {
42
42
  notionToken: process.env.NOTION_TOKEN,
43
43
  notionDatabaseId: process.env.NOTION_DATABASE_ID,
@@ -45,7 +45,7 @@ if (!fs.existsSync(configPath)) {
45
45
  theme: 'default',
46
46
  plugins: [],
47
47
  };`.trim();
48
-
48
+
49
49
  fs.writeFileSync(configPath, configTemplate);
50
50
  console.log("āœ… Created blog.config.js with default configuration");
51
51
  console.log("\nšŸ’” Now you can re-run 'npx chalknotes' to scaffold your blog pages!");
@@ -86,11 +86,12 @@ const appRouter = path.join(process.cwd(), '/app')
86
86
  // Generate templates based on theme
87
87
  function getTemplates(theme, routeBasePath) {
88
88
  const routePath = routeBasePath.replace(/^\//, ''); // Remove leading slash
89
-
89
+
90
90
  if (theme === 'dark') {
91
91
  return {
92
92
  pageRouter: `
93
93
  import { getStaticPropsForPost, getStaticPathsForPosts } from 'chalknotes';
94
+ import NotionRenderer from './NotionRenderer';
94
95
 
95
96
  export const getStaticProps = getStaticPropsForPost;
96
97
  export const getStaticPaths = getStaticPathsForPosts;
@@ -103,10 +104,7 @@ export default function BlogPost({ post }) {
103
104
  <h1 className="text-4xl font-bold text-white mb-6 leading-tight">
104
105
  {post.title}
105
106
  </h1>
106
- <div
107
- className="prose prose-lg prose-invert max-w-none text-gray-300 leading-relaxed"
108
- dangerouslySetInnerHTML={{ __html: post.content }}
109
- />
107
+ <NotionRenderer blocks={post.blocks} />
110
108
  </article>
111
109
  </main>
112
110
  </div>
@@ -114,6 +112,7 @@ export default function BlogPost({ post }) {
114
112
  }`.trim(),
115
113
  appRouter: `
116
114
  import { getPostBySlug } from 'chalknotes';
115
+ import NotionRenderer from './NotionRenderer';
117
116
 
118
117
  export default async function BlogPost({ params }) {
119
118
  const post = await getPostBySlug(params.slug);
@@ -125,10 +124,7 @@ export default async function BlogPost({ params }) {
125
124
  <h1 className="text-4xl font-bold text-white mb-6 leading-tight">
126
125
  {post.title}
127
126
  </h1>
128
- <div
129
- className="prose prose-lg prose-invert max-w-none text-gray-300 leading-relaxed"
130
- dangerouslySetInnerHTML={{ __html: post.content }}
131
- />
127
+ <NotionRenderer blocks={post.blocks} />
132
128
  </article>
133
129
  </main>
134
130
  </div>
@@ -140,6 +136,7 @@ export default async function BlogPost({ params }) {
140
136
  return {
141
137
  pageRouter: `
142
138
  import { getStaticPropsForPost, getStaticPathsForPosts } from 'chalknotes';
139
+ import NotionRenderer from './NotionRenderer';
143
140
 
144
141
  export const getStaticProps = getStaticPropsForPost;
145
142
  export const getStaticPaths = getStaticPathsForPosts;
@@ -152,10 +149,7 @@ export default function BlogPost({ post }) {
152
149
  <h1 className="text-4xl font-bold text-gray-900 mb-6 leading-tight">
153
150
  {post.title}
154
151
  </h1>
155
- <div
156
- className="prose prose-lg max-w-none text-gray-700 leading-relaxed"
157
- dangerouslySetInnerHTML={{ __html: post.content }}
158
- />
152
+ <NotionRenderer blocks={post.blocks} />
159
153
  </article>
160
154
  </main>
161
155
  </div>
@@ -163,6 +157,7 @@ export default function BlogPost({ post }) {
163
157
  }`.trim(),
164
158
  appRouter: `
165
159
  import { getPostBySlug } from 'chalknotes';
160
+ import NotionRenderer from './NotionRenderer';
166
161
 
167
162
  export default async function BlogPost({ params }) {
168
163
  const post = await getPostBySlug(params.slug);
@@ -174,10 +169,7 @@ export default async function BlogPost({ params }) {
174
169
  <h1 className="text-4xl font-bold text-gray-900 mb-6 leading-tight">
175
170
  {post.title}
176
171
  </h1>
177
- <div
178
- className="prose prose-lg max-w-none text-gray-700 leading-relaxed"
179
- dangerouslySetInnerHTML={{ __html: post.content }}
180
- />
172
+ <NotionRenderer blocks={post.blocks} />
181
173
  </article>
182
174
  </main>
183
175
  </div>
@@ -196,7 +188,94 @@ if (fs.existsSync(pageRouter)) {
196
188
 
197
189
  const templates = getTemplates(config.theme, config.routeBasePath);
198
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
+
199
275
  fs.mkdirSync(dirPath, { recursive: true });
276
+ fs.writeFileSync(path.join(dirPath, 'NotionRenderer.jsx'), notionRendererContent);
277
+ console.log(`āœ… Created ${routePath}/NotionRenderer.jsx`);
278
+
200
279
  fs.writeFileSync(slugDir, templates.pageRouter);
201
280
  console.log(`āœ… Created pages/${routePath}/[slug].js`);
202
281
 
@@ -208,7 +287,94 @@ if (fs.existsSync(pageRouter)) {
208
287
 
209
288
  const templates = getTemplates(config.theme, config.routeBasePath);
210
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
+
211
374
  fs.mkdirSync(dirPath, { recursive: true });
375
+ fs.writeFileSync(path.join(dirPath, 'NotionRenderer.jsx'), notionRendererContent);
376
+ console.log(`āœ… Created ${routePath}/[slug]/NotionRenderer.jsx`);
377
+
212
378
  fs.writeFileSync(slugDir, templates.appRouter);
213
379
  console.log(`āœ… Created app/${routePath}/[slug]/page.jsx`);
214
380
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chalknotes",
3
- "version": "0.0.29",
3
+ "version": "0.0.31",
4
4
  "description": "A tool that simplifies blogs.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -1,5 +1,5 @@
1
- const { notion, dbId } = require('./notion');
2
- const { slugify } = require('../utils');
1
+ const { notion, dbId } = require("./notion")
2
+ const { slugify } = require("../utils")
3
3
 
4
4
  const getPostBySlug = async (slug) => {
5
5
  try {
@@ -8,43 +8,116 @@ const getPostBySlug = async (slug) => {
8
8
  filter: {
9
9
  property: "Published",
10
10
  checkbox: {
11
- equals: true
12
- }
13
- }
11
+ equals: true,
12
+ },
13
+ },
14
14
  })
15
+
15
16
  if (response.results.length === 0) {
16
- throw new Error("No posts found");
17
+ throw new Error("No posts found")
17
18
  }
18
19
 
19
20
  for (const page of response.results) {
20
- const titleProperty = page.properties["Name"];
21
- const title = titleProperty?.title?.[0]?.plain_text;
22
- const pageSlug = slugify(title);
21
+ const titleProperty = page.properties["Name"]
22
+ const title = titleProperty?.title?.[0]?.plain_text
23
+ const pageSlug = slugify(title)
23
24
 
24
25
  if (pageSlug === slug) {
25
26
  const response = await notion.blocks.children.list({
26
27
  block_id: page.id,
27
28
  })
28
- let content = ""
29
+
30
+ let content = []
29
31
  for (const block of response.results) {
30
- content += processBlock(block);
32
+ content.push(convertBlockToStructuredJSON(block))
31
33
  }
34
+
32
35
  return {
33
36
  title,
34
37
  slug: pageSlug,
35
- content: content,
38
+ blocks: content,
36
39
  notionPageId: page.id,
37
- };
40
+ }
38
41
  }
39
42
  }
40
43
 
41
- throw new Error(`No post found with slug "${slug}"`);
44
+ throw new Error(`No post found with slug "${slug}"`)
42
45
  } catch (error) {
43
46
  console.error(error)
44
- throw new Error(`Error fetching posts from Notion: ${error.message}`);
47
+ throw new Error(`Error fetching posts from Notion: ${error.message}`)
45
48
  }
46
49
  }
47
50
 
51
+ function convertBlockToStructuredJSON(block) {
52
+ const base = { type: block.type };
53
+
54
+ switch (block.type) {
55
+ case "paragraph":
56
+ return {
57
+ ...base,
58
+ text: extractPlainText(block.paragraph.rich_text),
59
+ richText: block.paragraph.rich_text,
60
+ };
61
+
62
+ case "heading_1":
63
+ case "heading_2":
64
+ case "heading_3":
65
+ return {
66
+ ...base,
67
+ text: extractPlainText(block[block.type].rich_text),
68
+ richText: block[block.type].rich_text,
69
+ };
70
+
71
+ case "bulleted_list_item":
72
+ case "numbered_list_item":
73
+ return {
74
+ ...base,
75
+ text: extractPlainText(block[block.type].rich_text),
76
+ richText: block[block.type].rich_text,
77
+ };
78
+
79
+ case "image": {
80
+ const image = block.image;
81
+ const url = image.type === "external" ? image.external.url : image.file.url;
82
+ const caption = extractPlainText(image.caption);
83
+ return {
84
+ ...base,
85
+ imageUrl: url,
86
+ caption,
87
+ alt: caption || "Blog image from Notion",
88
+ };
89
+ }
90
+
91
+ case "quote":
92
+ return {
93
+ ...base,
94
+ text: extractPlainText(block.quote.rich_text),
95
+ richText: block.quote.rich_text,
96
+ };
97
+
98
+ case "code":
99
+ return {
100
+ ...base,
101
+ code: extractPlainText(block.code.rich_text),
102
+ language: block.code.language || "text",
103
+ };
104
+
105
+ case "divider":
106
+ return { ...base };
107
+
108
+ default:
109
+ return {
110
+ ...base,
111
+ unsupported: true,
112
+ };
113
+ }
114
+ }
115
+
116
+ function extractPlainText(richText = []) {
117
+ return richText.map(t => t.plain_text).join("");
118
+ }
119
+
120
+
48
121
  /**
49
122
  * Process individual Notion blocks and convert to HTML
50
123
  * @param {Object} block - Notion block object
@@ -53,58 +126,74 @@ const getPostBySlug = async (slug) => {
53
126
  function processBlock(block) {
54
127
  switch (block.type) {
55
128
  case "paragraph":
56
- return processRichText(block.paragraph.rich_text, "p", "mb-6 leading-7 text-gray-700 text-lg");
57
-
129
+ return processRichText(block.paragraph.rich_text, "p", "mb-6 leading-relaxed text-slate-700 text-base")
130
+
58
131
  case "heading_1":
59
- return processRichText(block.heading_1.rich_text, "h1", "text-4xl font-bold mb-8 mt-12 text-gray-900 border-b-2 border-gray-200 pb-4");
60
-
132
+ return processRichText(
133
+ block.heading_1.rich_text,
134
+ "h1",
135
+ "text-4xl font-extrabold mb-8 mt-12 text-slate-900 border-b border-slate-200 pb-6",
136
+ )
137
+
61
138
  case "heading_2":
62
- return processRichText(block.heading_2.rich_text, "h2", "text-3xl font-semibold mb-6 mt-10 text-gray-900");
63
-
139
+ return processRichText(block.heading_2.rich_text, "h2", "text-3xl font-bold mb-6 mt-10 text-slate-900")
140
+
64
141
  case "heading_3":
65
- return processRichText(block.heading_3.rich_text, "h3", "text-2xl font-medium mb-4 mt-8 text-gray-900");
66
-
142
+ return processRichText(block.heading_3.rich_text, "h3", "text-2xl font-semibold mb-4 mt-8 text-slate-900")
143
+
67
144
  case "bulleted_list_item":
68
- return processRichText(block.bulleted_list_item.rich_text, "li", "mb-3 ml-6 leading-7 text-gray-700");
69
-
145
+ return processRichText(
146
+ block.bulleted_list_item.rich_text,
147
+ "li",
148
+ "mb-2 ml-6 leading-relaxed text-slate-700 list-disc",
149
+ )
150
+
70
151
  case "numbered_list_item":
71
- return processRichText(block.numbered_list_item.rich_text, "li", "mb-3 ml-6 leading-7 text-gray-700");
72
-
152
+ return processRichText(
153
+ block.numbered_list_item.rich_text,
154
+ "li",
155
+ "mb-2 ml-6 leading-relaxed text-slate-700 list-decimal",
156
+ )
157
+
73
158
  case "quote":
74
- return processRichText(block.quote.rich_text, "blockquote", "border-l-4 border-blue-500 pl-8 italic text-gray-600 mb-8 bg-blue-50 py-6 rounded-r-lg text-lg leading-7");
75
-
159
+ return processRichText(
160
+ block.quote.rich_text,
161
+ "blockquote",
162
+ "border-l-4 border-indigo-400 pl-6 italic text-slate-600 mb-8 bg-slate-50 py-6 rounded-r-xl text-lg leading-relaxed font-medium",
163
+ )
164
+
76
165
  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-900 text-gray-100 p-6 rounded-lg overflow-x-auto mb-8 text-sm leading-6 shadow-lg"><code class="language-${language}">${escapeHtml(codeContent)}</code></pre>`;
80
-
166
+ const codeContent = block.code.rich_text.map((text) => text.plain_text).join("")
167
+ const language = block.code.language || "text"
168
+ return `<pre class="bg-slate-900 text-slate-100 p-6 rounded-xl overflow-x-auto mb-8 text-sm leading-6 shadow-xl border border-slate-800"><code class="language-${language}">${escapeHtml(codeContent)}</code></pre>`
169
+
81
170
  case "image":
82
- return processImage(block.image);
83
-
171
+ return processImage(block.image)
172
+
84
173
  case "divider":
85
- return '<hr class="my-12 border-gray-300" />';
86
-
174
+ return '<hr class="my-12 border-slate-200" />'
175
+
87
176
  case "callout":
88
- return processCallout(block.callout);
89
-
177
+ return processCallout(block.callout)
178
+
90
179
  case "toggle":
91
- return processToggle(block.toggle);
92
-
180
+ return processToggle(block.toggle)
181
+
93
182
  case "table_of_contents":
94
- return '<div class="bg-blue-50 border border-blue-200 rounded-lg p-6 mb-8 shadow-sm"><p class="text-blue-800 font-medium text-lg">šŸ“‹ Table of Contents</p><p class="text-blue-600 text-sm mt-1">(Generated automatically)</p></div>';
95
-
183
+ return '<div class="bg-indigo-50 border border-indigo-200 rounded-xl p-6 mb-8 shadow-sm"><p class="text-indigo-900 font-semibold text-lg">šŸ“‹ Table of Contents</p><p class="text-indigo-600 text-sm mt-1">(Generated automatically)</p></div>'
184
+
96
185
  case "bookmark":
97
- return processBookmark(block.bookmark);
98
-
186
+ return processBookmark(block.bookmark)
187
+
99
188
  case "equation":
100
- return `<div class="bg-gray-50 p-6 rounded-lg mb-8 text-center border border-gray-200 shadow-sm"><p class="text-gray-600 mb-3 font-medium">šŸ“ Mathematical equation</p><p class="font-mono text-sm bg-white p-4 rounded border shadow-sm">${block.equation.expression}</p></div>`;
101
-
189
+ return `<div class="bg-slate-50 p-6 rounded-xl mb-8 text-center border border-slate-200 shadow-sm"><p class="text-slate-600 mb-3 font-semibold">šŸ“ Mathematical equation</p><p class="font-mono text-sm bg-white p-4 rounded-lg border shadow-sm">${block.equation.expression}</p></div>`
190
+
102
191
  default:
103
192
  // For unsupported blocks, try to extract plain text
104
193
  if (block[block.type]?.rich_text) {
105
- return processRichText(block[block.type].rich_text, "p", "mb-6 text-gray-500 italic text-lg");
194
+ return processRichText(block[block.type].rich_text, "p", "mb-6 text-slate-500 italic text-base")
106
195
  }
107
- return "";
196
+ return ""
108
197
  }
109
198
  }
110
199
 
@@ -114,93 +203,65 @@ function processBlock(block) {
114
203
  * @returns {string} HTML string
115
204
  */
116
205
  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
- // Check for Notion's size property in the block itself
122
- let maxWidthClass = "max-w-full";
123
- let alignmentClass = "text-center";
124
-
125
- // Try to get size from block properties (Notion API structure)
126
- if (image.size) {
127
- switch (image.size) {
128
- case 'small':
129
- maxWidthClass = "max-w-sm";
130
- break;
131
- case 'medium':
132
- maxWidthClass = "max-w-lg";
133
- break;
134
- case 'large':
135
- maxWidthClass = "max-w-2xl";
136
- break;
137
- default:
138
- maxWidthClass = "max-w-full";
139
- }
140
- }
141
-
142
- // Check for alignment in block properties
143
- if (image.alignment) {
144
- switch (image.alignment) {
145
- case 'left':
146
- alignmentClass = "text-left";
147
- break;
148
- case 'center':
149
- alignmentClass = "text-center";
150
- break;
151
- case 'right':
152
- alignmentClass = "text-right";
153
- break;
154
- default:
155
- alignmentClass = "text-center";
156
- }
157
- }
158
-
159
- const responsiveClasses = `w-full ${maxWidthClass} h-auto rounded-lg shadow-sm`;
160
-
206
+ const imageUrl = image.type === "external" ? image.external.url : image.file.url;
207
+ const caption = image.caption?.map((text) => text.plain_text).join("") || "";
208
+ const altText = caption || "Blog image from Notion";
209
+
210
+ // Strict size constraints for blog layout
211
+ const containerClasses = "max-w-[400px] mx-auto px-4 my-4";
212
+ const figureClasses = "relative w-full max-w-[300px] sm:max-w-[400px] h-[300px]";
213
+
214
+ // Log image URL for debugging
215
+ console.log("Image URL:", imageUrl);
216
+
161
217
  return `
162
- <figure class="my-8 ${alignmentClass}">
163
- <img
218
+ <div className="${containerClasses}">
219
+ <figure className="${figureClasses}">
220
+ <Image
164
221
  src="${imageUrl}"
165
222
  alt="${escapeHtml(altText)}"
166
- class="${responsiveClasses}"
167
- loading="lazy"
223
+ fill
224
+ className="rounded-xl object-contain"
225
+ sizes="(max-width: 640px) 300px, 400px"
226
+ priority={false}
168
227
  />
169
- ${caption ? `<figcaption class="text-gray-500 mt-3 text-sm italic">${escapeHtml(caption)}</figcaption>` : ''}
228
+ ${caption ? `<figcaption className="text-slate-600 mt-2 text-sm text-center font-medium italic">${escapeHtml(caption)}</figcaption>` : ""}
170
229
  </figure>
230
+ </div>
171
231
  `.trim();
172
232
  }
173
233
 
234
+
174
235
  /**
175
236
  * Process callout block
176
237
  * @param {Object} callout - Notion callout block
177
238
  * @returns {string} HTML string
178
239
  */
179
240
  function processCallout(callout) {
180
- const content = processRichText(callout.rich_text, "div", "");
181
- const icon = callout.icon?.emoji || "šŸ’”";
182
- const bgColor = callout.color || "blue";
183
-
241
+ const content = processRichText(callout.rich_text, "div", "")
242
+ const icon = callout.icon?.emoji || "šŸ’”"
243
+ const bgColor = callout.color || "blue"
244
+
184
245
  const colorClasses = {
185
- blue: "bg-blue-50 border-blue-200 text-blue-800",
186
- gray: "bg-gray-50 border-gray-200 text-gray-800",
187
- yellow: "bg-yellow-50 border-yellow-200 text-yellow-800",
188
- red: "bg-red-50 border-red-200 text-red-800",
189
- green: "bg-green-50 border-green-200 text-green-800",
190
- purple: "bg-purple-50 border-purple-200 text-purple-800",
191
- pink: "bg-pink-50 border-pink-200 text-pink-800"
192
- };
193
-
194
- const colorClass = colorClasses[bgColor] || colorClasses.blue;
195
-
246
+ blue: "bg-blue-50 border-blue-200 text-blue-900",
247
+ gray: "bg-slate-50 border-slate-200 text-slate-900",
248
+ yellow: "bg-amber-50 border-amber-200 text-amber-900",
249
+ red: "bg-red-50 border-red-200 text-red-900",
250
+ green: "bg-emerald-50 border-emerald-200 text-emerald-900",
251
+ purple: "bg-purple-50 border-purple-200 text-purple-900",
252
+ pink: "bg-pink-50 border-pink-200 text-pink-900",
253
+ }
254
+
255
+ const colorClass = colorClasses[bgColor] || colorClasses.blue
256
+
196
257
  return `
197
- <div class="${colorClass} border-l-4 p-6 my-8 rounded-r-lg shadow-sm">
258
+ <div class="${colorClass} border-l-4 p-6 my-8 rounded-r-xl shadow-sm">
198
259
  <div class="flex items-start">
199
260
  <span class="mr-4 text-2xl flex-shrink-0">${icon}</span>
200
- <div class="flex-1 leading-7 text-lg">${content}</div>
261
+ <div class="flex-1 leading-relaxed text-base font-medium">${content}</div>
201
262
  </div>
202
263
  </div>
203
- `.trim();
264
+ `.trim()
204
265
  }
205
266
 
206
267
  /**
@@ -209,24 +270,24 @@ function processCallout(callout) {
209
270
  * @returns {string} HTML string
210
271
  */
211
272
  function processBookmark(bookmark) {
212
- const url = bookmark.url;
213
- const title = bookmark.caption?.[0]?.plain_text || "Bookmark";
214
-
273
+ const url = bookmark.url
274
+ const title = bookmark.caption?.[0]?.plain_text || "Bookmark"
275
+
215
276
  return `
216
277
  <div class="my-8">
217
- <a href="${url}" target="_blank" rel="noopener noreferrer" class="block border border-gray-200 rounded-lg p-6 hover:border-gray-300 hover:shadow-lg transition-all duration-200 bg-white">
278
+ <a href="${url}" target="_blank" rel="noopener noreferrer" class="block border border-slate-200 rounded-xl p-6 hover:border-slate-300 hover:shadow-lg transition-all duration-300 bg-white hover:bg-slate-50">
218
279
  <div class="flex items-center">
219
280
  <div class="flex-1 min-w-0">
220
- <p class="font-semibold text-gray-900 truncate text-lg">${escapeHtml(title)}</p>
221
- <p class="text-sm text-gray-500 truncate mt-1">${url}</p>
281
+ <p class="font-semibold text-slate-900 truncate text-lg">${escapeHtml(title)}</p>
282
+ <p class="text-sm text-slate-500 truncate mt-2">${url}</p>
222
283
  </div>
223
- <svg class="w-6 h-6 text-gray-400 ml-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
284
+ <svg class="w-6 h-6 text-slate-400 ml-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
224
285
  <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>
225
286
  </svg>
226
287
  </div>
227
288
  </a>
228
289
  </div>
229
- `.trim();
290
+ `.trim()
230
291
  }
231
292
 
232
293
  /**
@@ -235,18 +296,18 @@ function processBookmark(bookmark) {
235
296
  * @returns {string} HTML string
236
297
  */
237
298
  function processToggle(toggle) {
238
- const content = processRichText(toggle.rich_text, "div", "");
299
+ const content = processRichText(toggle.rich_text, "div", "")
239
300
  return `
240
301
  <details class="my-6">
241
- <summary class="cursor-pointer font-semibold text-gray-700 hover:text-gray-900 text-lg leading-7">
302
+ <summary class="cursor-pointer font-semibold text-slate-700 hover:text-slate-900 text-lg leading-relaxed transition-colors duration-200">
242
303
  ${content}
243
304
  </summary>
244
- <div class="mt-4 pl-6 border-l-2 border-gray-200">
305
+ <div class="mt-4 pl-6 border-l-2 border-slate-200">
245
306
  <!-- Toggle content would go here if Notion API provided it -->
246
- <p class="text-gray-600 text-base italic">Toggle content not available in current API</p>
307
+ <p class="text-slate-600 text-base italic">Toggle content not available in current API</p>
247
308
  </div>
248
309
  </details>
249
- `.trim();
310
+ `.trim()
250
311
  }
251
312
 
252
313
  /**
@@ -257,24 +318,28 @@ function processToggle(toggle) {
257
318
  * @returns {string} HTML string
258
319
  */
259
320
  function processRichText(richText, tag, className) {
260
- if (!richText || richText.length === 0) return "";
261
-
262
- const content = richText.map(text => {
263
- let result = text.plain_text;
264
-
265
- // Apply annotations
266
- if (text.annotations.bold) result = `<strong class="font-bold">${result}</strong>`;
267
- if (text.annotations.italic) result = `<em class="italic">${result}</em>`;
268
- if (text.annotations.strikethrough) result = `<del class="line-through">${result}</del>`;
269
- if (text.annotations.code) result = `<code class="bg-gray-100 px-2 py-1 rounded text-sm font-mono text-gray-800">${result}</code>`;
270
-
271
- // Apply links
272
- if (text.href) result = `<a href="${text.href}" class="text-blue-600 hover:text-blue-800 underline font-medium" target="_blank" rel="noopener noreferrer">${result}</a>`;
273
-
274
- return result;
275
- }).join("");
276
-
277
- return `<${tag} class="${className}">${content}</${tag}>`;
321
+ if (!richText || richText.length === 0) return ""
322
+
323
+ const content = richText
324
+ .map((text) => {
325
+ let result = text.plain_text
326
+
327
+ // Apply annotations
328
+ if (text.annotations.bold) result = `<strong class="font-bold">${result}</strong>`
329
+ if (text.annotations.italic) result = `<em class="italic">${result}</em>`
330
+ if (text.annotations.strikethrough) result = `<del class="line-through">${result}</del>`
331
+ if (text.annotations.code)
332
+ result = `<code class="bg-slate-100 px-2 py-1 rounded-md text-sm font-mono text-slate-800 border border-slate-200">${result}</code>`
333
+
334
+ // Apply links
335
+ if (text.href)
336
+ result = `<a href="${text.href}" class="text-indigo-600 hover:text-indigo-800 underline font-medium transition-colors duration-200" target="_blank" rel="noopener noreferrer">${result}</a>`
337
+
338
+ return result
339
+ })
340
+ .join("")
341
+
342
+ return `<${tag} class="${className}">${content}</${tag}>`
278
343
  }
279
344
 
280
345
  /**
@@ -284,15 +349,15 @@ function processRichText(richText, tag, className) {
284
349
  */
285
350
  function escapeHtml(text) {
286
351
  const map = {
287
- '&': '&amp;',
288
- '<': '&lt;',
289
- '>': '&gt;',
290
- '"': '&quot;',
291
- "'": '&#039;'
292
- };
293
- return text.replace(/[&<>"']/g, m => map[m]);
352
+ "&": "&amp;",
353
+ "<": "&lt;",
354
+ ">": "&gt;",
355
+ '"': "&quot;",
356
+ "'": "&#039;",
357
+ }
358
+ return text.replace(/[&<>"']/g, (m) => map[m])
294
359
  }
295
360
 
296
361
  module.exports = {
297
- getPostBySlug
362
+ getPostBySlug,
298
363
  }
package/blog.config.js DELETED
@@ -1,7 +0,0 @@
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
- };