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

Hay dos tipos de llave y cada uno sirve para una cosa. No son intercambiables.

LlaveFormatoDónde viajaPara qué
Privadarda_…Solo en tu servidor, en el encabezado Authorization: BearerPedir metadatos y contenido desde tu backend. Acceso completo.
Públicarda_pub_…En el navegador, como ?llave=Solo lectura, para pintar el blog. Exige que tu origen esté autorizado.

Llave privada (server-to-server)

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
Nunca expongas la llave privada

Este camino no lleva CORS: consúmelo desde PHP, Node o lo que corra en tu servidor. Si se filtra, revócala y genera otra desde el panel.

Llave pública (navegador)

Es la que usa el kit. Va en la URL de la petición y es de solo lectura:

GET?endpoint=portada&llave=rda_pub_…
https://axobit.mx/bknd/archivosPHP/redactaApi.php?endpoint=portada&llave=rda_pub_TU_LLAVE
Se puede filtrar sin miedo

Como solo lee, si se filtra no pierdes nada: la regeneras en el panel. El daño real lo hace quien la use para consumir tu cuota, por eso la API también limita peticiones por llave.

CORS: tu dominio tiene que estar autorizado

Cuando la petición viene del navegador, la API mira el encabezado Origin y lo compara contra Orígenes autorizados en el panel del blog:

  • La allowlist es estricta: no hay Access-Control-Allow-Origin: *. Un origen por línea.
  • Sin orígenes configurados el acceso desde navegador queda denegado (fail-closed). El blog se ve, pero no carga contenido.
  • El flujo server-to-server no depende de la allowlist porque no envía Origin.
  • El preflight OPTIONS se responde sin leer la llave —el navegador no la manda ahí—, pero la allowlist sí se valida en el GET real.
Síntoma típico

El esqueleto del blog aparece y los artículos no. Casi siempre es el origen sin autorizar: revisa que coincida con https:// y con el puerto si lo usas, sin barra final.

Obtener las llaves

  1. Entra al panel de Axobit

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

  2. Selecciona tu blog

    Cada proyecto tiene su propio blog, su propia llave y sus propios orígenes.

  3. Genera la que necesites

    La privada se muestra una sola vez: guárdala en tu servidor. La pública se regenera tantas veces quieras.

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 navegador?

Hay dos caminos, ambos soportados y con la misma cuenta:
· Server-side (esta página): pides los datos con la llave privada y renderizas tú. Máximo control, incluyendo el HTML.
· Kit en el navegador: subes el kit a tu hosting y redacta.js pide los datos con la llave pública. Es lo que quieres en el 90% de los casos — ver Kit de cliente.

Ojo con esto

El kit sirve el <head> desde PHP pero pinta la lista y el cuerpo de los artículos en el navegador. Google lo indexa, pero si necesitas el HTML completo en la primera respuesta (SSR, algunos renderers), usa el camino server-side.

Kit de cliente reutilizable

Lo más rápido es no integrar a mano: descargas el kit desde el panel, lo subes a tu hosting y editas cuatro constantes. Trae portada, artículo, SEO y estilos ya resueltos. Vive en axobit.mx/redacta-kit/.

Cómo se instala

  1. Descarga el ZIP desde el panel

    Se genera con tu llave pública ya incluida y sin llave privada: el ZIP no lleva ningún secreto.

  2. Súbelo a tu hosting

    En la raíz del dominio o en una subcarpeta (/blog, /noticias…). El kit se autodetecta con SCRIPT_NAME, así que funciona igual en ambos casos.

  3. Edita las constantes

    Todo vive en redacta-cliente.php. Cuatro valores y ya.

  4. Registra tu origen en el panel

    Sin esto el navegador no puede leer la API y el blog se ve vacío.

Archivos del kit

ArchivoRol
index.phpPortada: hero, filtros por categoría, rejilla de tarjetas, banners y pie con los últimos artículos.
articulo.phpArtículo por slug: hero, progreso de lectura, byline, compartir, banners y relacionados.
redacta-cliente.phpÚnico archivo a editar. Constantes REDACTA_* + helpers.
seo.phpHead por artículo (title, description, canonical, OG, Twitter), JSON-LD y caché local con TTL.
redacta.jsPinta portada, artículo y filtros desde la API en el navegador. Cachea 1 minuto.
uii.cssTema editorial completo, con tokens de marca en :root.
.htaccessURLs limpias, páginas legales y bloqueo de la carpeta cache/.
privacidad.php, terminos.php, cookies.phpPáginas legales, ya incluidas en el pie.
LEEME.txtInstrucciones del propio ZIP.

Las constantes que tocas

redacta-cliente.php
// Lo mínimo para que funcione (4 líneas)
const REDACTA_BLOG_URL      = 'https://tusitio.com/blog';  // base pública, CON carpeta
const REDACTA_SITIO_URL     = 'https://tusitio.com';        // raíz del dominio
const REDACTA_TITULAR       = 'Nombre de tu empresa';
const REDACTA_LLAVE_PUBLICA = 'rda_pub_TU_LLAVE';        // la del panel

// Opcionales
const REDACTA_AUTOR_MARCA   = 'Redacción Mi Sitio';  // firma de los artículos
const REDACTA_DESCRIPCION   = 'Tu eslogan.';
const REDACTA_LOGO          = '/logo.png';
const REDACTA_EMAIL_CONTACTO = 'hola@tusitio.com';
const REDACTA_URL_PUBLICIDAD = '';  // legacy: el destino real vive por banner en blog.banners

// ¿Tu Apache aplica el .htaccess? false -> /blog/articulo.php?slug=…
const REDACTA_URLS_LINDAS   = true;

// Opcional: sin llave privada el SEO cae a los valores por defecto
const REDACTA_LLAVE_PRIVADA = '';

Cómo funciona por dentro

  1. index.php pide los metadatos a la API desde el servidor y guarda el head en cache/ con TTL de 60 s. Usa la llave privada si existe; si no, la pública con un Origin permitido. Si la API no responde, degrada a los valores por defecto.
  2. El navegador carga redacta.js, que pide portada o detalle con la llave pública y pinta todo.
  3. Sin caché en el navegador: la respuesta no se guarda en localStorage, así que recargar siempre trae contenido recién publicado. fetch va con cache: 'no-store'.
  4. El <head> es lo único que se sirve en el HTML inicial; por eso es lo que Google lee sin ejecutar JS.
Tu hosting no es Apache

No pasa nada: pon REDACTA_URLS_LINDAS = false y el blog funciona con direcciones tipo articulo.php?slug=…. Los enlaces se arman solos. El .htaccess también bloquea cache/; si no usas Apache, ese archivo simplemente se ignora.

Si quieres partirte de cero y montar tu propia UI, ignora el kit: consume la API con la llave privada y renderiza tú. Ver Resumen de la API.

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
endpointstringsíconfiglistado
categoriastringno—Slug de categoría para filtrar.
paginaintno1Número de página (≥ 1).
porPaginaintno12Artículos por página, 1–50.
destacadoflagno—Presencia 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
endpointstringsídetalle
slugstringsíSlug 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 redacta.banner(i) cada 2 párrafos).

Banners desde el JSON (nov-2026): blog.bannersActivos + blog.banners[] ({id,src,href,target,rel,alt}) traen los banners del banco según la categoría del blog; con el switch apagado llega banners:[] y no se pinta nada. La portada y cada artículo usan el mismo set. La carpeta banners/ del kit es opcional: con tu propia implementación puedes quitarla. Cada banner pintado cuenta impresión y cada click cuenta click (redactaBanner.php), que Root monetiza por espacio. Contacto y logo del blog salen de la ficha del proyecto (no se capturan en el blog).

Open Graph por defecto: el artículo comparte su imagen de portada; la portada comparte blog.imagenSocial (campo Imagen para compartir en el panel) o tu valor en código, con caída a og-default.jpg. Siempre en URL absoluta.

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

  • Head en el servidor: PHP arma el <head> de cada artículo y lo guarda en cache/ con TTL. No hay una llamada a la API por visita.
  • Una sola capa de caché, en el servidor: la API responde con ETag y max-age=60 y seo.php guarda el head en disco con el mismo TTL. El navegador no cachea nada: recargar refresca al instante y, como la caché del servidor se revalida por ETag, no hay contenido viejo visible.
  • Render en servidor (opcional): si prefieres HTML listo e indexable sin depender de JS, renderiza tú con la llave privada.
  • 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.
Lo que todavía no hay

No generamos sitemap.xml, ni RSS/Atom, ni export a markdown. Si necesitas alguno, dilo: son pendientes conocidos, no olvidos.

Navegador

Acceso con llave pública

Si solo quieres leer contenido desde el navegador —sin montar backend— la llave pública lo hace. Se pasa como parámetro llave y da acceso de solo lectura.

GET?endpoint=portada&llave=rda_pub_…
fetch('https://axobit.mx/bknd/archivosPHP/redactaApi.php'
        + '?endpoint=portada&llave=rda_pub_TU_LLAVE', {
  headers: { 'Accept': 'application/json' }
})
  .then(r => r.json())
  .then(json => console.log(json.data.articulos));
Tres condiciones, todas obligatorias
  1. La llave debe ser pública (rda_pub_…). Una privada desde el navegador no funciona.
  2. Tu dominio debe estar en Orígenes autorizados. Sin eso, la respuesta se corta aunque la llave sea válida.
  3. La respuesta viene con { ok: true, data: … }. Si ok es false, mira error.code — la lista está en Códigos de error.
Y esto ya existe, listo para usar

No hace falta que escribas esa llamada: redacta.js del kit ya lo hace, con caché y los renderizadores de portada y artículo. El embed de una línea (embed.js con data-project) sigue siendo roadmap: si lo necesitas, dilo, pero por ahora el kit es la vía corta.

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/.

  • nov-2026

    Banners en el JSON + banco Root

    blog.bannersActivos/bannersCategoria/banners[] en portada/config/listado/detalle; switch por blog (apagado = banners:[]); banco Root con categorías, URL de click e impresiones/clicks; carpeta banners/ opcional; contacto y logo desde el proyecto (se quitaron los inputs repetidos).

  • 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.