redacta + / Documentación

Documentación · v1

Bienvenido a redacta +

Un blog editorial embebible para tu sitio, publicando desde un panel simple y consumiendo una API JSON privada desde tu propio servidor.

redacta + te da un blog que vive en tu dominio, con SEO real y datos estructurados, sin que tengas que montar un CMS. El contenido se administra en el panel de Axobit y se publica en tu sitio mediante una API REST de solo lectura, protegida por una llave privada por proyecto que se consume en servidor (nunca llega al navegador).

Esta documentación cubre la integración actual (server-side), la referencia completa de la API y el roadmap del widget por script tag que está en camino.

¿Qué necesitas para empezar?

Un proyecto activo en Axobit con el producto redacta +, una llave de API generada en el panel (sección redacta +) y un servidor capaz de ejecutar PHP para hacer las llamadas server-side.

Conceptos clave

Antes de integrar, conviene conocer estas piezas:

ConceptoQué es
Proyecto (blog)Un blog configurado en el panel: título, descripción, idioma, URL base y categorías.
Llave de APIToken privado rda_… por proyecto. Solo se guarda su hash en el servidor de Axobit.
Artículo publicadoSolo los artículos en estado publicado aparecen en la API.
Bloques de cuerpoEl campo cuerpo del detalle es una lista tipada de párrafos (p) e imágenes (img).
Render server-sideTu servidor arma la interfaz con la API. El navegador nunca ve la llave.

La llave privada de cada blog se genera en el panel y solo se almacena su hash SHA-256 — ni siquiera el panel puede volver a mostrarla después de crearla.

Autenticación

Todos los endpoints requieren una llave de API válida. Se envía en el encabezado Authorization con esquema Bearer:

GEThttps://axobit.mx/bknd/archivosPHP/redactaApi.php?endpoint=config
Authorization: Bearer rda_tu_llave_privada_de_proyecto

Obtener tu llave

  1. Entra al panel de Axobit

    Inicia sesión y abre la sección redacta +.

  2. Selecciona (o crea) tu blog

    Cada proyecto tiene su propio blog y su propia llave.

  3. Genera / copia la llave

    Se muestra una sola vez al crearla. Guárdala en un lugar seguro: tu servidor o variables de entorno.

Nunca expongas tu llave

La API es server-to-server (sin CORS). Consume los endpoints desde tu backend con cURL, no desde el navegador. Si una llave se filtra, revócala y regenera otra desde el panel.

Tu cliente es responsable de mantener la llave en un solo archivo de configuración del lado del servidor (ver Kit de cliente).

Inicio rápido

Este ejemplo hace una llamada completa y muestra la portada del blog en tu página. Sustituye TU_LLAVE por tu llave privada.

1. Trae la configuración del blog

GET?endpoint=config
$ch = curl_init('https://axobit.mx/bknd/archivosPHP/redactaApi.php?endpoint=config');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . 'TU_LLAVE'],
]);
$resp = json_decode(curl_exec($ch), true);
curl_close($ch);

2. Trae el listado de artículos

GET?endpoint=listado&porPagina=12
$url = 'https://axobit.mx/bknd/archivosPHP/redactaApi.php?endpoint=listado&porPagina=12';
$ch = curl_init($url);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . 'TU_LLAVE'],
]);
$resp = json_decode(curl_exec($ch), true);
curl_close($ch);

// $resp['data']['articulos'] = lista de artículos;
// $resp['data']['paginacion'] = { pagina, porPagina, total, paginas }

3. Renderiza las tarjetas

Con los datos del listado, arma tu portada con HTML y descripcionSeo para SEO. Cada artículo trae titulo, slug, url, resumen, categoria, portada y más.

¿Server-side o widget?

Hoy la vía recomendada y disponible es la API server-side (esta página). El widget por script tag (embed.js) es parte del roadmap — ver Embed por script tag.

Kit de cliente reutilizable

Para no reinventar la rueda, proveemos un kit base con la portada y la vista de artículo listas para copiar. Vive en axobit.mx/redacta-kit/ y se replica por proyecto (cliente de referencia: creditoautosnuevosyseminuevos.com).

Archivos del kit

ArchivoRol
index.phpPortada del blog (hero + grid de tarjetas + categorías + JSON-LD).
articulo.phpVista de artículo por slug, con SEO + datos estructurados y banners publicitarios.
redacta-cliente.phpÚnico archivo con la config y la llave (constantes REDACTA_* + helpers cURL).
uii.cssDiseño editorial (tokens CSS en :root).
.htaccessEnrutado limpio (portada en /, artículo en /<slug>) y bloqueo de la API.

Configuración en un solo archivo

redacta-cliente.php
// Al inicio del archivo -- única fuente de verdad
define('REDACTA_BLOG_URL', 'https://tusitio.com');      // portada (raíz)
define('REDACTA_SITIO_URL', 'https://tusitio.com');     // marca / schema
define('REDACTA_TITULAR',   'Tu Marca');
define('REDACTA_API_URL',  'https://axobit.mx/bknd/archivosPHP/redactaApi.php');
define('REDACTA_LLAVE',    'rda_tu_llave');             // secreto

Los helpers (r_api(), r_esc(), r_head(), r_footer(), r_hojaEstilo(), r_cuerpo(), r_espacioPublicidad()) centralizan el cURL y el render, de modo que index.php y articulo.php solo arman HTML.

Rutas de despliegue

El blog vive en la raíz del dominio (/ = portada, /<slug> = artículo). No lo despliegues bajo /blog/ salvo que lo ajustes en el kit. El .htaccess bloquea ^api/redacta\.php$.

Referencia

Resumen de la API

URL base (server-to-server, en Axobit):

¡Solo GET!Base
https://axobit.mx/bknd/archivosPHP/redactaApi.php

Cada endpoint usa el parámetro de query endpoint (alias resource). Todos son solo lectura GET. La API es sin CORS y debe consumirse en servidor.

EndpointMétodoDescripciónQuery
configGETConfiguración del blog + categorías
categoriasGETSolo las categorías activas
listadoGETArtículos publicados paginadoscategoria, pagina, porPagina, destacado
detalleGETUn artículo por slug (con cuerpo)slug obligatorio

Respuesta estándar de éxito:

200 OK
{ "ok" : true, "data": { // … según el endpoint } }

En caso de error, el cuerpo siempre es:

Error
{ "ok": false, "error": { "code": "codigo", "message": "mensaje" } }

config

Devuelve la configuración general del blog y sus categorías activas. Es el primer endpoint a llamar para obtener urlBase, urlImagenes y el mapa de rutas.

GEThttps://axobit.mx/bknd/archivosPHP/redactaApi.php?endpoint=config
$ch = curl_init('https://axobit.mx/bknd/archivosPHP/redactaApi.php?endpoint=config');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . 'TU_LLAVE'],
]);
$data = json_decode(curl_exec($ch), true)['data'];

Respuesta

200 OK · application/json
{
  "ok": true,
  "data": {
    "blog": {
      "id": 7,
      "titulo": "Blog de ejemplo",
      "descripcion": "Un blog de demostración",
      "idioma": "es",
      "urlBase": "https://tusitio.com",
      "urlImagenes": "https://axobit.mx/ftnd/graficos/blogs/7/"
    },
    "categorias": [
      { "id": 1, "nombre": "Diario", "slug": "diario", "orden": 1 }
    ],
    "urls": {
      "config": "?endpoint=config",
      "categorias": "?endpoint=categorias",
      "listado": "?endpoint=listado",
      "detalle": "?endpoint=detalle&slug={slug}"
    }
  }
}

categorias

Devuelve solo las categorías activas del blog, ordenadas por orden. Útil para construir un selector o un menú de filtros.

GEThttps://axobit.mx/bknd/archivosPHP/redactaApi.php?endpoint=categorias
$ch = curl_init('https://axobit.mx/bknd/archivosPHP/redactaApi.php?endpoint=categorias');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . 'TU_LLAVE'],
]);
$data = json_decode(curl_exec($ch), true)['data'];

Respuesta

200 OK
{
  "ok": true,
  "data": {
    "categorias": [
      { "id": 1, "nombre": "Diario", "slug": "diario", "orden": 1 },
      { "id": 2, "nombre": "Tutoriales", "slug": "tutoriales", "orden": 2 }
    ]
  }
}

listado

Devuelve los artículos publicados con paginación, ordenados por fecha de publicación descendente. Es el endpoint de la portada.

Parámetros de query

ParámetroTipoRequeridoDefectoDescripción
endpointstringconfiglistado
categoriastringnoSlug de categoría para filtrar.
paginaintno1Número de página (≥ 1).
porPaginaintno12Artículos por página, 1–50.
destacadoflagnoPresencia del parámetro = solo destacados.
GET?endpoint=listado&categoria=tutoriales&pagina=1&porPagina=6
$url = 'https://axobit.mx/bknd/archivosPHP/redactaApi.php'
      . '?endpoint=listado&categoria=tutoriales&pagina=1&porPagina=6';
$ch = curl_init($url);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . 'TU_LLAVE'],
]);
$data = json_decode(curl_exec($ch), true)['data'];

// $data['articulos']   -> array de artículos (resumen, sin cuerpo)
// $data['paginacion']  -> { pagina, porPagina, total, paginas }

Respuesta

200 OK
{
  "ok": true,
  "data": {
    "articulos": [ { "idArticulo": 4, "titulo": "…", "portada": { … } } ],
    "paginacion": { "pagina": 1, "porPagina": 6, "total": 23, "paginas": 4 }
  }
}

Cada artículo del listado usa el molde de artículo (sin el campo cuerpo).

detalle

Devuelve un artículo completo por slug. Es el único endpoint que incluye el cuerpo tipado y las imágenes de cuerpo. Además registra una vista (dedupe diario por IP + UA + fecha).

Parámetros de query

ParámetroTipoRequeridoDescripción
endpointstringdetalle
slugstringSlug del artículo. Vacío → 422.
GET?endpoint=detalle&slug=el-caso-por-la-simplicidad
$slug = 'el-caso-por-la-simplicidad';
$url = 'https://axobit.mx/bknd/archivosPHP/redactaApi.php?endpoint=detalle&slug=' . urlencode($slug);
$ch = curl_init($url);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . 'TU_LLAVE'],
]);
$data = json_decode(curl_exec($ch), true)['data'];
$articulo = $data['articulo'];

Respuesta

Además del molde de artículo, el detalle agrega:

200 OK · fragmento relevante
{
  "ok": true,
  "data": {
    "articulo": {
      # …molde de artículo…
      "cuerpo": [
        { "tipo": "p",   "html": "<p>…</p>" },
        { "tipo": "img", "url": "https://…/cuerpo.jpg", "alt": "Ilustración" },
        { "tipo": "p",   "html": "<p>…</p>" }
      ],
      "cuerpoImagenes": [ { "url": "…", "alt": "…", "ancho": 1280, "alto": 720 } ]
    }
  }
}

El campo cuerpo es una lista tipada de bloques que tu plantilla debe recorrer: renderiza html para los de tipo p y <img> para los de tipo img. Es ideal para insertar banners publicitarios entre párrafos (el kit lo hace con r_espacioPublicidad()).

Objetos de la API

Artículo (molde)

CampoTipoDescripción
idArticulointIdentificador único.
titulostringTítulo del artículo.
slugstringSlug ASCII usado en la URL.
urlstringURL absoluta de publicación (urlBase + slug).
resumenstringResumen / descripción SEO.
etiquetasarrayEtiquetas opcionales (array de strings).
categoriaobjeto|null{ id, nombre, slug } o null.
autorobjeto{ id, nombre }.
minLecturaintMinutos de lectura estimados.
destacadoboolSi el artículo está destacado.
publicadoEnstringFecha/hora de publicación (datetime).
actualizadoEnstringFecha/hora de última actualización.
portadaobjeto|nullImagen de portada: { url, alt, ancho, alto } o null.
contenidostringHTML sin estructurar (por compatibilidad).

Bloque de cuerpo

CampoTipoDescripción
tipo"p"|"img"Párrafo o imagen.
htmlstringSolo para p: HTML del párrafo.
urlstringSolo para img: URL de la imagen.
altstringSolo para img: texto alternativo.

Imagen (portada / cuerpo)

CampoTipoDescripción
urlstringURL absoluta del recurso (WebP optimizado).
altstringTexto alternativo.
ancho/altointDimensiones en píxeles (útil para evitar CLS).

Paginación

CampoTipoDescripción
paginaintPágina actual.
porPaginaintTamaño de página solicitado.
totalintTotal de artículos que coinciden.
paginasintTotal de páginas (ceil(total/porPagina)).

Operaciones

Códigos de error

Los errores siempre devuelven el mismo contenedor JSON y un código estable para que tu cliente pueda reaccionar de forma programática.

HTTPCódigoCuándo ocurre
401unauthorizedFalta el encabezado Authorization o la llave no es válida.
403service_unavailableEl blog no está activo, está en pausa, el proyecto no está activo o no hay suscripción vigente.
404not_foundEl slug del artículo no existe (o no está publicado).
404unknownEl endpoint solicitado no existe.
405method_not_allowedSe usó un método distinto a GET (POST, PUT, etc.).
422slug_requiredSe llamó a detalle sin el slug.
429rate_limitedSe superó el límite de peticiones por minuto.

Ejemplo de respuesta de error

401 · unauthorized
{
  "ok": false,
  "error": {
    "code": "unauthorized",
    "message": "Se requiere una llave de proyecto."
  }
}
Manejo recomendado

Revisa ok antes de usar data. Si ok === false, lee error.code y muestra un mensaje amigable. Implementa re-intentos con backoff en 429, respetando Retry-After.

Rate limits

Cada llave puede hacer hasta 180 peticiones por minuto, contadas por llave + IP de origen.

  • Si superas el límite, la API responde 429 con código rate_limited y la cabecera Retry-After: 60.
  • La ventana se reinicia cada minuto (por hora UTC + minuto).
  • 180/min es holgado para un uso típico de blog. Aplica cache del lado del cliente para evitar llamadas repetidas en cada visita.
Mejora práctica: cache

Como la API está pensada para render server-side, conviene cachear listado y detalle durante unos minutos (p. ej. en archivo o memoria) para reducir latencia y llamadas. El kit base ya lo facilita por diseño (los helpers son idempotentes).

Seguridad

  • Sin CORS: la API solo responde GET y no habilita CORS, así que no puede consumirse desde el navegador de un cliente. Se usa desde tu backend.
  • Llaves con hash: solo se almacena el hash SHA-256 de cada llave. La llave real se muestra una vez al crearla.
  • Bloqueo de la API: en el despliegue del cliente, el .htaccess bloquea ^api/redacta\.php$ para que la API pública nunca se exponga en el dominio del cliente.
  • HTTPS forzado: el dominio sirve solo por HTTPS; redirige www y HTTP a la versión canónica.
  • Sanitización: todo el HTML de contenido se escapa/limpia en servidor antes de devolverse al cliente.

Rendimiento

  • Render en servidor: arma el HTML en tu backend; el navegador recibe HTML listo e indexable. Este es el enfoque recomendado y el actual.
  • Imágenes WebP optimizadas: el panel optimiza las subidas a WebP y devuelve dimensiones (ancho/alto) para evitar cambios de layout (CLS).
  • Tipografía variable: Fraunces + Inter con display=swap, sin bloqueo de render.
  • Datos estructurados: la portada y el artículo emiten JSON-LD (Blog, BlogPosting, BreadcrumbList) para SEO.

Roadmap

Widget por script tag (próximamente)

Estamos preparando un embed por script tag para quienes quieren blog sin tocar backend. Con una sola etiqueta puedes montar la portada en tu HTML; la versión actual y recomendada sigue siendo la API server-side.

index.html (vista previa de la idea)
<!-- 1. El contenedor donde quieres el blog -->
<div id="redacta-blog"></div>

<!-- 2. El widget -->
<script src="https://axobit.mx/redacta+/embed.js"
        data-project="pk_live_xxxxx"
        data-mount="#redacta-blog"></script>
Estado

El widget aún no está disponible. Los endpoints públicos que hoy existen son solo lectura server-to-server. Las llaves de producción siguen el formato rda_…; las llaves públicas del widget (pk_live_…) son una idea de diseño.

Más

Changelog

  • v1.0

    API pública inicial

    Endpoints config, categorias, listado y detalle; autenticación Bearer por llave privada; rate limit de 180/min; cuerpo tipado por bloques; registro de visitas con dedupe.

  • ago-2026

    Simplificación del kit de cliente

    El blog se instala en la raíz del dominio. Config centralizada en constantes REDACTA_* en redacta-cliente.php (ya no existe redacta.config.php); hoja de estilos uii.css. Kit base en redacta-kit/.

  • próximamente

    Widget por script tag

    Embed de la portada sin backend. Detalles al liberarse.

Soporte

¿Necesitas ayuda para integrar o hay algo que no cuadra con la documentación?

  • Panel de Axobit: crea o configura tu blog en la sección redacta +.
  • Cliente de referencia: creditoautosnuevosyseminuevos.com (kit en redacta-kit/).
  • Guía interna: redacta-plus.md / documentacion.md del proyecto Axobit.