# Publicar en tu propia API

> Contrato HTTP que debe implementar tu web para que Blogenia le envíe los artículos: rutas, cabeceras, cuerpo, respuesta, tiempos y reintentos, con un receptor de ejemplo.

Versión web: https://blogelia.com/docs/publicar-en-tu-api

Si tu web tiene su propia API, Blogenia puede publicar en ella cada artículo: lo crea al publicarlo, lo actualiza cuando lo editas y lo retira si lo despublicas o lo borras. Tu web recibe el artículo en HTML y decide cómo mostrarlo.

Si tu web es WordPress no necesitas esto: elige **WordPress** en la misma pantalla.

> **Nota:** Próximamente: portada, preguntas frecuentes, SEO y etiquetas. Hoy el cuerpo no las incluye.

## Conectar tu API

1. En el [panel](https://app.blogelia.com/blog), abre **Blog** y la pestaña **Publicación**. Solo el propietario de la cuenta puede configurarla.
2. Elige **API propia** e indica la dirección de tu web, la clave de acceso y, si cambian, las rutas.
3. Guarda y comprueba la conexión.

| Campo | Reglas |
| --- | --- |
| Dirección de la web | `https`, sin usuario, parámetros ni fragmento, y puerto 443. Debe ser una dirección pública. |
| Clave de acceso | Se envía como `Authorization: Bearer <clave>`. Se guarda cifrada y no se vuelve a mostrar. |
| Ruta de artículos | Por defecto `/articles`. Debe empezar por `/` y llevar solo letras, números, `_`, `-` y `/`. |
| Ruta de comprobación | Por defecto `/health`. Mismas reglas. |

## Lo que debe implementar tu API

| Operación | Petición | Cuándo |
| --- | --- | --- |
| Comprobar | `GET {ruta de comprobación}` | Al comprobar la conexión. |
| Crear | `POST {ruta de artículos}` | Se publica un artículo por primera vez. |
| Actualizar | `PUT {ruta de artículos}/{id}` | Se edita un artículo ya publicado. |
| Despublicar | `PUT {ruta de artículos}/{id}` con `status: "draft"` | El artículo deja de estar publicado. |
| Borrar | `DELETE {ruta de artículos}/{id}` | Se borra el artículo en Blogenia. |
| Consultar | `GET {ruta de artículos}/{id}` | Solo al conciliar a mano una creación dudosa (ver más abajo). |

`{id}` es el identificador que tu API devolvió al crear el artículo.

### Cabeceras

| Cabecera | Valor |
| --- | --- |
| `Authorization` | `Bearer <clave de acceso>` |
| `Accept` | `application/json` |
| `Content-Type` | `application/json` |
| `Idempotency-Key` | En `POST`, `PUT` y `DELETE`: 64 caracteres hexadecimales. Los reintentos de la misma operación llevan la misma clave. |

### Cuerpo de POST y PUT

**Campos**

| Campo | Tipo | Descripción |
| --- | --- | --- |
| `sourceId` | string | UUID estable del artículo en Blogenia. Úsalo para no crear duplicados. |
| `title` | string | Título. |
| `slug` | string | Slug del artículo. |
| `content` | string | Contenido en HTML. Los enlaces a otros artículos ya publicados en tu API apuntan a su `url`; los enlaces a artículos aún no publicados allí llegan sin `href`. |
| `excerpt` | string | Extracto. Cadena vacía si no tiene. |
| `status` | string | `published`, o `draft` al despublicar. |

**Cuerpo**

```json
{
  "sourceId": "0b8e2f6c-6d63-4d0e-9c1a-5d8f3c1d7a11",
  "title": "¿Cuánto cuesta reformar una cocina?",
  "slug": "cuanto-cuesta-reformar-una-cocina",
  "content": "<h2>Precio medio</h2><p>…</p>",
  "excerpt": "Precios orientativos y qué incluye cada partida.",
  "status": "published"
}
```

`DELETE` no lleva cuerpo.

### Tu respuesta

Responde con un código `2xx` y un objeto JSON. `GET` de comprobación solo necesita un objeto JSON cualquiera.

**Campos de la respuesta a POST y PUT**

| Campo | Tipo | Descripción |
| --- | --- | --- |
| `id` | string \| number | Identificador del artículo en tu API: un entero positivo o un texto de hasta 200 caracteres con letras, números, `_` y `-`. En `PUT` debe ser el mismo del que está en la ruta. |
| `url` | string | Opcional. URL pública del artículo en tu web. Solo se guarda si es `https`, del mismo origen que la dirección configurada y de menos de 2048 caracteres. |

**Respuesta**

```json
{
  "id": "42",
  "url": "https://tuweb.example/blog/cuanto-cuesta-reformar-una-cocina"
}
```

- `DELETE`: cualquier `2xx` vale (también sin cuerpo). `404` y `410` se tratan como «ya borrado».
- No seguimos redirecciones: un `3xx` cuenta como error. Configura la dirección definitiva.

## Tiempos, tamaños y reintentos

| Concepto | Valor |
| --- | --- |
| Tiempo máximo por petición | 15 segundos. |
| Cuerpo de la petición | Hasta 2 MiB. |
| Respuesta | Hasta 256 KB. |
| Actualizar, despublicar y borrar | Un error de red, un `5xx` o un `429` se reintenta solo, hasta 3 intentos en total: tras 1 y 2 minutos. Otros `4xx` no se reintentan. |
| Crear | Un `429` se reintenta igual; otro `4xx` es un fallo definitivo. Un error de red o un `5xx` no se reintenta: Blogenia no sabe si tu API llegó a crear el artículo y pide al propietario que lo revise en el panel antes de repetir. |

Un fallo no automático se puede reintentar a mano desde el artículo en el panel.

### Creaciones dudosas

Si un `POST` termina en error de red o `5xx`, el propietario revisa en tu web si el artículo se creó. Si existe, indica su `id` en el panel y Blogenia lo enlaza tras comprobar que `GET {ruta de artículos}/{id}` devuelve el mismo `id` y el mismo `slug`. Implementa ese `GET` y haz el `POST` idempotente por `sourceId` o `Idempotency-Key` para evitar duplicados.

## Receptor de ejemplo

Un servidor Express mínimo que cumple el contrato. Guarda los artículos en memoria: úsalo como punto de partida.

**Node.js con Express**

```js
import express from 'express';

const TOKEN = process.env.BLOGENIA_TOKEN; // la clave de acceso que pegas en Blogenia
const articles = new Map(); // id -> artículo
const bySource = new Map(); // sourceId -> id
let nextId = 1;

const app = express();
app.use(express.json({ limit: '3mb' }));

// Autenticación Bearer
app.use((req, res, next) => {
  if (req.get('authorization') !== 'Bearer ' + TOKEN) return res.sendStatus(401);
  next();
});

// Ruta de comprobación
app.get('/health', (_req, res) => res.json({ ok: true }));

// Crear (idempotente por sourceId)
app.post('/articles', (req, res) => {
  const { sourceId, title, slug, content, excerpt, status } = req.body;
  let id = bySource.get(sourceId);
  if (!id) {
    id = String(nextId++);
    bySource.set(sourceId, id);
  }
  articles.set(id, { id, sourceId, title, slug, content, excerpt, status });
  res.status(201).json({ id, url: 'https://tuweb.example/blog/' + slug });
});

// Consultar (lo usa Blogenia al conciliar)
app.get('/articles/:id', (req, res) => {
  const a = articles.get(req.params.id);
  if (!a) return res.sendStatus(404);
  res.json({ id: a.id, slug: a.slug, url: 'https://tuweb.example/blog/' + a.slug });
});

// Actualizar o despublicar (status: "draft")
app.put('/articles/:id', (req, res) => {
  if (!articles.has(req.params.id)) return res.sendStatus(404);
  const { sourceId, title, slug, content, excerpt, status } = req.body;
  const id = req.params.id;
  articles.set(id, { id, sourceId, title, slug, content, excerpt, status });
  res.json({ id, url: 'https://tuweb.example/blog/' + slug });
});

// Borrar
app.delete('/articles/:id', (req, res) => {
  articles.delete(req.params.id);
  res.sendStatus(204);
});

app.listen(3000);
```

> **Consejo:** Buenas prácticas para tu receptor (credencial, idempotencia, HTML, límites) en [Seguridad](https://blogelia.com/docs/seguridad#publicar).

> **Consejo:** Para probarlo en local, publica el servidor con un túnel HTTPS (la conexión exige `https` y una dirección pública).
