Bước 1: Tạo tài khoản Notion
Đầu tiên các bạn truy cập vào https://www.notion.com
sau đó chọn login như hình

tiếp theo chọn đăng nhập bằng google như hình

Giao diện chính của notion

Để tạo database lư trữ cho các blog, chúng ta click vào Getting Started nhấn vào nút +

Tiếp theo là đặt tên cho database và chọn kiểu hiển thị

Sau khi hoàn thành bước trên chúng ta sẽ được giao diện như sau

Bước 2: Cấu hình cho page và tạo blog đầu tiên
- Thêm các nội dung cơ bản cho page

- Ngoài ra các bạn còn có thể thêm các thuộc tính tùy chỉnh khác bằng cách ấn vào + Add a property

- Chúng ta thêm một thuộc tính tùy chỉnh với kiểu là Formula, Formula là một công cụ mạnh mẽ cho phép bạn thực hiện các phép tính, xử lý dữ liệu, và tạo logic động trong cơ sở dữ liệu (database)


- Công thức mình để ở đây
lower(prop("Name")) .replaceAll("á", "a").replaceAll("à", "a").replaceAll("ã", "a") .replaceAll("ả", "a").replaceAll("ạ", "a").replaceAll("ă", "a") .replaceAll("ắ", "a").replaceAll("ằ", "a").replaceAll("ẵ", "a") .replaceAll("ẳ", "a").replaceAll("ặ", "a").replaceAll("â", "a") .replaceAll("ấ", "a").replaceAll("ầ", "a").replaceAll("ẩ", "a") .replaceAll("ẫ", "a").replaceAll("ậ", "a") .replaceAll("é", "e").replaceAll("è", "e").replaceAll("ẻ", "e") .replaceAll("ẽ", "e").replaceAll("ẹ", "e").replaceAll("ê", "e") .replaceAll("ế", "e").replaceAll("ề", "e").replaceAll("ể", "e") .replaceAll("ễ", "e").replaceAll("ệ", "e") .replaceAll("í", "i").replaceAll("ì", "i").replaceAll("ỉ", "i") .replaceAll("ĩ", "i").replaceAll("ị", "i") .replaceAll("ó", "o").replaceAll("ò", "o").replaceAll("ỏ", "o") .replaceAll("õ", "o").replaceAll("ọ", "o").replaceAll("ô", "o") .replaceAll("ố", "o").replaceAll("ồ", "o").replaceAll("ổ", "o") .replaceAll("ỗ", "o").replaceAll("ộ", "o").replaceAll("ơ", "o") .replaceAll("ớ", "o").replaceAll("ờ", "o").replaceAll("ở", "o") .replaceAll("ỡ", "o").replaceAll("ợ", "o") .replaceAll("ú", "u").replaceAll("ù", "u").replaceAll("ủ", "u") .replaceAll("ũ", "u").replaceAll("ụ", "u").replaceAll("ư", "u") .replaceAll("ứ", "u").replaceAll("ừ", "u").replaceAll("ử", "u") .replaceAll("ữ", "u").replaceAll("ự", "u") .replaceAll("ý", "y").replaceAll("ỳ", "y").replaceAll("ỷ", "y") .replaceAll("ỹ", "y").replaceAll("ỵ", "y") .replaceAll("đ", "d") .replaceAll(" ", "-") + "-"+id()
- Đổi tên cho 1 thuộc tính bất kì


- Thên ảnh cho ver cho page

- Sau khi hoàn thành các bước trên thì page của chúng ta sẽ trông như thế này


Bước 3: Tạo khóa truy cập vào Notion
Tiếp theo, các bạn truy cập notion-integration để tạo secret token liên kết với Notion cá nhân mình dùng để tạo database phía trên nhé. Lưu ý chỉ cần chọn quyền đọc: Read content, như hình bên dưới:

Sau khi tạo notion-integration, bước tiếp theo cần cho phép integration này có thể truy cập database. Ở mục Connect to, chọn integration vừa tạo (mình vừa tạo for-show-sample), như hình dưới:

Bước 4: tạo dự án với Nextjs
Các bạn có thể tham khảo tài liệu của Nextjs tại đây
Tạo dự án với pnpm
pnpm create next-app project-name
Làm theo hướng dẫn
What is your project named? my-app Would you like to use TypeScript? No / Yes Would you like to use ESLint? No / Yes Would you like to use Tailwind CSS? No / Yes Would you like your code inside a `src/` directory? No / Yes Would you like to use App Router? (recommended) No / Yes Would you like to use Turbopack for `next dev`? No / Yes Would you like to customize the import alias (`@/*` by default)? No / Yes What import alias would you like configured? @/*
Cài thêm các thư viện cần thiết
npm i react-notion-x @notionhq/client
Tạo 1 file .env để lưu trữ secret ket và page id
NOTION_DATABASE_ID= NOTION_SECRET=
Trong đó:
- Notion token là token đã được tạo ra từ Bước II

- Để lấy database id từ database chúng ta đã tạo, chỉ cần mở chính trang database bằng browser và copy chuỗi như hình:

Bước 6: tạo service trong Nextjs kết nối với Notion
Tạo 1 class quản lý việc kết nối với notion
import { Client } from '@notionhq/client'; import { NotionAPI } from 'notion-client'; export type Tag = { id: string; color: string; name: string; }; export type BlogPost = { id: string; slug: string; cover: string; title: string; tags: Tag[]; description: string; created: string; }; class NotionServiceBase { client: Client; notionAPI: NotionAPI; constructor() { this.client = new Client({ auth: process.env.NOTION_SECRET, }); this.notionAPI = new NotionAPI({ authToken: process.env.NOTION_SECRET, }); } } const NotionService = new NotionServiceBase(); export { NotionService };
Tạo 1 hàm giúp chuyển đổi dữ liệu thô thành kiểu dữ liệu ta mong muốn
private static pageToPostTransformer(page: any): BlogPost { let cover = ''; switch (page.cover?.type) { case 'file': cover = page.cover.file.url; break; case 'external': cover = page.cover.external.url; break; default: break; } return { id: page.id, cover, title: page.properties?.Name?.title[0]?.plain_text, tags: page.properties?.Tags?.multi_select, description: page?.properties?.descriptions?.rich_text?.[0]?.plain_text, slug: page?.properties?.slug?.formula?.string, created: page?.properties?.Updated?.last_edited_time, }; }
Tạo các hàm lấy danh sách các bài post, lấy bài post theo slug, getRecordMap để lấy các block content
async getPosts(): Promise<BlogPost[]> { const response = await this.client.databases.query({ database_id: process.env.NOTION_DATABASE_ID!, filter: { property: 'Status', status: { equals: 'Done', }, }, sorts: [ { property: 'Created', direction: 'descending', }, ], }); return response.results.map((res) => NotionServiceBase.pageToPostTransformer(res) ); } async getPostBySlug(slug: string) { const page = await this.client.databases .query({ database_id: process.env.NOTION_DATABASE_ID!, filter: { property: 'slug', rich_text: { equals: slug, }, }, }) .then((res) => res.results[0]); return NotionServiceBase.pageToPostTransformer(page); } getRecordMap(pageId: string) { return this.notionAPI.getPage(pageId); }
Cuối cùng chúng ta có 1 service hoàn chỉnh như sau
import { Client } from '@notionhq/client'; import { NotionAPI } from 'notion-client'; import { BlogPost } from '@/types/schema'; class NotionServiceBase { client: Client; notionAPI: NotionAPI; constructor() { this.client = new Client({ auth: process.env.NOTION_SECRET, }); this.notionAPI = new NotionAPI({ authToken: process.env.NOTION_SECRET, }); } async getPosts(): Promise<BlogPost[]> { const response = await this.client.databases.query({ database_id: process.env.NOTION_DATABASE_ID!, filter: { property: 'Status', status: { equals: 'Done', }, }, sorts: [ { property: 'Created', direction: 'descending', }, ], }); return response.results.map((res) => NotionServiceBase.pageToPostTransformer(res) ); } async getPostBySlug(slug: string) { const page = await this.client.databases .query({ database_id: process.env.NOTION_DATABASE_ID!, filter: { property: 'slug', rich_text: { equals: slug, }, }, }) .then((res) => res.results[0]); return NotionServiceBase.pageToPostTransformer(page); } getRecordMap(pageId: string) { return this.notionAPI.getPage(pageId); } private static pageToPostTransformer(page: any): BlogPost { let cover = ''; switch (page.cover?.type) { case 'file': cover = page.cover.file.url; break; case 'external': cover = page.cover.external.url; break; default: break; } return { id: page.id, cover, title: page.properties?.Name?.title[0]?.plain_text, tags: page.properties?.Tags?.multi_select, description: page?.properties?.descriptions?.rich_text?.[0]?.plain_text, slug: page?.properties?.slug?.formula?.string, created: page?.properties?.pdated?.last_edited_time, }; } } const NotionService = new NotionServiceBase(); export { NotionService };
Bước 7: Gọi service và nhận thành quả
Để gọi api lấy tất cả danh sách các bài post ta làm tương tự như hình dưới, sau đó log kết quả để kiểm tra
import { Container, Grid2 as Grid } from '@mui/material'; import { NotionService } from '@/services'; import { AboutMe, Blogs, Firefly, Projects, Skills, TableOfContent, } from './_components'; export const revalidate = 60; export default async function Home() { const data = await NotionService.getPosts(); console.log({ data }); return ( <Container maxWidth="xl"> <Grid container spacing={2}> <Grid size={{ xs: 12, xl: 9.5 }}> <Firefly /> <AboutMe /> <Skills /> <Projects /> <Blogs blogs={data} /> </Grid> <Grid size={{ xl: 2.5 }} sx={{ display: { xs: 'none', xl: 'block' } }}> <TableOfContent /> </Grid> </Grid> </Container> ); }
Kết quả sau khi log ra ta thu được như sau

Bước 8: Sử dụng NotionRenderer để hiển thị nội dung
chúng ta sẽ sử dụng component của react-notion-x để hiển thị nội dung bủa blog, tao tạo 1 component như hình bên dưới
'use client'; import 'katex/dist/katex.min.css'; import dynamic from 'next/dynamic'; import Image, { ImageProps } from 'next/image'; import Link from 'next/link'; import * as types from 'notion-types'; import 'prismjs/themes/prism-tomorrow.css'; import 'rc-dropdown/assets/index.css'; import { useMemo } from 'react'; import { NotionRenderer } from 'react-notion-x'; import 'react-notion-x/src/styles.css'; const Code = dynamic(() => import('react-notion-x/build/third-party/code').then((m) => m.Code) ); const Equation = dynamic(() => import('react-notion-x/build/third-party/equation').then((m) => m.Equation) ); const Pdf = dynamic( () => import('react-notion-x/build/third-party/pdf').then((m) => m.Pdf), { ssr: false, } ); const Modal = dynamic( () => import('react-notion-x/build/third-party/modal').then((m) => m.Modal), { ssr: false, } ); function NextImageComponent(props: ImageProps) { // eslint-disable-next-line jsx-a11y/alt-text return <Image {...props} width={400} height={400} />; } export const RenderNotionPage = ({ recordMap, }: { recordMap: types.ExtendedRecordMap; }) => { const components = useMemo( () => ({ // eslint-disable-next-line react/no-unstable-nested-components, no-unused-vars, react/jsx-no-useless-fragment Collection: ({ block, className, ctx }: any) => <></>, Code, Equation, Pdf, Modal, nextImage: NextImageComponent, Link, }), [] ); return ( <div className="notion__container"> <NotionRenderer fullPage recordMap={recordMap} rootPageId={process.env.NOTION_DATABASE_ID} previewImages forceCustomImages components={components} /> </div> ); };
Gọi Api và hiển thị chi tiết của blog, chúng ta sẽ lấy bài blog dựa vào slug từ request
import { Container, Grid2 as Grid } from '@mui/material'; import { RenderNotionPage, TableOfBlogContent } from '@/app/_components'; import { NotionService } from '@/services'; export const revalidate = 60; export default async function BlogPage({ params, }: { params: { slug: string }; }) { const { slug } = params; const page = await NotionService.getPostBySlug(slug); const { id } = page; const recordMap = await NotionService.getRecordMap(id); return ( <Container maxWidth="xl"> <Grid container spacing={2}> <Grid size={{ xs: 12, xl: 9.5 }}> <RenderNotionPage recordMap={recordMap} /> </Grid> <Grid size={{ xl: 2.5 }} sx={{ display: { xs: 'none', xl: 'block' } }}> <TableOfBlogContent /> </Grid> </Grid> </Container> ); }
Thành quả
Cuối cùng chúng ta có thể trạo ra 1 trang blog tương tự như mình, chúc các bạn thành công 😁. nếu có thắc mắc gì thì các bạn có thể inbox cho mình, mình sẵn lòng giải quyết những thắc mắc của các bạn.
