notro-loader 0.0.1 → 0.0.3
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/LICENSE +21 -21
- package/README.md +276 -276
- package/image-service.ts +45 -45
- package/index.ts +21 -21
- package/integration.ts +13 -13
- package/package.json +5 -4
- package/src/components/NotroContent.astro +37 -37
- package/src/loader/live-loader.ts +181 -181
- package/src/loader/loader.ts +268 -268
- package/src/loader/schema.ts +837 -837
- package/src/utils/HtmlElements.ts +27 -27
- package/src/utils/compile-mdx.ts +159 -159
- package/src/utils/default-components.ts +62 -62
- package/src/utils/mdx-pipeline.ts +501 -501
- package/src/utils/notion-url.ts +49 -49
- package/src/utils/notion.ts +127 -127
- package/src/utils/notro-config.ts +35 -35
- package/utils.ts +11 -11
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2024 mosugi
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 mosugi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,276 +1,276 @@
|
|
|
1
|
-
# notro-loader
|
|
2
|
-
|
|
3
|
-

|
|
4
|
-

|
|
5
|
-
|
|
6
|
-
An Astro Content Loader library that fetches Notion database content via the [Markdown Content API](https://developers.notion.com/) into [Astro Content Collections](https://docs.astro.build/en/guides/content-collections/).
|
|
7
|
-
|
|
8
|
-
> [!TIP]
|
|
9
|
-
>
|
|
10
|
-
|
|
11
|
-
## What NotroContent renders
|
|
12
|
-
|
|
13
|
-
`NotroContent` compiles Notion markdown into HTML. Each Notion block type maps to a semantic HTML element by default. You can replace any element with your own styled component via the `components` prop.
|
|
14
|
-
|
|
15
|
-
| Notion block | Default HTML | notro-ui component |
|
|
16
|
-
|---|---|---|
|
|
17
|
-
| Paragraph | `<p>` | `ColoredParagraph` |
|
|
18
|
-
| Heading 1–4 | `<h1>`–`<h4>` | `H1`–`H4` |
|
|
19
|
-
| Callout | `<aside>` | `Callout` |
|
|
20
|
-
| Quote | `<blockquote>` | `Quote` |
|
|
21
|
-
| Toggle | `<details>` + `<summary>` | `Toggle` + `ToggleTitle` |
|
|
22
|
-
| Divider | `<hr>` | — |
|
|
23
|
-
| Code | `<pre>` | — |
|
|
24
|
-
| Image | `<img>` | `ImageBlock` |
|
|
25
|
-
| Video | `<figure>` | `Video` |
|
|
26
|
-
| Audio | `<figure>` | `Audio` |
|
|
27
|
-
| File | `<div>` | `FileBlock` |
|
|
28
|
-
| PDF | `<figure>` | `PdfBlock` |
|
|
29
|
-
| Table | `<table>` | `TableBlock` |
|
|
30
|
-
| Table of contents | `<nav>` | `TableOfContents` |
|
|
31
|
-
| Columns / Column | `<div>` / `<div>` | `Columns` / `Column` |
|
|
32
|
-
| Page link | `<a>` | `PageRef` |
|
|
33
|
-
| Database link | `<a>` | `DatabaseRef` |
|
|
34
|
-
| Empty block | `<div>` | `EmptyBlock` |
|
|
35
|
-
| Inline text (colored/underline) | `<span>` | `StyledSpan` |
|
|
36
|
-
| @mention | `<span>` | `Mention` |
|
|
37
|
-
| Date mention | `<time>` | `MentionDate` |
|
|
38
|
-
|
|
39
|
-
`notro-ui` is an optional style layer. See [notro-ui](https://github.com/mosugi/notro/tree/main/packages/notro-ui) for details.
|
|
40
|
-
|
|
41
|
-
## Installation
|
|
42
|
-
|
|
43
|
-
```sh
|
|
44
|
-
npx astro add notro-loader
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
This installs the package and automatically adds the `notro()` integration to `astro.config.mjs`.
|
|
48
|
-
|
|
49
|
-
Alternatively, install manually:
|
|
50
|
-
|
|
51
|
-
```sh
|
|
52
|
-
npm install notro-loader
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
## Setup
|
|
56
|
-
|
|
57
|
-
### 1. `astro.config.mjs`
|
|
58
|
-
|
|
59
|
-
`astro add notro-loader` configures this automatically. If you installed manually, add:
|
|
60
|
-
|
|
61
|
-
```js
|
|
62
|
-
import { defineConfig } from "astro/config";
|
|
63
|
-
import { notro } from "notro-loader/integration";
|
|
64
|
-
|
|
65
|
-
export default defineConfig({
|
|
66
|
-
integrations: [notro()],
|
|
67
|
-
});
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
This registers `@astrojs/mdx` with the required plugin pipeline and the Astro JSX renderer that `NotroContent` depends on at runtime.
|
|
71
|
-
|
|
72
|
-
### 2. `src/content.config.ts`
|
|
73
|
-
|
|
74
|
-
Define your collection using the `loader` function. Extend `pageWithMarkdownSchema` with your database properties. Use the `notroProperties` shorthand for concise property schemas.
|
|
75
|
-
|
|
76
|
-
```typescript
|
|
77
|
-
import { defineCollection } from "astro:content";
|
|
78
|
-
import { loader, pageWithMarkdownSchema, notroProperties } from "notro-loader";
|
|
79
|
-
import { z } from "zod";
|
|
80
|
-
|
|
81
|
-
const posts = defineCollection({
|
|
82
|
-
loader: loader({
|
|
83
|
-
queryParameters: {
|
|
84
|
-
data_source_id: import.meta.env.NOTION_DATASOURCE_ID,
|
|
85
|
-
filter: {
|
|
86
|
-
property: "Public",
|
|
87
|
-
checkbox: { equals: true },
|
|
88
|
-
},
|
|
89
|
-
},
|
|
90
|
-
clientOptions: {
|
|
91
|
-
auth: import.meta.env.NOTION_TOKEN,
|
|
92
|
-
},
|
|
93
|
-
}),
|
|
94
|
-
schema: pageWithMarkdownSchema.extend({
|
|
95
|
-
properties: z.object({
|
|
96
|
-
Name: notroProperties.title,
|
|
97
|
-
Description: notroProperties.richText,
|
|
98
|
-
Public: notroProperties.checkbox,
|
|
99
|
-
Tags: notroProperties.multiSelect,
|
|
100
|
-
Date: notroProperties.date,
|
|
101
|
-
}),
|
|
102
|
-
}),
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
export const collections = { posts };
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
### 3. Page component
|
|
109
|
-
|
|
110
|
-
#### Option A — Headless (no styling)
|
|
111
|
-
|
|
112
|
-
`NotroContent` from `notro-loader` renders semantic HTML with no classes.
|
|
113
|
-
|
|
114
|
-
```astro
|
|
115
|
-
---
|
|
116
|
-
import { NotroContent, getPlainText } from "notro-loader";
|
|
117
|
-
|
|
118
|
-
const { entry } = Astro.props;
|
|
119
|
-
const title = getPlainText(entry.data.properties.Name);
|
|
120
|
-
---
|
|
121
|
-
|
|
122
|
-
<h1>{title}</h1>
|
|
123
|
-
<NotroContent markdown={entry.data.markdown} />
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
#### Option B — With notro-ui
|
|
127
|
-
|
|
128
|
-
Install the styled components once:
|
|
129
|
-
|
|
130
|
-
```sh
|
|
131
|
-
npx notro-ui init
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
Then pass the component map to `NotroContent`:
|
|
135
|
-
|
|
136
|
-
```astro
|
|
137
|
-
---
|
|
138
|
-
import { NotroContent, getPlainText } from "notro-loader";
|
|
139
|
-
import { notroComponents } from "@/components/notro";
|
|
140
|
-
|
|
141
|
-
const { entry } = Astro.props;
|
|
142
|
-
---
|
|
143
|
-
|
|
144
|
-
<NotroContent markdown={entry.data.markdown} components={notroComponents} />
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
Components are copied into `src/components/notro/` so you can edit them directly.
|
|
148
|
-
|
|
149
|
-
## Markdown processing (remark-
|
|
150
|
-
|
|
151
|
-
`notro-loader` delegates Notion Markdown preprocessing and directive syntax support to the [`remark-
|
|
152
|
-
|
|
153
|
-
`remark-
|
|
154
|
-
|
|
155
|
-
If you want to use `remark-
|
|
156
|
-
|
|
157
|
-
```ts
|
|
158
|
-
// ✅ Import directly from remark-
|
|
159
|
-
import { remarkNfm, preprocessNotionMarkdown } from "remark-
|
|
160
|
-
|
|
161
|
-
// ❌ Not needed from notro-loader (internal use only)
|
|
162
|
-
// import { remarkNfm } from "notro-loader";
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
## Notion API limitations
|
|
166
|
-
|
|
167
|
-
> Reference: [Retrieve a page as Markdown – Notion API](https://developers.notion.com/reference/retrieve-page-markdown)
|
|
168
|
-
|
|
169
|
-
### Content truncation (`truncated`)
|
|
170
|
-
|
|
171
|
-
`GET /v1/pages/{page_id}/markdown` truncates content at approximately **20,000 blocks**.
|
|
172
|
-
|
|
173
|
-
- Detectable via `truncated: true` in the response, but **there is no pagination API to fetch the rest**
|
|
174
|
-
- notro logs a warning when `truncated === true` and continues the build with the available content
|
|
175
|
-
- Workaround: split large Notion pages into multiple smaller pages
|
|
176
|
-
|
|
177
|
-
```
|
|
178
|
-
⚠ Page abc123: markdown content was truncated by the Notion API (~20,000 block limit).
|
|
179
|
-
No pagination is available for this endpoint.
|
|
180
|
-
Consider splitting this Notion page into smaller pages to avoid truncation.
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
### Unrenderable blocks (`unknown_block_ids`)
|
|
184
|
-
|
|
185
|
-
`unknown_block_ids` in the response lists block IDs that the Notion API could not convert to Markdown (unsupported block types, etc.).
|
|
186
|
-
|
|
187
|
-
- These blocks are **silently omitted** from the `markdown` field
|
|
188
|
-
- There is no way to retrieve their content via this endpoint
|
|
189
|
-
- notro logs the block IDs as a warning and continues the build
|
|
190
|
-
|
|
191
|
-
```
|
|
192
|
-
⚠ Page abc123: 2 block(s) could not be rendered to Markdown by the Notion API and were omitted.
|
|
193
|
-
Block IDs: xxxxxxxx-..., yyyyyyyy-...
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
### API errors and automatic retries
|
|
197
|
-
|
|
198
|
-
| Error | Handling |
|
|
199
|
-
|---|---|
|
|
200
|
-
| `429 rate_limited` / `500 internal_server_error` / `503 service_unavailable` | Retry with exponential backoff (1s / 2s / 4s, up to 3 times) |
|
|
201
|
-
| `401 unauthorized` / `403 restricted_resource` / `404 object_not_found` | No retry. Logs a warning and skips the page |
|
|
202
|
-
| Other unexpected errors | Logs a warning and skips the page (build continues) |
|
|
203
|
-
|
|
204
|
-
## Environment variables
|
|
205
|
-
|
|
206
|
-
| Variable | Description |
|
|
207
|
-
|---|---|
|
|
208
|
-
| `NOTION_TOKEN` | Notion Internal Integration Token |
|
|
209
|
-
| `NOTION_DATASOURCE_ID` | Notion data source ID |
|
|
210
|
-
|
|
211
|
-
## API Reference
|
|
212
|
-
|
|
213
|
-
### `loader(options)`
|
|
214
|
-
|
|
215
|
-
Astro Content Loader. Pass Notion API `dataSources.query` parameters via `queryParameters`.
|
|
216
|
-
|
|
217
|
-
### `pageWithMarkdownSchema`
|
|
218
|
-
|
|
219
|
-
Base Zod schema returned by the loader. Extends `pageObjectResponseSchema` with `markdown: z.string()`. Extend with `.extend()` for custom schemas.
|
|
220
|
-
|
|
221
|
-
### Property schemas
|
|
222
|
-
|
|
223
|
-
Use the `notroProperties` shorthand to define database property types in `content.config.ts` (see [`notroProperties`](#notroproperties)).
|
|
224
|
-
|
|
225
|
-
Individual schemas (e.g. `titlePropertyPageObjectResponseSchema`) remain exported for backwards compatibility.
|
|
226
|
-
|
|
227
|
-
### Components
|
|
228
|
-
|
|
229
|
-
| Component | Description |
|
|
230
|
-
|---|---|
|
|
231
|
-
| `NotroContent` | Renders Notion Markdown to HTML. Unstyled by default; pass `components` to customize |
|
|
232
|
-
| `DatabaseCover` | Renders a Notion cover image with optimization |
|
|
233
|
-
| `DatabaseProperty` | Renders a Notion property value by type |
|
|
234
|
-
| `compileMdxCached` | Low-level MDX compile API. Use when building a custom `NotroContent` |
|
|
235
|
-
|
|
236
|
-
### `notroProperties`
|
|
237
|
-
|
|
238
|
-
Zod schema shorthands for defining property schemas in `content.config.ts`. Each key maps to a Notion property type.
|
|
239
|
-
|
|
240
|
-
```typescript
|
|
241
|
-
import { notroProperties } from "notro-loader";
|
|
242
|
-
|
|
243
|
-
// notroProperties.title → titlePropertyPageObjectResponseSchema
|
|
244
|
-
// notroProperties.richText → richTextPropertyPageObjectResponseSchema
|
|
245
|
-
// notroProperties.checkbox → checkboxPropertyPageObjectResponseSchema
|
|
246
|
-
// notroProperties.multiSelect → multiSelectPropertyPageObjectResponseSchema
|
|
247
|
-
// notroProperties.select → selectPropertyPageObjectResponseSchema
|
|
248
|
-
// notroProperties.date → datePropertyPageObjectResponseSchema
|
|
249
|
-
// notroProperties.number → numberPropertyPageObjectResponseSchema
|
|
250
|
-
// notroProperties.url → urlPropertyPageObjectResponseSchema
|
|
251
|
-
// notroProperties.email → emailPropertyPageObjectResponseSchema
|
|
252
|
-
// notroProperties.phoneNumber → phoneNumberPropertyPageObjectResponseSchema
|
|
253
|
-
// notroProperties.files → filesPropertyPageObjectResponseSchema
|
|
254
|
-
// notroProperties.people → peoplePropertyPageObjectResponseSchema
|
|
255
|
-
// notroProperties.relation → relationPropertyPageObjectResponseSchema
|
|
256
|
-
// notroProperties.rollup → rollupPropertyPageObjectResponseSchema
|
|
257
|
-
// notroProperties.formula → formulaPropertyPageObjectResponseSchema
|
|
258
|
-
// notroProperties.uniqueId → uniqueIdPropertyPageObjectResponseSchema
|
|
259
|
-
// notroProperties.status → statusPropertyPageObjectResponseSchema
|
|
260
|
-
// notroProperties.createdTime → createdTimePropertyPageObjectResponseSchema
|
|
261
|
-
// notroProperties.createdBy → createdByPropertyPageObjectResponseSchema
|
|
262
|
-
// notroProperties.lastEditedTime → lastEditedTimePropertyPageObjectResponseSchema
|
|
263
|
-
// notroProperties.lastEditedBy → lastEditedByPropertyPageObjectResponseSchema
|
|
264
|
-
// notroProperties.button → buttonPropertyPageObjectResponseSchema
|
|
265
|
-
// notroProperties.verification → verificationPropertyPageObjectResponseSchema
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
### Utilities
|
|
269
|
-
|
|
270
|
-
| Function | Description |
|
|
271
|
-
|---|---|
|
|
272
|
-
| `getPlainText(property)` | Extracts plain text from Title, Rich Text, Select, Multi-select, Number, URL, Email, Phone, Date, and Unique ID properties |
|
|
273
|
-
| `getMultiSelect(property)` | Returns the options array for a multi-select property. Returns an empty array for unsupported types or `undefined` — no type guard needed |
|
|
274
|
-
| `hasTag(property, tagName)` | Returns whether a multi-select property contains the given tag name. Safe to call without a type guard |
|
|
275
|
-
| `buildLinkToPages(entries, options)` | Builds a `linkToPages` map from collection entries. Pass to `NotroContent` for resolving inter-page Notion links |
|
|
276
|
-
| `colorToCSS(color)` | Converts a Notion color name to an inline CSS style string (for use in custom components) |
|
|
1
|
+
# notro-loader
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+
|
|
6
|
+
An Astro Content Loader library that fetches Notion database content via the [Markdown Content API](https://developers.notion.com/) into [Astro Content Collections](https://docs.astro.build/en/guides/content-collections/).
|
|
7
|
+
|
|
8
|
+
> [!TIP]
|
|
9
|
+
> This package is part of the [mosugi/notro](https://github.com/mosugi/notro) monorepo. See the blog template at `templates/blog/` for a full working example.
|
|
10
|
+
|
|
11
|
+
## What NotroContent renders
|
|
12
|
+
|
|
13
|
+
`NotroContent` compiles Notion markdown into HTML. Each Notion block type maps to a semantic HTML element by default. You can replace any element with your own styled component via the `components` prop.
|
|
14
|
+
|
|
15
|
+
| Notion block | Default HTML | notro-ui component |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| Paragraph | `<p>` | `ColoredParagraph` |
|
|
18
|
+
| Heading 1–4 | `<h1>`–`<h4>` | `H1`–`H4` |
|
|
19
|
+
| Callout | `<aside>` | `Callout` |
|
|
20
|
+
| Quote | `<blockquote>` | `Quote` |
|
|
21
|
+
| Toggle | `<details>` + `<summary>` | `Toggle` + `ToggleTitle` |
|
|
22
|
+
| Divider | `<hr>` | — |
|
|
23
|
+
| Code | `<pre>` | — |
|
|
24
|
+
| Image | `<img>` | `ImageBlock` |
|
|
25
|
+
| Video | `<figure>` | `Video` |
|
|
26
|
+
| Audio | `<figure>` | `Audio` |
|
|
27
|
+
| File | `<div>` | `FileBlock` |
|
|
28
|
+
| PDF | `<figure>` | `PdfBlock` |
|
|
29
|
+
| Table | `<table>` | `TableBlock` |
|
|
30
|
+
| Table of contents | `<nav>` | `TableOfContents` |
|
|
31
|
+
| Columns / Column | `<div>` / `<div>` | `Columns` / `Column` |
|
|
32
|
+
| Page link | `<a>` | `PageRef` |
|
|
33
|
+
| Database link | `<a>` | `DatabaseRef` |
|
|
34
|
+
| Empty block | `<div>` | `EmptyBlock` |
|
|
35
|
+
| Inline text (colored/underline) | `<span>` | `StyledSpan` |
|
|
36
|
+
| @mention | `<span>` | `Mention` |
|
|
37
|
+
| Date mention | `<time>` | `MentionDate` |
|
|
38
|
+
|
|
39
|
+
`notro-ui` is an optional style layer. See [notro-ui](https://github.com/mosugi/notro/tree/main/packages/notro-ui) for details.
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
npx astro add notro-loader
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
This installs the package and automatically adds the `notro()` integration to `astro.config.mjs`.
|
|
48
|
+
|
|
49
|
+
Alternatively, install manually:
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
npm install notro-loader
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Setup
|
|
56
|
+
|
|
57
|
+
### 1. `astro.config.mjs`
|
|
58
|
+
|
|
59
|
+
`astro add notro-loader` configures this automatically. If you installed manually, add:
|
|
60
|
+
|
|
61
|
+
```js
|
|
62
|
+
import { defineConfig } from "astro/config";
|
|
63
|
+
import { notro } from "notro-loader/integration";
|
|
64
|
+
|
|
65
|
+
export default defineConfig({
|
|
66
|
+
integrations: [notro()],
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
This registers `@astrojs/mdx` with the required plugin pipeline and the Astro JSX renderer that `NotroContent` depends on at runtime.
|
|
71
|
+
|
|
72
|
+
### 2. `src/content.config.ts`
|
|
73
|
+
|
|
74
|
+
Define your collection using the `loader` function. Extend `pageWithMarkdownSchema` with your database properties. Use the `notroProperties` shorthand for concise property schemas.
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
import { defineCollection } from "astro:content";
|
|
78
|
+
import { loader, pageWithMarkdownSchema, notroProperties } from "notro-loader";
|
|
79
|
+
import { z } from "zod";
|
|
80
|
+
|
|
81
|
+
const posts = defineCollection({
|
|
82
|
+
loader: loader({
|
|
83
|
+
queryParameters: {
|
|
84
|
+
data_source_id: import.meta.env.NOTION_DATASOURCE_ID,
|
|
85
|
+
filter: {
|
|
86
|
+
property: "Public",
|
|
87
|
+
checkbox: { equals: true },
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
clientOptions: {
|
|
91
|
+
auth: import.meta.env.NOTION_TOKEN,
|
|
92
|
+
},
|
|
93
|
+
}),
|
|
94
|
+
schema: pageWithMarkdownSchema.extend({
|
|
95
|
+
properties: z.object({
|
|
96
|
+
Name: notroProperties.title,
|
|
97
|
+
Description: notroProperties.richText,
|
|
98
|
+
Public: notroProperties.checkbox,
|
|
99
|
+
Tags: notroProperties.multiSelect,
|
|
100
|
+
Date: notroProperties.date,
|
|
101
|
+
}),
|
|
102
|
+
}),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
export const collections = { posts };
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### 3. Page component
|
|
109
|
+
|
|
110
|
+
#### Option A — Headless (no styling)
|
|
111
|
+
|
|
112
|
+
`NotroContent` from `notro-loader` renders semantic HTML with no classes.
|
|
113
|
+
|
|
114
|
+
```astro
|
|
115
|
+
---
|
|
116
|
+
import { NotroContent, getPlainText } from "notro-loader";
|
|
117
|
+
|
|
118
|
+
const { entry } = Astro.props;
|
|
119
|
+
const title = getPlainText(entry.data.properties.Name);
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
<h1>{title}</h1>
|
|
123
|
+
<NotroContent markdown={entry.data.markdown} />
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
#### Option B — With notro-ui
|
|
127
|
+
|
|
128
|
+
Install the styled components once:
|
|
129
|
+
|
|
130
|
+
```sh
|
|
131
|
+
npx notro-ui init
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Then pass the component map to `NotroContent`:
|
|
135
|
+
|
|
136
|
+
```astro
|
|
137
|
+
---
|
|
138
|
+
import { NotroContent, getPlainText } from "notro-loader";
|
|
139
|
+
import { notroComponents } from "@/components/notro";
|
|
140
|
+
|
|
141
|
+
const { entry } = Astro.props;
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
<NotroContent markdown={entry.data.markdown} components={notroComponents} />
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Components are copied into `src/components/notro/` so you can edit them directly.
|
|
148
|
+
|
|
149
|
+
## Markdown processing (remark-notro)
|
|
150
|
+
|
|
151
|
+
`notro-loader` delegates Notion Markdown preprocessing and directive syntax support to the [`remark-notro`](https://www.npmjs.com/package/remark-notro) package.
|
|
152
|
+
|
|
153
|
+
`remark-notro` is used inside notro-loader's MDX compile pipeline and is applied automatically when using `NotroContent`.
|
|
154
|
+
|
|
155
|
+
If you want to use `remark-notro` directly (in a custom unified pipeline or `@mdx-js/mdx`'s `evaluate()`), import it from the `remark-notro` package directly rather than from `notro-loader`.
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
// ✅ Import directly from remark-notro
|
|
159
|
+
import { remarkNfm, preprocessNotionMarkdown } from "remark-notro";
|
|
160
|
+
|
|
161
|
+
// ❌ Not needed from notro-loader (internal use only)
|
|
162
|
+
// import { remarkNfm } from "notro-loader";
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Notion API limitations
|
|
166
|
+
|
|
167
|
+
> Reference: [Retrieve a page as Markdown – Notion API](https://developers.notion.com/reference/retrieve-page-markdown)
|
|
168
|
+
|
|
169
|
+
### Content truncation (`truncated`)
|
|
170
|
+
|
|
171
|
+
`GET /v1/pages/{page_id}/markdown` truncates content at approximately **20,000 blocks**.
|
|
172
|
+
|
|
173
|
+
- Detectable via `truncated: true` in the response, but **there is no pagination API to fetch the rest**
|
|
174
|
+
- notro logs a warning when `truncated === true` and continues the build with the available content
|
|
175
|
+
- Workaround: split large Notion pages into multiple smaller pages
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
⚠ Page abc123: markdown content was truncated by the Notion API (~20,000 block limit).
|
|
179
|
+
No pagination is available for this endpoint.
|
|
180
|
+
Consider splitting this Notion page into smaller pages to avoid truncation.
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Unrenderable blocks (`unknown_block_ids`)
|
|
184
|
+
|
|
185
|
+
`unknown_block_ids` in the response lists block IDs that the Notion API could not convert to Markdown (unsupported block types, etc.).
|
|
186
|
+
|
|
187
|
+
- These blocks are **silently omitted** from the `markdown` field
|
|
188
|
+
- There is no way to retrieve their content via this endpoint
|
|
189
|
+
- notro logs the block IDs as a warning and continues the build
|
|
190
|
+
|
|
191
|
+
```
|
|
192
|
+
⚠ Page abc123: 2 block(s) could not be rendered to Markdown by the Notion API and were omitted.
|
|
193
|
+
Block IDs: xxxxxxxx-..., yyyyyyyy-...
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### API errors and automatic retries
|
|
197
|
+
|
|
198
|
+
| Error | Handling |
|
|
199
|
+
|---|---|
|
|
200
|
+
| `429 rate_limited` / `500 internal_server_error` / `503 service_unavailable` | Retry with exponential backoff (1s / 2s / 4s, up to 3 times) |
|
|
201
|
+
| `401 unauthorized` / `403 restricted_resource` / `404 object_not_found` | No retry. Logs a warning and skips the page |
|
|
202
|
+
| Other unexpected errors | Logs a warning and skips the page (build continues) |
|
|
203
|
+
|
|
204
|
+
## Environment variables
|
|
205
|
+
|
|
206
|
+
| Variable | Description |
|
|
207
|
+
|---|---|
|
|
208
|
+
| `NOTION_TOKEN` | Notion Internal Integration Token |
|
|
209
|
+
| `NOTION_DATASOURCE_ID` | Notion data source ID |
|
|
210
|
+
|
|
211
|
+
## API Reference
|
|
212
|
+
|
|
213
|
+
### `loader(options)`
|
|
214
|
+
|
|
215
|
+
Astro Content Loader. Pass Notion API `dataSources.query` parameters via `queryParameters`.
|
|
216
|
+
|
|
217
|
+
### `pageWithMarkdownSchema`
|
|
218
|
+
|
|
219
|
+
Base Zod schema returned by the loader. Extends `pageObjectResponseSchema` with `markdown: z.string()`. Extend with `.extend()` for custom schemas.
|
|
220
|
+
|
|
221
|
+
### Property schemas
|
|
222
|
+
|
|
223
|
+
Use the `notroProperties` shorthand to define database property types in `content.config.ts` (see [`notroProperties`](#notroproperties)).
|
|
224
|
+
|
|
225
|
+
Individual schemas (e.g. `titlePropertyPageObjectResponseSchema`) remain exported for backwards compatibility.
|
|
226
|
+
|
|
227
|
+
### Components
|
|
228
|
+
|
|
229
|
+
| Component | Description |
|
|
230
|
+
|---|---|
|
|
231
|
+
| `NotroContent` | Renders Notion Markdown to HTML. Unstyled by default; pass `components` to customize |
|
|
232
|
+
| `DatabaseCover` | Renders a Notion cover image with optimization |
|
|
233
|
+
| `DatabaseProperty` | Renders a Notion property value by type |
|
|
234
|
+
| `compileMdxCached` | Low-level MDX compile API. Use when building a custom `NotroContent` |
|
|
235
|
+
|
|
236
|
+
### `notroProperties`
|
|
237
|
+
|
|
238
|
+
Zod schema shorthands for defining property schemas in `content.config.ts`. Each key maps to a Notion property type.
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
import { notroProperties } from "notro-loader";
|
|
242
|
+
|
|
243
|
+
// notroProperties.title → titlePropertyPageObjectResponseSchema
|
|
244
|
+
// notroProperties.richText → richTextPropertyPageObjectResponseSchema
|
|
245
|
+
// notroProperties.checkbox → checkboxPropertyPageObjectResponseSchema
|
|
246
|
+
// notroProperties.multiSelect → multiSelectPropertyPageObjectResponseSchema
|
|
247
|
+
// notroProperties.select → selectPropertyPageObjectResponseSchema
|
|
248
|
+
// notroProperties.date → datePropertyPageObjectResponseSchema
|
|
249
|
+
// notroProperties.number → numberPropertyPageObjectResponseSchema
|
|
250
|
+
// notroProperties.url → urlPropertyPageObjectResponseSchema
|
|
251
|
+
// notroProperties.email → emailPropertyPageObjectResponseSchema
|
|
252
|
+
// notroProperties.phoneNumber → phoneNumberPropertyPageObjectResponseSchema
|
|
253
|
+
// notroProperties.files → filesPropertyPageObjectResponseSchema
|
|
254
|
+
// notroProperties.people → peoplePropertyPageObjectResponseSchema
|
|
255
|
+
// notroProperties.relation → relationPropertyPageObjectResponseSchema
|
|
256
|
+
// notroProperties.rollup → rollupPropertyPageObjectResponseSchema
|
|
257
|
+
// notroProperties.formula → formulaPropertyPageObjectResponseSchema
|
|
258
|
+
// notroProperties.uniqueId → uniqueIdPropertyPageObjectResponseSchema
|
|
259
|
+
// notroProperties.status → statusPropertyPageObjectResponseSchema
|
|
260
|
+
// notroProperties.createdTime → createdTimePropertyPageObjectResponseSchema
|
|
261
|
+
// notroProperties.createdBy → createdByPropertyPageObjectResponseSchema
|
|
262
|
+
// notroProperties.lastEditedTime → lastEditedTimePropertyPageObjectResponseSchema
|
|
263
|
+
// notroProperties.lastEditedBy → lastEditedByPropertyPageObjectResponseSchema
|
|
264
|
+
// notroProperties.button → buttonPropertyPageObjectResponseSchema
|
|
265
|
+
// notroProperties.verification → verificationPropertyPageObjectResponseSchema
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Utilities
|
|
269
|
+
|
|
270
|
+
| Function | Description |
|
|
271
|
+
|---|---|
|
|
272
|
+
| `getPlainText(property)` | Extracts plain text from Title, Rich Text, Select, Multi-select, Number, URL, Email, Phone, Date, and Unique ID properties |
|
|
273
|
+
| `getMultiSelect(property)` | Returns the options array for a multi-select property. Returns an empty array for unsupported types or `undefined` — no type guard needed |
|
|
274
|
+
| `hasTag(property, tagName)` | Returns whether a multi-select property contains the given tag name. Safe to call without a type guard |
|
|
275
|
+
| `buildLinkToPages(entries, options)` | Builds a `linkToPages` map from collection entries. Pass to `NotroContent` for resolving inter-page Notion links |
|
|
276
|
+
| `colorToCSS(color)` | Converts a Notion color name to an inline CSS style string (for use in custom components) |
|