Imágenes OG en Python
Esta guía te muestra cómo generar imágenes OG en Python con FastOG — Django, FastAPI o Flask — firmando una URL HMAC y renderizando imágenes Open Graph dinámicas sobre la marcha. Basado en suscripción con una cuenta gratuita (elige un plan de Dynamic API; crea tu clave de API en el dashboard); usa el Probador de Imágenes OG Gratuito para ver una plantilla antes de integrar.
Guía completa para implementar URLs de imágenes OG firmadas con HMAC en Python.
Inicio rápido
1. Crear el módulo del firmante
# utils/fastog.py
import hmac
import hashlib
import os
from urllib.parse import quote, urlencode
def _enc(s):
return quote(str(s), safe='-._~') # RFC-3986: only unreserved chars
def generate_signed_og_url(params: dict, options: dict = None) -> str:
options = options or {}
base_url = options.get('base_url', os.environ.get('FASTOG_BASE_URL', 'https://fastog.com/api/v1/og'))
api_key = options.get('api_key', os.environ.get('FASTOG_API_KEY'))
hmac_secret = options.get('hmac_secret', os.environ.get('FASTOG_HMAC_SECRET'))
if not api_key or not hmac_secret:
raise ValueError("Missing FASTOG_API_KEY or FASTOG_HMAC_SECRET")
all_params = {**params, 'key': api_key}
pairs = []
for k, v in all_params.items():
for vv in sorted(v if isinstance(v, list) else [v]):
pairs.append(f"{_enc(k)}={_enc(vv)}")
canonical = '&'.join(sorted(pairs))
signature = hmac.new(
hmac_secret.encode(),
f"GET/api/v1/og\n{canonical}".encode(),
hashlib.sha256
).hexdigest()
return f"{base_url}?{urlencode({**all_params, 's': signature})}"
2. Variables de entorno
# .env
FASTOG_API_KEY=sk_your_full_api_key_here
FASTOG_HMAC_SECRET=your_64_char_hex_secret_here
pip install python-dotenv
# config.py
from dotenv import load_dotenv
load_dotenv()
Django
Servicio de firma (Signer Service)
# services/fastog.py
from utils.fastog import generate_signed_og_url
class QuickogService:
@staticmethod
def get_og_url(template: str, **params) -> str:
return generate_signed_og_url({
'template': template,
**params,
})
Vista (View)
# views.py
from django.shortcuts import render
from services.fastog import QuickogService
def blog_post(request, slug):
post = get_post(slug)
og_image_url = QuickogService.get_og_url(
template='blog',
title=post.title,
subtitle=post.excerpt,
author=post.author.name,
)
return render(request, 'blog/post.html', {
'post': post,
'og_image_url': og_image_url,
})
Plantilla (Template)
{# templates/blog/post.html #}
<!DOCTYPE html>
<html>
<head>
<meta property="og:image" content="{{ og_image_url }}" />
<meta property="og:title" content="{{ post.title }}" />
<meta property="og:description" content="{{ post.excerpt }}" />
</head>
<body>
<h1>{{ post.title }}</h1>
<p>{{ post.excerpt }}</p>
</body>
</html>
Etiqueta de plantilla personalizada (Custom Template Tag)
# templatetags/fastog_tags.py
from django import template
from services.fastog import QuickogService
register = template.Library()
@register.simple_tag
def og_image_url(template_name, **params):
return QuickogService.get_og_url(template_name, **params)
{# In template #}
{% load fastog_tags %}
<meta property="og:image" content="{% og_image_url 'blog' title=post.title subtitle=post.excerpt %}" />
FastAPI
# main.py
from fastapi import FastAPI, Query
from pydantic import BaseModel
from utils.fastog import generate_signed_og_url
app = FastAPI()
class OgRequest(BaseModel):
title: str
template: str = "default"
subtitle: str | None = None
@app.get("/api/og")
def generate_og(title: str = Query(...), template: str = Query("default")):
og_url = generate_signed_og_url({
'template': template,
'title': title,
})
return {"ogImageUrl": og_url}
@app.get("/blog/{slug}")
async def blog_post(slug: str):
post = await get_post(slug)
og_url = generate_signed_og_url({
'template': 'blog',
'title': post.title,
'subtitle': post.excerpt,
})
return {"post": post, "ogImageUrl": og_url}
Flask
# app.py
from flask import Flask, render_template, request, jsonify
from utils.fastog import generate_signed_og_url
app = Flask(__name__)
@app.route("/blog/<slug>")
def blog_post(slug):
post = get_post(slug)
og_image_url = generate_signed_og_url({
'template': 'blog',
'title': post['title'],
'subtitle': post['excerpt'],
})
return render_template('blog/post.html', post=post, og_image_url=og_image_url)
@app.route("/api/og")
def api_og():
title = request.args.get('title')
template = request.args.get('template', 'default')
if not title:
return jsonify({'error': 'Missing title'}), 400
og_url = generate_signed_og_url({
'template': template,
'title': title,
})
return jsonify({'ogImageUrl': og_url})
Ejemplo de comercio electrónico
# views.py
def product_detail(request, product_id):
product = get_product(product_id)
og_image_url = generate_signed_og_url({
'template': 'ecommerce',
'title': product.name,
'price': product.formatted_price,
'image': product.image_url,
})
return render(request, 'products/detail.html', {
'product': product,
'og_image_url': og_image_url,
})
Ejemplo de Entrada de Blog
def user_profile(request, username):
user = get_user(username)
og_image_url = generate_signed_og_url({
'template': 'blog',
'title': user.name,
'subtitle': user.bio,
'author': user.name,
})
return render(request, 'profile/show.html', {
'user': user,
'og_image_url': og_image_url,
})
Errores comunes
Firmar el mensaje equivocado
❌ Incorrecto: f"GET/api/v1/og{canonical}" — falta el separador de nueva línea.
✅ Correcto: f"GET/api/v1/og\n{canonical}" — método + ruta, una nueva línea y, después, la consulta canónica.
Dejar key fuera de la firma
❌ Incorrecto: firmar solo los parámetros de contenido y añadir key a la URL después.
✅ Correcto: key se integra en los parámetros antes de construir la consulta canónica y aparece en la URL final.
Olvidar codificar claves y valores con URL
❌ Incorrecto: construir la consulta canónica con valores sin codificar.
✅ Correcto: codifica tanto claves como valores con porcentaje según RFC-3986 (safe='-._~') antes de firmar — usa quote, no quote_plus (que emite + para los espacios y no coincidirá con el servidor).
401 missing_signature / invalid_signature
missing_signature (401) significa que key o s falta en la URL. invalid_signature (401) significa que la consulta canónica no coincide — confirma la nueva línea en el mensaje firmado, el mismo hmac_secret en ambos lados y quote(safe='-._~') (no quote_plus) para cada clave y valor.