Hướng dẫn tạo blog với Nextjs và Notion
🧪

Hướng dẫn tạo blog với Nextjs và Notion

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
notion image
tiếp theo chọn đăng nhập bằng google như hình
notion image
Giao diện chính của notion
notion image
Để tạo database lư trữ cho các blog, chúng ta click vào Getting Started nhấn vào nút +
notion image
Tiếp theo là đặt tên cho database và chọn kiểu hiển thị
notion image
Sau khi hoàn thành bước trên chúng ta sẽ được giao diện như sau
notion image

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
notion image
  • 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
notion image
  • 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)
notion image
notion image
  • 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ì
notion image
notion image
  • Thên ảnh cho ver cho page
notion image
  • 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
    notion image
    notion image

    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:
    notion image
    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:
    notion image

    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
    notion image
    • Để 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:
    notion image

    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
    notion image

    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.
      Congratulations!thank for your attention!