UNPKG

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
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); }, }; }