notionpress 0.1.0
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 -0
- package/README.md +250 -0
- package/dist/client.d.ts +42 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +167 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/schema.d.ts +33 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +34 -0
- package/dist/schema.js.map +1 -0
- package/dist/types.d.ts +16 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vivillies
|
|
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
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# notionpress
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
**Turn a Notion page into a headless CMS for your blog.**
|
|
8
|
+
|
|
9
|
+
`notionpress` is a lightweight TypeScript client that provisions a blog database on any Notion page (with a ready-made schema for title, slug, status, publish date, and cover image), and gives you a typed API to fetch posts and their content as markdown — no manual database setup required.
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- 🗄️ **Zero-config provisioning** — point it at a Notion page and it creates a database with the right schema automatically.
|
|
14
|
+
- 📝 **Typed posts** — `getPosts()` returns clean, typed `Post` objects instead of raw Notion API responses.
|
|
15
|
+
- 📄 **Markdown content** — `getPostContent(id)` returns a post's body as ready-to-render markdown.
|
|
16
|
+
- 💾 **Local caching** — database/data source IDs are cached on disk so you're not re-creating a database on every run.
|
|
17
|
+
- 🔌 **Bring your own database** — already have a Notion database? Pass its `datasource_id` and skip auto-provisioning entirely.
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
bun add notionpress
|
|
23
|
+
# or
|
|
24
|
+
npm install notionpress
|
|
25
|
+
# or
|
|
26
|
+
pnpm add notionpress
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Prerequisites
|
|
30
|
+
|
|
31
|
+
1. **Create a Notion integration** at [notion.so/my-integrations](https://www.notion.so/my-integrations) and copy its **Internal Integration Secret** — this is your `api_key`.
|
|
32
|
+
2. **Share a Notion page** with that integration (`•••` menu → *Connections* → select your integration). This page is where your blog database will live — copy its **page ID** from the URL.
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { Notionpress } from "notionpress";
|
|
38
|
+
|
|
39
|
+
const blog = new Notionpress({
|
|
40
|
+
api_key: process.env.NOTION_API!,
|
|
41
|
+
page_id: process.env.NOTION_PAGE_ID!,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Fetch all posts
|
|
45
|
+
const posts = await blog.getPosts();
|
|
46
|
+
console.log(posts);
|
|
47
|
+
|
|
48
|
+
// Fetch the markdown body of a specific post
|
|
49
|
+
if (posts[0]) {
|
|
50
|
+
const markdown = await blog.getPostContent(posts[0].id);
|
|
51
|
+
console.log(markdown);
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
On first run, `notionpress` creates a new database titled **"blog_database"** on the given page and caches its IDs in `.notionpress-cache.json` (add this to your `.gitignore`) so subsequent runs reuse the same database instead of creating a new one.
|
|
56
|
+
|
|
57
|
+
## Environment variables
|
|
58
|
+
|
|
59
|
+
Create a `.env` file (see `.env.example`):
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
NOTION_API=secret_xxx # your integration's internal secret
|
|
63
|
+
NOTION_PAGE_ID=xxxxxxxxxxxx # the page to host/host your blog database
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## API Reference
|
|
67
|
+
|
|
68
|
+
### `new Notionpress(params)`
|
|
69
|
+
|
|
70
|
+
| Param | Type | Required | Description |
|
|
71
|
+
| --------------- | -------- | -------- | ----------------------------------------------------------------------------|
|
|
72
|
+
| `api_key` | `string` | ✅ | Your Notion integration secret. |
|
|
73
|
+
| `page_id` | `string` | ✅ | ID of the Notion page to provision (or that already hosts) the blog database. |
|
|
74
|
+
|
|
75
|
+
### `getPosts(): Promise<Post[]>`
|
|
76
|
+
|
|
77
|
+
Returns every post in the blog database, fully paginated.
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
type Post = {
|
|
81
|
+
id: string;
|
|
82
|
+
url: string;
|
|
83
|
+
title: string;
|
|
84
|
+
slug: string;
|
|
85
|
+
status: "Published" | "Editing" | "Deprived" | null;
|
|
86
|
+
published_date: string | null;
|
|
87
|
+
cover_image: string | null;
|
|
88
|
+
};
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### `getPostContent(page_id: string): Promise<string>`
|
|
92
|
+
|
|
93
|
+
Returns the markdown body of a single post page.
|
|
94
|
+
|
|
95
|
+
## Fetching a post by slug
|
|
96
|
+
|
|
97
|
+
`notionpress` doesn't expose a dedicated "get by slug" method — instead, pull the full list from `getPosts()`, find the matching slug, then fetch its body with `getPostContent()`. A tiny shared helper keeps this logic in one place:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
// lib/getPostBySlug.ts
|
|
101
|
+
import { blog } from "./notionpress"; // your Notionpress instance
|
|
102
|
+
|
|
103
|
+
export async function getPostBySlug(slug: string) {
|
|
104
|
+
const posts = await blog.getPosts();
|
|
105
|
+
const post = posts.find((p) => p.slug === slug);
|
|
106
|
+
|
|
107
|
+
if (!post) return null;
|
|
108
|
+
|
|
109
|
+
const content = await blog.getPostContent(post.id);
|
|
110
|
+
return { ...post, content };
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Next.js (App Router)
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
// app/blog/[slug]/page.tsx
|
|
118
|
+
import { notFound } from "next/navigation";
|
|
119
|
+
import Markdown from "react-markdown";
|
|
120
|
+
import { getPostBySlug } from "@/lib/getPostBySlug";
|
|
121
|
+
import { blog } from "@/lib/notionpress";
|
|
122
|
+
|
|
123
|
+
export default async function BlogPostPage({ params }: { params: { slug: string } }) {
|
|
124
|
+
const post = await getPostBySlug(params.slug);
|
|
125
|
+
|
|
126
|
+
if (!post) notFound();
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<article>
|
|
130
|
+
<h1>{post.title}</h1>
|
|
131
|
+
<Markdown>{post.content}</Markdown>
|
|
132
|
+
</article>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Optional: pre-render every known slug at build time
|
|
137
|
+
export async function generateStaticParams() {
|
|
138
|
+
const posts = await blog.getPosts();
|
|
139
|
+
return posts.map((post) => ({ slug: post.slug }));
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### SvelteKit
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
// src/routes/blog/[slug]/+page.server.ts
|
|
147
|
+
import { error } from "@sveltejs/kit";
|
|
148
|
+
import { getPostBySlug } from "$lib/getPostBySlug";
|
|
149
|
+
import type { PageServerLoad } from "./$types";
|
|
150
|
+
|
|
151
|
+
export const load: PageServerLoad = async ({ params }) => {
|
|
152
|
+
const post = await getPostBySlug(params.slug);
|
|
153
|
+
|
|
154
|
+
if (!post) throw error(404, "Post not found");
|
|
155
|
+
|
|
156
|
+
return { post };
|
|
157
|
+
};
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
```svelte
|
|
161
|
+
<!-- src/routes/blog/[slug]/+page.svelte -->
|
|
162
|
+
<script lang="ts">
|
|
163
|
+
import { marked } from "marked";
|
|
164
|
+
export let data;
|
|
165
|
+
</script>
|
|
166
|
+
|
|
167
|
+
<article>
|
|
168
|
+
<h1>{data.post.title}</h1>
|
|
169
|
+
{@html marked(data.post.content)}
|
|
170
|
+
</article>
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### SolidStart
|
|
174
|
+
|
|
175
|
+
```tsx
|
|
176
|
+
// src/routes/blog/[slug].tsx
|
|
177
|
+
import { createAsync, query } from "@solidjs/router";
|
|
178
|
+
import { marked } from "marked";
|
|
179
|
+
import { getPostBySlug } from "~/lib/getPostBySlug";
|
|
180
|
+
|
|
181
|
+
const findPost = query(async (slug: string) => {
|
|
182
|
+
"use server";
|
|
183
|
+
return getPostBySlug(slug);
|
|
184
|
+
}, "post-by-slug");
|
|
185
|
+
|
|
186
|
+
export default function BlogPostPage(props: { params: { slug: string } }) {
|
|
187
|
+
const post = createAsync(() => findPost(props.params.slug));
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
<article>
|
|
191
|
+
<h1>{post()?.title}</h1>
|
|
192
|
+
<div innerHTML={marked(post()?.content ?? "")} />
|
|
193
|
+
</article>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Astro
|
|
199
|
+
|
|
200
|
+
```astro
|
|
201
|
+
---
|
|
202
|
+
// src/pages/blog/[slug].astro
|
|
203
|
+
import { getPostBySlug } from "../../lib/getPostBySlug";
|
|
204
|
+
import { marked } from "marked";
|
|
205
|
+
|
|
206
|
+
const { slug } = Astro.params;
|
|
207
|
+
const post = await getPostBySlug(slug!);
|
|
208
|
+
|
|
209
|
+
if (!post) return Astro.redirect("/404");
|
|
210
|
+
|
|
211
|
+
const html = marked(post.content);
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
<article>
|
|
215
|
+
<h1>{post.title}</h1>
|
|
216
|
+
<Fragment set:html={html} />
|
|
217
|
+
</article>
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
> `getPostContent()` returns raw markdown, so pick whichever markdown-to-HTML renderer fits your stack (`react-markdown`, `marked`, `markdown-it`, etc.) to turn it into rendered output.
|
|
221
|
+
|
|
222
|
+
Full, typed-out, copy-pasteable versions of each of these live in [`examples/`](./examples).
|
|
223
|
+
|
|
224
|
+
## Default schema
|
|
225
|
+
|
|
226
|
+
When `notionpress` provisions a database for you, it creates the following properties:
|
|
227
|
+
|
|
228
|
+
| Property | Notion type |
|
|
229
|
+
| ----------------- | ------------- |
|
|
230
|
+
| `Title` | Title |
|
|
231
|
+
| `Slug` | Rich text |
|
|
232
|
+
| `Status` | Select (`Published`, `Editing`, `Deprived`) |
|
|
233
|
+
| `Published Date` | Date |
|
|
234
|
+
| `Cover Image` | Files |
|
|
235
|
+
|
|
236
|
+
## Development
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
bun install # install dependencies
|
|
240
|
+
bun run test # run tests
|
|
241
|
+
bun run build # compile to dist/
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## Contributing
|
|
245
|
+
|
|
246
|
+
Issues and pull requests are welcome! If you're proposing a larger change, please open an issue first to discuss what you'd like to change.
|
|
247
|
+
|
|
248
|
+
## License
|
|
249
|
+
|
|
250
|
+
[MIT](./LICENSE)
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Post, RootClientProps } from "./types.js";
|
|
2
|
+
export declare class Notionpress {
|
|
3
|
+
private api_key;
|
|
4
|
+
private page_id;
|
|
5
|
+
private database_id;
|
|
6
|
+
private notionClient;
|
|
7
|
+
private dataSourceId;
|
|
8
|
+
private ready;
|
|
9
|
+
/**
|
|
10
|
+
* Creates a Notionpress client bound to a single Notion page.
|
|
11
|
+
* Kicks off async setup (finding or creating the underlying database/data source)
|
|
12
|
+
* in the background; awaited internally by any public method via `this.ready`.
|
|
13
|
+
*/
|
|
14
|
+
constructor(params: RootClientProps);
|
|
15
|
+
/**
|
|
16
|
+
* Runs the async initialization steps required before the client is usable.
|
|
17
|
+
*/
|
|
18
|
+
private initiateTable;
|
|
19
|
+
/**
|
|
20
|
+
* Resolves `this.database_id` / `this.dataSourceId` for `this.page_id`, in priority order:
|
|
21
|
+
* 1. Use the caller-supplied `datasource_id` as-is.
|
|
22
|
+
* 2. Reuse a previously cached database/data source for this page.
|
|
23
|
+
* 3. Otherwise, create a new database on the page with the default schema.
|
|
24
|
+
* Any newly resolved IDs are written back to the cache file.
|
|
25
|
+
*/
|
|
26
|
+
private createDataSources;
|
|
27
|
+
/**
|
|
28
|
+
* Maps a raw Notion page object (from the blog database) to our `Post` shape,
|
|
29
|
+
* pulling out title, slug, status, published date, and cover image.
|
|
30
|
+
*/
|
|
31
|
+
private toPost;
|
|
32
|
+
/**
|
|
33
|
+
* Fetches all posts from the configured data source, paging through
|
|
34
|
+
* results until every entry has been collected.
|
|
35
|
+
*
|
|
36
|
+
* @throws if no `dataSourceId` is available (must be set manually after
|
|
37
|
+
* a table/database is created).
|
|
38
|
+
*/
|
|
39
|
+
getPosts(): Promise<Post[]>;
|
|
40
|
+
getPostContent(page_id: string): Promise<string>;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAc,eAAe,EAAE,MAAM,YAAY,CAAC;AAkCpE,qBAAa,WAAW;IACpB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,YAAY,CAAqB;IACzC,OAAO,CAAC,KAAK,CAAgB;IAE7B;;;;OAIG;gBACS,MAAM,EAAE,eAAe;IAYnC;;OAEG;YACW,aAAa;IAI3B;;;;;;OAMG;YACW,iBAAiB;IAyC/B;;;OAGG;IACH,OAAO,CAAC,MAAM;IAyCd;;;;;;OAMG;IACU,QAAQ,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;IA4B3B,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAShE"}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { Client as NotionClient } from "@notionhq/client";
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { defaultDatasource } from "./schema.js";
|
|
4
|
+
const CACHE_FILE = ".notionpress-cache.json";
|
|
5
|
+
/**
|
|
6
|
+
* Reads the on-disk cache file mapping page IDs to their database/data source IDs.
|
|
7
|
+
* Returns an empty object if the file doesn't exist or fails to parse.
|
|
8
|
+
*/
|
|
9
|
+
function readCache() {
|
|
10
|
+
if (!existsSync(CACHE_FILE)) {
|
|
11
|
+
return {};
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(readFileSync(CACHE_FILE, "utf-8"));
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return {};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Persists (or overwrites) the cache entry for a given page ID.
|
|
22
|
+
*/
|
|
23
|
+
function writeCacheEntry(page_id, entry) {
|
|
24
|
+
const cache = readCache();
|
|
25
|
+
cache[page_id] = entry;
|
|
26
|
+
writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2));
|
|
27
|
+
}
|
|
28
|
+
export class Notionpress {
|
|
29
|
+
api_key;
|
|
30
|
+
page_id;
|
|
31
|
+
database_id;
|
|
32
|
+
notionClient;
|
|
33
|
+
dataSourceId;
|
|
34
|
+
ready;
|
|
35
|
+
/**
|
|
36
|
+
* Creates a Notionpress client bound to a single Notion page.
|
|
37
|
+
* Kicks off async setup (finding or creating the underlying database/data source)
|
|
38
|
+
* in the background; awaited internally by any public method via `this.ready`.
|
|
39
|
+
*/
|
|
40
|
+
constructor(params) {
|
|
41
|
+
this.api_key = params.api_key;
|
|
42
|
+
this.page_id = params.page_id;
|
|
43
|
+
this.notionClient = new NotionClient({ auth: this.api_key });
|
|
44
|
+
this.ready = this.initiateTable().catch((error) => {
|
|
45
|
+
console.error("Notionpress failed to initialize:", error);
|
|
46
|
+
throw error;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Runs the async initialization steps required before the client is usable.
|
|
51
|
+
*/
|
|
52
|
+
async initiateTable() {
|
|
53
|
+
await this.createDataSources();
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Resolves `this.database_id` / `this.dataSourceId` for `this.page_id`, in priority order:
|
|
57
|
+
* 1. Use the caller-supplied `datasource_id` as-is.
|
|
58
|
+
* 2. Reuse a previously cached database/data source for this page.
|
|
59
|
+
* 3. Otherwise, create a new database on the page with the default schema.
|
|
60
|
+
* Any newly resolved IDs are written back to the cache file.
|
|
61
|
+
*/
|
|
62
|
+
async createDataSources() {
|
|
63
|
+
// Caller already told us exactly which data source to use.
|
|
64
|
+
if (this.dataSourceId) {
|
|
65
|
+
writeCacheEntry(this.page_id, { database_id: this.database_id, datasource_id: this.dataSourceId });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
// We've already set this exact page up before; reuse it instead of
|
|
69
|
+
// creating a new database on it every time.
|
|
70
|
+
const cached = readCache()[this.page_id];
|
|
71
|
+
if (cached) {
|
|
72
|
+
this.database_id = cached.database_id;
|
|
73
|
+
this.dataSourceId = cached.datasource_id;
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
// Brand new page: create a database on it with our schema baked in.
|
|
77
|
+
const database = await this.notionClient.databases.create({
|
|
78
|
+
parent: {
|
|
79
|
+
page_id: this.page_id,
|
|
80
|
+
type: "page_id",
|
|
81
|
+
},
|
|
82
|
+
title: [{ text: { content: "blog_database" } }],
|
|
83
|
+
initial_data_source: {
|
|
84
|
+
// @ts-expect-error - Old and New SDK and API calls collide, will fix later
|
|
85
|
+
properties: { ...defaultDatasource }
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
this.database_id = database.id;
|
|
89
|
+
if ("data_sources" in database) {
|
|
90
|
+
this.dataSourceId = database.data_sources[0]?.id;
|
|
91
|
+
}
|
|
92
|
+
if (this.dataSourceId) {
|
|
93
|
+
writeCacheEntry(this.page_id, { database_id: this.database_id, datasource_id: this.dataSourceId });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Maps a raw Notion page object (from the blog database) to our `Post` shape,
|
|
98
|
+
* pulling out title, slug, status, published date, and cover image.
|
|
99
|
+
*/
|
|
100
|
+
toPost(page) {
|
|
101
|
+
const properties = page.properties;
|
|
102
|
+
const title = properties.title;
|
|
103
|
+
const slug = properties.slug;
|
|
104
|
+
const status = properties.status;
|
|
105
|
+
const published_date = properties.published_date;
|
|
106
|
+
const cover_image = properties.cover_image;
|
|
107
|
+
const titleText = title?.type === "title"
|
|
108
|
+
? title.title.map((item) => item.plain_text).join("")
|
|
109
|
+
: "";
|
|
110
|
+
const slugText = slug?.type === "rich_text"
|
|
111
|
+
? slug.rich_text.map((item) => item.plain_text).join("")
|
|
112
|
+
: "";
|
|
113
|
+
const statusName = status?.type === "select"
|
|
114
|
+
? status.select?.name ?? null
|
|
115
|
+
: null;
|
|
116
|
+
const publishedDateValue = published_date?.type === "date"
|
|
117
|
+
? published_date.date?.start ?? null
|
|
118
|
+
: null;
|
|
119
|
+
const coverImageFile = cover_image?.type === "files" ? cover_image.files[0] : undefined;
|
|
120
|
+
const coverImageUrl = coverImageFile
|
|
121
|
+
? (coverImageFile.type === "file" ? coverImageFile.file.url : coverImageFile.external.url)
|
|
122
|
+
: null;
|
|
123
|
+
return {
|
|
124
|
+
id: page.id,
|
|
125
|
+
url: page.url,
|
|
126
|
+
title: titleText,
|
|
127
|
+
slug: slugText,
|
|
128
|
+
status: statusName,
|
|
129
|
+
published_date: publishedDateValue,
|
|
130
|
+
cover_image: coverImageUrl
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Fetches all posts from the configured data source, paging through
|
|
135
|
+
* results until every entry has been collected.
|
|
136
|
+
*
|
|
137
|
+
* @throws if no `dataSourceId` is available (must be set manually after
|
|
138
|
+
* a table/database is created).
|
|
139
|
+
*/
|
|
140
|
+
async getPosts() {
|
|
141
|
+
await this.ready;
|
|
142
|
+
if (!this.dataSourceId) {
|
|
143
|
+
throw new Error("Once a table/database is created you have to manually add the datasource_id to the variable initiator");
|
|
144
|
+
}
|
|
145
|
+
const posts = [];
|
|
146
|
+
let cursor = undefined;
|
|
147
|
+
do {
|
|
148
|
+
const response = await this.notionClient.dataSources.query({
|
|
149
|
+
data_source_id: this.dataSourceId,
|
|
150
|
+
start_cursor: cursor
|
|
151
|
+
});
|
|
152
|
+
for (const result of response.results) {
|
|
153
|
+
if (result.object === "page" && "properties" in result) {
|
|
154
|
+
posts.push(this.toPost(result));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
cursor = response.next_cursor ?? undefined;
|
|
158
|
+
} while (cursor);
|
|
159
|
+
return posts;
|
|
160
|
+
}
|
|
161
|
+
async getPostContent(page_id) {
|
|
162
|
+
await this.ready;
|
|
163
|
+
const { markdown } = await this.notionClient.pages.retrieveMarkdown({ page_id });
|
|
164
|
+
return markdown;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,YAAY,EAAE,IAAI,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/schema.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export declare const defaultDatasource: {
|
|
2
|
+
cover_image: {
|
|
3
|
+
name: string;
|
|
4
|
+
type: string;
|
|
5
|
+
files: {};
|
|
6
|
+
};
|
|
7
|
+
published_date: {
|
|
8
|
+
name: string;
|
|
9
|
+
type: string;
|
|
10
|
+
date: {};
|
|
11
|
+
};
|
|
12
|
+
slug: {
|
|
13
|
+
name: string;
|
|
14
|
+
type: string;
|
|
15
|
+
rich_text: {};
|
|
16
|
+
};
|
|
17
|
+
title: {
|
|
18
|
+
name: string;
|
|
19
|
+
type: string;
|
|
20
|
+
title: {};
|
|
21
|
+
};
|
|
22
|
+
status: {
|
|
23
|
+
name: string;
|
|
24
|
+
type: string;
|
|
25
|
+
select: {
|
|
26
|
+
options: {
|
|
27
|
+
name: string;
|
|
28
|
+
color: string;
|
|
29
|
+
}[];
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
//# sourceMappingURL=schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgC7B,CAAA"}
|
package/dist/schema.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export const defaultDatasource = {
|
|
2
|
+
cover_image: {
|
|
3
|
+
name: "Cover Image",
|
|
4
|
+
type: "files",
|
|
5
|
+
files: {}
|
|
6
|
+
},
|
|
7
|
+
published_date: {
|
|
8
|
+
name: "Published Date",
|
|
9
|
+
type: "date",
|
|
10
|
+
date: {}
|
|
11
|
+
},
|
|
12
|
+
slug: {
|
|
13
|
+
name: "Slug",
|
|
14
|
+
type: "rich_text",
|
|
15
|
+
rich_text: {}
|
|
16
|
+
},
|
|
17
|
+
title: {
|
|
18
|
+
name: "Title",
|
|
19
|
+
type: "title",
|
|
20
|
+
title: {}
|
|
21
|
+
},
|
|
22
|
+
status: {
|
|
23
|
+
name: "Status",
|
|
24
|
+
type: "select",
|
|
25
|
+
select: {
|
|
26
|
+
options: [
|
|
27
|
+
{ name: "Published", color: "green" },
|
|
28
|
+
{ name: "Editing", color: "blue" },
|
|
29
|
+
{ name: "Deprived", color: "red" },
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAC7B,WAAW,EAAE;QACT,IAAI,EAAE,aAAa;QACnB,IAAI,EAAE,OAAO;QACb,KAAK,EAAE,EAAE;KACZ;IACD,cAAc,EAAE;QACZ,IAAI,EAAE,gBAAgB;QACtB,IAAI,EAAE,MAAM;QACZ,IAAI,EAAE,EAAE;KACX;IACD,IAAI,EAAE;QACF,IAAI,EAAE,MAAM;QACZ,IAAI,EAAE,WAAW;QACjB,SAAS,EAAE,EAAE;KAChB;IACD,KAAK,EAAE;QACH,IAAI,EAAE,OAAO;QACb,IAAI,EAAE,OAAO;QACb,KAAK,EAAE,EAAE;KACZ;IACD,MAAM,EAAE;QACJ,IAAI,EAAE,QAAQ;QACd,IAAI,EAAE,QAAQ;QACd,MAAM,EAAE;YACJ,OAAO,EAAE;gBACL,EAAE,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,OAAO,EAAE;gBACrC,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE;gBAClC,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,EAAE;aACrC;SACJ;KACJ;CACJ,CAAA"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface RootClientProps {
|
|
2
|
+
api_key: string;
|
|
3
|
+
page_id: string;
|
|
4
|
+
datasource_id?: string;
|
|
5
|
+
}
|
|
6
|
+
export type PostStatus = "Published" | "Editing" | "Deprived";
|
|
7
|
+
export interface Post {
|
|
8
|
+
id: string;
|
|
9
|
+
url: string;
|
|
10
|
+
title: string;
|
|
11
|
+
slug: string;
|
|
12
|
+
status: PostStatus | null;
|
|
13
|
+
published_date: string | null;
|
|
14
|
+
cover_image: string | null;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,eAAe;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,MAAM,UAAU,GAAG,WAAW,GAAG,SAAS,GAAG,UAAU,CAAC;AAE9D,MAAM,WAAW,IAAI;IACjB,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,UAAU,GAAG,IAAI,CAAC;IAC1B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "notionpress",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Turn a Notion page into a headless CMS for your blog — typed post fetching and markdown content, no manual database setup required.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"notion",
|
|
7
|
+
"cms",
|
|
8
|
+
"headless-cms",
|
|
9
|
+
"blog",
|
|
10
|
+
"markdown",
|
|
11
|
+
"typescript"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": "Vivillies",
|
|
15
|
+
"homepage": "https://github.com/Vivillies/notionpress#readme",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/Vivillies/notionpress.git"
|
|
19
|
+
},
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/Vivillies/notionpress/issues"
|
|
22
|
+
},
|
|
23
|
+
"type": "module",
|
|
24
|
+
"module": "src/index.ts",
|
|
25
|
+
"main": "./dist/index.js",
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"import": "./dist/index.js"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist"
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsc -p tsconfig.build.json",
|
|
38
|
+
"test": "bun run test/client.test.ts",
|
|
39
|
+
"prepublishOnly": "bun run build"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/bun": "latest",
|
|
43
|
+
"@types/node": "^26.6.2"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"typescript": "^5"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@notionhq/client": "^5.26.0",
|
|
50
|
+
"zod": "^4.5.4"
|
|
51
|
+
}
|
|
52
|
+
}
|