A Nuxt 3 & 4 module for rendering Notion pages with full support for Notion's block types.
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
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...
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 ✨
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>
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;
});
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,
};
});
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;
}
- 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
- Bulleted List - Unordered lists with bullets
- Numbered List - Ordered lists with numbers
- To-Do List - Interactive checkbox lists
- Image - Images with captions and proper sizing
- Video - Embedded videos (YouTube, Vimeo, etc.)
- Bookmark - Rich link previews
- Code Block - Syntax-highlighted code blocks with language support
- Divider - Horizontal rules to separate content
- Child Page - Links to child pages
All text blocks support Notion's rich text formatting:
- Bold, Italic,
Strikethrough, Underline Inline code
- Links
- Text colors and background colors
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
];
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
},
});
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
The module automatically installs and configures:
- Tailwind CSS - For styling (via
@nuxtjs/tailwindcss
) - Nuxt Icon - For rendering icons in callouts and other components
When fetching Notion blocks:
- Use caching - Cache API responses to reduce Notion API calls
- Limit page size - Use pagination for pages with many blocks
- 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,
},
);
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>
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;
}
<!-- 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>
<!-- 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>
If styles aren't applying correctly:
- Ensure Tailwind CSS is properly configured
- Check that your
tailwind.config.ts
includes the module's components - Verify that the CSS file is being loaded
If components are not auto-imported:
- Make sure the module is listed in your
nuxt.config.ts
- Run
npm run dev:prepare
to regenerate type stubs - Restart your Nuxt dev server
Notion API has rate limits. To avoid hitting them:
- Implement caching with
defineCachedFunction
- Use ISR (Incremental Static Regeneration) for static content
- Consider using a webhook to invalidate cache when Notion content changes
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
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");
});
});
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
This module is compatible with:
- ✅ Nuxt 3.x
- ✅ Nuxt 4.x
- ✅ Vue 3
- ✅ TypeScript
The module works in all modern browsers that support:
- ES6+ JavaScript
- CSS Grid and Flexbox
- Tailwind CSS
- Notion API Documentation
- @notionhq/client - Official Notion JavaScript SDK
- Nuxt Documentation
- Tailwind CSS
MIT License © 2025 Gideon Wanjohi
- Built with Nuxt Module Builder
- Styled with Tailwind CSS
- Inspired by the Notion API and its amazing developer community
Made with ❤️ for the Nuxt community