Write in Notion
Publish in Svelte
Control with notion2svelte

Introducing notion2svelte

Just like it sounds, notion2svelte transforms Notion-based content into .svelte pages:
Notionnotion2svelteSvelteNotion → notion2svelte → Svelte

Source (Notion)

😃 Awesome editing experience

🤨 Domain-locked to notion.so

😐 Proprietary limitations

Destination (Svelte)

👘 Your styles

🤔 Your domain

🪡 Infinite possibilities

Fancy horizontal divider

Why notion2svelte?

I like writing in Notion
I like coding in Svelte
I like the safety of git
That, in a nutshell, is what got me started on notion2svelte. It’s not the best project out there, but it’s the only one that does what I want. Namely:

Store intermediary artifacts to-disk

Benefits

My content is safely stored in git

I can access the full, raw JSON for every page from within my app, allowing me to add interactivity that Notion doesn’t allow

Command-line tool, rather than runtime npm package

Benefits

100% decoupled tooling: want to “eject” from notion2svelte? No problem. Just stop using it. From the perspective of your Svelte app, the pages created by notion2svelte are no different than pages you code yourself

Write-your-own components

Benefits

Provides wide creative freedom

Keeps notion2svelte simple

Why NOT notion2svelte?

Why not use it? Mainly because it’s early days and, while notion2svelte isn’t capable of breaking your app, you may not have the patience for its idiosyncrasies.
It’s a bit of an adventure. Kinda like driving stick for the first time: it’s fun!…but takes some getting used to.
I hope you’ll give it a quick try and, if you’re feeling the vibe, let me know what you think in the Discussions.
 
Fancy horizontal divider

What’s new in notion2svelte 0.2.0

Updated to work with SvelteKit’s newer routing system

Intermediary data
<project>/src/routes/<slug>.json.js → <project>/src/routes/<slug>/notion-export.js
Final output
<project>/src/routes/<slug>.svelte → <project>/src/routes/<slug>/+page.svelte

Underline now gets its own component

You can now selectively expose props in a Notion database for use in your components. I haven’t got this well-documented yet, but the TL;DR is: prepend props to be exposed with §. Any prop thus marked will be added (minus the §) to the pageStuff variable which you can reference from within your components.

 
Fancy horizontal divider

How it works, grossly oversimplified

Everything you see on this site was…

1.…written in Notion

2.…exported as a .svelte file by running notion2svelte

3.…published normally using Svelte Kit

 
The screenshot on the right is from early-ish in the project, when I was using the Svelte Kit demo app as my test site; hence the light blue background & (if you zoom way in) familiar top nav
The screenshot on the right is from early-ish in the project, when I was using the Svelte Kit demo app as my test site; hence the light blue background & (if you zoom way in) familiar top nav
 
The .svelte files output by notion2svelte are incomplete in that they rely on a pre-defined set of Svelte components — stored in src/lib/notion2svelte — to do the actual rendering. That’s where you come in!
As the author of these components, you get to make them look & behave however you want. As a for-instance: you can hover over this — or any other — paragraph to access metadata about its source block in Notion.
See blockProps.
See blockProps.

Example Time! 🧐

How do we get from this…
Screenshot of original Notion callout
Screenshot of original Notion callout
…to something like this ↓?
The same callout, after transformation by notion2svelte
 
The first step notion2svelte takes is to query the Notion API.
How notion2svelte uses the Notion API
Before
This big JSON object shows our example callout as rendered by the Notion API.
{
  "object": "block",
  "id": "011d46be-4bf6-4817-a0da-a849ec16f1aa",
  "created_time": "2021-12-20T10:00:00.000Z",
  "last_edited_time": "2021-12-20T10:04:00.000Z",
  "has_children": false,
  "archived": false,
  "type": "callout",
  "callout": {
    "text": [
      {
        "type": "text",
        "text": {
          "content": "Hi. I’m a sea otter.",
          "link": null
        },
        "annotations": {
          "bold": false,
          "italic": false,
          "strikethrough": false,
          "underline": false,
          "code": false,
          "color": "default"
        },
        "plain_text": "Hi. I’m a sea otter.",
        "href": null
      }
    ],
    "icon": {
      "type": "emoji",
      "emoji": "🦦"
    }
  }
}
  json
After
It’s pretty noisy, with all those time stamps, repetitions, and heavy nesting! For rendering purposes, most of it can be ignored.
Here’s what notion2svelte spits out from all that JSON:
<Callout emoji="🦦"
  blockProps={{
	  pageId: 'fdc65179a8bd451caf67x9019204cfde',
		id: '011d46be-4bf6-4817-a0da-a849ec16f1aa',
		created_time: '2021-12-20T10:00:00.000Z',
		last_edited_time: '2021-12-20T10:04:00.000Z'
	}}>
  Hi. I’m a sea otter.
</Callout>
  html
The block-props attribute gives your component implementations access to Notion’s most relevant block metadata.
Better, right?
Note that, to keep things simple, our example leaves out complicating factors like Notion “annotations” (i.e., character formatting), and sub-blocks.
To render this particular example, all we really need from the JSON are the values for callout.icon.emoji, callout.text[0].content, and, well, the blocks’ overall type: "callout". The type tells notion2svelte which component to use, in this case, <Callout>.
That’s the basic idea. All that remains is to create a Svelte component called Callout. And that’s when things get fun.
Do you want a simple, gray callout with minimal styling, like this:
Here’s the code (simple gray)
Fancy horizontal divider
Or would you prefer something more colorful?
Here’s the code (blue border)
Fancy horizontal divider
Or you could go crazy and have a callout that doubles as a toggle. Wat!? 🙀
Sure.
You can add keyboard interactions, or make callouts behave according to user settings. Anything Svelte allows you to do, you can (probably) do it!
 
Toggle closed
Toggle closed
Toggle open
Toggle open
Here’s the code (blue border, with toggle)

A word about imports

It should go without saying, but your Sveltekit app needs to know where to actually find the components that match notion2svelte’s output. At present, this means putting them in src/lib/notion2svelte. The reason why becomes apparent when we look at the import statements rendered by notion2svelte.
Don’t worry. You don’t need to explicitly remember this location. You’ll get a standard Svelte error if you try to run your app without the necessary components in place. After all, they’re just regular Svelte imports. Nothing special.
import Callout from '$lib/notion2svelte/Callout.svelte';
  html
Fancy horizontal divider

How to use notion2svelte

For now, the fastest way to get started is to follow along with the video tutorial, notion2svelte in 10 minutes
Fancy horizontal divider

Where to learn more

Find the code, start a discussion, or report an issue on GitHub

Ready to try it?

Open your Terminal.app* and follow along to 👉🏿 “notion2svelte in 10** minutes”!
Caveats
Fancy horizontal divider

🏠 Browse the docs ⚘

High-level Discussion

Turn-intoable Block Components

Toggle Headings (not yet implemented)

Layout-only Components

Page-level Components

Annotation Components

bold → <strong>

italic → <em>

strikethrough → <s>

Other Components