Dùng Notion làm CMS cho website

✍🏻
engineering
Posted on July 21, 2024

Gần đây mình đã chính thức chuyển toàn bộ dữ liệu website sang Notion thay vì sử dụng markdown. Bài viết này mình sẽ tổng hợp cách mình dùng Notion để làm CMS cho toàn bộ website. Cùng xem mình đã làm như thế nào nhé!

Lí do

Trước đây mình viết các case study và bài blog trên website bằng các file markdown sử dụng hashicorp/next-mdx-remote. Mỗi lần muốn viết bài mới lại phải đụng tới code, commit lên github. Vì vậy rất mất thời gian làm mình lười viết bài hơn hẳn.

Trong khi đó, mình thường xuyên sử dụng Notion để soạn thảo văn bản. Notion lại cung cấp API miễn phí nên mình nghĩ sao không dùng để làm CMS cho website luôn vậy sẽ soạn thảo trực quan và tiện lợi hơn. Ngoài ra với việc sử dụng Notion mình có thể làm thêm các chức năng động như comment hoặc reaction cho các bài đăng thay vì chỉ là nội dung tĩnh như trước.

Cấu trúc

Website mình sử dụng Next JS và deploy lên Vercel, và dùng JavaScript SDK của notion để gọi dữ liệu. Toàn bộ trang trong website đều là render static nên tốc độ load sẽ rất nhanh. Riêng với chức năng comment và reaction mình sẽ tạo các api route riêng để tương tác với Notion.

Mình sẽ tạo các Notion database như sau:

  • Projects: chứa các project của portfolio.
  • Blog: chứa các bài đăng blog.
  • Comments: chứa các bình luận trên website.
  • Reactions: chứa các reaction trên website.
    Post Content Image
    Blog database
export const getStaticPaths: GetStaticPaths = async () => {
  const projects = await getNotionProjectsWithCache();

  const paths = projects.map((project) => ({
    params: { slug: project.slug },
  }));

  return { paths, fallback: false };
};
export const getStaticProps: GetStaticProps = async ({ params }) => {
  const slug = params?.slug as string;
  // Get all posts from the Notion database
  const projects = await getNotionProjects(isDevEnvironment);
  // Find the post with a matching slug property
  let project: NotionProject | undefined = projects.find(
    (proj) => proj.slug === slug
  );
  if (!project) {
    return {
      notFound: true,
    };
  }
  // Get the Notion page data and all child block data
  const notionContent = await getNotionProjectContent(project.id);
  
  return {
    props: {
      project: project,
      projects: projects,
      notionContent,
    },
    revalidate: 120,
  };
};

Khó khăn

  • Vấn đề #1: Link image và video của Notion chỉ tồn tại trong vòng 1 tiếng.
  • Vấn đề #2: Layout của các case study trong portfolio của mình khá phức tạp mà các block có sẵn của notion không đủ đáp ứng.

Giải quyết vấn đề #1

Mình có đọc bài viết của Cory (designer ở Notion) và Jake (software engineer ở Notion) về cách họ tiếp cận để giải quyết vấn đề về hosting asset khi sử dụng Notion để làm CMS.

Về phần asset, Cory có chỉ ra 3 hướng tiếp cận:

There are essentially three options:

  • Use getServerSideProps instead of getStaticProps to ensure a new and valid image URL is returned on each page visit.
  • Write a script that crawls all posts for image blocks, downloads the images, and places them in the /public directory.
  • Use getStaticProps and incremental static regeneration to crawl an individual post’s blocks at request and upload the image assets to your own S3 bucket.

[...] I ended up going with option #3, but it was a lot of work to implement. Unless you want to burn 8 hours, I’d recommend going with getServerSideProps for now.

Anh ấy dùng là hướng thứ ba viết một đoạn script để upload asset lên Amazon S3 trong khi build. Trong khi đó, Jake thì viết một api route để tải asset theo block id.

Mình chọn tiếp cận theo hướng giống Cory. Tuy nhiên mình không dùng Amazon S3 mà sử dụng Cloudinary (vì nó miễn phí 😅). Với mỗi block image hoặc video trong bài đăng, mình sẽ upload lên cloudinary lấy link asset và width/height của asset. Sau đó lưu tất cả thông tin của asset vào property Assets của trang dưới dạng JSON. Khi render bài đăng, mình tìm block id trong property Assets là sẽ lấy được link hình ảnh và kích thước.

Post Content Image
Một trang trong database Projects với thông tin Assets được lưu trữ

Giải quyết vấn đề #2

Đối với vấn đề này, cách giải quyết cũng không quá khó khăn. Mình tham khảo cách tiếp cận của super.so. Nền tảng này sử dụng callout block như là một block flexible để làm container. Mình sẽ lấy nội dung của callout để xác định component sẽ sử dụng để render block. Trong khi đó, nội dung của block sẽ là các block con. Các tiếp cận này giống như short code trong WordPress.

Post Content Image
Sử dụng callout block làm container trình bày trang

Thành quả

Với mỗi Notion page như sau: https://auduongtuan.notion.site/Eware-22d34891c88344f0ae3c54592b04ebde sẽ được render thành https://auduongtuan.com/project/eware.

Từ đó cách mình viết và trình bày case study hoặc bài đăng sẽ tiện lợi hơn. Với mỗi bài đăng, mình không phải truy cập vào code. Hay phải đo size của image/video mà vẫn giữ được lazy load không bị shift như khi sử dụng markdown lúc trước.

Post Content Image

Toàn bộ code của website mình có public ở Github, dù hơi “sida” một chút nhưng hi vọng có ích cho ai đó: https://github.com/auduongtuan/auduongtuan.com