Cách Mình Tích Hợp SEO Services

Làm việc với DataForSEO, SerpAPI, và các nhà cung cấp dữ liệu SEO khác

Các công cụ SEO của mình sống nhờ dữ liệu bên ngoài -- SERP, keyword difficulty, backlink, AI Overviews. Mà dữ liệu bên ngoài thì đắt, nên tích hợp kiểu "gọi thoải mái, tính tiền sau" là cách nhanh nhất để bill phình to mà không ai hay.

Nguyên tắc cốt lõi: Các API này đắt. Tối ưu cho hiệu quả và cache mạnh tay.

Các Nhà Cung Cấp Dữ Liệu SEO

ProviderTốt nhất choMô hình giá
DataForSEODữ liệu SERP toàn diện, AI OverviewsTheo request
SerpAPITìm kiếm Google nhanhTheo search
Google APIsSearch Console, AnalyticsMiễn phí (có giới hạn)

DataForSEO

Tổng Quan

DataForSEO cung cấp dữ liệu SEO phong phú:

  • Kết quả SERP (organic, paid, featured snippet)
  • Dữ liệu AI Overviews
  • Keyword difficulty
  • Dữ liệu backlink

Xác Thực

DataForSEO dùng HTTP Basic Auth:

// lib/dataforseo.ts
const credentials = Buffer.from(
  `${process.env.DATAFORSEO_LOGIN}:${process.env.DATAFORSEO_PASSWORD}`
).toString('base64');

const headers = {
  'Authorization': `Basic ${credentials}`,
  'Content-Type': 'application/json',
};

Ví Dụ SERP API

Lấy kết quả tìm kiếm cho từ khóa:

interface SerpTask {
  keyword: string;
  location_code: number;
  language_code: string;
  device: 'desktop' | 'mobile';
}

interface SerpResult {
  keyword: string;
  items: Array<{
    type: string;
    rank_group: number;
    rank_absolute: number;
    title?: string;
    url?: string;
    description?: string;
  }>;
  ai_overview?: {
    text: string;
    sources: Array<{ url: string; title: string }>;
  };
}

async function fetchSerpResults(keywords: string[]): Promise<SerpResult[]> {
  const tasks: SerpTask[] = keywords.map(keyword => ({
    keyword,
    location_code: 2704,  // Việt Nam
    language_code: 'vi',
    device: 'desktop',
  }));

  const response = await fetch(
    'https://api.dataforseo.com/v3/serp/google/organic/live/advanced',
    {
      method: 'POST',
      headers,
      body: JSON.stringify(tasks),
    }
  );

  if (!response.ok) {
    throw new Error(`Lỗi DataForSEO: ${response.status}`);
  }

  const data = await response.json();
  return data.tasks.map(parseTaskResult);
}

Dữ Liệu AI Overviews

Cho công cụ AI Overviews Checker:

async function checkAiOverviews(keywords: string[]): Promise<AiOverviewResult[]> {
  const tasks = keywords.map(keyword => ({
    keyword,
    location_code: 2840,  // USA (AI Overviews phổ biến hơn)
    language_code: 'en',
    device: 'desktop',
  }));

  const response = await fetch(
    'https://api.dataforseo.com/v3/serp/google/organic/live/advanced',
    {
      method: 'POST',
      headers,
      body: JSON.stringify(tasks),
    }
  );

  const data = await response.json();

  return data.tasks.map(task => {
    const aiOverview = task.result?.[0]?.items?.find(
      item => item.type === 'ai_overview'
    );

    return {
      keyword: task.data.keyword,
      hasAiOverview: !!aiOverview,
      content: aiOverview?.text || null,
      sources: aiOverview?.references || [],
    };
  });
}

Rate Limiting

DataForSEO rate limit khá rộng rãi, nhưng batch hợp lý vẫn nên làm:

// Xử lý theo batch 100 (kích thước batch khuyến nghị của họ)
async function batchProcess<T, R>(
  items: T[],
  processor: (batch: T[]) => Promise<R[]>,
  batchSize = 100
): Promise<R[]> {
  const results: R[] = [];

  for (let i = 0; i < items.length; i += batchSize) {
    const batch = items.slice(i, i + batchSize);
    const batchResults = await processor(batch);
    results.push(...batchResults);

    // Delay nhỏ giữa các batch
    if (i + batchSize < items.length) {
      await new Promise(resolve => setTimeout(resolve, 100));
    }
  }

  return results;
}

SerpAPI

Tổng Quan

API đơn giản hơn cho tìm kiếm Google. Phù hợp cho tra cứu nhanh, không cần dữ liệu phức tạp.

Cách Dùng Cơ Bản

async function searchGoogle(query: string): Promise<SearchResult> {
  const params = new URLSearchParams({
    q: query,
    location: 'Vietnam',
    hl: 'vi',
    gl: 'vn',
    api_key: process.env.SERPAPI_KEY!,
  });

  const response = await fetch(
    `https://serpapi.com/search.json?${params}`
  );

  if (!response.ok) {
    throw new Error(`Lỗi SerpAPI: ${response.status}`);
  }

  return response.json();
}

Khi Nào Dùng Cái Nào

Use caseProviderLý do
Phân tích từ khóa hàng loạtDataForSEOHỗ trợ batch tốt hơn, nhiều dữ liệu hơn
Tìm kiếm đơn lẻ nhanhSerpAPIĐơn giản hơn, nhanh hơn
AI OverviewsDataForSEOCó dữ liệu AI Overview cụ thể
Dữ liệu lịch sửDataForSEODựa trên task có lưu trữ

Tối Ưu Chi Phí

Cache Response

Dữ liệu SEO không thay đổi từng giây -- SERP hôm nay với 5 phút sau gần như giống nhau. Không cache mà cứ gọi lại thì tiền đội lên vô ích.

import { Redis } from '@upstash/redis';

const redis = new Redis({
  url: process.env.UPSTASH_REDIS_URL!,
  token: process.env.UPSTASH_REDIS_TOKEN!,
});

async function getCachedOrFetch<T>(
  cacheKey: string,
  fetcher: () => Promise<T>,
  ttlSeconds = 3600  // Mặc định 1 giờ
): Promise<T> {
  // Thử cache trước
  const cached = await redis.get<T>(cacheKey);
  if (cached) {
    return cached;
  }

  // Lấy dữ liệu mới
  const data = await fetcher();

  // Cache cho lần sau
  await redis.setex(cacheKey, ttlSeconds, data);

  return data;
}

// Cách dùng
const results = await getCachedOrFetch(
  `serp:${keyword}:${location}`,
  () => fetchSerpResults([keyword]),
  86400  // Cache 24 giờ
);

Chiến Lược Cache Theo Loại Dữ Liệu

Loại dữ liệuTTLLý do
Kết quả SERP24 giờThay đổi hàng ngày
AI Overview hiện diện12 giờCó thể xuất hiện/biến mất
Keyword difficulty7 ngàyThay đổi chậm
Số lượng backlink7 ngàyThay đổi chậm

Loại Bỏ Trùng Lặp

Cùng một từ khóa mà fetch hai lần thì phí tiền. Deduplicate trước khi gọi API:

async function fetchUniqueKeywords(keywords: string[]): Promise<SerpResult[]> {
  // Loại trùng và chuẩn hóa
  const uniqueKeywords = [...new Set(
    keywords.map(k => k.toLowerCase().trim())
  )];

  // Kiểm tra cache cho những gì đã fetch
  const cacheKeys = uniqueKeywords.map(k => `serp:${k}`);
  const cachedResults = await redis.mget<SerpResult[]>(...cacheKeys);

  const toFetch: string[] = [];
  const results: Map<string, SerpResult> = new Map();

  uniqueKeywords.forEach((keyword, i) => {
    if (cachedResults[i]) {
      results.set(keyword, cachedResults[i]);
    } else {
      toFetch.push(keyword);
    }
  });

  // Chỉ fetch những gì còn thiếu
  if (toFetch.length > 0) {
    const freshResults = await fetchSerpResults(toFetch);
    freshResults.forEach(result => {
      results.set(result.keyword.toLowerCase(), result);
    });
  }

  return uniqueKeywords.map(k => results.get(k)!);
}

Xử Lý Lỗi

Các Lỗi API Thường Gặp

LỗiNguyên nhânGiải pháp
401Sai thông tin xác thựcKiểm tra API key/password
402Hết creditNạp thêm tài khoản
429Bị rate limitImplement backoff
500Lỗi phía providerRetry với backoff

Retry Với Exponential Backoff

async function fetchWithRetry<T>(
  fetcher: () => Promise<T>,
  maxRetries = 3
): Promise<T> {
  let lastError: Error | null = null;

  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await fetcher();
    } catch (error) {
      lastError = error as Error;

      // Không retry lỗi client (4xx trừ 429)
      if (error instanceof Error && error.message.includes('4')) {
        if (!error.message.includes('429')) {
          throw error;
        }
      }

      // Exponential backoff: 1s, 2s, 4s
      const delay = Math.pow(2, attempt) * 1000;
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }

  throw lastError;
}

Chuyển Đổi Dữ Liệu

Chuẩn Hóa Response Từ Các Provider

Các provider khác nhau trả về format khác nhau. Chuẩn hóa về domain type của mình để phần còn lại của app không cần biết data từ đâu tới:

// Domain type của mình
interface SerpEntry {
  position: number;
  url: string;
  title: string;
  description: string;
  type: 'organic' | 'featured' | 'ai_overview';
}

// Transformer cho DataForSEO
function transformDataForSeoResult(raw: any): SerpEntry[] {
  return raw.items
    .filter((item: any) => item.type === 'organic')
    .map((item: any) => ({
      position: item.rank_absolute,
      url: item.url,
      title: item.title,
      description: item.description,
      type: 'organic',
    }));
}

// Transformer cho SerpAPI
function transformSerpApiResult(raw: any): SerpEntry[] {
  return raw.organic_results.map((item: any) => ({
    position: item.position,
    url: item.link,
    title: item.title,
    description: item.snippet,
    type: 'organic',
  }));
}

Giám Sát Chi Phí

Theo Dõi Sử Dụng API

// Log lời gọi API với ước tính chi phí
async function trackApiCall(
  provider: 'dataforseo' | 'serpapi',
  endpoint: string,
  itemCount: number
) {
  const costs = {
    dataforseo: {
      serp: 0.002,  // $0.002 mỗi từ khóa
      backlinks: 0.01,
    },
    serpapi: {
      search: 0.005,  // $0.005 mỗi tìm kiếm
    },
  };

  const cost = (costs[provider] as any)[endpoint] * itemCount;

  // Log vào database hoặc monitoring service
  await supabase.from('api_usage').insert({
    provider,
    endpoint,
    item_count: itemCount,
    estimated_cost: cost,
    timestamp: new Date().toISOString(),
  });

  console.log(`Lời gọi API: ${provider}/${endpoint} - ${itemCount} item - $${cost.toFixed(4)}`);
}

Những Lỗi Hay Gặp

Không batch request

1000 lời gọi API riêng lẻ thay vì 10 batch 100 -- chậm gấp mấy lần mà tốn tiền hơn. Tất cả SEO provider đều hỗ trợ batch, dùng đi.

Phớt lờ rate limit

Bắn request liên tục cho tới khi nhận 429, rồi bị đình chỉ truy cập. Rate limiting phía client, tôn trọng giới hạn provider -- phòng bệnh hơn chữa bệnh.

Không cache

Fetch cùng từ khóa nhiều lần trong ngày mà dữ liệu SEO có thay đổi từng phút đâu. Cache với TTL phù hợp, tiết kiệm được kha khá.

Để lộ raw API response

Code frontend tham chiếu tên field đặc thù provider (rank_absolute, organic_results). Ngày nào đó provider đổi format là vỡ khắp nơi. Transform về domain type của mình, để phần còn lại của app không biết data từ DataForSEO hay SerpAPI.

Checklist Đánh Giá

Ổn nếu:

  • Request được batch phù hợp
  • Response được cache
  • Lỗi được xử lý mượt mà
  • Chi phí được giám sát
  • Dữ liệu được transform về domain type
  • Rate limit được tôn trọng

Cần sửa nếu:

  • Gọi API riêng lẻ cho từng từ khóa
  • Cùng dữ liệu bị fetch lặp lại
  • Xuất hiện lỗi 429
  • Không biết chi phí API bao nhiêu
  • Frontend biết về tên field của DataForSEO

Tham Khảo Nhanh

Các API Endpoint Mình Dùng

ProviderEndpointUse case
DataForSEO/v3/serp/google/organic/live/advancedDữ liệu SERP đầy đủ
DataForSEO/v3/keywords_data/google/search_volume/liveLượng tìm kiếm từ khóa
SerpAPI/search.jsonTìm kiếm nhanh

Chi Phí Ước Tính

Thao tácChi phí
DataForSEO SERP (1 từ khóa)$0.002
DataForSEO Keywords (1 từ khóa)$0.001
SerpAPI Search$0.005
Phân tích 1000 từ khóa~$2-5