Introduction
Svelte headless notion is a library that allows you tu use your Notion as CMS and conveniently render your pages the way you want with snippets.
It is lightweight, fully typed and has no runtime dependencies.
Installation
pnpm add svelte-headless-notion
npm install svelte-headless-notion
bun add svelte-headless-notion
Fetching
This library provides two functions for fetching pages: getPage and findPage.
The getPage function is designed for situations where you already know the ID of the page you want to retrieve, providing a straightforward way to fetch the desired page."
In contrast, findPage enables you to retrieve the first page from a database that matches a specified filter criterion, given its ID. This is particularly useful when you want to retrieve a page by its slug, allowing for more readable and user-friendly URLs within your application.
import { getPage } from "svelte-headless-notion/server";
import { PRIVATE_NOTION_TOKEN } from '$env/static/private';
export async function load({ platform }) {
return {
page: getPage({
auth: PRIVATE_NOTION_TOKEN,
id: pageId
})
};
}
import { findPage } from "svelte-headless-notion/server";
import { PRIVATE_NOTION_TOKEN } from '$env/static/private';
export async function load({ params }) {
return {
page: findPage({
auth: PRIVATE_NOTION_TOKEN,
database_id: databaseId,
filter: {
property: 'Slug',
rich_text: {
equals: params.slug
}
}
})
};
}
The fetching is painfully slow, so you will certainly want to implement a SWR mechanism. The following is an example of how to do it in a simple way with Cloudflare and a KVNamespace
import { PRIVATE_NOTION_TOKEN } from '$env/static/private';
import { getPage, type PageWithBlocks } from 'svelte-headless-notion/server';
const pageId = "10b1ca42e1a8800caab2e6a73c3c11d0";
const getFromOrigin = () => {
return getPage({
auth: PRIVATE_NOTION_TOKEN,
id: pageId
});
};
const setInCache = async (platform: App.Platform) => {
return platform.env.SVELTE_NOTION.put(pageId, JSON.stringify(await getFromOrigin()), {
metadata: {
expiration: Date.now() + 1000 * 60
}
});
};
export async function load({ platform }) {
const cachedValue = await platform?.env.SVELTE_NOTION.getWithMetadata<
PageWithBlocks,
{ expiration: number }
>(pageId, 'json');
const isStale = cachedValue && (cachedValue.metadata?.expiration || Infinity) < Date.now();
const page = cachedValue?.value || (await getFromOrigin());
if (isStale || !cachedValue?.value) {
platform?.context.waitUntil(setInCache(platform));
}
return {
page
};
}
Rendering
Marks
Notion is using 6 types of marks bold, italic, strikethrough, underline, inlineCode and color.
To customize the rendering just append a snippet with the name of the mark as a child of the Page like in the example bellow. Every mark have a children that you must render.
The color mark also comes with a color property.
<script lang="ts">
import Page from "svelte-headless-notion";
</script>
<Page page={data.page}>
{#snippet bold({ children })}
<strong>
{@render children()}
</strong>
{/snippet}
</Page>
Full example
<Page page={data.page} class="prose mx-auto">
{#snippet bold({ children })}
<strong>
{@render children()}
</strong>
{/snippet}
{#snippet italic({ children })}
<em>
{@render children()}
</em>
{/snippet}
{#snippet strikethrough({ children })}
<s>
{@render children()}
</s>
{/snippet}
{#snippet underline({ children })}
<u>
{@render children()}
</u>
{/snippet}
{#snippet inlineCode({ children })}
<code>
{@render children()}
</code>
{/snippet}
{#snippet color({ children, color })}
<span style="color: var(--notion-{color})">
{@render children()}
</span>
{/snippet}
</Page>
Blocks
Notion uses 36 types of blocks: unsupported, paragraph, heading_1, heading_2, heading_3, bulleted_list_item, numbered_list_item, to_do, toggle, code, callout, quote, child_page, child_database, embed, image, video, file, pdf, bookmark, equation, divider, table_of_contents, breadcrumb, column_list, column, synced_block, template, link_to_page, link_preview, table, table_row, table_cell, audio and 2 types of "inline block": link and mention.
To customize block rendering, use snippets named after the block type you want to render. Each snippet receives a block prop containing the JSON data for that block. Depending on the block type, snippets may also receive children , content and caption snippets, which should be rendered within your markup. The block object may contain several properties specific to the block type (url for media block, isHeader boolean for table cells etc.)
Block types can be categorized as follows:
- Textual blocks (e.g., paragraphs): Can have both children (nested blocks) and content (text)
- Structural blocks (e.g., columns): Have children but no content
- Media blocks (e.g., images): May have captions but no children or content
- Void blocks (e.g., Divider): Have neither children nor content nor caption
<script lang="ts">
import Page from "svelte-headless-notion";
</script>
<Page page={data.page}>
{#snippet image({ block, caption })}
<!-- This is a media block, no children nor content but a caption -->
<figure>
<img src={block.url} />
<figcaption>{@render caption()}</figcaption>
</figure>
{/snippet}
<!-- This is a void block, no children nor content no caption -->
{#snippet divider()}
<hr />
{/snippet}
<!-- This is a textual block, it has a content and children, but no caption -->
{#snippet paragraph({block, content, children})}
<div>
<p>
<!-- Content is textual markup of the block -->
{@render content()}
</p>
<!-- Children are nested blocks inside this one, you may one to add some indentation or something -->
{@render children()}
</div>
{/snippet}
<!-- This is a structural block, it has children but no content and no caption -->
{#snippet column({block, children)}
<div style="display: grid; grid-template-columns: repeat({block.children.length}, minmax(0, 1fr)); gap: 1rem">
{@render children()}
</div>
{/snippet}
</Page>
Header and Footer
header and footer are two snippets you can pass to the Page component to further customize the rendering of your Notion page. They receive the page object, which is a simplified JSON representation of the page (compared to the object returned by the official Notion API).
The properties are straightforward and similar to those in the official spec, except they are stored in arrays.
To render rich text properties or the page title, you may want to use the Text component exported by the library.
<script lang="ts">
import Page from "svelte-headless-notion";
import Text from "svelte-headless-notion/Text";
</script>
<Page page={data.page}>
{#snippet header({ page, title })}
<div>
<img style="width: 100%; height: 220px;" src={page.cover} alt="" />
<h1>
<Text content={page.title} />
<!-- OR {@render title()} -->
</h1>
</div>
{/snippet}
</Page>
Props of the Page
key | type | description |
---|---|---|
as | string | undefined | HTMl tag of the page |
class | string | undefined | Class to add to the page |
page | PageWithBlocks | The result of the getPage or findPage function |
wrapper | Snippet<[{children: Snippet}]> | undefined | Optional wrapper for the blocks |
header | Snippet<[{ page: PageWithBlocks; title: Snippet }]> | undefined | Render a header inside the page |
footer | Snippet<[{ page: PageWithBlocks; title: Snippet }]> | undefined | Render a footer inside the page |
Styling
This lib is headless and comes unstyled, though it has default renderers for every blocks and marks. The quickest way to render a passable design is to use @tailwind/typography.