Skip to content

สร้างคลังภาพของคุณเองด้วยแผนฟรีของ Cloudflare — เริ่มต้นที่ $0

คุณเคยคิดบ้างไหมว่าจะรวบรวมภาพฟรีเชิงพาณิชย์ที่ใช้บ่อยไว้ในที่เดียว ค้นหาได้ทันที — และไม่เสียเงินสักบาท?

เราทำแล้ว บทความนี้แชร์สถาปัตยกรรมทั้งหมด


ทำไมต้องสร้างเอง?

นักออกแบบ นักพัฒนาเว็บ และนักการตลาดต้องการภาพอยู่เสมอ Pixabay, Pexels, Unsplash ดีทั้งคู่ แต่การเปิดแต่ละไซต์แยกกันและค้นหาทีละที่เสียเวลามาก

ยิ่งน่ารำคาญกว่านั้น: คุณเคยใช้ภาพหนึ่ง แต่ครั้งต่อมาจำไม่ได้ว่าเจอที่ไหน

ถ้ามีจุดเข้าถึงเดียวที่รวมทุกแหล่ง ค้นครั้งเดียวได้จากทุกแหล่ง — นั่นแหละถึงจะประหยัดเวลาจริงๆ

นั่นคือแนวคิดเบื้องหลัง FreePicHub


โควต้าฟรีของ Cloudflare ครอบคลุมแค่ไหน?

หลายคนไม่รู้ว่า Cloudflare ใจกว้างแค่ไหน นี่คือบริการที่โปรเจกต์นี้ใช้จริง ทั้งหมดอยู่ในแผนฟรี:

บริการการใช้งานโควต้าฟรี
R2 Object Storageเก็บไฟล์ภาพ10 GB / GET 10 ล้านครั้ง/เดือน
D1 Databaseเก็บ metadata5 GB / อ่าน 5 ล้านครั้ง/วัน
WorkersAPI backend100K request/วัน
PagesFrontendDeploy static ไม่จำกัด

สำหรับคลังภาพส่วนตัวขนาดเล็กถึงกลาง โควต้านี้เกินพอ และไม่ต้องใช้บัตรเครดิตเพื่อเริ่มต้น


ภาพรวมสถาปัตยกรรม

[Python Script อัตโนมัติ]

  ประมวลผลภาพ (บีบอัด WebP)

 ┌──────────────────┐
 │    Cloudflare    │
 │                  │
 │  R2 (ไฟล์)       │
 │  D1 (ฐานข้อมูล)  │
 │  Workers (API)   │
 │  Pages (UI)      │
 └──────────────────┘

    เบราว์เซอร์ / ผู้ใช้

สี่บริการ Cloudflare แต่ละตัวมีบทบาทชัดเจน:

  • R2: เก็บไฟล์ภาพที่บีบอัดแล้ว (thumbnail / preview / original)
  • D1: SQLite database — ชื่อ ขนาด tag เส้นทาง R2
  • Workers: สอง API — /api/search และ /api/featured
  • Pages: Frontend แบบ static, HTML + CSS + JS ล้วนๆ ไม่มี framework dependency

Step 1: สร้าง R2 Bucket

ไปที่ Cloudflare Dashboard → R2 → สร้าง Bucket (เช่น my-image-library)

จากนั้นที่ Manage R2 API Tokens สร้าง User API Token:

  • สิทธิ์: "Object Read & Write"
  • จำกัดเฉพาะ Bucket ของคุณ

จดบันทึก Access Key ID, Secret Access Key และ Account ID — Python script จะใช้สิ่งเหล่านี้

R2 ใช้ S3-compatible API ดังนั้น boto3 ของ Python ใช้ได้โดยตรง


Step 2: สร้าง D1 Database

รันใน Wrangler CLI:

bash
npx wrangler d1 create my-image-db

จด database_id ที่ได้รับ จากนั้นสร้าง Schema:

sql
CREATE TABLE IF NOT EXISTS images (
  id            TEXT PRIMARY KEY,
  source        TEXT NOT NULL,
  source_id     TEXT,
  title         TEXT,
  tags          TEXT DEFAULT '[]',
  width         INTEGER,
  height        INTEGER,
  aspect_ratio  TEXT,
  license_type  TEXT NOT NULL,
  thumb_key     TEXT,
  preview_key   TEXT,
  original_key  TEXT,
  created_at    TEXT DEFAULT (datetime('now'))
);

CREATE INDEX IF NOT EXISTS idx_source    ON images(source);
CREATE INDEX IF NOT EXISTS idx_created   ON images(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_source_id ON images(source, source_id);
bash
npx wrangler d1 execute my-image-db --file=schema.sql

Step 3: Workers API (ผ่าน Pages Functions)

วิธีที่สะดวกที่สุดคือ Cloudflare Pages Functions — วางไฟล์ JS ใน /functions/ แล้ว Cloudflare จะ deploy อัตโนมัติเป็น Worker

Search API (functions/api/search.js):

javascript
export async function onRequestGet({ request, env }) {
  const url    = new URL(request.url)
  const q      = url.searchParams.get('q') || ''
  const source = url.searchParams.get('source') || ''
  const page   = parseInt(url.searchParams.get('page') || '1')
  const limit  = 24
  const offset = (page - 1) * limit

  let sql    = 'SELECT * FROM images WHERE 1=1'
  let params = []

  if (q) {
    sql += ' AND (title LIKE ? OR tags LIKE ?)'
    params.push(`%${q}%`, `%${q}%`)
  }
  if (source) {
    sql += ' AND source = ?'
    params.push(source)
  }

  sql += ` ORDER BY created_at DESC LIMIT ${limit + 1} OFFSET ${offset}`

  const { results } = await env.MY_DB.prepare(sql).bind(...params).all()
  const hasMore = results.length > limit

  return Response.json({
    results: results.slice(0, limit).map(r => ({
      ...r,
      tags: JSON.parse(r.tags || '[]')
    })),
    has_more: hasMore
  }, { headers: { 'Access-Control-Allow-Origin': '*' } })
}

Step 4: ประมวลผลภาพและการนำเข้าอัตโนมัติ

นี่คือส่วนที่น่าสนใจที่สุด Python script ประมวลผลภาพ อัปโหลดไปยัง R2 และเขียน metadata ลง D1

ประมวลผลภาพ — สามเวอร์ชันต่อภาพ ทั้งหมดเก็บเป็น WebP:

python
from PIL import Image

def process_image(source_path, output_dir):
    img = Image.open(source_path).convert("RGB")
    w, h = img.size

    # Thumbnail 512px (แกลเลอรี)
    thumb = img.copy()
    thumb.thumbnail((512, 512), Image.LANCZOS)
    thumb.save(output_dir / "thumb.webp", "WEBP", quality=82)

    # Preview 1024px (modal / lightbox)
    preview = img.copy()
    preview.thumbnail((1024, 1024), Image.LANCZOS)
    preview.save(output_dir / "preview.webp", "WEBP", quality=85)

    # ต้นฉบับแปลงเป็น WebP บีบอัด
    img.save(output_dir / "original.webp", "WEBP", quality=90)

    return {"width": w, "height": h}

WebP มักเล็กกว่า JPEG 25–35% — ประหยัด storage R2 ได้มากในระยะยาว

อัปโหลดไปยัง R2 (boto3 S3-compatible):

python
import boto3

s3 = boto3.client(
    "s3",
    endpoint_url=f"https://{ACCOUNT_ID}.r2.cloudflarestorage.com",
    aws_access_key_id=ACCESS_KEY,
    aws_secret_access_key=SECRET_KEY,
    region_name="auto",
)
s3.put_object(Bucket="my-image-library", Key="source/uuid/thumb.webp", Body=file_bytes)

เขียนลง D1 — ป้องกันข้อมูลซ้ำ:

python
import requests, uuid, json

def write_to_d1(record):
    resp = requests.post(
        f"https://api.cloudflare.com/client/v4/accounts/{ACCOUNT_ID}/d1/database/{DB_ID}/query",
        headers={"Authorization": f"Bearer {D1_TOKEN}"},
        json={
            "sql": """
                INSERT INTO images (id, source, source_id, title, tags,
                  width, height, aspect_ratio, license_type,
                  thumb_key, preview_key, original_key)
                SELECT ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
                WHERE NOT EXISTS (
                  SELECT 1 FROM images WHERE source=? AND source_id=?
                )
            """,
            "params": [str(uuid.uuid4()), record["source"], record["source_id"],
                       record["title"], json.dumps(record["tags"]),
                       record["width"], record["height"], record["aspect_ratio"],
                       record["license_type"], record["thumb_key"],
                       record["preview_key"], record["original_key"],
                       record["source"], record["source_id"]]
        }
    )
    return resp.json().get("success", False)

WHERE NOT EXISTS ทำให้รัน script ซ้ำกี่ครั้งก็ไม่เขียนข้อมูลซ้ำ — สำคัญมากสำหรับ pipeline อัตโนมัติรายวัน


Step 5: Deploy Frontend

ไฟล์ static สามไฟล์ — index.html, style.css, app.js — push ไปยัง Cloudflare Pages:

bash
npx wrangler pages deploy ./frontend --project-name my-image-library

Layout masonry waterfall ด้วย CSS ไม่ต้องใช้ library JS:

css
.image-grid {
  columns: 4 220px;
  gap: 14px;
}
.image-card {
  break-inside: avoid;
  margin-bottom: 14px;
}

คำนวณต้นทุน

สมมตินำเข้า 500 ภาพ/วัน คลัง 10,000 ภาพ (thumbnail ~50KB, preview ~200KB):

รายการการใช้งานค่าใช้จ่าย
R2 Storage≈ 2.5 GB$0
D1 Reads (ผู้เยี่ยมชม 1,000 คน/วัน)5,000/วัน$0
Workers Requests5,000/วัน$0
Pages Deployไม่จำกัด$0
รวม$0 / เดือน

โควต้าฟรีของ Cloudflare แทบไม่มีวันหมดสำหรับคลังภาพส่วนตัวหรือเชิงพาณิชย์ขนาดเล็ก


สรุป

แนวคิดหลักของระบบนี้ง่ายมาก:

  1. R2 เก็บไฟล์ — ราคาถูกและเร็ว
  2. D1 เก็บ index — ขับเคลื่อนการค้นหา
  3. Workers จัดการ query logic
  4. Pages แสดง UI ให้ผู้ใช้
  5. Python scripts ป้อนข้อมูลตามกำหนดเวลา

ทุกอย่างรันบน edge network ของ Cloudflare — เร็วทั่วโลก ไม่มีค่าบำรุงรักษา

ดูผลลัพธ์จริง: FreePicHub


มีคำถามเกี่ยวกับข้อกำหนดทางเทคนิคหรือสถาปัตยกรรม?
ติดต่อเราได้ที่ ascentek.info ไม่ว่าจะเป็นการออกแบบสถาปัตยกรรม Cloudflare, pipeline อัตโนมัติ Python หรือการวางแผนคลังภาพ เรายินดีพูดคุย


อ่านเพิ่มเติม

คลังความรู้ดิจิทัล Ascentek