As developers, we often face the challenge of creating a blog system that's both powerful and easy to manage. At Hyperly.ai, we found our solution by integrating Notion with Astro. This combination offers the best of both worlds: Astro's performance and flexibility, paired with Notion's user-friendly content management capabilities.
Why Notion + Astro?
- Content Management: Notion provides an intuitive interface for creating and organizing content.
- Performance: Astro generates static sites, ensuring fast load times and excellent SEO.
- Flexibility: Both tools are highly customizable, allowing for a tailored solution.
- Developer Experience: Astro's component-based architecture simplifies development.
- Cost-Effective: Both Notion and Astro offer robust free tiers.
Besides this, I have already developed a few apps with Notion and it just felt easy and convenient.
Setting Up the Development Environment
First, we set up our Astro project and installed necessary dependencies:
npm create astro@latest hyperly-blog
cd hyperly-blog
npm install @notionhq/client dotenv
We then created a Notion integration and set up our environment variables:
NOTION_API_KEY=your_notion_api_key_here
NOTION_DATABASE_ID=your_notion_database_id_here
Fetching Data from Notion
We created a simple Notion Database with the following data structure:
We created a utility function to fetch blog posts from Notion:
// utils/getNotionBlogPosts.ts
import { Client } from "@notionhq/client";
const notion = new Client({ auth: process.env.NOTION_API_KEY });
const databaseId = process.env.NOTION_DATABASE_ID;
let cachedPosts: CollectionEntry<"blogs">[] | null = null;
export async function getAllBlogPosts(): Promise<CollectionEntry<"blogs">[]> {
if (cachedPosts) return cachedPosts;
try {
const response = await notion.databases.query({
database_id: databaseId,
filter: {
property: "draft",
checkbox: {
equals: false,
},
},
sorts: [
{
property: "pubDatetime",
direction: "descending",
},
],
});
const posts = response.results.map((page: any) => ({
id: page.id,
slug: page.properties.slug.rich_text[0].plain_text,
title: page.properties.title.title[0].plain_text,
description: page.properties.description.rich_text[0].plain_text,
pubDatetime: new Date(page.properties.pubDatetime.date.start),
modDatetime: new Date(page.last_edited_time),
author: page.properties.author.rich_text[0].plain_text,
featured: page.properties.featured.checkbox,
draft: page.properties.draft.checkbox,
tags: page.properties.tags.multi_select.map((tag: any) => tag.name),
}));
cachedPosts = posts;
return posts;
} catch (error) {
console.error("Error fetching blog posts from Notion:", error);
throw error;
}
}
This function queries our Notion database, filters out draft posts, and maps the Notion page properties to our blog post structure.
Rendering Notion Content in Astro
To render Notion's rich text content in Astro, we used the notion-to-md
library to convert Notion blocks to Markdown, and then the marked
library to parse the Markdown into HTML:
import { NotionToMarkdown } from "notion-to-md";
import { marked } from "marked";
const n2m = new NotionToMarkdown({ notionClient: notion });
export async function getPostContent(id: string) {
const mdblocks = await n2m.pageToMarkdown(id);
const mdString = n2m.toMarkdownString(mdblocks);
const parsedContent = await marked.parse(mdString.parent);
return parsedContent;
}
Creating Dynamic Blog Post Pages
We created dynamic routes for our blog posts using Astro's file-based routing system:
---
// src/pages/blog/[slug].astro
import { getAllBlogPosts, getPostContent } from "../../../utils/getNotionBlogPosts";
import BlogPost from "../../layouts/BlogPost.astro";
export async function getStaticPaths() {
const posts = await getAllBlogPosts();
return posts.map(post => ({
params: { slug: post.slug },
props: { post },
}));
}
const { post } = Astro.props;
const content = await getPostContent(post.id);
---
<BlogPost {...post}>
<article set:html={content} />
</BlogPost>
Optimizing for Performance
We implemented a simple caching mechanism to reduce API calls to Notion:
let cachedPosts: CollectionEntry<"blogs">[] | null = null;
let lastFetchTime = 0;
const CACHE_DURATION = 60 * 60 * 1000; // 1 hour
export async function getAllBlogPosts() {
const now = Date.now();
if (cachedPosts && now - lastFetchTime < CACHE_DURATION) {
return cachedPosts;
}
// Fetch posts from Notion
// ...
cachedPosts = posts;
lastFetchTime = now;
return posts;
}
SEO Considerations
We created a SEO.astro
component to generate appropriate meta tags for each page:
---
// components/SEO.astro
const { title, description, image, article } = Astro.props;
---
<title>{title}</title>
<meta name="description" content={description}>
<!-- Open Graph / Facebook -->
<meta property="og:type" content={article ? 'article' : 'website'}>
<meta property="og:title" content={title}>
<meta property="og:description" content={description}>
<meta property="og:image" content={image}>
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image">
<meta property="twitter:title" content={title}>
<meta property="twitter:description" content={description}>
<meta property="twitter:image" content={image}>
Deployment
We deployed our blog on Vercel, which provides a streamlined deployment process integrated with our GitHub repository. Our setup automatically deploys new versions of the blog whenever changes are pushed to the main branch of our GitHub repository.
Challenges and Lessons Learned
- Outdated Resources: Many existing tutorials on Notion integration were outdated, requiring significant trial and error in our implementation.
- Rich Text Handling: Converting Notion's rich text content to Markdown and then to HTML required careful handling to preserve formatting.
- API Rate Limits: We had to implement caching to stay within Notion's API rate limits.
Future Improvements
- Automatic Internal Linking: Implement a system to automatically suggest and create internal links between related blog posts.
- Related Blog Posts: Develop an algorithm to identify and display related posts at the end of each article.
- Real-time Updates: Set up a webhook to trigger rebuilds when content is updated in Notion.
Conclusion
Integrating Notion with Astro has allowed us to create a powerful, flexible, and user-friendly blog system. The seamless transition from note-taking to blog creation in Notion has streamlined our content production process.
While the integration process involved some challenges, particularly due to the lack of up-to-date resources, the result has been worth the effort. We've created a blog system that's easy to maintain, fast to load, and a joy to use for both content creators and developers.
For those considering a similar integration, our advice is to be prepared for some trial and error. The process may involve challenges, but the result – a seamless integration between your note-taking and blogging processes – is incredibly rewarding. Stay curious, be willing to experiment, and don't hesitate to reach out to developer communities for help along the way.
By sharing our experience, we hope to inspire and assist others in creating efficient, user-friendly content management systems that leverage the strengths of tools like Notion and Astro.
P.S: If you want to read a detailed version of this blog, head over to our in depth blog post