Astro + Notion + MDX = ❤️
- You will need a Notion account.
- Copy this template to bootstrap your project and create a sub-page called 'index' then write some content.
- Then check out the Getting Started page for the Notion API. You will need to create an integration and 'invite' the integration just like if it was a guest to the root page of your web project.
We will need the integration's API key as an environment variable here.
git clone https://github.com/m4rrc0/poko-notion.git
cd poko-notion
npm install
Rename or copy .env.dist
file in .env
and paste your API key as the NOTION_TOKEN
and optionnaly the ID of the Notion page that is the root of your web project as NOTION_ROOT_ID
.
Then the magic should operate with
npm run dev
- Create free accounts: Github, Notion, A static host (see list bellow)
- Connect accounts as explained on the website
- Write in Notion
- Hit 'Publish'
- Share your brand new digital home! 🚀
This is an alpha release. I you like the idea, please share your thoughts to encourage our work.
This is a starter template for Astro.
Its goal is to make it unbearably easy to publish content on the web.
Its primary target user is 'The Tech Savvy Writer'.
It is making use of Notion as its CMS; Just write in Notion, click 'Publish' and 30 seconds later your site is online.
If you want extra functionnality you can even directly write MDX in Notion.
To me, Astro is one of the most exciting tech being built in the world of front-end web development. It is a complicated tool aimed at making things easy and performant for the end user (the developer in this case). It builds zero-JS websites by default with smart enhancement.
Poko is a project of the Supertera collective.
Supertera is:
An overground movement of independant minds aiming for collective growth through individual emancipation and shared knowledge.
'Growth' is defined as 'becoming better and better humans' and opposed to the traditionnal concept of 'making profit' above all.
Poko is meant to be an answer to 'yes, I'd like to [write a blog] [publish my digital garden] [share what I am working on] [...] but I need to create a website first...'.
No more excuses. Be the master of your faith. Write, write, write and publish your content on your own terms.
As to why really the name 'Poko'... That is a secret. Maybe if you go on reading you'll find out...
It is free. It is easy. It is popular. It is more powerful than a cloud document. It is less complicated than a classic headless CMS. It has an (sometimes shity) API.
The goal is one day to support more sources, especialy more FOSS software. The front-end API would stay the same (somewhat opiniated) structure to allow easily building on that side too (components, themes, ...).
Users do not write code.
For a lambda user, the maximum effort is a copy/paste. The tech-savvy user will happily copy/paste to achieve something cool but most won't dive into the code.
With the approach we are taking, components, themes, and even complex functionnality are all a copy/paste away from one notion page to another.
NOTE: It also means that you need to be careful of the code you copy/paste. If you don't understand what you are putting onto your site, at least make sure it comes from a reputable source!
In the current example, we use MDX to render the content. It is not mandatory though.
For now, (as far as I know) MDX allows more freedom and functionnality than the native Astro <Markdown />
component. But we definitely look forward to using more of Astro!
NOTE: The Astro <Markdown />
component should work just fine for simple content without variables, custom components, ...
NOTE 2: We also envision a 'markdownless' version where we use structured data all the way to the HTML composition without resorting to markdown at all.
A few static hosting providers:
- Cloudflare Pages
- netlify
- firebase
- github pages
- gitlab pages
- surge
- zeit
- forge
- aerobatic
- Amazon S3
- Improve many things in Poko Astro-Notion... (list to come)
- Examples using different libraries, CSS frameworks, ...
- More default components ready to use
- Finalize front-end API to allow for 'markdownless' rendering
- Support Mark Doc as a markdown renderer
- Transform data format from Notion to MDX using Unist directly (will allow nesting elements like columns more easily)
- Automatic RSS feed on DBs?
- [ ]
All commands are run from the root of the project, from a terminal:
Command | Action |
---|---|
npm install |
Installs dependencies |
npm run dev |
Starts local dev server at localhost:3000 |
npm run build |
Build your production site to ./dist/ |
npm run preview |
Preview your build locally, before deploying |
-
Fetching all Notion Pages the
NOTION_TOKEN
(environment variable) has access to -
Transform 'raw' Notion pages to make them more usable on next steps.
-
Populate pages with their blocks recursively
- Exceptions for the following blocks:
link_to_page
,child_page
,child_database
. These will be filled a few steps later when pages get their full data. - We use the library
notion-to-markdown
to convert blocks to md then generate the markdown for the page. (This was a quick win but will be technical debt in the short term) - Each page is parsed with
MDX
as well so we can extract frontmatter and otherexports
early and use these as build options if necessary.
- Exceptions for the following blocks:
-
Find the root of the project (settings page) with these rules. Either
- the only Notion page (the token can access) placed at the root in Notion, or
- the page corresponding to the ID provided in the
NOTION_ROOT_ID
environment variable
-
Create a full tree of the Notion data by placing pages into their respective
block
s of typeslink_to_page
,child_page
,child_database
-
Transform the tree of Notion Pages to a more friendly data structure (and more Unist friendly hopefully)
-
3 types of nodes:
-
root
is the settings page = the root page in Notion. This node has a propertydata.role = "settings"
-
page
: can be a classic page or a database page (the latter is not properly supported yet though) with respectivelydata.role = "page"
anddata.role = "collection"
-
block
for Notion blocks. These are more or less equivalent to HTML block elements. -
This is the current mapping of Notion block types into our node's
data.role
const rawTypes = { paragraph: "p", heading_1: "h1", heading_2: "h2", heading_3: "h3", bulleted_list_item: "ul", numbered_list_item: "ol", code: "code", to_do: "todo", toggle: "toggle", child_page: "page", child_database: "collection", embed: "embed", image: "img", video: "video", file: "file", pdf: "pdf", bookmark: "bookmark", callout: "callout", quote: "blockquote", equation: "equation", divider: "hr", table_of_contents: "toc", column_list: "columns", column: "column", link_preview: "a", synced_block: "skip", template: "none", link_to_page: "link", table: "table", table_row: "tr", unsupported: "none", };
-
-
NOTE: Inline elements are currently described inside their block (as an 'Notion-style' array of data and as markdown). In the near future, we might want to transform this data in a proper subtree to allow easier composability and improve compatibility with the Unified ecosystem.
-
-
Extract relevant info from the settings page.
globalStylesString
will be injected in auser-styles.css
thanks to the 'non-html page' Astro API. (See in/src/pages/user-styles.css.js
)headString
will be injected into the head of each page with a<Fragment slot="head" set:html={headString} />
inside our<SkeletonPage ... />
layout.
-
Complement page info:
- Construct the full URL of the page based on own page name and parents' names (keeping '/' in place). For example, a structure in Notion like "root" > "Super" > "SeCrEt" > " / hidden / / SECRET - Page / " would create this path
super/secret/hidden/secret-page
- Merge MDX
exports
from root to each page. The data cascade seems the most convenient way to handle data to avoid repetition.
- Construct the full URL of the page based on own page name and parents' names (keeping '/' in place). For example, a structure in Notion like "root" > "Super" > "SeCrEt" > " / hidden / / SECRET - Page / " would create this path
-
Record a list of all pages with Notion ID, Notion URLs for this page and our generated path. This is usefull to be able to replace internal Notion links with our internal website's links.
-
Process Images and files
- We have 5 properties that can describe an image or file
featuredImage
(only on nodes withtype = 'page'
) is a page's icon (if the icon is an image. See further in case it is an Emoji)cover
(only on nodes withtype = 'page'
) is a page's cover image.icon
(only on nodes withdata.role = 'callout'
) is a callout's icon (if the icon is an image. See further in case it is an Emoji)image
(only on nodes withdata.role = 'img'
) is the content of an independantimg
blockfile
(only on nodes withdata.role = 'file'
) is the content of an independantfile
block
- An image of file description has the following properties:
const imageOrFile = { last_edited_time, // comes directly from Notion originalUrl, // Notion URL (expires for uploaded ressources) filename, // like 'my-image.png' extension, // like '.png' url, // our local file URL }
- We have 5 properties that can describe an image or file
-
An
emoji
property can contain the emoji character chosen in Page Icon or Callout Icon. -
Process Notion's Page Properties (the properties attributed to a page when it is a child of a database)
- ... This is a work in progress ...
- We try to make this props as easily usable as possible on the front-end to make it easy for components to consume the data (as props since these are passed as props to the page).
- For example, a property called 'country' defined as a single
select
can be accessed asprops.country
on the front-end and will return a single string (for example"Belgium"
) - For the moment, these are handled:
rich_text
will be returned as plain stringselect
will be returned as single stringmulti_select
will be returned as array of stringsfiles
will be returned as array offileObject
(as described above)
-
Merge Notion page properties and MDX exports into
data.props
-
Record a list of all assets (images and files) with the same properties as described earlier. This is usefull to be able to replace Notion's generated links with our internal website's links.
-
Download all assets locally (currently hard coded to
public/user-assets
) and unzip.zip
files in place. -
Replace Notion URLs (pages and assets) in blocks data and in markdown.
-
Isolate Settings for convenience
-
Flatten Pages as an array for convenience
-
Save computed data to disk for access during build and on the front-end.
- A JSON file is created at
src/_data/poko.json
(currently hard coded) - The structure is the following:
const poko = { settings, // the isolated root node pages, // array of pages with all info we gathered and full list of children recursively files, // array of `fileObject` definitions for assets downloaded from Notion paths, // array of `pathsMap` for all our pages websiteTree, // the full tree of nodes from root (settings) to leaves (blocks on the most nested pages) }
- A JSON file is created at
NOTE: this part is currently handled by a minimal Astro integration I called fetch-ahead
. The only role of this integration is to execute an async function before the actual build begins.
The goals of using this could be:
- to execute the code only once even on dev
- to run code early and potentially fail early too
- to play with meaningful Astro paths without interfering (like dumping assets in the
public/
directory.
TODO: There is a potential for improving developer experience tenfold by running this in parallel to the dev server to live reload on changes in Notion.
- A 'catch-all'
[...path].astro
page- Imports poko data
- Re-process pages and settings with MDX + data cascade
- Generate pages according to their
path
s - Dispatch the page to the desired Renderer Layout
- A
SkeletonPage.astro
layout- Handles metadata and head injection
- Default MDX
components
- ... Work in progress ...
- A set of smart default components
- Can be overwritten in code AND in Notion directly through exports
- Renderer layouts. Currently
MDXPage.astro
is the only useful one in all practicality.- Renders the desired flavour of Markdown and/or the data tree directly