CMS y webs
Sube imágenes a Ghost y publica el post correcto
Flujo con la Admin API de Ghost: firma el JWT, sube imagenes a /ghost/api/admin/images/upload y crea o actualiza un post con feature_image.
- Ghost
- Admin API
- JWT
- images upload
- feature_image
- SocialCutter
- Node
El problema: una portada, muchas redes
Un post de Ghost lleva una feature_image que se reutiliza al compartir en redes y en las tarjetas del tema. Si esa imagen no tiene las medidas correctas, cada red la recorta a su manera. El mismo problema aparece con las imágenes dentro del cuerpo del post.
El flujo de esta guía procesa un solo maestro con SocialCutter y sube a Ghost la salida correcta, de modo que la portada y las imágenes del post salgan en su medida.
Requisitos e integración
Necesitas una integración personalizada de Ghost (Settings → Integrations → Add custom integration). De ahí sale la Admin API key, con forma id:secret. La Admin API vive bajo /ghost/api/admin/ en la misma instalación que tu blog.
1. Firmar el JWT con la Admin API key
Ghost no usa la clave directamente: se firma un JWT HS256 por petición. El id va en la cabecera kid, el secret (decodificado de hexadecimal) es la clave de firma y el token caduca en 5 minutos como máximo. Snippet en Node:
import jwt from 'jsonwebtoken'
const [id, secret] = process.env.GHOST_ADMIN_API_KEY.split(':')
const token = jwt.sign({}, Buffer.from(secret, 'hex'), {
keyid: id,
algorithm: 'HS256',
expiresIn: '5m',
audience: '/admin/'
})
console.log(token)
Para usarlo desde la terminal, genera el token con node y guárdalo en una variable:
export GHOST_URL="https://tu-blog.com"
export GHOST_ADMIN_API_KEY="id:secret"
TOKEN=$(node -e "const jwt=require('jsonwebtoken');const [id,secret]=process.env.GHOST_ADMIN_API_KEY.split(':');console.log(jwt.sign({},Buffer.from(secret,'hex'),{keyid:id,algorithm:'HS256',expiresIn:'5m',audience:'/admin/'}))")
2. Aviso de versión de la API
La Admin API se versiona por cabecera. Envía Accept-Version: v6.0 (o v5.0 según tu instalación):
curl -s "$GHOST_URL/ghost/api/admin/site/" \
-H "Authorization: Ghost $TOKEN" \
-H "Accept-Version: v6.0" | jq
El número sigue al major de Ghost. Si envías una versión que no corresponde, el formato de algunas respuestas cambia. Fija la versión que devuelva tu instalación y revisa la documentación oficial en https://ghost.org/docs/admin-api/ cuando actualices Ghost.
3. Procesar el maestro con SocialCutter
Antes de subir nada, procesa el maestro para obtener la medida de portada. Para una feature_image de red social suele servir un 1.91:1:
curl -s -X POST "https://api.socialcutter.theboomer.dev/api/v1/images/process" \
-H "X-API-Key: *** \
-H "Content-Type: application/json" \
-d '{
"source": { "type": "url", "value": "https://tu-cdn.com/maestro.jpg" },
"destinations": [
{ "platform": "linkedin", "format": "post" },
{ "platform": "facebook", "format": "link" }
]
}' > sc.json
jq '.image_id, (.outputs[] | {url, platform, format, width, height})' sc.json
Descarga la salida que quieras subir:
curl -s -o portada.jpg "$(jq -r '.outputs[0].url' sc.json)"
4. Subir la imagen a Ghost
Sube el fichero a /ghost/api/admin/images/upload/ en multipart, con Content-Type: multipart/form-data. La documentación de Ghost define tres campos en ese formulario:
file(obligatorio): los datos de la imagen, como Blob o File. Se sube una imagen por petición.purpose(opcional, por defectoimage): el uso previsto, que cambia las validaciones. Admitidosimage,profile_imageeicon. Los formatos soportados en los tres casos son WEBP, JPEG, GIF, PNG y SVG;profile_imagedebe ser cuadrada, eicontambién, además de admitir ICO.ref(opcional): una referencia, por ejemplo la ruta del fichero original. Ghost la devuelve tal cual, lo que sirve para reemplazar rutas locales por las URL ya subidas.
Todo va con el mismo JWT del paso 1:
curl -s -X POST "$GHOST_URL/ghost/api/admin/images/upload/" \
-H "Authorization: Ghost $TOKEN" \
-H "Accept-Version: v6.0" \
-F "file=@./portada.jpg" \
-F "purpose=image" \
-F "ref=portada.jpg" > img.json
jq '.images[0] | {url, ref}' img.json
La respuesta trae una lista images, cada una con su url (la dirección desde la que se puede recuperar) y su ref. Usa esa url como feature_image. Con el adaptador de almacenamiento por defecto, Ghost guarda el fichero en /content/images/ sin más cambios que la limpieza del nombre del fichero.
5. Crear el post con feature_image
FEATURE=$(jq -r '.images[0].url' img.json)
curl -s -X POST "$GHOST_URL/ghost/api/admin/posts/?source=html" \
-H "Authorization: Ghost $TOKEN" \
-H "Accept-Version: v6.0" \
-H "Content-Type: application/json" \
-d "{
\"posts\": [{
\"title\": \"Entrada de ejemplo\",
\"html\": \"<p>Contenido del post.</p>\",
\"feature_image\": \"$FEATURE\",
\"status\": \"draft\"
}]
}" | jq '.posts[0] | {id, updated_at, feature_image}'
El parámetro ?source=html indica que html ya es HTML renderizado. Guarda el id y el updated_at que devuelve la respuesta: los necesitas para actualizar.
feature_image, og_image y twitter_image
El objeto de una entrada de Ghost expone tres campos de imagen distintos, y no son sinónimos:
| Campo | Para qué es |
|---|---|
feature_image | La portada de la entrada: la que usan el tema y las tarjetas del feed. Va acompañada de feature_image_alt y feature_image_caption. |
og_image | La imagen de la tarjeta de Open Graph. Ghost documenta la del sitio como la que “se usa al compartir en Facebook y en la web”. |
twitter_image | La imagen de la tarjeta de X. |
Cada una tiene además su título y su descripción: og_title, og_description, twitter_title y twitter_description. Como son campos independientes, puedes llevar la portada al recorte que pide tu tema y a las tarjetas el 1.91:1 que suelen pedir las redes. Sube cada salida de SocialCutter con el paso 4 y reparte las URL:
curl -s -X POST "$GHOST_URL/ghost/api/admin/posts/" \
-H "Authorization: Ghost $TOKEN" \
-H "Accept-Version: v6.0" \
-H "Content-Type: application/json" \
-d "{\"posts\":[{\"title\":\"Entrada de ejemplo\",\"feature_image\":\"$FEATURE\",\"og_image\":\"$OG_IMAGE\",\"twitter_image\":\"$TW_IMAGE\"}]}" \
| jq '.posts[0] | {id, feature_image, og_image, twitter_image}'
6. Actualizar un post existente
Para cambiar la feature_image de un post ya creado usa PUT con el updated_at actual. Ghost lo exige para detectar colisiones:
curl -s -X PUT "$GHOST_URL/ghost/api/admin/posts/$POST_ID/" \
-H "Authorization: Ghost $TOKEN" \
-H "Accept-Version: v6.0" \
-H "Content-Type: application/json" \
-d "{
\"posts\": [{
\"updated_at\": \"$UPDATED_AT\",
\"feature_image\": \"$FEATURE\"
}]
}"
Si el updated_at no coincide con el del servidor, Ghost responde 409 y tienes que volver a leer el post antes de reintentar.
7. Imágenes dentro del cuerpo
Para las imágenes del cuerpo repite el paso 4 con cada salida de SocialCutter y coloca la url devuelta en el HTML del post:
<figure>
<img src="https://tu-blog.com/content/images/2026/09/salida-instagram.jpg" alt="Salida procesada">
</figure>
Cada imagen subida una vez queda servida por Ghost con su medida ya resuelta.
Coste
- 1 uso por destino (combinación de plataforma y formato) por petición a SocialCutter.
- Destinos repetidos en la misma petición no se cobran dos veces.
- Los procesamientos fallidos se devuelven.
Errores típicos
| Situación | Causa probable |
|---|---|
| 401 de Ghost | JWT caducado (más de 5 min), mal firmado o kid incorrecto |
| 403 de Ghost | La integración no tiene permiso para esa ruta |
| 409 al actualizar | updated_at desfasado; relee el post antes de reintentar |
| Imagen sin subir | Falta el campo file o el purpose no es válido |
| Formato de respuesta raro | Accept-Version no coincide con tu Ghost |
| 413 de SocialCutter | El maestro supera 5 MB |
| 429 de SocialCutter | Cuota del monedero agotada |
Siguientes pasos
- Guía de WordPress: Publica las medidas correctas en WordPress
- Guía de Shopify: Imágenes de producto y blog en Shopify
- Automatización: Orquesta el flujo con n8n
- API desde la terminal: Procesa imágenes con curl
- Documentación de Ghost: https://ghost.org/docs/admin-api/
Preguntas frecuentes
¿De dónde salen el id y el secret de la Admin API key?
De una integración personalizada en Ghost: Settings → Integrations → Add custom integration. La clave tiene la forma id:secret; el id va en la cabecera kid del JWT y el secret (en hexadecimal) firma el token.
¿Cuánto dura el token JWT?
Ghost documenta un máximo de 5 minutos. Firma uno nuevo por cada tanda de peticiones; si caduca, la Admin API responde 401.
¿Qué cabecera de versión hay que enviar?
Accept-Version, por ejemplo v6.0. El número sigue al major de tu instalación de Ghost; si no coincide, algunas respuestas cambian. Comprueba la versión con GET /ghost/api/admin/site/.
¿Cómo actualizo un post existente?
Con PUT /ghost/api/admin/posts/{id}/. Ghost exige el updated_at del post para detectar colisiones; si no coincide, devuelve 409.
¿Qué papel juega SocialCutter?
Procesa un solo maestro y devuelve las medidas correctas por red. Subes la salida que necesites a Ghost y la pones como feature_image o como imagen del cuerpo. Cuesta 1 uso por destino.
¿Qué formatos acepta la subida y cuántas imágenes van por petición?
WEBP, JPEG, GIF, PNG y SVG. Se sube una imagen por petición. El campo purpose admite image, profile_image e icon; las dos últimas deben ser cuadradas y el icono admite además ICO.
¿Qué diferencia hay entre feature_image, og_image y twitter_image?
Son tres campos de imagen independientes en el objeto de la entrada. feature_image es la portada que usan el tema y las tarjetas del feed. og_image es la imagen de la tarjeta de Open Graph, que en Ghost se documenta como la que se usa al compartir en Facebook y en la web. twitter_image es la imagen de la tarjeta de X. Cada una tiene sus propios título y descripción: og_title, og_description, twitter_title y twitter_description.