# Construir tu dashboard

> Guía para llevar las visitas y los artículos de Blogenia a tu propio dashboard: qué rutas llamar, cuánto cachear y un ejemplo en Node.js.

Versión web: https://blogelia.com/docs/construir-dashboard

Con la API puedes mostrar en tu propio panel las visitas de tus blogs, los artículos más leídos y el estado de la cuenta. Esta guía resume qué rutas llamar y cómo hacerlo de forma segura y sin gastar peticiones de más.

## 1. Crea una clave con el permiso justo

- Para un dashboard de analítica, crea una clave **solo con `stats:read`**. No da acceso a artículos ni a datos de la cuenta, así que, si se filtra, el daño es mínimo.
- Si además necesitas créditos, plan o estado de artículos, crea **otra clave aparte** con `account:read` y úsala solo en el servidor. No mezcles los dos permisos en una clave.
- Los permisos se eligen al crear la clave, en [**API** del panel](https://app.blogelia.com/api), y no se pueden ampliar después. Si te falta uno, crea otra clave y revoca la antigua. `account:read` solo puede concederlo el propietario de la cuenta.

## 2. Llama siempre desde tu servidor

Las rutas de analítica y de cuenta no admiten CORS, a propósito: el navegador no puede llamarlas. Tu servidor pide los datos y tu front-end los pide a tu servidor. Nunca pongas la clave en código del navegador.

## 3. Qué rutas llamar

| Quieres mostrar… | Ruta | Permiso |
| --- | --- | --- |
| Visitas totales de la cuenta y por blog | [`GET /account/stats`](https://blogelia.com/docs/analitica#cuenta) | `stats:read` |
| Visitas de un blog por día, semana o mes | [`GET /blogs/{blogId}/stats`](https://blogelia.com/docs/analitica#blog) | `stats:read` |
| Artículos más leídos | [`GET /blogs/{blogId}/stats/articles`](https://blogelia.com/docs/analitica#ranking) | `stats:read` |
| Visitas de un artículo | [`GET /articles/{id}/stats`](https://blogelia.com/docs/analitica#articulo) | `stats:read` |
| Créditos, plan, uso del mes y blogs | [`GET /account`](https://blogelia.com/docs/cuenta) | `account:read` |
| Cuántos artículos hay por estado | [`GET /blogs/{blogId}/articles/summary`](https://blogelia.com/docs/articulos-todos#resumen) | `account:read` |
| Qué está en marcha o ha fallado | [`GET /blogs/{blogId}/generations`](https://blogelia.com/docs/generaciones) | `account:read` |
| Próximos huecos del piloto | [`GET /blogs/{blogId}/autopilot`](https://blogelia.com/docs/piloto-automatico) | `account:read` |

## 4. Cachea 10 minutos

Las estadísticas se actualizan cada 10 minutos, así que pedirlas más a menudo no te da datos nuevos y gasta tu [límite de uso](https://blogelia.com/docs/limites). Guarda la respuesta en tu servidor 10 minutos (en memoria, Redis o donde prefieras) y sirve esa copia a todos tus visitantes.

## 5. Ejemplo en Node.js

Sin SDK, solo `fetch` (Node 18 o posterior). Pide el total de la cuenta y los cinco artículos más leídos de un blog, y lo guarda 10 minutos. Si Blogenia responde `429` o falla, sirve el dato anterior.

**Módulo blogenia.js**

```js
const API = 'https://api.blogelia.com/v1';
const TTL_MS = 10 * 60 * 1000; // los datos se actualizan cada 10 minutos
const cache = new Map();

async function blogenia(path, query = {}) {
  const url = new URL(API + path);
  for (const [k, v] of Object.entries(query)) url.searchParams.set(k, String(v));
  const res = await fetch(url, {
    headers: { Authorization: 'Bearer ' + process.env.BLOGENIA_STATS_KEY },
  });
  const remaining = res.headers.get('X-RateLimit-Remaining');
  if (remaining !== null && Number(remaining) < 5) console.warn('Quedan pocas peticiones:', remaining);
  if (res.status === 429) {
    const err = new Error('rate_limited');
    err.retryAfter = Number(res.headers.get('Retry-After') ?? 60);
    throw err;
  }
  if (!res.ok) throw new Error('HTTP ' + res.status);
  return res.json();
}

async function cached(key, load) {
  const hit = cache.get(key);
  if (hit && Date.now() - hit.at < TTL_MS) return hit.value;
  try {
    const value = await load();
    cache.set(key, { at: Date.now(), value });
    return value;
  } catch (err) {
    if (hit) return hit.value; // ante un 429 o un fallo, sirve el dato anterior
    throw err;
  }
}

export function dashboardData(blogId) {
  return cached('dashboard:' + blogId, async () => {
    const [account, top] = await Promise.all([
      blogenia('/account/stats', { interval: 'day' }),
      blogenia('/blogs/' + blogId + '/stats/articles', { limit: 5 }),
    ]);
    return {
      views: account.totals.views,
      uniquesApproxSum: account.totals.uniquesApproxSum,
      series: account.series, // el último punto puede llevar partial: true
      blogs: account.blogs,
      topArticles: top.data,
      updatedAt: account.updatedAt, // puede ser null
    };
  });
}
```

**Ruta de tu servidor**

```js
import express from 'express';
import { dashboardData } from './blogenia.js';

const app = express();

// Tu navegador llama a TU servidor; solo tu servidor conoce la clave.
app.get('/api/dashboard/:blogId', async (req, res) => {
  try {
    res.json(await dashboardData(req.params.blogId));
  } catch (err) {
    res.status(err.retryAfter ? 503 : 502).json({ error: 'No hay datos por ahora' });
  }
});
```

## 6. Casos que debes tratar

- **`partial: true`.** El último grupo de la serie incluye hoy y aún no ha terminado. Píntalo distinto (por ejemplo, con una línea discontinua) o déjalo fuera de las comparaciones con periodos completos.
- **`updatedAt: null`.** Todavía no se conoce la hora del último volcado de estadísticas. No muestres «actualizado a las…»; las cifras siguen siendo válidas.
- **`429`.** Superaste el límite del plan. Espera los segundos de `Retry-After` antes de reintentar y, mientras tanto, sirve la copia en caché. Vigila `X-RateLimit-Remaining`. Ver [Límites de uso](https://blogelia.com/docs/limites).
- **`403` `insufficient_scope`.** La clave no tiene el permiso de esa ruta. Crea otra con el permiso adecuado.
- **Únicos.** `uniquesApprox` es aproximado y `uniquesApproxSum` no es el número de personas distintas del periodo. Etiquétalo como «visitantes (aprox.)».

> **Nota:** Todas las rutas de la clave comparten el mismo límite por minuto. Con la caché de 10 minutos, un dashboard necesita solo unas pocas peticiones cada 10 minutos.

Más sobre las claves en [Seguridad](https://blogelia.com/docs/seguridad).
