# Host a Blog Website Without Cost with Notion Backend

> Learn how to build and host a completely free blog website using Notion as your content management backend. No database costs, no hosting fees - just your ideas and Notion.

Source: https://edesy.in/blog/host-a-blog-website-without-cost-with-notion-backend

Building a blog doesn't have to cost you anything. In this comprehensive guide, we'll show you how to create a professional blog website using **Notion as your backend** and deploy it for free using modern JAMstack tools.

## Why Use Notion as a Blog Backend?

Notion has become one of the most popular productivity tools, and for good reason:

- **Beautiful writing experience** - Write in a distraction-free environment with rich formatting
- **No database costs** - Notion handles all your content storage for free
- **Real-time collaboration** - Work with team members on blog posts
- **Media management** - Easily add images, videos, and embeds
- **Version history** - Track changes and revert when needed

## What You'll Build

By the end of this tutorial, you'll have:

1. A Notion database to manage your blog posts
2. A Next.js website that fetches content from Notion
3. Free hosting on Vercel or Netlify
4. A custom domain (optional)

## Prerequisites

Before we start, make sure you have:

- A [Notion](https://notion.so) account (free tier works)
- [Node.js](https://nodejs.org) installed (v16 or higher)
- Basic knowledge of JavaScript/React
- A [GitHub](https://github.com) account
- A [Vercel](https://vercel.com) or [Netlify](https://netlify.com) account (free tier)

## Step 1: Set Up Your Notion Database

### Create a Blog Database

1. Open Notion and create a new page
2. Add a **Database - Full page** block
3. Set up the following properties:

| Property | Type | Description |
|----------|------|-------------|
| Title | Title | Blog post title |
| Slug | Text | URL-friendly identifier |
| Published | Checkbox | Whether post is live |
| Date | Date | Publication date |
| Excerpt | Text | Short description |
| Cover | Files | Featured image |
| Tags | Multi-select | Categories/tags |

### Get Your Notion API Key

1. Go to [Notion Integrations](https://www.notion.so/my-integrations)
2. Click **"+ New integration"**
3. Name it (e.g., "My Blog")
4. Select your workspace
5. Copy the **Internal Integration Token**

### Share Database with Integration

1. Open your blog database in Notion
2. Click **Share** in the top right
3. Click **"Invite"** and search for your integration name
4. Give it access

### Get Your Database ID

The database ID is in the URL when you open your database:

```
https://notion.so/myworkspace/a8aec43384f447ed84390e8e42c2e089?v=...
                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                              This is your database ID
```

## Step 2: Create Your Next.js Project

### Initialize the Project

```bash
npx create-next-app@latest my-notion-blog
cd my-notion-blog
```

Choose the following options:
- TypeScript: Yes
- ESLint: Yes
- Tailwind CSS: Yes
- App Router: Yes

### Install Notion Client

```bash
npm install @notionhq/client
```

### Set Up Environment Variables

Create a `.env.local` file:

```env
NOTION_API_KEY=your_integration_token_here
NOTION_DATABASE_ID=your_database_id_here
```

## Step 3: Build the Notion Integration

### Create the Notion Client

Create `lib/notion.ts`:

```typescript
import { Client } from '@notionhq/client';

const notion = new Client({
  auth: process.env.NOTION_API_KEY,
});

export const getDatabase = async () => {
  const response = await notion.databases.query({
    database_id: process.env.NOTION_DATABASE_ID!,
    filter: {
      property: 'Published',
      checkbox: {
        equals: true,
      },
    },
    sorts: [
      {
        property: 'Date',
        direction: 'descending',
      },
    ],
  });
  return response.results;
};

export const getPage = async (pageId: string) => {
  const response = await notion.pages.retrieve({ page_id: pageId });
  return response;
};

export const getBlocks = async (blockId: string) => {
  const blocks = [];
  let cursor;

  while (true) {
    const { results, next_cursor } = await notion.blocks.children.list({
      block_id: blockId,
      start_cursor: cursor,
    });
    blocks.push(...results);
    if (!next_cursor) break;
    cursor = next_cursor;
  }

  return blocks;
};
```

### Create the Blog List Page

Update `app/page.tsx`:

```tsx
import Link from 'next/link';
import { getDatabase } from '@/lib/notion';

export const revalidate = 60; // Revalidate every 60 seconds

export default async function Home() {
  const posts = await getDatabase();

  return (
    <main className="max-w-4xl mx-auto px-4 py-8">
      <h1 className="text-4xl font-bold mb-8">My Blog</h1>

      <div className="grid gap-6">
        {posts.map((post: any) => {
          const title = post.properties.Title?.title[0]?.plain_text;
          const slug = post.properties.Slug?.rich_text[0]?.plain_text;
          const excerpt = post.properties.Excerpt?.rich_text[0]?.plain_text;
          const date = post.properties.Date?.date?.start;

          return (
            <Link
              key={post.id}
              href={`/blog/${slug}`}
              className="block p-6 border rounded-lg hover:shadow-lg transition"
            >
              <h2 className="text-2xl font-semibold mb-2">{title}</h2>
              {excerpt && <p className="text-gray-600 mb-2">{excerpt}</p>}
              {date && <time className="text-sm text-gray-500">{date}</time>}
            </Link>
          );
        })}
      </div>
    </main>
  );
}
```

### Create the Blog Post Page

Create `app/blog/[slug]/page.tsx`:

```tsx
import { getDatabase, getPage, getBlocks } from '@/lib/notion';
import { notFound } from 'next/navigation';

// Render different block types
function renderBlock(block: any) {
  const { type, id } = block;
  const value = block[type];

  switch (type) {
    case 'paragraph':
      return (
        <p key={id} className="mb-4">
          {value.rich_text.map((text: any) => text.plain_text).join('')}
        </p>
      );
    case 'heading_1':
      return (
        <h1 key={id} className="text-3xl font-bold mt-8 mb-4">
          {value.rich_text.map((text: any) => text.plain_text).join('')}
        </h1>
      );
    case 'heading_2':
      return (
        <h2 key={id} className="text-2xl font-bold mt-6 mb-3">
          {value.rich_text.map((text: any) => text.plain_text).join('')}
        </h2>
      );
    case 'heading_3':
      return (
        <h3 key={id} className="text-xl font-bold mt-4 mb-2">
          {value.rich_text.map((text: any) => text.plain_text).join('')}
        </h3>
      );
    case 'bulleted_list_item':
      return (
        <li key={id} className="ml-4">
          {value.rich_text.map((text: any) => text.plain_text).join('')}
        </li>
      );
    case 'numbered_list_item':
      return (
        <li key={id} className="ml-4 list-decimal">
          {value.rich_text.map((text: any) => text.plain_text).join('')}
        </li>
      );
    case 'code':
      return (
        <pre key={id} className="bg-gray-900 text-gray-100 p-4 rounded-lg overflow-x-auto my-4">
          <code>{value.rich_text.map((text: any) => text.plain_text).join('')}</code>
        </pre>
      );
    case 'image':
      const src = value.type === 'external' ? value.external.url : value.file.url;
      return (
        <img key={id} src={src} alt="" className="rounded-lg my-4" />
      );
    default:
      return null;
  }
}

export default async function BlogPost({ params }: { params: { slug: string } }) {
  const posts = await getDatabase();
  const post = posts.find((p: any) =>
    p.properties.Slug?.rich_text[0]?.plain_text === params.slug
  );

  if (!post) {
    notFound();
  }

  const blocks = await getBlocks(post.id);
  const title = post.properties.Title?.title[0]?.plain_text;

  return (
    <article className="max-w-3xl mx-auto px-4 py-8">
      <h1 className="text-4xl font-bold mb-8">{title}</h1>
      <div className="prose prose-lg max-w-none">
        {blocks.map((block: any) => renderBlock(block))}
      </div>
    </article>
  );
}
```

## Step 4: Deploy for Free

### Push to GitHub

```bash
git init
git add .
git commit -m "Initial commit"
git remote add origin https://github.com/yourusername/my-notion-blog.git
git push -u origin main
```

### Deploy to Vercel

1. Go to [vercel.com](https://vercel.com)
2. Click **"New Project"**
3. Import your GitHub repository
4. Add your environment variables:
   - `NOTION_API_KEY`
   - `NOTION_DATABASE_ID`
5. Click **Deploy**

Your blog is now live at `your-project.vercel.app`!

### Custom Domain (Optional)

1. In Vercel, go to your project settings
2. Click **Domains**
3. Add your custom domain
4. Update your DNS settings as instructed

## Advanced Features

### Adding Comments

Integrate a free commenting system like:
- [Giscus](https://giscus.app/) - Uses GitHub Discussions
- [Utterances](https://utteranc.es/) - Uses GitHub Issues

### SEO Optimization

Add metadata to your pages:

```tsx
export async function generateMetadata({ params }: { params: { slug: string } }) {
  const post = await getPostBySlug(params.slug);

  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      type: 'article',
    },
  };
}
```

### RSS Feed

Create an RSS feed endpoint for subscribers:

```typescript
// app/rss.xml/route.ts
import { getDatabase } from '@/lib/notion';

export async function GET() {
  const posts = await getDatabase();

  const feed = `<?xml version="1.0" encoding="UTF-8"?>
    <rss version="2.0">
      <channel>
        <title>My Blog</title>
        <link>https://yourdomain.com</link>
        <description>My awesome blog</description>
        ${posts.map((post: any) => `
          <item>
            <title>${post.properties.Title?.title[0]?.plain_text}</title>
            <link>https://yourdomain.com/blog/${post.properties.Slug?.rich_text[0]?.plain_text}</link>
            <pubDate>${post.properties.Date?.date?.start}</pubDate>
          </item>
        `).join('')}
      </channel>
    </rss>`;

  return new Response(feed, {
    headers: {
      'Content-Type': 'application/xml',
    },
  });
}
```

## Cost Breakdown

| Service | Cost |
|---------|------|
| Notion | Free |
| Vercel Hosting | Free |
| GitHub | Free |
| Custom Domain | ~$10-15/year (optional) |

**Total: $0/year** (or ~$12/year with custom domain)

## Conclusion

You've just built a fully functional blog with:

- Zero hosting costs
- A beautiful writing experience in Notion
- Automatic deployments when you push to GitHub
- Server-side rendering for SEO
- Incremental static regeneration for performance

This setup scales incredibly well. Notion handles your content, Vercel handles your traffic, and you focus on writing great content.

## Next Steps

1. **Style your blog** - Add custom CSS or use a component library
2. **Add analytics** - Integrate Plausible or PostHog (both have free tiers)
3. **Set up a newsletter** - Use Buttondown or Substack embeds
4. **Write your first post** - The best time to start is now!

---

Have questions? Feel free to reach out to us at [suvojit@edesy.in](mailto:suvojit@edesy.in) or explore our [blog platform](https://edesy.in/blog-platform) for a complete hosted solution.
