Referencia completa de la API: https://api.mailoo.app/docs/v1
Esta página muestra cómo integrar la API pública de Blog de Mailoo en una aplicación Next.js: listado mediante BFF, artículo por slug mediante BFF, sin clave API en el cliente. Prefiere @mailoo/blog --- consulta nextjs-packages{.interpreted-text role="doc"}.
Requisitos previos
- Un proyecto de Mailoo con una integración Blog y al menos un artículo publicado
- El UID del proyecto, el ID de integración y una clave API (panel de control → proyecto → Claves API)
Los valores exactos de MAILOO_BLOG_PROJECT_UID y MAILOO_BLOG_INTEGRATION_ID (y un ejemplo .env copiable) se muestran en la página de ajustes de la integración en el panel de control (bloque Conexión (API externa)). Usa una clave API con alcance Blog (lectura externa).
Entorno (solo servidor)
Añade a .env.local:
MAILOO_BLOG_API=<same value as API_BASE_URL>
MAILOO_BLOG_API_KEY=your-api-key-here
MAILOO_BLOG_PROJECT_UID=<from dashboard integration page>
MAILOO_BLOG_INTEGRATION_ID=<from dashboard integration page>
Usa solo en el servidor; no uses NEXT_PUBLIC_* para la URL de la API ni la clave.
Para los módulos instalables @mailoo/blog / @mailoo/next-core (registro de paquetes GitLab, fábricas de rutas, componentes), consulta nextjs-packages{.interpreted-text role="doc"}.
Página de lista (p. ej. app/blog/page.tsx)
Usando rutas BFF (la misma aplicación expone /api/v1/blog):
export default async function BlogPage() {
const res = await fetch('/api/v1/blog?limit=20', { next: { revalidate: 60 } })
if (!res.ok) return <div>Failed to load blog</div>
const json = await res.json()
const posts = json.data || []
return (
<div>
<h1>Blog</h1>
<ul>
{posts.map((p: { id: string; title: string; slug: string }) => (
<li key={p.id}><a href={`/blog/${p.slug}`}>{p.title}</a></li>
))}
</ul>
</div>
)
}
Opcional: categorías (p. ej. para filtros)
Para mostrar filtros o enlaces de categorías, obtén las categorías desde el BFF:
const res = await fetch('/api/v1/blog/categories', { next: { revalidate: 60 } })
const json = await res.json()
const categories = json.data || [] // [{ id, slug, name }, ...]
Usa categoryId en la consulta de lista al filtrar: /api/v1/blog?categoryId=... (consulta blog-headless-cms{.interpreted-text role="doc"}).
Página de artículo (p. ej. app/blog/[slug]/page.tsx)
Usa la ruta BFF /api/v1/blog/slug/[slug] o llama a la API con X-API-Key del lado del servidor. Devuelve 404 si no se encuentra. Para contenido de artículo localizado, pasa el idioma actual en la consulta (p. ej. ?locale=en); consulta blog-headless-cms{.interpreted-text role="doc"}.
Metadatos SEO: Prefiere post.metaTitle ?? post.title, post.metaDescription ?? post.excerpt, post.ogImageUrl (sino primera imagen del cuerpo), y post.canonicalUrl (sino construye desde origen del sitio + idioma + slug) en generateMetadata (establece alternates.canonical). JSON-LD opcional: GET /api/v1/blog/slug/{slug}/json-ld --- si quedan marcadores, reemplaza {{canonicalUrl}} / {{origin}} / {{locale}}; si la plantilla canónica de la integración ya rellenó URLs absolutas, embébelo tal cual. Palabras clave opcionales: une post.seoWords[].word y/o post.tags. Configura la plantilla en la pestaña Conexión y ajustes de la integración de Blog (publicBaseUrl + patrón de ruta como /{locale}/blog/{slug}).
IndexNOW: Para notificar a motores de búsqueda cuando Mailoo publica o actualiza artículos, implementa el endpoint de notificación del integrador y establece indexNow en la integración de Blog --- consulta indexnow-notify{.interpreted-text role="doc"}.
import { notFound } from 'next/navigation'
export default async function BlogPostPage({ params }: { params: Promise<{ slug: string; locale: string }> }) {
const { slug, locale } = await params
const res = await fetch(`/api/v1/blog/slug/${encodeURIComponent(slug)}?locale=${encodeURIComponent(locale)}`, { next: { revalidate: 60 } })
if (!res.ok) notFound()
const json = await res.json()
const post = json.data
if (!post?.id) notFound()
const links = Array.isArray(post.linkedLinks) ? post.linkedLinks : []
return (
<article>
<h1>{post.title}</h1>
<p>{post.excerpt}</p>
<div dangerouslySetInnerHTML={{ __html: post.htmlContent || post.content }} />
{links.length > 0 && (
<section aria-labelledby="related-links">
<h2 id="related-links">Related links</h2>
{['internal_article', 'update_announcement', 'external_resource'].map((type) => {
const group = links.filter((l: { type: string }) => l.type === type)
if (!group.length) return null
return (
<div key={type}>
<h3 className="text-sm uppercase text-gray-500">{type}</h3>
<ul>
{group
.sort((a: { sortOrder: number }, b: { sortOrder: number }) => a.sortOrder - b.sortOrder)
.map((item: { id: string; label: string; url: string; intro?: string | null; date?: string | null }) => (
<li key={item.id}>
<a href={item.url.startsWith('/blog/') ? `/${locale}${item.url}` : item.url}>
{item.label}
</a>
{item.date && <p><time dateTime={item.date}>{new Date(item.date).toLocaleDateString()}</time></p>}
{item.intro && <p>{item.intro}</p>}
</li>
))}
</ul>
</div>
)
})}
</section>
)}
</article>
)
}
Bloques de resumen imprimible
Cuando el Markdown del artículo incluye un bloque mailoo-print (consulta blog-headless-cms{.interpreted-text role="doc"}), htmlContent contiene marcadores <section data-mailoo-print="true">. Añade un pequeño componente cliente después del cuerpo del artículo para inyectar botones de impresión:
'use client'
import { useEffect } from 'react'
function printMailooSection(section: HTMLElement) {
const body = section.querySelector('.mailoo-printable-body')
if (!body) return
const title = section.getAttribute('data-print-title')?.trim() || document.title
const win = window.open('', '_blank', 'noopener,noreferrer')
if (!win) return
win.document.write(`<!DOCTYPE html><html><head><meta charset="utf-8"><title>${title}</title></head><body>${body.innerHTML}</body></html>`)
win.document.close()
win.focus()
win.print()
}
export function BlogPrintableButtons() {
useEffect(() => {
document.querySelectorAll<HTMLElement>('[data-mailoo-print]:not([data-mailoo-print-enhanced])').forEach((section) => {
section.setAttribute('data-mailoo-print-enhanced', 'true')
const btn = document.createElement('button')
btn.type = 'button'
btn.textContent = 'Print summary'
btn.addEventListener('click', () => printMailooSection(section))
const actions = document.createElement('div')
actions.className = 'mailoo-printable-actions'
actions.appendChild(btn)
section.insertBefore(actions, section.firstChild)
})
}, [])
return null
}
Monta <BlogPrintableButtons /> junto al contenedor dangerouslySetInnerHTML del cuerpo del artículo.
Estructura de respuesta y errores
Para los campos de respuesta completos y códigos de error (p. ej. 404 para slug desconocido), consulta la referencia de la API.