Integración con Agentes y LLM — FastOG
Una guía completa y autónoma para integrar FastOG desde cualquier lenguaje del lado del servidor, escrita tanto para desarrolladores humanos como para agentes de codificación de IA. Toda la superficie del cliente es una única función: signedUrl(params, key, secret) → una URL <meta property="og:image"> lista para incrustar.
Descargar la skill
Llévate la skill completa contigo: coloca ambos archivos en tu directorio agents/skills/fastog-integration/ para cargarla en Claude Code, Codex, o cualquier agente:
SKILL.md—curl -O https://fastog.com/skill/SKILL.mdimplementations.md—curl -O https://fastog.com/skill/implementations.md
¿Prefieres un archivo combinado? Esta página es la skill (contrato + implementaciones en línea): curl -O https://fastog.com/docs-files/agent-integration.md
El contrato (el único)
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))
Invariantes (romper cualquiera → 401)
- Codificación RFC-3986 — codifica con porcentaje todo excepto
A-Za-z0-9-._~; espacio →%20, nunca+. PHPrawurlencode; JSencodeURIComponent+ recodifica!'()*; Pythonquote(safe='-._~'); Ruby/Go codificador de bytes personalizado. keyestá firmado — forma parte de la consulta canónica, por lo que la firma está vinculada a la clave.- Los arrays necesitan claves entre corchetes en la URL — firma el canónico con la clave simple (
features=…&features=…), pero la URL debe usarfeatures[0]=…ofeatures[]=…. El backend es PHP: las claves repetidas sin corchetes (features=a&features=b) se decodifican como un escalar (gana la última) → discrepancia canónica → 401. - Sin marca de tiempo — direccionado por contenido. Parámetros idénticos → URL idéntica → cacheable para siempre; cualquier cambio → 401 → sin renderizado, sin consumo de cuota.
Parámetros reservados: s (firma — excluida del canónico), key (incluida), t (reservado — no usar nunca).
Seguridad (no negociable)
hmac_secretes privada — solo en el servidor. Nunca en un navegador, nunca en un script de CDN público. La firma en cliente/navegador nunca puede funcionar.keyes pública (aparece en cada URL) — esperado y seguro, porque falsificarla requiere el secreto.- Solo GET (los crawlers nunca hacen POST). Firma en el servidor en el momento de la petición; la URL firmada es lo que incrustas.
Ejemplo práctico
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
Firmadores de un solo archivo (cero dependencias, sin gestor de paquetes)
Todavía no existe un paquete npm i/pip install — estos son firmadores de un solo archivo: cada uno se descarga como un único archivo y solo produce la URL de la imagen OG firmada (toda la lógica HMAC + RFC-3986, sin runtime). Descarga y firma en el servidor del cliente:
| Lenguaje/Framework | Archivo | Una línea |
|---|---|---|
| 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) |
Incrusta el resultado:
<meta property="og:image" content="https://fastog.com/api/v1/og?…&key=…&s=…" />
Vector dorado (autoverificación de CUALQUIER implementación)
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
Una implementación es correcta si y solo si produce ese canónico y esa firma para esas entradas, y un parámetro o clave diferente debe cambiar la firma. El vector dorado lo genera el propio servidor de FastOG, por lo que es la fuente de verdad.
Implementaciones de referencia (sin SDK / cualquier lenguaje)
JavaScript (Node.js, CommonJS)
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
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
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
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
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, "&")
}
Depuración de errores 401
| Código | Significado | Solución |
|---|---|---|
missing_signature |
Falta key o s en la URL |
vuelve a generar con un firmante (signer); verifica que ambos parámetros estén presentes |
invalid_signature |
Discrepancia canónica o secreto incorrecto | vuelve a firmar después de CUALQUIER cambio de contenido; confirma el mismo hmac_secret en ambos lados; comprueba que el espacio sea %20 (no +); verifica que los arrays usen claves entre corchetes en la URL; confirma que key esté en el canónico |