# 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.
- URL: https://socialcutter.theboomer.dev/guias/ghost/
- Idioma: es
- Familia: cms
- Actualizado: 2026-09-24
- Palabras clave: 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:

```js
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:

```bash
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):

```bash
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:

```bash
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:

```bash
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 defecto `image`): el uso previsto, que cambia las validaciones. Admitidos `image`, `profile_image` e `icon`. Los formatos soportados en los tres casos son **WEBP, JPEG, GIF, PNG y SVG**; `profile_image` debe ser cuadrada, e `icon` tambié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:

```bash
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

```bash
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:

```bash
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:

```bash
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:

```html
<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](/guias/wordpress/)
- Guía de Shopify: [Imágenes de producto y blog en Shopify](/guias/shopify/)
- Automatización: [Orquesta el flujo con n8n](/guias/n8n/)
- API desde la terminal: [Procesa imágenes con curl](/guias/curl/)
- Documentación de Ghost: https://ghost.org/docs/admin-api/