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
| Provider | Tốt nhất cho | Mô hình giá |
|---|---|---|
| DataForSEO | Dữ liệu SERP toàn diện, AI Overviews | Theo request |
| SerpAPI | Tìm kiếm Google nhanh | Theo search |
| Google APIs | Search Console, Analytics | Miễ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 case | Provider | Lý do |
|---|---|---|
| Phân tích từ khóa hàng loạt | DataForSEO | Hỗ trợ batch tốt hơn, nhiều dữ liệu hơn |
| Tìm kiếm đơn lẻ nhanh | SerpAPI | Đơn giản hơn, nhanh hơn |
| AI Overviews | DataForSEO | Có dữ liệu AI Overview cụ thể |
| Dữ liệu lịch sử | DataForSEO | Dự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ệu | TTL | Lý do |
|---|---|---|
| Kết quả SERP | 24 giờ | Thay đổi hàng ngày |
| AI Overview hiện diện | 12 giờ | Có thể xuất hiện/biến mất |
| Keyword difficulty | 7 ngày | Thay đổi chậm |
| Số lượng backlink | 7 ngày | Thay đổ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ỗi | Nguyên nhân | Giải pháp |
|---|---|---|
| 401 | Sai thông tin xác thực | Kiểm tra API key/password |
| 402 | Hết credit | Nạp thêm tài khoản |
| 429 | Bị rate limit | Implement backoff |
| 500 | Lỗi phía provider | Retry 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
| Provider | Endpoint | Use case |
|---|---|---|
| DataForSEO | /v3/serp/google/organic/live/advanced | Dữ liệu SERP đầy đủ |
| DataForSEO | /v3/keywords_data/google/search_volume/live | Lượng tìm kiếm từ khóa |
| SerpAPI | /search.json | Tìm kiếm nhanh |
Chi Phí Ước Tính
| Thao tác | Chi 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 |