notion-astro-loader
Version:
Notion loader for the Astro Content Layer API. It allows you to load pages from a Notion database then render them as pages in a collection.
140 lines (126 loc) • 4.16 kB
text/typescript
import { Client, isFullPage, iteratePaginatedAPI } from "@notionhq/client";
import type { RehypePlugins } from "astro";
import type { Loader } from "astro/loaders";
import { propertiesSchemaForDatabase } from "./database-properties.js";
import {
buildProcessor,
NotionPageRenderer,
type RehypePlugin,
} from "./render.js";
import { notionPageSchema } from "./schemas/page.js";
import type { ClientOptions, QueryDatabaseParameters } from "./types.js";
export interface NotionLoaderOptions
extends Pick<
ClientOptions,
"auth" | "timeoutMs" | "baseUrl" | "notionVersion" | "fetch" | "agent"
>,
Pick<
QueryDatabaseParameters,
"database_id" | "filter_properties" | "sorts" | "filter" | "archived"
> {
/**
* Pass rehype plugins to customize how the Notion output HTML is processed.
* You can import and apply the plugin function (recommended), or pass the plugin name as a string.
*/
rehypePlugins?: RehypePlugins;
}
/**
* Notion loader for the Astro Content Layer API.
*
* It allows you to load pages from a Notion database then render them as pages in a collection.
*
* @param options Takes in same options as `notionClient.databases.query` and `Client` constructor.
*
* @example
* // src/content/config.ts
* import { defineCollection } from "astro:content";
* import { notionLoader } from "notion-astro-loader";
*
* const database = defineCollection({
* loader: notionLoader({
* auth: import.meta.env.NOTION_TOKEN,
* database_id: import.meta.env.NOTION_DATABASE_ID,
* filter: {
* property: "Hidden",
* checkbox: { equals: false },
* }
* }),
* });
*/
export function notionLoader({
database_id,
filter_properties,
sorts,
filter,
archived,
rehypePlugins = [],
...clientOptions
}: NotionLoaderOptions): Loader {
const notionClient = new Client(clientOptions);
const resolvedRehypePlugins = Promise.all(
rehypePlugins.map(async (config) => {
let plugin: RehypePlugin | string;
let options: any;
if (Array.isArray(config)) {
[plugin, options] = config;
} else {
plugin = config;
}
if (typeof plugin === "string") {
plugin = (await import(/* @vite-ignore */ plugin))
.default as RehypePlugin;
}
return [plugin, options] as const;
}),
);
const processor = buildProcessor(resolvedRehypePlugins);
return {
name: "notion-loader",
schema: async () =>
notionPageSchema({
properties: await propertiesSchemaForDatabase(
notionClient,
database_id,
),
}),
async load({ store, logger, parseData }) {
logger.info("Loading notion pages");
const existingPageIds = new Set<string>(store.keys());
const renderPromises: Promise<void>[] = [];
const pages = iteratePaginatedAPI(notionClient.databases.query, {
database_id,
filter_properties,
sorts,
filter,
archived,
});
for await (const page of pages) {
if (!isFullPage(page)) {
continue;
}
existingPageIds.delete(page.id);
const existingPage = store.get(page.id);
// If the page has been updated, re-render it
if (existingPage?.digest !== page.last_edited_time) {
const renderer = new NotionPageRenderer(notionClient, page, logger);
const data = await parseData(await renderer.getPageData());
const renderPromise = renderer.render(processor).then((rendered) => {
store.set({
id: page.id,
digest: page.last_edited_time,
data,
rendered,
});
});
renderPromises.push(renderPromise);
}
}
// Remove any pages that have been deleted
for (const deletedPageId of existingPageIds) {
store.delete(deletedPageId);
}
// Wait for rendering to complete
await Promise.all(renderPromises);
},
};
}