bimals.net
I wanted a personal site that worked as a portfolio as well as a place where I could post whatever I felt like. I saw a lot of setups with people writing directly on markdown files. But editing content directly on code level and having to re-deploy every time I wrote something was too much of a friction point for me.
Enter Notion. What started as my note-taking app, now powers this portfolio.
I decided to go the easier route and develop the whole thing from scratch instead of using existing setups I found online. That way I could incrementally add what I need when I need, instead of having to understand everything from the jump.
Notion Integration
The setup on Notion’s side is pretty straightforward. First thing you need is a Notion integration.
That’s it.
Here’s the properties of my Notion database for these posts.
# Posts properties
- Title (title field)
- Description (rich text)
- slug (rich text)
- Status (select: Draft, Published, Archived)
- Tags (multi-select)
- Updated (last edited time)
API Consumption with SDK
Now in the frontend Next.js app, I fetch the content from Notion using it’s official SDK.
// Notion Integration Key
const notion = new Client({
auth: process.env.NOTION_KEY,
});
// Function to fetch pages of a database
export async function fetchDatabasePages(
databaseId: string,
statusFilter: string = "Published"
): Promise<PageObjectResponse[]> {
const response = await notion.databases.query({
database_id: databaseId,
filter: {
property: "Status",
status: {
equals: statusFilter,
},
},
});
return response.results.filter(
(item): item is PageObjectResponse =>
"properties" in item && "parent" in item
);
}
The function above returns the list of pages in a given database. Each page content can then be fetched directly using the page id.
export async function fetchNotionPageContent(
pageId: string
): Promise<BlockObjectResponse[]> {
const response = await notion.blocks.children.list({ block_id: pageId });
return response.results as BlockObjectResponse[];
}
But because I’m using custom slugs for my site, I have an additional step to first fetch the page from the database using the slug property.
...
// Just core part of a bigger function
const response = await notion.databases.query({
database_id: dbId,
filter: {
property: "slug",
rich_text: {
equals: slug,
},
},
});
return response.results[0] as PageObjectResponse;
...
Custom Renderer
Now each blocks of a particular page are available, I created custom components for items and used a custom renderer to render the blocks on a page.
...
// A simplified version of the block renderer
switch (block.type) {
case "heading_1":
return <Heading1 {...block} />;
case "paragraph":
return <Paragraph {...block} />;
case "code":
return <Code {...block} />;
// ... and so on
}
...
Content Blocks
Following are the custom blocks that I’ve implemented at the moment.
Performance and Rate Limit
Notion’s API has rate limits, which forces you to be smart about calling it regularly. That’s where Next.js becomes a powerful choice. Content is fetched at build time, pre-rendered and served as static HTML. As a result, users don’t have to wait for pages to complete background api calls. Combined with ISR (Incremental Static Regeneration) to periodically refresh page content without having to rebuild gives the perfect balance of fresh content and speed.
// Example ISR revalidation time
export const revalidate = 300; // Refresh every 5 minutes
Todo
I plan to advance this project to include more custom components as I need them. Implementing webhooks to automatically fetch fresh content is also in the pipeline for the future.
URLs
Github: https://github.com/bimalpaudels/portfolio
Notion: https://bimals.notion.site