A minimal blogging platform built with Next.js and powered by Notion API, featuring support for math equations, code highlighting, and responsive design.
- 📝 Notion as a headless CMS
- 🧮 KaTeX math equations support
- 🌈 Syntax highlighting for code blocks
- 📱 Responsive design with Tailwind CSS
- 🖼️ Image optimization
- 🏷️ Tag-based article categorization
- Node.js
- Notion account
- Notion integration with API key
- Clone the repository:
git clone https://github.com/hash1go/notion-cms-articles.git
cd notion-cms-articles
- Install dependencies:
npm install
- Copy the environment variables file and configure it:
cp .env.example .env.local
- Update the
.env.local
file with your Notion API credentials:
NOTION_KEY=your_notion_api_key
NOTION_DATABASE_ID=your_notion_database_id
NEXT_PUBLIC_SITE_URL=https://example.com
- Configure the required configuration files:
This project uses several configuration files that must be properly set up:
site.config.ts
: Main site information, social links, and titlesauthor.config.ts
: Author information for article attributionmeta.config.ts
: Controls what metadata displays on article pages
Without these configurations properly set, the blog won't function correctly. See the Configuration section below for detailed setup instructions.
- Start the development server:
npm run dev
Visit http://localhost:3000
to see your blog.
This diagram illustrates the flow of data from Notion to your browser with the App Router architecture:
┌─────────────┐ ┌─────────────────┐ ┌───────────────┐ ┌──────────────┐
│ │ │ │ │ │ │ │
│ Notion │────▶│ Next.js Server │────▶│ React Server │────▶│ Browser │
│ Database │ │ Components │ │ Components │ │ Display │
│ │ │ │ │ │ │ │
└─────────────┘ └─────────────────┘ └───────────────┘ └──────────────┘
│ │ │ ▲
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────┐ ┌────────────────────┐ │
│ │ │ │ │ │
└────────────▶│ Notion API │ │ CustomNotionRenderer│─────────┘
│ Integration │ │ │
│ │ │ │
└─────────────────┘ └────────────────────┘
-
Content Creation: You create and maintain your content in Notion databases
-
Data Fetching: Using Server Components and Cache:
- Pages are rendered on the server with React Server Components
- Server components fetch data from Notion API using your integration credentials
- Data fetching is optimized with Next.js cache strategies and revalidation
- Content is automatically refreshed based on your revalidation interval (default: 60 seconds)
-
Content Processing:
- Page metadata (title, date, tags, etc.) is extracted by server components
- Block content is processed by CustomNotionRenderer (client component)
- Mathematical equations, code snippets, and images are specially handled with optimized rendering
-
Rendering:
- Server Components render the initial HTML on the server
- Client Components handle interactive elements like navigation and dynamic content
- React Suspense manages loading states for dynamic imports
-
Optimization:
- Images are optimized through Next.js Image component with automatic sizing
- Code is highlighted with syntax highlighting (on client)
- Math equations are rendered with KaTeX (on client)
-
Delivery:
- Initial HTML is sent to the browser (fast initial load)
- Client-side hydration adds interactivity where needed
- The user sees your Notion content as a beautifully styled blog
This architecture allows you to use Notion as a CMS while presenting your content with custom design and enhanced features, leveraging the performance benefits of Next.js App Router and React Server Components.
Create a Notion database with the following properties:
name
(title): Article titletags
(multi-select): Article categoriesslug
(text): URL slug for the articleauthor
(text): Article authorurl
(text): Optional external linkdescription
(text): Article summaryisPublic
(checkbox): Whether the article is publisheddate
(date): Publication date
Add a cover image to your Notion pages to display as featured images on your blog.
This project uses several configuration files to customize different aspects of the blog:
Main site configuration including metadata, social links, and UI text:
export const siteConfig: SiteConfig = {
// Language code (e.g., "ja" for Japanese, "en" for English)
lang: "en",
// The title of your website
title: "Your Blog Title",
// A short description of your site
description: "Your blog description",
// Social media URLs
twitterUrl: "your Twitter URL", // e.g., https://twitter.com/yourusername
farcasterUrl: "your Farcaster URL", // e.g., https://warpcast.com/yourusername
instagramUrl: "your Instagram URL", // e.g., https://instagram.com/yourusername
githubUrl: "your GitHub URL", // e.g., https://github.com/yourusername
discordId: "your Discord ID", // e.g., yourdiscordusername (without #)
// Twitter username without @ for metadata
twitterUsername: "your Twitter username",
// Default image for social sharing (path from public directory)
defaultImage: "/site-cover.png",
// Administrator's name for display purposes
adminName: "Your Name",
// Copyright information
copyright: "2025 Your Name",
copyrightUrl: "your copyright URL",
// Error page customization
notFoundMessage: "Page not found",
notFoundTitle: "Page Not Found | 404 Error",
notFoundDescription: "The page you are looking for could not be found.",
returnToHome: "Return to Home"
};
Author information for article attribution:
export const authors: AuthorInfo[] = [
{
// Your name for the author
id: "your-author-name",
// URL to the author's Twitter profile
twitterUrl: "your Twitter URL",
// URL to the author's profile image
icon: "your author image URL",
},
// Add more authors as needed
];
Configure the display settings for metadata on article pages:
export const metaConfig = {
author: {
display: true, // Set to false if you do not want to display the author
label: "Author", // Custom label for author metadata
},
date: {
display: true, // Set to false if you do not want to display publication date
label: "Published", // Custom label for date metadata
},
url: {
display: true, // Set to false if you do not want to display URL
label: "External Link", // Custom label for URL metadata
},
tags: {
display: true, // Set to false if you do not want to display tags
label: "Tags", // Custom label for tags metadata
},
};
Customize the styling of your blog with Tailwind CSS:
import type { Config } from "tailwindcss";
const config: Config = {
content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
backgroundImage: {
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
"gradient-conic":
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
},
fontFamily: {
sans: ["your-preferred-font", "system-ui", "sans-serif"],
},
colors: {
// Add custom colors if needed
},
},
},
plugins: [],
};
export default config;
This blog uses Google Fonts (Noto Sans JP by default). To customize fonts:
- First, import your preferred font in
app/layout.tsx
:
// In app/layout.tsx
import { YourPreferredFont } from "next/font/google";
const yourFont = YourPreferredFont({
subsets: ["latin"],
weight: ["400", "700"],
// other font options
});
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
{/* ... */}
);
}
- Then, update the
tailwind.config.ts
to use your font:
// ... tailwind font configuration
Configure Next.js settings, especially for image domains:
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
images: {
remotePatterns: [
{
protocol: "https",
hostname: "www.notion.so",
},
{
protocol: "https",
hostname: "images.unsplash.com", // Notion's default image source
},
{
protocol: "https",
hostname: "prod-files-secure.s3.us-west-2.amazonaws.com",
// IMPORTANT:
// This hostname is used to fetch images stored by Notion.
// If your images are saved in a different domain,
// please check the image URL in your Notion pages and update this hostname accordingly.
// An incorrect domain here will cause image fetching to fail.
},
// Add any other domains you need for images
],
formats: ["image/avif", "image/webp"],
minimumCacheTTL: 60 * 60 * 24, // 24 hours
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
},
// Redirect Configuration
// This section defines URL redirects for the application.
// - /articles -> / (home)
// - /tags -> / (home)
// No additional redirects should be necessary for basic functionality.
async redirects() {
return [
{
source: "/articles",
destination: "/",
permanent: true,
},
{
source: "/tags",
destination: "/",
permanent: true,
},
];
},
};
module.exports = nextConfig;
Each configuration file serves a specific purpose to make your blog customizable without touching the core code.
Your Notion images require configuration in two places to ensure optimal loading:
-
First, in
next.config.js
as shown above to allow Next.js Image Optimization. -
Then, in
app/layout.tsx
to enable DNS prefetching for faster image loading:
// In app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang={siteConfig.lang}>
<head>
{/* Preconnect to Notion domains for faster image loading */}
<link rel="preconnect" href="https://www.notion.so" />
<link
rel="preconnect"
href="https://prod-files-secure.s3.us-west-2.amazonaws.com"
/>
{/* Optional: Add this if you frequently use Unsplash images in your Notion pages */}
<link rel="preconnect" href="https://images.unsplash.com" />
</head>
<body className={someFont.className}>
{/* Your site layout components */}
{children}
</body>
</html>
);
}
Important: If you modify the domains in next.config.js
, ensure you update the corresponding preconnect links in layout.tsx
to match. This dual configuration ensures both proper image rendering and optimal loading performance.
Make sure your .env.local
file contains the necessary API credentials:
NOTION_KEY=your_notion_api_key
NOTION_DATABASE_ID=your_notion_database_id
NEXT_PUBLIC_SITE_URL=https://example.com
Each configuration file serves a specific purpose to make your blog customizable without touching the core code.
This blog can be deployed on various platforms. Here are some common options:
While Vercel offers a straightforward deployment process:
vercel
Note: Be aware that Vercel's free tier has limitations on Next.js Image Optimization API usage. If your blog contains many images, you might reach these limits quickly.
This Next.js application also works well with AWS Amplify. To deploy:
- Set up your Amplify project in the AWS console
- Connect your repository
- Configure your
amplify.yml
file properly, especially for environment variables handling
Important: Research the correct amplify.yml
configuration for Next.js applications, as it requires specific build settings.
For any deployment platform, remember to:
- Set up environment variables in the platform's console/dashboard
- Never commit your
.env
files to your repository - Make sure your environment variables are properly configured in your deployment environment
Each platform has its own method for setting environment variables, so check their documentation for specific instructions.
This project is licensed under the MIT License - see the LICENSE file for details.# notion-cms-articles