chalknotes 0.0.29 → 0.0.30

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
 
@@ -97,7 +100,7 @@ Creates:
97
100
 
98
101
  ```js
99
102
  // pages/blog/[slug].js (or custom route)
100
- import { getStaticPropsForPost, getStaticPathsForPosts } from 'chalknotes';
103
+ import { getStaticPropsForPost, getStaticPathsForPosts, NotionRenderer } from 'chalknotes';
101
104
 
102
105
  export const getStaticProps = getStaticPropsForPost;
103
106
  export const getStaticPaths = getStaticPathsForPosts;
@@ -110,10 +113,7 @@ export default function BlogPost({ post }) {
110
113
  <h1 className="text-4xl font-bold text-gray-900 mb-6 leading-tight">
111
114
  {post.title}
112
115
  </h1>
113
- <div
114
- className="prose prose-lg max-w-none text-gray-700 leading-relaxed"
115
- dangerouslySetInnerHTML={{ __html: post.content }}
116
- />
116
+ <NotionRenderer blocks={post.blocks} />
117
117
  </article>
118
118
  </main>
119
119
  </div>
@@ -129,7 +129,7 @@ Creates:
129
129
 
130
130
  ```jsx
131
131
  // app/blog/[slug]/page.jsx (or custom route)
132
- import { getPostBySlug } from 'chalknotes';
132
+ import { getPostBySlug, NotionRenderer } from 'chalknotes';
133
133
 
134
134
  export default async function BlogPost({ params }) {
135
135
  const post = await getPostBySlug(params.slug);
@@ -141,10 +141,7 @@ export default async function BlogPost({ params }) {
141
141
  <h1 className="text-4xl font-bold text-gray-900 mb-6 leading-tight">
142
142
  {post.title}
143
143
  </h1>
144
- <div
145
- className="prose prose-lg max-w-none text-gray-700 leading-relaxed"
146
- dangerouslySetInnerHTML={{ __html: post.content }}
147
- />
144
+ <NotionRenderer blocks={post.blocks} />
148
145
  </article>
149
146
  </main>
150
147
  </div>
@@ -157,10 +154,11 @@ export default async function BlogPost({ params }) {
157
154
  ## 🧩 API
158
155
 
159
156
  ### `getPostBySlug(slug: string)`
160
- Fetches a post and renders Notion blocks as HTML.
157
+ Fetches a post and returns structured data for rendering.
161
158
 
162
159
  ```js
163
160
  const post = await getPostBySlug('my-post-title');
161
+ // Returns: { title, slug, blocks, notionPageId }
164
162
  ```
165
163
 
166
164
  ---
@@ -191,6 +189,31 @@ For use with `getStaticPaths` in Page Router.
191
189
 
192
190
  ---
193
191
 
192
+ ### `NotionRenderer`
193
+ React component for rendering Notion blocks:
194
+
195
+ ```jsx
196
+ import { NotionRenderer } from 'chalknotes';
197
+
198
+ <NotionRenderer blocks={post.blocks} />
199
+ ```
200
+
201
+ ---
202
+
203
+ ## 🖼️ Supported Content Types
204
+
205
+ The `NotionRenderer` component supports all major Notion block types:
206
+
207
+ - **Text blocks**: Paragraphs, headings (H1, H2, H3)
208
+ - **Lists**: Bulleted and numbered lists
209
+ - **Code blocks**: With syntax highlighting support
210
+ - **Images**: With captions and Next.js optimization
211
+ - **Quotes**: Styled blockquotes
212
+ - **Dividers**: Horizontal rules
213
+ - **Rich text**: Bold, italic, strikethrough, code, links
214
+
215
+ ---
216
+
194
217
  ## 🎨 Styling
195
218
 
196
219
  The generated templates use Tailwind CSS with:
@@ -199,6 +222,7 @@ The generated templates use Tailwind CSS with:
199
222
  - Typography optimized for readability
200
223
  - Proper spacing and hierarchy
201
224
  - Light and dark mode support
225
+ - **Rich content styling** for all Notion block types
202
226
 
203
227
  Make sure you have Tailwind CSS installed in your project:
204
228
 
@@ -211,7 +235,7 @@ npm install -D tailwindcss @tailwindcss/typography
211
235
  ## 📅 Roadmap
212
236
 
213
237
  - [ ] Plugin system for custom components
214
- - [ ] More Notion block support (images, lists, code blocks)
238
+ - [ ] More Notion block support (callouts, bookmarks, toggles)
215
239
  - [ ] RSS feed support
216
240
  - [ ] MDX or Markdown output option
217
241
  - [ ] Custom theme templates
package/bin/cli.js CHANGED
@@ -90,7 +90,7 @@ function getTemplates(theme, routeBasePath) {
90
90
  if (theme === 'dark') {
91
91
  return {
92
92
  pageRouter: `
93
- import { getStaticPropsForPost, getStaticPathsForPosts } from 'chalknotes';
93
+ import { getStaticPropsForPost, getStaticPathsForPosts, NotionRenderer } from 'chalknotes';
94
94
 
95
95
  export const getStaticProps = getStaticPropsForPost;
96
96
  export const getStaticPaths = getStaticPathsForPosts;
@@ -103,17 +103,14 @@ export default function BlogPost({ post }) {
103
103
  <h1 className="text-4xl font-bold text-white mb-6 leading-tight">
104
104
  {post.title}
105
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
- />
106
+ <NotionRenderer blocks={post.blocks} />
110
107
  </article>
111
108
  </main>
112
109
  </div>
113
110
  );
114
111
  }`.trim(),
115
112
  appRouter: `
116
- import { getPostBySlug } from 'chalknotes';
113
+ import { getPostBySlug, NotionRenderer } from 'chalknotes';
117
114
 
118
115
  export default async function BlogPost({ params }) {
119
116
  const post = await getPostBySlug(params.slug);
@@ -125,10 +122,7 @@ export default async function BlogPost({ params }) {
125
122
  <h1 className="text-4xl font-bold text-white mb-6 leading-tight">
126
123
  {post.title}
127
124
  </h1>
128
- <div
129
- className="prose prose-lg prose-invert max-w-none text-gray-300 leading-relaxed"
130
- dangerouslySetInnerHTML={{ __html: post.content }}
131
- />
125
+ <NotionRenderer blocks={post.blocks} />
132
126
  </article>
133
127
  </main>
134
128
  </div>
@@ -139,7 +133,7 @@ export default async function BlogPost({ params }) {
139
133
  // Default theme (light mode)
140
134
  return {
141
135
  pageRouter: `
142
- import { getStaticPropsForPost, getStaticPathsForPosts } from 'chalknotes';
136
+ import { getStaticPropsForPost, getStaticPathsForPosts, NotionRenderer } from 'chalknotes';
143
137
 
144
138
  export const getStaticProps = getStaticPropsForPost;
145
139
  export const getStaticPaths = getStaticPathsForPosts;
@@ -152,17 +146,14 @@ export default function BlogPost({ post }) {
152
146
  <h1 className="text-4xl font-bold text-gray-900 mb-6 leading-tight">
153
147
  {post.title}
154
148
  </h1>
155
- <div
156
- className="prose prose-lg max-w-none text-gray-700 leading-relaxed"
157
- dangerouslySetInnerHTML={{ __html: post.content }}
158
- />
149
+ <NotionRenderer blocks={post.blocks} />
159
150
  </article>
160
151
  </main>
161
152
  </div>
162
153
  );
163
154
  }`.trim(),
164
155
  appRouter: `
165
- import { getPostBySlug } from 'chalknotes';
156
+ import { getPostBySlug, NotionRenderer } from 'chalknotes';
166
157
 
167
158
  export default async function BlogPost({ params }) {
168
159
  const post = await getPostBySlug(params.slug);
@@ -174,10 +165,7 @@ export default async function BlogPost({ params }) {
174
165
  <h1 className="text-4xl font-bold text-gray-900 mb-6 leading-tight">
175
166
  {post.title}
176
167
  </h1>
177
- <div
178
- className="prose prose-lg max-w-none text-gray-700 leading-relaxed"
179
- dangerouslySetInnerHTML={{ __html: post.content }}
180
- />
168
+ <NotionRenderer blocks={post.blocks} />
181
169
  </article>
182
170
  </main>
183
171
  </div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chalknotes",
3
- "version": "0.0.29",
3
+ "version": "0.0.30",
4
4
  "description": "A tool that simplifies blogs.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -0,0 +1,82 @@
1
+ import React from "react";
2
+ import Image from "next/image";
3
+
4
+ export default function NotionRenderer({ blocks }) {
5
+ if (!blocks || blocks.length === 0) return null;
6
+
7
+ return (
8
+ <div className="prose prose-lg max-w-none text-slate-700 leading-relaxed dark:prose-invert dark:text-slate-300">
9
+ {blocks.map((block, i) => {
10
+ switch (block.type) {
11
+ case "heading_1":
12
+ return <h1 key={i}>{block.text}</h1>;
13
+
14
+ case "heading_2":
15
+ return <h2 key={i}>{block.text}</h2>;
16
+
17
+ case "heading_3":
18
+ return <h3 key={i}>{block.text}</h3>;
19
+
20
+ case "paragraph":
21
+ return <p key={i}>{block.text}</p>;
22
+
23
+ case "bulleted_list_item":
24
+ return (
25
+ <ul key={i} className="list-disc ml-6">
26
+ <li>{block.text}</li>
27
+ </ul>
28
+ );
29
+
30
+ case "numbered_list_item":
31
+ return (
32
+ <ol key={i} className="list-decimal ml-6">
33
+ <li>{block.text}</li>
34
+ </ol>
35
+ );
36
+
37
+ case "quote":
38
+ return (
39
+ <blockquote key={i} className="border-l-4 pl-4 italic text-slate-600 bg-slate-50 p-4 rounded-r">
40
+ {block.text}
41
+ </blockquote>
42
+ );
43
+
44
+ case "code":
45
+ return (
46
+ <pre key={i} className="bg-slate-900 text-slate-100 p-4 rounded-xl overflow-x-auto text-sm">
47
+ <code className={`language-${block.language}`}>{block.code}</code>
48
+ </pre>
49
+ );
50
+
51
+ case "divider":
52
+ return <hr key={i} className="my-8 border-slate-300" />;
53
+
54
+ case "image":
55
+ return (
56
+ <figure key={i} className="max-w-[400px] mx-auto my-6 px-4">
57
+ <Image
58
+ src={block.imageUrl}
59
+ alt={block.alt || "Image"}
60
+ width={400}
61
+ height={300}
62
+ className="rounded-xl object-contain"
63
+ />
64
+ {block.caption && (
65
+ <figcaption className="text-sm text-center text-slate-500 mt-2 italic">
66
+ {block.caption}
67
+ </figcaption>
68
+ )}
69
+ </figure>
70
+ );
71
+
72
+ default:
73
+ return (
74
+ <p key={i} className="text-sm text-red-500 italic">
75
+ ⚠️ Unsupported block: {block.type}
76
+ </p>
77
+ );
78
+ }
79
+ })}
80
+ </div>
81
+ );
82
+ }
package/src/index.js CHANGED
@@ -6,5 +6,6 @@ module.exports = {
6
6
  getAllPosts,
7
7
  getPostBySlug,
8
8
  getStaticPropsForPost,
9
- getStaticPathsForPosts
9
+ getStaticPathsForPosts,
10
+ NotionRenderer: require('./components/NotionRenderer').default
10
11
  }
@@ -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
- };