How I built a Streamlit app to share my Notion notes
Hi hi hi 👋
Let’s see how I built a Streamlit app to share my Notion notes about golden formulas (link to the live app).
Goals
I had two major goals here:
- Consume content from Notion via the API
- Generate as much of the codebase as I can dynamically to minimise manual code changes
You could also argue I had a third goal of trying out the multi-page functionality in Streamlit which I had not used yet.
The pre-requisites
The big elephant in the room for me was the Notion API — not that I am new to the world of APIs, but I had yet to work with this particular one. Who knew what hellscape its functionality and documentation would be?
Turns out the documentation is extensive and the free tier limitation is plenty for my use case.
I do admit I also used this project to practice more with GenAI, especially Claude AI. While the Notion documentation was great, I really don’t enjoy reading through documentation to find the technical details I’m looking for. One prompt and Claude AI handed me the requests I wanted all ready to use in Python.
My work was to create a token (or a Connection as they call it), and then grant read-access to the connection on my target pages. I did fiddle around with the output of the requests for the sake of still having to do a bit of the legwork but Claude did provide the bulk of the code, especially the details to the API requests.
Other than that I spent a few minutes reading Streamlit docs on how to create multi-page apps. I know I said I dislike reading docs, but Streamlit docs are straight to the point and provide clear instructions and plenty of examples.
Project repository
Coding was actually easy. Have a look at the project repo below.
Home.py holds the code for the Home page of the app, while everything under /pages represents individual sub-pages.
notion_api.py is a utils script for pulling data from the Notion API.
The /.streamlit folder holds internal files to run the app, namely a file with the API token which is ignored in the .gitignore (more on secrets in a moment).
The rest are the standard README, as well as the pipfile files for dependency management.
Connecting to the Notion API
It’s probably a good idea to show you some of the code as well.
If we have a look at the utils script to consume from the API
import streamlit as st
import requests
NOTION_KEY = st.secrets["NOTION_KEY"]
BASE_PAGE_ID = "10f16f5e14c18095863ccadd6d9140de"
def get_child_pages(page_id, token):
url = f"https://api.notion.com/v1/blocks/{page_id}/children"
headers = {
"Authorization": f"Bearer {token}",
"Notion-Version": "2022-06-28"
}
response = requests.get(url, headers=headers)
if response.status_code == 200:
data = response.json()
child_pages = [
block for block in data["results"]
if block["type"] == "child_page"
]
return child_pages
else:
print(f"Error: {response.status_code}")
return None
def get_page_bullets(page_id, token):
url = f"https://api.notion.com/v1/blocks/{page_id}/children"
headers = {
"Authorization": f"Bearer {token}",
"Notion-Version": "2022-06-28"
}
response = requests.get(url, headers=headers)
if response.status_code == 200:
data = response.json()
child_pages = [
block["bulleted_list_item"]["rich_text"][0]["plain_text"] for block in data["results"]
if block["type"] == "bulleted_list_item"
]
return child_pages
else:
print(f"Error: {response.status_code}")
return None
if __name__ == "__main__":
child_pages = get_child_pages(BASE_PAGE_ID, NOTION_KEY)
formula_pages = {page["child_page"]["title"]: page["id"] for page in child_pages}
print(formula_pages)
formula_content = {formula: get_page_bullets(formula_pages[formula], NOTION_KEY) for formula in formula_pages}
print(formula_content)
I use the
https://api.notion.com/v1/blocks/{page_id}/children
endpoint to consume the contents on my base Notion page. For reference, this is what it looks like in Notion
(you can see I have more pages in there than in the app because I’m still cooking up more content as I go through podcasts, books, videos, etc.)
And the Golden Formulas page itself is made up of references to children pages
So, that first endpoint will return each individual child page reference. I then use the same endpoint to retrieve the contents of each child page — the difference being I look for bulleted list items instead of child pages.
Actually now that I look at the code again I could have made a single function because it’s the same endpoint and performs similar logic. Difference being I could have a third function argument to pick the block type to filter for.
Oh well it’s not terrible to have a function for each but an idea I got while writing this post.
In summary, the idea with this script is to call a function to retrieve all children pages, and then query the API for the contents of child page currently open on Streamlit. The output will be a Python list of strings (bullet point text).
Coding the Streamlit app
Okay the Streamit app was smooth sailing really — and I used too much list/dict comprehensions honestly.
The home page has nothing to write home about, only a block of markdown to greet the user.
The real show stealer if you will are the sub pages. At this point in time I have to manually create the script for each page to match the Notion page (e.g. I don’t have a page on Streamlit for “wealth” or “storytelling” yet).
The idea down the line is to dynamically generate the subpages, i.e. the Python scripts, based on the pages available in Notion — a query that runs when the user opens the Home page and then generates the pages, but a) that assumes a user will always go to Home and b) that can become a heavy load on the API.
Currently I query the API once when a user navigates to a sub page.
import streamlit as st
from notion_api import *
import os
page_name = os.path.basename(__file__).replace(".py", "")
st.set_page_config(
page_title = page_name
)
st.markdown(f"# {page_name}")
child_pages = get_child_pages(BASE_PAGE_ID, NOTION_KEY)
formula_pages = {page["child_page"]["title"]: page["id"] for page in child_pages}
page_bullets = {formula: get_page_bullets(formula_pages[formula], NOTION_KEY) for formula in formula_pages}
display_string = "\n".join([f"* {bullet_item}" for bullet_item in page_bullets[page_name]])
st.markdown(display_string)
The above is all the code written in the Bouldering page script, but the exact same code is used in each subpage — as you can it is a template in and of itself. And so any concrete content for these golden formulas is all derived by querying the API for the corresponding page in Notion.
The list of strings extracted from the API drive the generated of a markdown bullet list for Streamilt.
This does mean upon changing pages you need to wait up to 3 seconds for the contents to load but it averages 1 second from my testing.
And that’s it for the code base really. Home is the only different page and that’s because it’s pure markdown contents, the rest follow the logic of read the script name to set the page name in the browser > query the API for the list of text to go on this page > render the text as a bullet list.
Deployment and secret management
Streamlit cloud makes this by far the easiest part and all for free.
All the code lives in GitHub. And guess what Streamlit allows for? Deploying a Streamlit app directly from a GitHub repository.
I can even give it a custom subdomain, still for free.
And secret management is as easy as copy pasting the contents of my local TOML file into the Secrets section of the app settings.
import streamlit as st
NOTION_KEY = st.secrets["NOTION_KEY"]
The above is all Streamlit needs to go into the /.streamlit folder and read the contents from secrets.toml. On my local machine I have created the file myself. In the deployment, I only paste TOML contents into the settings of the app. Really really smooth experience with Streamlit from begin to end.
Closing thoughts
That’s it folks. This whole experience is by no means complex, but it illustrates how cool Streamlit can be to display your work and how you can use Python to integrate an API with your web app.
In the future I will still explore the idea of generating the sub pages on the fly as well, but as mentioned earlier there are a few challenges, perhaps even deal breakers for that functionality to go ahead.
Never lose your spirit to build things on your own for your own pleasure and/or personal life.