chalknotes 0.0.28 → 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.28",
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-relaxed text-gray-700");
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-3xl font-bold mb-6 mt-8 text-gray-900 border-b border-gray-200 pb-2");
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-2xl font-semibold mb-4 mt-6 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-xl font-medium mb-3 mt-5 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-2 ml-4");
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-2 ml-4");
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-6 italic text-gray-600 mb-6 bg-blue-50 py-4 rounded-r-lg");
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-4 rounded-lg overflow-x-auto mb-6 text-sm"><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-8 border-gray-200" />';
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-4 mb-6"><p class="text-blue-800 font-medium">📋 Table of Contents</p><p class="text-blue-600 text-sm">(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-4 rounded-lg mb-6 text-center border border-gray-200"><p class="text-gray-600 mb-2">📐 Mathematical equation</p><p class="font-mono text-sm bg-white p-2 rounded border">${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-4 text-gray-500 italic");
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,96 +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
- // Get image size from Notion properties
122
- let sizeClass = "w-full"; // Default full width
123
- let maxWidthClass = "max-w-full";
124
-
125
- // Check if image has size information
126
- if (image.width && image.height) {
127
- // Calculate aspect ratio and apply appropriate sizing
128
- const aspectRatio = image.width / image.height;
129
-
130
- if (aspectRatio > 2) {
131
- // Wide images
132
- sizeClass = "w-full";
133
- maxWidthClass = "max-w-4xl";
134
- } else if (aspectRatio < 0.5) {
135
- // Tall images
136
- sizeClass = "w-full";
137
- maxWidthClass = "max-w-2xl";
138
- } else {
139
- // Square-ish images
140
- sizeClass = "w-full";
141
- maxWidthClass = "max-w-3xl";
142
- }
143
- }
144
-
145
- // Check for Notion's size property (if available)
146
- if (image.size) {
147
- switch (image.size) {
148
- case 'small':
149
- maxWidthClass = "max-w-md";
150
- break;
151
- case 'medium':
152
- maxWidthClass = "max-w-2xl";
153
- break;
154
- case 'large':
155
- maxWidthClass = "max-w-4xl";
156
- break;
157
- default:
158
- maxWidthClass = "max-w-full";
159
- }
160
- }
161
-
162
- const responsiveClasses = `${sizeClass} ${maxWidthClass} h-auto rounded-lg shadow-sm`;
163
-
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
+
164
217
  return `
165
- <figure class="my-6 text-center">
166
- <img
218
+ <div className="${containerClasses}">
219
+ <figure className="${figureClasses}">
220
+ <Image
167
221
  src="${imageUrl}"
168
222
  alt="${escapeHtml(altText)}"
169
- class="${responsiveClasses}"
170
- loading="lazy"
223
+ fill
224
+ className="rounded-xl object-contain"
225
+ sizes="(max-width: 640px) 300px, 400px"
226
+ priority={false}
171
227
  />
172
- ${caption ? `<figcaption class="text-center 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>` : ""}
173
229
  </figure>
230
+ </div>
174
231
  `.trim();
175
232
  }
176
233
 
234
+
177
235
  /**
178
236
  * Process callout block
179
237
  * @param {Object} callout - Notion callout block
180
238
  * @returns {string} HTML string
181
239
  */
182
240
  function processCallout(callout) {
183
- const content = processRichText(callout.rich_text, "div", "");
184
- const icon = callout.icon?.emoji || "💡";
185
- const bgColor = callout.color || "blue";
186
-
241
+ const content = processRichText(callout.rich_text, "div", "")
242
+ const icon = callout.icon?.emoji || "💡"
243
+ const bgColor = callout.color || "blue"
244
+
187
245
  const colorClasses = {
188
- blue: "bg-blue-50 border-blue-200 text-blue-800",
189
- gray: "bg-gray-50 border-gray-200 text-gray-800",
190
- yellow: "bg-yellow-50 border-yellow-200 text-yellow-800",
191
- red: "bg-red-50 border-red-200 text-red-800",
192
- green: "bg-green-50 border-green-200 text-green-800",
193
- purple: "bg-purple-50 border-purple-200 text-purple-800",
194
- pink: "bg-pink-50 border-pink-200 text-pink-800"
195
- };
196
-
197
- const colorClass = colorClasses[bgColor] || colorClasses.blue;
198
-
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
+
199
257
  return `
200
- <div class="${colorClass} border-l-4 p-4 my-6 rounded-r-lg shadow-sm">
258
+ <div class="${colorClass} border-l-4 p-6 my-8 rounded-r-xl shadow-sm">
201
259
  <div class="flex items-start">
202
- <span class="mr-3 text-xl flex-shrink-0">${icon}</span>
203
- <div class="flex-1 leading-relaxed">${content}</div>
260
+ <span class="mr-4 text-2xl flex-shrink-0">${icon}</span>
261
+ <div class="flex-1 leading-relaxed text-base font-medium">${content}</div>
204
262
  </div>
205
263
  </div>
206
- `.trim();
264
+ `.trim()
207
265
  }
208
266
 
209
267
  /**
@@ -212,24 +270,24 @@ function processCallout(callout) {
212
270
  * @returns {string} HTML string
213
271
  */
214
272
  function processBookmark(bookmark) {
215
- const url = bookmark.url;
216
- const title = bookmark.caption?.[0]?.plain_text || "Bookmark";
217
-
273
+ const url = bookmark.url
274
+ const title = bookmark.caption?.[0]?.plain_text || "Bookmark"
275
+
218
276
  return `
219
- <div class="my-6">
220
- <a href="${url}" target="_blank" rel="noopener noreferrer" class="block border border-gray-200 rounded-lg p-4 hover:border-gray-300 hover:shadow-md transition-all duration-200">
277
+ <div class="my-8">
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">
221
279
  <div class="flex items-center">
222
280
  <div class="flex-1 min-w-0">
223
- <p class="font-medium text-gray-900 truncate">${escapeHtml(title)}</p>
224
- <p class="text-sm text-gray-500 truncate">${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>
225
283
  </div>
226
- <svg class="w-5 h-5 text-gray-400 ml-3 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">
227
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>
228
286
  </svg>
229
287
  </div>
230
288
  </a>
231
289
  </div>
232
- `.trim();
290
+ `.trim()
233
291
  }
234
292
 
235
293
  /**
@@ -238,18 +296,18 @@ function processBookmark(bookmark) {
238
296
  * @returns {string} HTML string
239
297
  */
240
298
  function processToggle(toggle) {
241
- const content = processRichText(toggle.rich_text, "div", "");
299
+ const content = processRichText(toggle.rich_text, "div", "")
242
300
  return `
243
- <details class="my-4">
244
- <summary class="cursor-pointer font-medium text-gray-700 hover:text-gray-900">
301
+ <details class="my-6">
302
+ <summary class="cursor-pointer font-semibold text-slate-700 hover:text-slate-900 text-lg leading-relaxed transition-colors duration-200">
245
303
  ${content}
246
304
  </summary>
247
- <div class="mt-2 pl-4 border-l-2 border-gray-200">
305
+ <div class="mt-4 pl-6 border-l-2 border-slate-200">
248
306
  <!-- Toggle content would go here if Notion API provided it -->
249
- <p class="text-gray-600 text-sm">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>
250
308
  </div>
251
309
  </details>
252
- `.trim();
310
+ `.trim()
253
311
  }
254
312
 
255
313
  /**
@@ -260,24 +318,28 @@ function processToggle(toggle) {
260
318
  * @returns {string} HTML string
261
319
  */
262
320
  function processRichText(richText, tag, className) {
263
- if (!richText || richText.length === 0) return "";
264
-
265
- const content = richText.map(text => {
266
- let result = text.plain_text;
267
-
268
- // Apply annotations
269
- if (text.annotations.bold) result = `<strong>${result}</strong>`;
270
- if (text.annotations.italic) result = `<em>${result}</em>`;
271
- if (text.annotations.strikethrough) result = `<del>${result}</del>`;
272
- if (text.annotations.code) result = `<code class="bg-gray-100 px-1 py-0.5 rounded text-sm font-mono">${result}</code>`;
273
-
274
- // Apply links
275
- if (text.href) result = `<a href="${text.href}" class="text-blue-600 hover:text-blue-800 underline" target="_blank" rel="noopener noreferrer">${result}</a>`;
276
-
277
- return result;
278
- }).join("");
279
-
280
- 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}>`
281
343
  }
282
344
 
283
345
  /**
@@ -287,15 +349,15 @@ function processRichText(richText, tag, className) {
287
349
  */
288
350
  function escapeHtml(text) {
289
351
  const map = {
290
- '&': '&amp;',
291
- '<': '&lt;',
292
- '>': '&gt;',
293
- '"': '&quot;',
294
- "'": '&#039;'
295
- };
296
- 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])
297
359
  }
298
360
 
299
361
  module.exports = {
300
- getPostBySlug
362
+ getPostBySlug,
301
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
- };