トピック

エージェント&LLM統合 — FastOG

FastOGをあらゆるサーバーサイド言語から統合するための、完全かつ自己完結型のガイドです。人間の開発者とAIコーディングエージェントの両方を対象としています。クライアントの全表面は1つの関数、signedUrl(params, key, secret) → 埋め込み可能な<meta property="og:image"> URL、で構成されています。

スキルのダウンロード

スキル全体をそのまま持ち運べます。agents/skills/fastog-integration/ ディレクトリに両方のファイルを配置すると、Claude Code、Codex、または任意のエージェントで読み込めます。

  • SKILL.md — curl -O https://fastog.com/skill/SKILL.md
  • implementations.md — curl -O https://fastog.com/skill/implementations.md

1つのファイルにまとめたい場合は?このページ自体がスキルです(契約と実装がインラインで含まれています):curl -O https://fastog.com/docs-files/agent-integration.md

契約(唯一のもの)

URL        = https://fastog.com/api/v1/og?<content>&key=<apiKey>&s=<signature>
canonical  = every query param except `s` (INCLUDING `key`), keys sorted,
             array values sorted, each pair enc(key)=enc(value) RFC-3986,
             joined with "&"
message    = "GET/api/v1/og" + "\n" + canonical
signature  = hex(HMAC-SHA256(message, hmac_secret))

不変条件(いずれかに違反すると401)

  1. RFC-3986エンコーディング — A-Za-z0-9-._~以外はすべてパーセントエンコードする。スペースは%20、決して+ではない。PHPはrawurlencode、JSはencodeURIComponent+!'()*の再エンコード、Pythonはquote(safe='-._~')、Ruby/Goはカスタムバイトエンコーダー。
  2. keyは署名対象 — 正規クエリの一部であるため、署名はキーに紐づく。
  3. 配列はURL内でブラケットキーが必要 — 正規化はプレーンキー(features=…&features=…)で署名するが、URLはfeatures[0]=…またはfeatures[]=…を使用する必要がある。バックエンドはPHPであり、ブラケットなしの繰り返しキー(features=a&features=b)はスカラーにデコードされる(最後が優先)→ 正規化の不一致 → 401。
  4. タイムスタンプなし — コンテンツアドレス方式。同一パラメータ → 同一URL → 永久にキャッシュ可能。変更があれば → 401 → レンダリングなし、クォータ消費なし。

予約パラメータ: s(署名 — 正規化から除外)、key(含まれる)、t(予約済み — 使用禁止)。

セキュリティ(交渉の余地なし)

  • hmac_secretは秘密 — サーバーサイドのみ。ブラウザや公開CDNスクリプトには絶対に置かない。クライアントサイド/ブラウザでの署名は決して機能しない。
  • keyは公開(すべてのURLに出現する)— 想定どおりで安全。偽造には秘密鍵が必要だからである。
  • GETのみ(クローラーはPOSTしない)。リクエスト時にサーバー側で署名し、署名付きURLを埋め込む。

実例

params    = {template:"blog", title:"Hello World", features:["Free shipping","30-day returns"], key:"sk_test123"}
secret    = 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef

canonical = features=30-day%20returns&features=Free%20shipping&key=sk_test123&template=blog&title=Hello%20World
signature = a79232b15b45002aee76174f542e0a661b37cb1259df01e263c952ca4052a54d
url       = https://fastog.com/api/v1/og?template=blog&title=Hello%20World&features%5B0%5D=Free%20shipping&features%5B1%5D=30-day%20returns&key=sk_test123&s=a79232b15b45002aee76174f542e0a661b37cb1259df01e263c952ca4052a54d

単一ファイルサイナー(依存関係ゼロ、パッケージマネージャー不要)

npm i/pip installパッケージはまだ存在しません。これらは単一ファイルサイナーです。各ファイルは単一ファイルとしてダウンロードされ、署名済みのOG画像URLのみを生成します(HMAC + RFC-3986ロジックをすべて含み、ランタイムは不要)。クライアントのサーバー上でダウンロードして署名します。

言語/フレームワーク ファイル ワンライナー
Node.js (CJS) / Express https://fastog.com/sdk/node.js · /sdk/express/fastog.js ogImageUrl(params, { key, secret })
ESM (SvelteKit / Next / Astro / Vue-Nuxt) /sdk/svelte/fastog.js · /sdk/next/fastog.js · /sdk/astro/fastog.js · /sdk/vue/fastog.js ogImageUrl(params, { key, secret })
PHP https://fastog.com/sdk/php/fastog.php.txt FastOg::signedUrl($params, $key, $secret)
Ruby / Rails https://fastog.com/sdk/ruby/fastog.rb FastOg.signed_url(params, key, secret)
Python https://fastog.com/sdk/python/fastog.py fastog.signed_url(params, key, secret)
Go https://fastog.com/sdk/go/fastog.go fastog.SignedURL(params, key, secret)

結果を埋め込みます:

html
<meta property="og:image" content="https://fastog.com/api/v1/og?…&key=…&s=…" />

ゴールデンベクター(任意の実装を自己検証するための基準)

params    = {template:"blog", title:"Hello World", features:["Free shipping","30-day returns"], key:"sk_test123"}
secret    = 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
canonical = features=30-day%20returns&features=Free%20shipping&key=sk_test123&template=blog&title=Hello%20World
signature = a79232b15b45002aee76174f542e0a661b37cb1259df01e263c952ca4052a54d

ある実装が正しいのは、その入力に対してそのカノニカル値とその署名を生成する場合、かつその場合に限ります。また、異なるパラメータやキーを使用した場合には署名が変化する必要があります。ゴールデンベクターはFastOGサーバー自体によって生成されるため、これが基準となる真実(ground truth)です。

リファレンス実装(SDKなし/任意の言語)

JavaScript(Node.js、CommonJS)

js
const crypto = require("crypto");

const enc = (s) =>
    encodeURIComponent(String(s)).replace(
        /[!'()*]/g,
        (c) => "%" + c.charCodeAt(0).toString(16).toUpperCase(),
    );

function canonicalize(params, apiKey) {
    const all = { ...params, key: apiKey };
    const pairs = [];
    for (const [k, v] of Object.entries(all)) {
        for (const vv of Array.isArray(v) ? [...v].sort() : [v]) {
            pairs.push(`${enc(k)}=${enc(vv)}`);
        }
    }
    return pairs.sort().join("&");
}

function sign(params, apiKey, secret) {
    return crypto
        .createHmac("sha256", secret)
        .update(`GET/api/v1/og\n${canonicalize(params, apiKey)}`)
        .digest("hex");
}

function signedUrl(
    params,
    apiKey,
    secret,
    baseUrl = "https://fastog.com/api/v1/og",
) {
    const url = new URL(baseUrl);
    for (const [k, v] of Object.entries({ ...params, key: apiKey })) {
        const isArray = Array.isArray(v);
        for (const vv of isArray ? v : [v]) {
            url.searchParams.append(isArray ? `${k}[]` : k, vv);
        }
    }
    url.searchParams.set("s", sign(params, apiKey, secret));
    return url.toString();
}

module.exports = { signedUrl, sign, canonicalize };

PHP

php
function fastog_canonicalize(array $params, string $apiKey): string
{
    $pairs = [];
    foreach (array_merge($params, ['key' => $apiKey]) 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);
    return implode('&', $pairs);
}

function fastog_sign(array $params, string $apiKey, string $secret): string
{
    return hash_hmac('sha256', 'GET/api/v1/og' . "\n" . fastog_canonicalize($params, $apiKey), $secret);
}

function fastog_signed_url(array $params, string $apiKey, string $secret, string $baseUrl = 'https://fastog.com/api/v1/og'): string
{
    $all = array_merge($params, ['key' => $apiKey, 's' => fastog_sign($params, $apiKey, $secret)]);
    return $baseUrl . '?' . http_build_query($all, '', '&', PHP_QUERY_RFC3986);
}

Python

python
import hashlib, hmac
from urllib.parse import quote

_enc = lambda s: quote(str(s), safe="-._~")

def canonicalize(params, api_key):
    pairs = []
    for k, v in {**params, "key": api_key}.items():
        for vv in sorted(v) if isinstance(v, list) else [v]:
            pairs.append(f"{_enc(k)}={_enc(vv)}")
    return "&".join(sorted(pairs))

def sign(params, api_key, secret):
    return hmac.new(secret.encode(), f"GET/api/v1/og\n{canonicalize(params, api_key)}".encode(), hashlib.sha256).hexdigest()

def signed_url(params, api_key, secret, base_url="https://fastog.com/api/v1/og"):
    all_p = {**params, "key": api_key, "s": sign(params, api_key, secret)}
    pairs = []
    for k, v in all_p.items():
        if isinstance(v, list):
            for i, vv in enumerate(v):
                pairs.append(f"{_enc(f'{k}[{i}]')}={_enc(vv)}")
        else:
            pairs.append(f"{_enc(k)}={_enc(v)}")
    return f"{base_url}?{'&'.join(pairs)}"

Ruby

ruby
require "openssl"

def enc(s)
  s.to_s.gsub(/[^A-Za-z0-9\-._~]/) { |c| c.bytes.map { |b| "%%%02X" % b }.join }
end

def canonicalize(params, api_key)
  pairs = params.merge("key" => api_key).flat_map do |k, v|
    values = v.is_a?(Array) ? v.sort : [v]
    values.map { |vv| "#{enc(k)}=#{enc(vv)}" }
  end
  pairs.sort.join("&")
end

def sign(params, api_key, secret)
  OpenSSL::HMAC.hexdigest("SHA256", secret, "GET/api/v1/og\n#{canonicalize(params, api_key)}")
end

def signed_url(params, api_key, secret, base_url = "https://fastog.com/api/v1/og")
  all = params.merge("key" => api_key, "s" => sign(params, api_key, secret))
  pairs = all.flat_map do |k, v|
    if v.is_a?(Array)
      v.each_with_index.map { |vv, i| "#{enc("#{k}[#{i}]")}=#{enc(vv)}" }
    else
      ["#{enc(k)}=#{enc(v)}"]
    end
  end
  "#{base_url}?#{pairs.join("&")}"
end

Go

go
import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"sort"
	"strings"
)

func enc(s string) string {
	const hex = "0123456789ABCDEF"
	var b strings.Builder
	for i := 0; i < len(s); i++ {
		c := s[i]
		if (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '.' || c == '_' || c == '~' {
			b.WriteByte(c)
		} else {
			b.WriteByte('%')
			b.WriteByte(hex[c>>4])
			b.WriteByte(hex[c&0x0f])
		}
	}
	return b.String()
}

func canonicalize(params map[string]any, apiKey string) string {
	all := map[string]any{}
	for k, v := range params {
		all[k] = v
	}
	all["key"] = apiKey
	keys := make([]string, 0, len(all))
	for k := range all {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	var pairs []string
	for _, k := range keys {
		vals := []string{fmt.Sprint(all[k])}
		if arr, ok := all[k].([]string); ok {
			vals = arr
			sort.Strings(vals)
		}
		for _, v := range vals {
			pairs = append(pairs, enc(k)+"="+enc(v))
		}
	}
	sort.Strings(pairs)
	return strings.Join(pairs, "&")
}

func sign(params map[string]any, apiKey, secret string) string {
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte("GET/api/v1/og\n" + canonicalize(params, apiKey)))
	return hex.EncodeToString(mac.Sum(nil))
}

func signedURL(params map[string]any, apiKey, secret, baseURL string) string {
	all := map[string]any{}
	for k, v := range params {
		all[k] = v
	}
	all["key"] = apiKey
	all["s"] = sign(params, apiKey, secret)
	keys := make([]string, 0, len(all))
	for k := range all {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	var pairs []string
	for _, k := range keys {
		if arr, ok := all[k].([]string); ok {
			for i, v := range arr {
				pairs = append(pairs, enc(k+"["+fmt.Sprint(i)+"]")+"="+enc(v))
			}
		} else {
			pairs = append(pairs, enc(k)+"="+enc(fmt.Sprint(all[k])))
		}
	}
	return baseURL + "?" + strings.Join(pairs, "&")
}

401エラーのデバッグ

コード 意味 修正方法
missing_signature URLにkeyまたはsがありません 署名者(signer)で再生成し、両方のパラメータが存在することを確認してください
invalid_signature カノニカル不一致またはシークレットの誤り コンテンツを変更した場合は必ず再署名してください。両側で同じhmac_secretを確認し、スペースが%20(+ではない)であることを確認し、配列がURL内でブラケットキーを使用していることを確認し、keyがカノニカルに含まれていることを確認してください

無料で試す — サインアップ不要

動的OG画像を数秒でプレビューおよびテスト。

無料のOG画像テスターを開く

A BeCaSimulations site