Skip to content
Go back

ตรวจสอบอัพเดต Notion ด้วย Cloudflare Workers

Published  at  05:20 PM

By : Pickyzz | 5 min read

Cover image for blog post "ตรวจสอบอัพเดต Notion ด้วย Cloudflare Workers" by Pickyzz

เกริ่นก่อน

หลังจากที่ผมได้เขียนถึงการ รื้อ Stack หลังบ้าน โดยเปลี่ยนมาใช้ Notion เป็น CMS และสั่ง Build เว็บไซต์ผ่าน GitHub Actions ก็พบว่ามันทำงานได้ดีครับ แต่ก็ยังมีจุดที่รู้สึกว่า “น่าจะดีกว่านี้ได้”

ปัญหานึงคือ GitHub Actions มักจะมี delay ในการทำงาน และการรัน Cron Job ทุกๆ ชั่วโมงก็อาจจะไม่ทันใจ (หรือถ้าถี่กว่านั้นก็เปลือง resource โดยใช่เหตุ) ประกอบกับช่วงหลังเห็น Cloudflare Workers มาแรง ด้วย Free Tier ที่ใจป้ำมาก (100,000 requests/day!) เลยเกิดไอเดียว่า ทำไมเราไม่ย้าย logic การเช็คอัพเดตมาไว้ที่ Edge ซะเลยล่ะ?

ทำไมต้อง Cloudflare Workers?

  1. ฟรีและเยอะ: ให้ quota มาเหลือเฟือสำหรับการเช็คอัพเดต Notion ทุก 15 นาที
  2. Cron Triggers: ตั้งเวลาทำงานได้ง่ายๆ ผ่าน wrangler.toml ไม่ต้องพึ่ง external cron
  3. Edge Network: ทำงานเร็ว ไม่ต้อง spin up container นานเหมือน CI/CD
  4. Integration: ทำงานร่วมกับ Upstash Redis (ที่ใช้อยู่แล้ว) ได้เนียนๆ

Architecture

คอนเซปต์ยังคงเดิม คือ “เช็คว่ามีอะไรเปลี่ยนแปลงไหม? ถ้ามี ก็สั่ง Build” แต่เปลี่ยนคนทำงานจาก GitHub Runner มาเป็น Worker ครับ

+-------------------+       (1) Check         +------------------+
| Cloudflare Worker | ----------------------> |    Notion API    |
| (Cron every 15m)  | <---------------------- | (Last Edited Time)|
+-------------------+       Result            +------------------+
          |
          | (2) Compare
          v
+-------------------+       If New Update     +------------------+
|   Upstash Redis   | ----------------------> |      Vercel      |
|  (Last Checked)   |      Trigger Build      |   (Deploy Hook)  |
+-------------------+                         +------------------+

ลงมือทำ

1. Setup โปรเจ็กต์

เริ่มจากสร้าง directory ใหม่ และ init wrangler ครับ (ผมใช้ bun เป็นหลักนะช่วงนี้)

mkdir notion-monitor-worker
cd notion-monitor-worker
bun init
bun add -d wrangler

2. Config wrangler.toml

หัวใจสำคัญอยู่ที่ [triggers] ครับ เราตั้งให้มันทำงานทุกๆ 15 นาที

name = "notion-monitor-worker"
main = "src/index.js"
compatibility_date = "2024-12-24"

# Cron Triggers - every 15 minutes
[triggers]
crons = ["*/15 * * * *"]

# Environment variables จะถูก set ผ่าน wrangler secret

3. เขียน Code (src/index.js)

Logic ไม่มีอะไรซับซ้อน:

  1. ดึงเวลา last_edited_time จาก Notion Database
  2. เทียบกับค่าล่าสุดที่เก็บไว้ใน Redis
  3. ถ้า ใหม่กว่า -> ยิง Webhook ไปหา Vercel ให้ Rebuild -> อัพเดต Redis
  4. ถ้า เท่าเดิม -> จบงาน แยกย้าย
// src/index.js

const REDIS_KEY = 'notion:last_checked_time';

async function getRedisValue(env) {
  const url = `${env.UPSTASH_REDIS_REST_URL}/get/${REDIS_KEY}`;
  
  try {
    const response = await fetch(url, {
      headers: {
        Authorization: `Bearer ${env.UPSTASH_REDIS_REST_TOKEN}`,
      },
    });

    const data = await response.json();
    if (data.result === null) return null;
    
    let result = data.result;
    if (typeof result === 'string' && result.startsWith('"') && result.endsWith('"')) {
      result = result.slice(1, -1);
    }
    
    return result;
  } catch (error) {
    console.error('Error fetching from Redis:', error.message);
    throw new Error('Failed to connect to Redis.');
  }
}

async function setRedisValue(env, timestamp) {
  const url = `${env.UPSTASH_REDIS_REST_URL}/set/${REDIS_KEY}/${encodeURIComponent(timestamp)}`;
  
  try {
    const response = await fetch(url, {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${env.UPSTASH_REDIS_REST_TOKEN}`,
      },
    });

    const data = await response.json();
    if (data.result !== 'OK') {
      throw new Error(`Redis SET failed: ${JSON.stringify(data)}`);
    }
  } catch (error) {
    console.error('Error setting Redis:', error.message);
    throw new Error('Failed to set value to Redis.');
  }
}

async function getNotionLastEdited(env) {
  const url = `https://api.notion.com/v1/databases/${env.DATABASE_ID}/query`;
  
  try {
    const response = await fetch(url, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${env.NOTION_KEY}`,
        'Notion-Version': '2022-06-28',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        page_size: 1,
        sorts: [
          {
            timestamp: 'last_edited_time',
            direction: 'descending',
          },
        ],
      }),
    });

    if (!response.ok) throw new Error(`Notion API error: ${response.status}`);

    const data = await response.json();
    return data.results.length > 0 ? data.results[0].last_edited_time : null;
  } catch (error) {
    console.error('Error fetching from Notion:', error.message);
    throw error;
  }
}

async function triggerVercelDeploy(env) {
  try {
    const response = await fetch(env.VERCEL_DEPLOYMENT_HOOK, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'User-Agent': 'Cloudflare-Worker-Notion-Monitor',
      },
    });

    if (!response.ok) throw new Error(`Deploy hook failed: ${response.status}`);
    return true;
  } catch (error) {
    console.error('Error triggering deploy:', error.message);
    throw error;
  }
}

async function checkAndDeploy(env) {
  const lastEdited = await getNotionLastEdited(env);
  const lastChecked = await getRedisValue(env);
  
  let needsDeploy = false;
  
  if (!lastChecked) {
    needsDeploy = true;
  } else {
    const lastEditedDate = new Date(lastEdited);
    const lastCheckedDate = new Date(lastChecked);
    if (lastEditedDate > lastCheckedDate) needsDeploy = true;
  }
  
  if (needsDeploy) {
    await triggerVercelDeploy(env);
    await setRedisValue(env, lastEdited);
    return { deployed: true, lastEdited };
  }
  
  return { deployed: false, lastEdited };
}

export default {
  // Scheduled handler (Cron Trigger)
  async scheduled(event, env, ctx) {
    try {
      await checkAndDeploy(env);
    } catch (error) {
      console.error('❌ Error during scheduled check:', error);
    }
  },

  // Fetch handler (Manual Trigger via POST)
  async fetch(request, env, ctx) {
    if (request.method !== 'POST') {
      return new Response(JSON.stringify({ message: 'Use POST to trigger check' }), {
        status: 200,
        headers: { 'Content-Type': 'application/json' },
      });
    }

    try {
      const result = await checkAndDeploy(env);
      return new Response(JSON.stringify({ success: true, ...result }), {
        status: 200,
        headers: { 'Content-Type': 'application/json' },
      });
    } catch (error) {
      return new Response(JSON.stringify({ success: false, error: error.message }), {
        status: 500,
        headers: { 'Content-Type': 'application/json' },
      });
    }
  },
};

Code เต็มๆ ผมใส่ function ย่อยไว้จัดการ fetch/error handling ด้วยนะครับ เพื่อความสะอาด

4. Deploy & Secrets

จัดการ set secret keys ต่างๆ ให้ครบ (ปลอดภัยกว่าใส่ใน code นะ)

bun wrangler secret put NOTION_KEY
bun wrangler secret put DATABASE_ID
bun wrangler secret put UPSTASH_REDIS_REST_URL
bun wrangler secret put UPSTASH_REDIS_REST_TOKEN
bun wrangler secret put VERCEL_DEPLOYMENT_HOOK

แล้วก็ Deploy โลด!

bun wrangler deploy

ปัญหาที่เจอ และวิธีแก้

ระหว่างทำก็เจอปัญหาจุดนึงครับ คือค่า Timestamp ที่ได้จาก Upstash Redis นั้นดันมี Quotes แถมมาด้วย (เช่น "\"2025-12-24...\"") ทำให้เวลาเอาไป new Date() แล้วได้ค่าเป็น Invalid Date ส่งผลให้ Script ของเราเข้าใจผิดว่าไม่มีการอัพเดตตลอดเวลา

วิธีแก้: ผมเพิ่ม logic ในการเช็คและตัด Quotes ออกทั้งตอน Get และ Set ค่าครับ:

// ตอน Get: ตัด quotes หัว-ท้ายออก
if (typeof result === 'string' && result.startsWith('"') && result.endsWith('"')) {
  result = result.slice(1, -1);
}

// ตอน Set: ใช้ encodeURIComponent แทนการใส่ quotes เอง
const url = `${env.UPSTASH_REDIS_REST_URL}/set/${REDIS_KEY}/${encodeURIComponent(timestamp)}`;

ผลลัพธ์

หลังจาก Deploy เสร็จ ผมลองยิง request เข้าไปเทสดู ผลลัพธ์ออกมาสวยงาม:

{
  "success": true,
  "deployed": false,
  "lastEdited": "2025-12-17T06:40:00.000Z"
}

และเมื่อดู Logs ผ่าน wrangler tail ก็เห็นการทำงานชัดเจน (อันนี้ตัวอย่างตอนไม่มีอัพเดต)

🔍 Checking for Notion updates...
Notion last edited: 2025-12-17T06:40:00.000Z
Last checked (Redis): 2025-12-17T06:40:00.000Z
⏸️ No updates detected.

สรุป

การย้ายมาใช้ Cloudflare Workers ช่วยให้:

  • ประหยัด: ไม่เปลือง Github Actions runner minutes
  • Real-time ขึ้น: เช็คได้ถี่ขึ้น (ทุก 15 นาที) โดยไม่ต้องเกรงใจ quota
  • Scalable: ถ้าวันหน้าอยากเพิ่ม logic อะไรก็ใส่ใน Worker ได้เลย

ใครที่ทำ Static Site แล้วอยากได้ความ Dynamic แบบไม่ต้องรัน Server ตลอดเวลา ท่านี้แนะนำเลยครับ!


Older Post
Uptime 32 years of me.
Later Post
2025 and me

โพสต์ที่เกี่ยวข้อง