เจาะลึกการดึงข้อมูลด้วย @notionhq/client

เจาะลึกการดึงข้อมูลด้วย @notionhq/client

byAvatar Nook - The Duckrr

บทความนี้จะพาไปผ่าโค้ดเบื้องหลังการดึงข้อมูลจาก Notion API โดยใช้ไลบรารี @notionhq/client อธิบายตั้งแต่การติดตั้ง การ query database เพื่อดึงรายการบทความทั้งหมด และเจาะลึกเทคนิคสำคัญในการดึงข้อมูลบล็อกทั้งหมดแบบ Recursive เพื่อให้สามารถจัดการกับบล็อกที่ซ้อนกันอยู่ลึกๆ ได้ เหมาะสำหรับผู้ที่ต้องการเข้าใจการทำงานของ API และนำไปต่อยอด

ภาพรวมของการเชื่อมต่อ

ในโปรเจกต์ Astro Notion นี้ หัวใจสำคัญของการสื่อสารระหว่างเว็บไซต์ของเรากับฐานข้อมูลบน Notion คือไลบรารี @notionhq/client ซึ่งเป็น Official JavaScript SDK จาก Notion เอง หน้าที่หลักของมันคือการสร้าง Client instance เพื่อส่งคำขอ (Request) ไปยัง Notion API และรับข้อมูลกลับมาใช้งาน

การตั้งค่าเริ่มต้นอยู่ในไฟล์ src/lib/notion.ts ซึ่งเรียบง่าย

// src/lib/notion.ts

import { Client } from "@notionhq/client";

// สร้าง Notion client instance สำหรับเชื่อมต่อกับ Notion API
// โดยใช้ NOTION_TOKEN จาก environment variables
export const notion = new Client({
  auth: (import.meta.env as any).NOTION_TOKEN
});

// กำหนดค่า DATABASE_ID จาก environment variables
export const DATABASE_ID = (import.meta.env as any).NOTION_DATABASE_ID;

เพียงแค่สร้าง Client instance โดยส่ง auth token เข้าไป เราก็พร้อมที่จะเริ่มดึงข้อมูลแล้ว

Part 1 การ Query Database เพื่อดึงรายการบทความทั้งหมด

ขั้นตอนแรกของการสร้างบล็อกคือการแสดงรายการบทความทั้งหมดที่มี ในโปรเจกต์นี้ ฟังก์ชัน getAllPosts ในไฟล์ src/lib/notion.ts รับหน้าที่นี้

ลองมาดูโค้ดฉบับเต็มและแยกส่วนประกอบเพื่อทำความเข้าใจกัน

// src/lib/notion.ts

export async function getAllPosts(): Promise<BlogPostSummary[]> {
  const dataSourceId = await getDataSourceId(); // สมมติว่าฟังก์ชันนี้ return DATABASE_ID

  // Query ข้อมูลจาก Data Source โดยกรองเอาเฉพาะหน้าที่ Published เป็น true
  // และเรียงลำดับตามวันที่สร้างล่าสุด
  const res = await (notion as any).dataSources.query({
    data_source_id: dataSourceId,
    filter: {
      property: "Published",
      checkbox: { equals: true },
    },
    sorts: [
      { property: "Created", direction: "ascending" },
    ],
  });

  // แปลงข้อมูลที่ได้จาก Notion API ให้อยู่ในรูปแบบที่เราต้องการ
  return res.results.map((page: any) => {
    // ... (ส่วนของการ map ข้อมูล)
  });
}

เจาะลึก notion.databases.query()

method .query() คือเครื่องมือหลักในการดึงข้อมูลจาก Database โดยเราสามารถส่ง object ที่มี property สำคัญๆ เข้าไปเพื่อควบคุมผลลัพธ์ได้ดังนี้:

  1. database_id (หรือ data_source_id) ระบุ ID ของฐานข้อมูลที่เราต้องการดึงข้อมูล
  • filter ส่วนที่ทรงพลังที่สุด ใช้สำหรับกรองข้อมูลตามเงื่อนไขที่เราต้องการ
    • ในโค้ดตัวอย่าง เรากรอง (filter) โดยใช้เงื่อนไขว่า property ที่ชื่อ "Published" ซึ่งเป็นประเภท checkbox ต้องมีค่า equals: true เท่านั้น
    • นี่คือเหตุผลว่าทำไมในหน้าเว็บของเราถึงแสดงเฉพาะบทความที่เราติ๊กช่อง "Published" ใน Notion
  • sorts ใช้สำหรับเรียงลำดับข้อมูลที่ได้กลับมา
    • ในโค้ดตัวอย่าง เราเรียง (sorts) จาก property ที่ชื่อ "Created" (ซึ่งเป็น created\_time) ในทิศทางจากน้อยไปมาก (ascending)
    • ผลลัพธ์คือบทความที่เก่าที่สุดจะถูกแสดงก่อน หากต้องการให้บทความใหม่ล่าสุดอยู่บนสุด ก็แค่เปลี่ยนเป็น descending

หลังจากการ Query สำเร็จ Notion API จะส่งข้อมูลกลับมาในรูปแบบของอาร์เรย์ results ซึ่งเรานำมา .map() เพื่อจัดรูปแบบข้อมูลให้อยู่ใน BlogPostSummary ที่เรากำหนดไว้ ทำให้ง่ายต่อการนำไปใช้งานในหน้าเว็บต่อไป

Part 2 เจาะลึกการดึงเนื้อหาด้วย Recursive Function

เมื่อผู้ใช้คลิกเข้าไปอ่านบทความใดบทความหนึ่ง เราไม่ได้ต้องการแค่หัวข้อ แต่เราต้องการ "เนื้อหาทั้งหมด" ของหน้านั้นๆ ซึ่งใน Notion จะอยู่ในรูปแบบของ Blocks

ความท้าทายคือ Notion API จะคืนค่าแค่บล็อกระดับแรก (level 1) เท่านั้น หากเรามีบล็อกที่ซ้อนกันอยู่ เช่น รายการย่อย (nested list) หรือเนื้อหาใน toggle block เราจำเป็นต้องเรียก API ซ้ำเพื่อดึงข้อมูลลูกๆ ของมันออกมา นี่คือที่มาของเทคนิค "Recursive Fetching"

ในโปรเจกต์นี้ ฟังก์ชัน getBlocksDeep ในไฟล์ src/lib/renderNotionChildBlocks.ts คือพระเอกของงานนี้

// src/lib/renderNotionChildBlocks.ts

export async function getBlocksDeep(blockId: string) {
  const blocks: any[] = [];
  let cursor: string | undefined = undefined;

  do {
    const res = await notion.blocks.children.list({
      block_id: blockId,
      page_size: 100, // ดึงข้อมูลทีละ 100 บล็อก
      start_cursor: cursor,
    });

    for (const block of res.results) {
      // 🔁 ถ้าบล็อกมีบล็อกลูก (has_children) — ให้เรียกฟังก์ชันนี้ซ้ำ
      if ((block as any).has_children) {
        (block as any).children = await getBlocksDeep((block as any).id);
      }
      blocks.push(block);
    }

    cursor = res.has_more ? (res.next_cursor || undefined) : undefined;
  } while (cursor);

  return blocks;
}

การทำงานของ getBlocksDeep ทีละขั้นตอน

  1. เริ่มต้นด้วย blockId ฟังก์ชันนี้รับ ID ของหน้า (Page) หรือบล็อกแม่ (Parent Block) ที่เราต้องการดึงเนื้อหาเข้ามา
  • จัดการ Pagination ด้วย do-while และ cursor
    • Notion API จะส่งข้อมูลกลับมาเป็นหน้าๆ (Paginated) โดยจำกัดจำนวนผลลัพธ์ต่อครั้ง (สูงสุด 100)
    • เราใช้ do-while loop เพื่อให้แน่ใจว่าโค้ดจะทำงานอย่างน้อยหนึ่งครั้ง
    • notion.blocks.children.list คือ method สำหรับดึงรายการบล็อกลูก
    • start_cursor ใช้เพื่อบอก API ว่าให้เริ่มดึงข้อมูล "หลังจาก" ตำแหน่งล่าสุดที่เราดึงไป
    • หลังจากดึงข้อมูลแต่ละหน้า (res) เราจะตรวจสอบว่า res.has_more เป็น true หรือไม่ ถ้าใช่ หมายความว่ายังมีข้อมูลหน้าถัดไป เราจึงอัปเดตค่า cursor ด้วย res.next_cursor เพื่อใช้ในการวนลูปครั้งต่อไป ลูปนี้จะหยุดก็ต่อเมื่อ has_more เป็น false
  • หัวใจของ Recursion:
    • เราวนลูป (for...of) ใน res.results ที่ได้มา
    • สำหรับแต่ละ block เราจะตรวจสอบ property ที่ชื่อ has_children
    • ถ้า has_children เป็น true: หมายความว่าบล็อกนี้มีบล็อกลูกซ้อนอยู่ข้างใน เราจึงเรียกฟังก์ชัน getBlocksDeep ซ้ำอีกครั้ง! แต่คราวนี้เราส่ง ID ของ บล็อกปัจจุบัน (block as any).id เข้าไปแทน
    • ผลลัพธ์ที่ได้จากการเรียกซ้ำ จะถูกเก็บไว้ใน property ใหม่ที่เราสร้างขึ้นมาชื่อว่า children ((block as any).children = ...)
    • กระบวนการนี้จะเกิดซ้ำไปเรื่อยๆ จนกว่าจะเจอบล็อกที่ไม่มีลูกอีกแล้ว

ด้วยเทคนิคนี้ ไม่ว่าเนื้อหาของคุณจะซ้อนกันกี่ชั้น getBlocksDeep ก็จะไล่ดึงข้อมูลออกมาจนครบถ้วน ทำให้ได้โครงสร้างข้อมูลแบบ tree ที่สมบูรณ์พร้อมที่จะนำไปแปลงเป็น HTML เพื่อแสดงผลบนหน้าเว็บต่อไป

สรุป

การใช้ @notionhq/client ช่วยให้การดึงข้อมูลจาก Notion API เป็นเรื่องที่ตรงไปตรงมาและจัดการได้ง่าย ผ่านการเรียนรู้สองเทคนิคหลักที่ใช้ในโปรเจกต์นี้

  1. notion.databases.query สำหรับการดึงรายการข้อมูลจากฐานข้อมูล พร้อมความสามารถในการกรองและจัดเรียงที่ยืดหยุ่น
  2. Recursive Fetching สำหรับการดึงข้อมูลเนื้อหาแบบเจาะลึก เพื่อจัดการกับโครงสร้างบล็อกที่ซ้อนกันได้อย่างสมบูรณ์

เมื่อเข้าใจหลักการทำงานเบื้องหลังเหล่านี้แล้ว คุณก็จะสามารถนำไปปรับใช้หรือต่อยอดฟังก์ชันการทำงานอื่นๆ ที่ซับซ้อนมากขึ้นได้อย่างมั่นใจ