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 +36 -12
- package/bin/cli.js +8 -20
- package/package.json +1 -1
- package/src/components/NotionRenderer.jsx +82 -0
- package/src/index.js +2 -1
- package/src/lib/getPostBySlug.js +223 -161
- package/blog.config.js +0 -7
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
|
-
<
|
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
|
-
<
|
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
|
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 (
|
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
|
-
<
|
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
|
-
<
|
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
|
-
<
|
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
|
-
<
|
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
@@ -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
package/src/lib/getPostBySlug.js
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
-
const { notion, dbId } = require(
|
2
|
-
const { slugify } = require(
|
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
|
-
|
29
|
+
|
30
|
+
let content = []
|
29
31
|
for (const block of response.results) {
|
30
|
-
content
|
32
|
+
content.push(convertBlockToStructuredJSON(block))
|
31
33
|
}
|
34
|
+
|
32
35
|
return {
|
33
36
|
title,
|
34
37
|
slug: pageSlug,
|
35
|
-
|
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-
|
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(
|
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-
|
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-
|
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(
|
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(
|
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(
|
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 ||
|
79
|
-
return `<pre class="bg-
|
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-
|
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-
|
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-
|
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-
|
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 ===
|
118
|
-
const caption = image.caption?.map(text => text.plain_text).join("") || "";
|
119
|
-
const altText = caption || "
|
120
|
-
|
121
|
-
//
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
//
|
126
|
-
|
127
|
-
|
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
|
-
|
166
|
-
|
218
|
+
<div className="${containerClasses}">
|
219
|
+
<figure className="${figureClasses}">
|
220
|
+
<Image
|
167
221
|
src="${imageUrl}"
|
168
222
|
alt="${escapeHtml(altText)}"
|
169
|
-
|
170
|
-
|
223
|
+
fill
|
224
|
+
className="rounded-xl object-contain"
|
225
|
+
sizes="(max-width: 640px) 300px, 400px"
|
226
|
+
priority={false}
|
171
227
|
/>
|
172
|
-
${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-
|
189
|
-
gray: "bg-
|
190
|
-
yellow: "bg-
|
191
|
-
red: "bg-red-50 border-red-200 text-red-
|
192
|
-
green: "bg-
|
193
|
-
purple: "bg-purple-50 border-purple-200 text-purple-
|
194
|
-
pink: "bg-pink-50 border-pink-200 text-pink-
|
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-
|
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-
|
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-
|
220
|
-
<a href="${url}" target="_blank" rel="noopener noreferrer" class="block border border-
|
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-
|
224
|
-
<p class="text-sm text-
|
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-
|
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-
|
244
|
-
<summary class="cursor-pointer font-
|
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-
|
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-
|
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
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
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
|
-
|
291
|
-
|
292
|
-
|
293
|
-
'"':
|
294
|
-
"'":
|
295
|
-
}
|
296
|
-
return text.replace(/[&<>"']/g, m => map[m])
|
352
|
+
"&": "&",
|
353
|
+
"<": "<",
|
354
|
+
">": ">",
|
355
|
+
'"': """,
|
356
|
+
"'": "'",
|
357
|
+
}
|
358
|
+
return text.replace(/[&<>"']/g, (m) => map[m])
|
297
359
|
}
|
298
360
|
|
299
361
|
module.exports = {
|
300
|
-
getPostBySlug
|
362
|
+
getPostBySlug,
|
301
363
|
}
|