Skip to content

Xây Dựng Thư Viện Ảnh Riêng Với Gói Miễn Phí Cloudflare — Bắt Đầu Từ $0

Bạn đã bao giờ nghĩ đến việc tập trung tất cả ảnh thương mại miễn phí yêu thích vào một nơi, tìm kiếm một lần là ra — mà không tốn đồng nào?

Chúng tôi đã làm. Bài này chia sẻ toàn bộ kiến trúc.


Tại Sao Phải Tự Xây?

Designer, lập trình viên web và marketer đều thường xuyên cần ảnh. Pixabay, Pexels, Unsplash đều tốt, nhưng mở từng trang và tìm kiếm riêng lẻ thì tốn thời gian.

Khó chịu hơn: bạn đã dùng một bức ảnh, nhưng lần sau không nhớ tìm thấy ở đâu.

Nếu có một cổng vào duy nhất tích hợp tất cả nguồn, tìm một lần ra kết quả từ mọi nơi — đó mới thực sự tiết kiệm thời gian.

Đó là ý tưởng đằng sau FreePicHub.


Hạn Mức Miễn Phí Của Cloudflare Bao Phủ Đến Đâu?

Nhiều người không biết Cloudflare hào phóng đến thế nào. Đây là các dịch vụ dự án này thực sự dùng, tất cả đều trong gói miễn phí:

Dịch vụMục đíchHạn mức miễn phí
R2 Object StorageLưu file ảnh10 GB / 10 triệu GET/tháng
D1 DatabaseLưu metadata5 GB / 5 triệu đọc/ngày
WorkersAPI backend100K request/ngày
PagesFrontendDeploy tĩnh không giới hạn

Với thư viện ảnh cá nhân quy mô nhỏ đến trung bình, hạn mức này hoàn toàn đủ dùng — và không cần thẻ tín dụng để bắt đầu.


Tổng Quan Kiến Trúc

[Script Python tự động]

  Xử lý ảnh (nén WebP)

 ┌──────────────────┐
 │    Cloudflare    │
 │                  │
 │  R2 (file)       │
 │  D1 (database)   │
 │  Workers (API)   │
 │  Pages (UI)      │
 └──────────────────┘

    Trình duyệt / Người dùng

Bốn dịch vụ Cloudflare, mỗi cái có vai trò rõ ràng:

  • R2: Lưu file ảnh đã nén (thumbnail / preview / original)
  • D1: SQLite database — tiêu đề, kích thước, tag, đường dẫn R2
  • Workers: Hai API — /api/search/api/featured
  • Pages: Frontend tĩnh, HTML + CSS + JS thuần túy, không phụ thuộc framework

Bước 1: Tạo R2 Bucket

Vào Cloudflare Dashboard → R2 → Tạo Bucket (ví dụ my-image-library).

Sau đó tại Manage R2 API Tokens, tạo User API Token:

  • Quyền: "Object Read & Write"
  • Giới hạn cho Bucket của bạn

Ghi lại Access Key ID, Secret Access Key và Account ID — script Python sẽ cần chúng.

R2 dùng S3-compatible API, nên boto3 của Python dùng trực tiếp được.


Bước 2: Tạo D1 Database

Chạy trong Wrangler CLI:

bash
npx wrangler d1 create my-image-db

Ghi lại database_id được trả về. Sau đó tạo 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

Bước 3: Workers API (qua Pages Functions)

Cách thuận tiện nhất là Cloudflare Pages Functions — đặt file JS vào /functions/, Cloudflare tự động deploy thành 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': '*' } })
}

Bước 4: Xử Lý Ảnh & Nhập Dữ Liệu Tự Động

Đây là phần thú vị nhất. Script Python đọc nguồn ảnh, xử lý từng ảnh, upload lên R2 và ghi metadata vào D1.

Xử lý ảnh — ba phiên bản mỗi ảnh, tất cả lưu dạng 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 (danh sách gallery)
    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)

    # Ảnh gốc chuyển sang WebP nén
    img.save(output_dir / "original.webp", "WEBP", quality=90)

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

WebP thường nhỏ hơn JPEG 25–35% — tiết kiệm đáng kể cho storage R2 lâu dài.

Upload lên 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)

Ghi vào D1 — an toàn khỏi trùng lặp:

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)

Mệnh đề WHERE NOT EXISTS đảm bảo chạy script bao nhiêu lần cũng không ghi dữ liệu trùng — cần thiết cho pipeline tự động hàng ngày.


Bước 5: Deploy Frontend

Ba file tĩnh — index.html, style.css, app.js — push lên Cloudflare Pages:

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

Layout masonry waterfall bằng CSS, không cần thư viện JS:

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

Tính Toán Chi Phí

Giả sử nhập 500 ảnh/ngày, thư viện 10.000 ảnh (thumbnail ~50KB, preview ~200KB):

Hạng mụcSử dụngChi phí
R2 Storage≈ 2.5 GB$0
D1 Reads (1.000 khách/ngày)5.000/ngày$0
Workers Requests5.000/ngày$0
Pages DeployKhông giới hạn$0
Tổng$0 / tháng

Hạn mức miễn phí của Cloudflare gần như không bao giờ cạn với thư viện ảnh cá nhân hoặc thương mại quy mô nhỏ.


Tóm Tắt

Ý tưởng cốt lõi của hệ thống rất đơn giản:

  1. R2 lưu file — rẻ và nhanh
  2. D1 lưu index — sức mạnh tìm kiếm
  3. Workers xử lý logic query
  4. Pages hiển thị UI cho người dùng
  5. Script Python nạp dữ liệu theo lịch

Tất cả chạy trên edge network của Cloudflare — tăng tốc toàn cầu, không tốn chi phí bảo trì.

Xem kết quả thực tế: FreePicHub


Có câu hỏi về thông số kỹ thuật hoặc kiến trúc?
Liên hệ chúng tôi tại ascentek.info — dù là thiết kế kiến trúc Cloudflare, pipeline tự động hóa Python hay lên kế hoạch thư viện ảnh, chúng tôi sẵn sàng trao đổi.


Đọc thêm

Cơ sở tri thức số Ascentek