Skip to content

waWanjohi/nuxt-notion-renderer

Repository files navigation

Notion Renderer

npm version npm downloads License Nuxt

A Nuxt 3 & 4 module for rendering Notion pages with full support for Notion's block types.

Features

Transform Notion API responses into beautifully rendered content in your Nuxt application:

  • �  Rich Text Support - Full support for Notion's rich text formatting (bold, italic, code, links, etc.)
  • 🎨  Tailwind CSS Styling - Pre-styled components with Tailwind CSS for modern, responsive design
  • 🧩  Comprehensive Block Support - Render all major Notion block types
  • 🔄  Auto-Import Components - Components are automatically available throughout your app
  • ⚡  TypeScript Support - Fully typed for the best developer experience
  • 🎭  Smooth Animations - Built-in transitions for a polished user experience

Supported Block Types

The module supports the following Notion block types:

  • Text Blocks: Paragraph, Headings (H1, H2, H3), Quote, Callout
  • Lists: Bulleted Lists, Numbered Lists, To-Do Lists
  • Media: Images, Videos, Bookmarks
  • Code: Code blocks with syntax highlighting
  • Layout: Dividers, Child Pages
  • And more...

Quick Setup

Install the module to your Nuxt application:

npm install nuxt-notion-renderer

Add the module to your nuxt.config.ts:

export default defineNuxtConfig({
  modules: ["nuxt-notion-renderer"],
});

That's it! You can now use the NotionRenderer component in your Nuxt app ✨

Usage

Basic Usage

The NotionRenderer component takes a single prop blocks which is an array of Notion block objects:

<template>
  <div>
    <NotionRenderer :blocks="notionBlocks" />
  </div>
</template>

<script setup lang="ts">
const { data: notionBlocks } = await useFetch("/api/notion/page");
</script>

Fetching Notion Blocks

You'll need to fetch blocks from the Notion API. Here's an example server API route:

// server/api/notion/page.get.ts
import { Client } from "@notionhq/client";

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

export default defineEventHandler(async (event) => {
  const pageId = "your-notion-page-id";

  const response = await notion.blocks.children.list({
    block_id: pageId,
    page_size: 100,
  });

  return response.results;
});

Working with Dynamic Pages

For blog posts or dynamic content, you can fetch blocks based on a slug or ID:

<!-- pages/blog/[slug].vue -->
<template>
  <article>
    <h1>{{ page.title }}</h1>
    <NotionRenderer :blocks="page.blocks" />
  </article>
</template>

<script setup lang="ts">
const route = useRoute();
const { data: page } = await useFetch(`/api/posts/${route.params.slug}`);
</script>
// server/api/posts/[slug].get.ts
import { Client } from "@notionhq/client";

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

export default defineEventHandler(async (event) => {
  const slug = getRouterParam(event, "slug");

  // Fetch page metadata from your database
  const page = await getPageBySlug(slug);

  // Fetch blocks
  const blocks = await notion.blocks.children.list({
    block_id: page.id,
    page_size: 100,
  });

  return {
    title: page.title,
    blocks: blocks.results,
  };
});

Styling

The module comes with pre-configured Tailwind CSS styles. The styles are automatically injected, so you don't need to import anything manually.

If you want to customize the styles, you can override the default classes in your global CSS:

/* assets/css/main.css */
.notion-renderer {
  @apply prose prose-lg max-w-none;
}

.notion-renderer h1 {
  @apply text-4xl font-bold mb-4;
}

.notion-renderer p {
  @apply mb-4 leading-relaxed;
}

Supported Block Types Reference

Text Content

  • Paragraph - Standard text paragraphs with rich text support
  • Heading 1, 2, 3 - Hierarchical headings
  • Quote - Blockquote styling for quoted text
  • Callout - Highlighted text boxes with icons

Lists

  • Bulleted List - Unordered lists with bullets
  • Numbered List - Ordered lists with numbers
  • To-Do List - Interactive checkbox lists

Media

  • Image - Images with captions and proper sizing
  • Video - Embedded videos (YouTube, Vimeo, etc.)
  • Bookmark - Rich link previews

Code

  • Code Block - Syntax-highlighted code blocks with language support

Layout

  • Divider - Horizontal rules to separate content
  • Child Page - Links to child pages

Rich Text Support

All text blocks support Notion's rich text formatting:

  • Bold, Italic, Strikethrough, Underline
  • Inline code
  • Links
  • Text colors and background colors

TypeScript Support

The module is fully typed. Import types when needed:

import type {
  NotionBlock,
  BlockType,
  RichText,
} from "nuxt-notion-renderer/types";

const blocks: NotionBlock[] = [
  // Your Notion blocks
];

Configuration

The module works out of the box with zero configuration. However, you can customize it if needed:

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ["nuxt-notion-renderer"],
  notionRenderer: {
    // Future configuration options will be available here
  },
});

Module Architecture

Components

All components are automatically registered and available globally:

  • <NotionRenderer> - Main component that orchestrates rendering
  • <NotionParagraphBlock> - Renders paragraph blocks
  • <NotionHeadingBlock> - Renders heading blocks (H1, H2, H3)
  • <NotionBulletedListItemBlock> - Renders bulleted list items
  • <NotionNumberedListItemBlock> - Renders numbered list items
  • <NotionToDoBlock> - Renders to-do/checkbox items
  • <NotionQuoteBlock> - Renders quote blocks
  • <NotionCalloutBlock> - Renders callout blocks
  • <NotionCodeBlock> - Renders code blocks
  • <NotionImageBlock> - Renders image blocks
  • <NotionVideoBlock> - Renders video blocks
  • <NotionBookmarkBlock> - Renders bookmark/link preview blocks
  • <NotionDividerBlock> - Renders horizontal dividers
  • <NotionChildPageBlock> - Renders child page links
  • <NotionRichTextRenderer> - Handles rich text formatting

Dependencies

The module automatically installs and configures:

  • Tailwind CSS - For styling (via @nuxtjs/tailwindcss)
  • Nuxt Icon - For rendering icons in callouts and other components

Best Practices

Performance

When fetching Notion blocks:

  1. Use caching - Cache API responses to reduce Notion API calls
  2. Limit page size - Use pagination for pages with many blocks
  3. Server-side rendering - Fetch blocks on the server for better performance
// Example with caching
const cachedFetch = defineCachedFunction(
  async (pageId: string) => {
    const blocks = await notion.blocks.children.list({
      block_id: pageId,
    });
    return blocks.results;
  },
  {
    maxAge: 60 * 60, // Cache for 1 hour
    swr: true,
  },
);

Error Handling

Always handle potential errors when fetching Notion data:

<script setup lang="ts">
const { data: blocks, error } = await useFetch("/api/notion/page");

if (error.value) {
  console.error("Failed to fetch Notion blocks:", error.value);
}
</script>

<template>
  <div>
    <div v-if="error" class="error">Failed to load content</div>
    <NotionRenderer v-else-if="blocks" :blocks="blocks" />
  </div>
</template>

Nested Blocks

Notion supports nested blocks (like toggle lists or nested pages). If you need to handle nested content:

async function fetchBlocksRecursively(blockId: string) {
  const response = await notion.blocks.children.list({
    block_id: blockId,
  });

  const blocks = response.results;

  // Fetch children for blocks that have them
  for (const block of blocks) {
    if (block.has_children) {
      block.children = await fetchBlocksRecursively(block.id);
    }
  }

  return blocks;
}

Examples

Blog Application

<!-- pages/blog/[slug].vue -->
<template>
  <div class="container mx-auto px-4 py-8">
    <article class="prose prose-lg mx-auto">
      <header class="mb-8">
        <h1>{{ post.title }}</h1>
        <div class="text-gray-600">
          <time>{{ formatDate(post.date) }}</time>
          <span class="mx-2">·</span>
          <span>{{ post.author }}</span>
        </div>
      </header>

      <NotionRenderer :blocks="post.blocks" />
    </article>
  </div>
</template>

<script setup lang="ts">
const route = useRoute();
const { data: post } = await useFetch(`/api/posts/${route.params.slug}`);

const formatDate = (date: string) => {
  return new Date(date).toLocaleDateString("en-US", {
    year: "numeric",
    month: "long",
    day: "numeric",
  });
};
</script>

Documentation Site

<!-- pages/docs/[...slug].vue -->
<template>
  <div class="flex">
    <aside class="w-64 h-screen sticky top-0">
      <DocsSidebar :pages="navigation" />
    </aside>

    <main class="flex-1 p-8">
      <NotionRenderer :blocks="page.blocks" />
    </main>
  </div>
</template>

<script setup lang="ts">
const route = useRoute();
const { data: page } = await useFetch(
  `/api/docs/${route.params.slug.join("/")}`,
);
const { data: navigation } = await useFetch("/api/docs/navigation");
</script>

Troubleshooting

Styles Not Applying

If styles aren't applying correctly:

  1. Ensure Tailwind CSS is properly configured
  2. Check that your tailwind.config.ts includes the module's components
  3. Verify that the CSS file is being loaded

Components Not Found

If components are not auto-imported:

  1. Make sure the module is listed in your nuxt.config.ts
  2. Run npm run dev:prepare to regenerate type stubs
  3. Restart your Nuxt dev server

API Rate Limits

Notion API has rate limits. To avoid hitting them:

  1. Implement caching with defineCachedFunction
  2. Use ISR (Incremental Static Regeneration) for static content
  3. Consider using a webhook to invalidate cache when Notion content changes

Contributing

Contributions are welcome! Please follow these steps:

Development Setup
# Clone the repository
git clone https://github.com/waWanjohi/nuxt-notion-renderer.git
cd nuxt-notion-renderer

# Install dependencies

npm install

# Generate type stubs

npm run dev:prepare

# Start the playground in development mode

npm run dev

# Build the playground for production

npm run dev:build

# Run linter

npm run lint

# Run tests

npm run test
npm run test:watch

# Build the module

npm run prepack

# Release a new version

npm run release

Testing

The module uses Vitest for testing. Write tests for new features:

// test/basic.test.ts
import { describe, it, expect } from "vitest";
import { fileURLToPath } from "node:url";
import { setup, $fetch } from "@nuxt/test-utils/e2e";

describe("nuxt-notion-renderer", async () => {
await setup({
  rootDir: fileURLToPath(new URL("./fixtures/basic", import.meta.url)),
});

it("renders notion blocks", async () => {
  const html = await $fetch("/");
  expect(html).toContain("nuxt-notion-renderer");
});
});

Module Structure

nuxt-notion-renderer/
├── src/
│   ├── module.ts              # Module definition
│   └── runtime/
│       ├── components/        # Vue components
│       ├── types/            # TypeScript types
│       ├── assets/           # CSS styles
│       └── plugin.ts         # Nuxt plugin
├── playground/               # Development playground
├── test/                    # Test files
└── README.md               # This file

Compatibility

This module is compatible with:

  • ✅ Nuxt 3.x
  • ✅ Nuxt 4.x
  • ✅ Vue 3
  • ✅ TypeScript

Browser Support

The module works in all modern browsers that support:

  • ES6+ JavaScript
  • CSS Grid and Flexbox
  • Tailwind CSS

Related

License

MIT License © 2025 Gideon Wanjohi

Acknowledgments


Made with ❤️ for the Nuxt community

About

A Nuxt 3 & 4 module for rendering Notion pages with full support for Notion's block types.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages