Chủ đề

OG Images trong PHP & Laravel

Hướng dẫn này bao gồm cách sử dụng OG images trong PHP & Laravel với FastOG — ký một URL HMAC trên backend của bạn, đưa nó vào thẻ meta, và các bài viết trên blog của bạn sẽ tự động có Open Graph images động. Dịch vụ theo mô hình đăng ký với tài khoản miễn phí (chọn gói Dynamic API; tạo API key trong dashboard); dùng Free OG Image Tester để xem trước template trước khi tích hợp.

Hướng dẫn đầy đủ để triển khai URL OG image được ký HMAC trong PHP và Laravel.

Bắt đầu nhanh

1. Tạo dịch vụ ký (signer service)

php
// app/Services/QuickogSigner.php
namespace App\Services;

class QuickogSigner
{
    public function generateSignedOgUrl(array $params, array $options = []): string
    {
        $baseUrl = $options['base_url'] ?? config('services.fastog.base_url', 'https://fastog.com/api/v1/og');
        $apiKey = $options['api_key'] ?? config('services.fastog.api_key');
        $hmacSecret = $options['hmac_secret'] ?? config('services.fastog.hmac_secret');

        if (!$apiKey || !$hmacSecret) {
            throw new \RuntimeException('Missing FASTOG_API_KEY or FASTOG_HMAC_SECRET');
        }

        // Canonical query: every param except `s`, including `key`
        $all = array_merge($params, ['key' => $apiKey]);

        $pairs = [];
        foreach ($all as $k => $v) {
            $values = is_array($v) ? $v : [$v];
            sort($values, SORT_STRING);
            foreach ($values as $vv) {
                $pairs[] = rawurlencode((string) $k) . '=' . rawurlencode((string) $vv);
            }
        }
        sort($pairs, SORT_STRING);
        $canonical = implode('&', $pairs);
        $signature = hash_hmac('sha256', 'GET/api/v1/og' . "\n" . $canonical, $hmacSecret);

        return $baseUrl . '?' . http_build_query(array_merge($all, ['s' => $signature]), '', '&', PHP_QUERY_RFC3986);
    }
}

Tại sao dùng rawurlencode? Truy vấn chuẩn được mã hóa phần trăm theo RFC-3986 (khoảng trắng → %20, không bao giờ +). rawurlencode khớp chính xác điều này — urlencode thì không (nó phát ra +), điều này làm hỏng chữ ký và trả về invalid_signature. Truyền PHP_QUERY_RFC3986 cho http_build_query giữ URL cuối cùng ở cùng một kiểu mã hóa.

2. Cấu hình

php
// config/services.php
return [
    // ...
    'fastog' => [
        'api_key' => env('FASTOG_API_KEY'),
        'hmac_secret' => env('FASTOG_HMAC_SECRET'),
        'base_url' => env('FASTOG_BASE_URL', 'https://fastog.com/api/v1/og'),
    ],
];
bash
# .env
FASTOG_API_KEY=sk_your_full_api_key_here
FASTOG_HMAC_SECRET=your_64_char_hex_secret_here

3. Đăng ký dưới dạng singleton

php
// app/Providers/AppServiceProvider.php
use App\Services\QuickogSigner;

public function register(): void
{
    $this->app->singleton(QuickogSigner::class);
}

Laravel Blade

blade
{{-- resources/views/blog/show.blade.php --}}
@extends('layouts.app')

@section('head')
    <meta property="og:image" content="{{ $ogImageUrl }}" />
    <meta property="og:title" content="{{ $post->title }}" />
    <meta property="og:description" content="{{ $post->excerpt }}" />
@endsection

@section('content')
    <article>
        <h1>{{ $post->title }}</h1>
        <p>{{ $post->excerpt }}</p>
    </article>
@endsection

Controller

php
// app/Http/Controllers/BlogController.php
namespace App\Http\Controllers;

use App\Services\QuickogSigner;
use App\Models\Post;

class BlogController extends Controller
{
    public function show(Post $post, QuickogSigner $signer)
    {
        $ogImageUrl = $signer->generateSignedOgUrl([
            'template' => 'blog',
            'title' => $post->title,
            'subtitle' => $post->excerpt,
            'author' => $post->author->name,
        ]);

        return view('blog.show', compact('post', 'ogImageUrl'));
    }
}

API Route

php
// routes/api.php
use App\Services\QuickogSigner;
use Illuminate\Http\Request;

Route::get('/og', function (Request $request, QuickogSigner $signer) {
    $validated = $request->validate([
        'title' => 'required|string',
        'template' => 'nullable|string',
    ]);

    $ogImageUrl = $signer->generateSignedOgUrl([
        'template' => $validated['template'] ?? 'default',
        'title' => $validated['title'],
    ]);

    return response()->json(['ogImageUrl' => $ogImageUrl]);
});

Ví dụ về Thương mại điện tử

php
// app/Http/Controllers/ProductController.php
public function show(Product $product, QuickogSigner $signer)
{
    $ogImageUrl = $signer->generateSignedOgUrl([
        'template' => 'ecommerce',
        'title' => $product->name,
        'price' => $product->formatted_price,
        'image' => $product->image_url,
    ]);

    return view('products.show', compact('product', 'ogImageUrl'));
}

Ví dụ Bài Đăng Blog

php
// app/Http/Controllers/ProfileController.php
public function show(User $user, QuickogSigner $signer)
{
    $ogImageUrl = $signer->generateSignedOgUrl([
        'template' => 'blog',
        'title' => $user->name,
        'subtitle' => $user->bio,
        'author' => $user->name,
    ]);

    return view('profile.show', compact('user', 'ogImageUrl'));
}

Plain PHP (Không sử dụng Framework)

php
// lib/fastog-signer.php
function generateSignedOgUrl(array $params, array $options = []): string
{
    $baseUrl = $options['base_url'] ?? 'https://fastog.com/api/v1/og';
    $apiKey = $options['api_key'] ?? $_ENV['FASTOG_API_KEY'];
    $hmacSecret = $options['hmac_secret'] ?? $_ENV['FASTOG_HMAC_SECRET'];

    $all = array_merge($params, ['key' => $apiKey]);

    $pairs = [];
    foreach ($all as $k => $v) {
        $values = is_array($v) ? $v : [$v];
        sort($values, SORT_STRING);
        foreach ($values as $vv) {
            $pairs[] = rawurlencode((string) $k) . '=' . rawurlencode((string) $vv);
        }
    }
    sort($pairs, SORT_STRING);
    $canonical = implode('&', $pairs);
    $signature = hash_hmac('sha256', 'GET/api/v1/og' . "\n" . $canonical, $hmacSecret);

    return $baseUrl . '?' . http_build_query(array_merge($all, ['s' => $signature]), '', '&', PHP_QUERY_RFC3986);
}
php
// index.php
require_once 'lib/fastog-signer.php';

$ogUrl = generateSignedOgUrl([
    'template' => 'blog',
    'title' => 'My Blog Post',
]);
?>
<!DOCTYPE html>
<html>
<head>
    <meta property="og:image" content="<?= htmlspecialchars($ogUrl) ?>" />
    <meta property="og:title" content="My Blog Post" />
</head>
<body>
    <h1>My Blog Post</h1>
</body>
</html>

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

Ký Sai Nội Dung

❌ Sai: hash_hmac('sha256', 'GET/api/v1/og' . $canonical, $hmacSecret) — thiếu dấu xuống dòng phân tách. ✅ Đúng: hash_hmac('sha256', 'GET/api/v1/og' . "\n" . $canonical, $hmacSecret) — method + đường dẫn, một dấu xuống dòng, sau đó là chuỗi truy vấn chuẩn hóa.

Bỏ key Khỏi Chữ Ký

❌ Sai: chỉ ký các tham số nội dung của bạn rồi thêm key vào URL sau đó. ✅ Đúng: key được hợp nhất vào các tham số trước khi chuỗi truy vấn chuẩn hóa được xây dựng và xuất hiện trong URL cuối cùng.

Tham Số Lặp Lại (Mảng)

Truyền giá trị mảng cho generateSignedOgUrl và để http_build_query ghi URL: nó tạo ra các khóa có dấu ngoặc (features[0]=a&features[1]=b) trong khi chuỗi chuẩn hóa ký các khóa lặp lại dạng thường (features=a&features=b). Đừng tự tay xây dựng features=a&features=b trong URL — PHP giải mã các khóa lặp lại thành một giá trị vô hướng (giá trị cuối cùng được giữ), vì vậy máy chủ xây dựng lại một chuỗi chuẩn hóa khác và từ chối với invalid_signature.

401 missing_signature / invalid_signature

missing_signature (401) nghĩa là key hoặc s bị thiếu trong URL. invalid_signature (401) nghĩa là chuỗi chuẩn hóa không khớp — hãy xác nhận dấu xuống dòng trong nội dung đã ký, cùng một hmac_secret ở cả hai phía, rawurlencode (không phải urlencode) cho mọi khóa và giá trị, cũng như các khóa có dấu ngoặc cho các tham số lặp lại trong URL.

Lộ Bí Mật HMAC

Không bao giờ để lộ FASTOG_HMAC_SECRET trong JavaScript phía frontend hoặc các tệp cấu hình công khai.

Tài liệu tham khảo API đầy đủ

Dùng thử miễn phí — không cần đăng ký

Xem trước và kiểm tra hình ảnh OG động trong vài giây.

Mở Trình kiểm tra hình ảnh OG miễn phí

A BeCaSimulations site