Mailoo publica paquetes npm headless para aplicaciones Next.js con App Router. Proporcionan fábricas de rutas BFF, clientes del lado del servidor y componentes opcionales sin estilos. La aplicación host mantiene el enrutamiento por idioma, i18n, tema, autenticación y layouts de página.
Los hosts externos de Next.js usan los mismos patrones: credenciales solo en el servidor, BFF del mismo origen, sin claves API NEXT_PUBLIC_*.
Hay un ejemplo local ejecutable en tests/integrator-demo (puerto 3080): arráncalo desde Dev Manager Optional o pnpm run dev:integrator-demo. Consulta tests/integrator-demo/README.md.
Referencia completa de la API: https://api.mailoo.app/docs/v1
Lista de paquetes
Paquete Función
@mailoo/next-core Helpers compartidos: configuración de entorno, proxy JSON de webhook, URL de origen de solicitud para RSC
@mailoo/blog Fábricas BFF de lista/slug/json-ld/categorías de blog, cliente de servidor, piezas de UI de artículos
@mailoo/forms BFF de envío de FORM (boletín) y CONTACT_FORM (comentarios) + hooks del cliente; eventos de ciclo de vida (./events, ./server)
@mailoo/chat BFF de chat de visitante JSBOX, hooks headless y widget flotante (WebSocket hacia la API de Mailoo)
@mailoo/images Fábrica de ruta proxy de imagen pública + rewriteMailooPublicImageUrls
@mailoo/market Fábricas BFF de catálogo Market + pedido por email-checkout y cliente de servidor (esqueleto; la UI de offer-generator es un follow-up)
SOCIAL no tiene paquete npm: compose/publish solo en el dashboard de Mailoo (Bearer). Los sitios externos no montan runtime SOCIAL --- consulta social-networks{.interpreted-text role="doc"}.
Las versiones coinciden con el VERSION del monorepo (se publican juntos). Dependencia peer de @mailoo/next-core para los paquetes de funcionalidades.
Qué pertenece al paquete vs al host
Dentro del paquete
- Llamar a Mailoo con
X-API-Key/ URL base desde entorno solo del servidor - Entorno faltante en el BFF → JSON HTTP 503 (no un throw no capturado)
- Fallo de upstream / red →
{ ok: false },nullo JSON HTTP 502 - Tipos para payloads de la API
- Presentación opcional con props
className/ label (sin next-intl)
Aplicación host
- Montar controladores de ruta bajo
app/api/... - Enrutamiento
[locale]yLink - Traducciones, tema Tailwind / tipografía, Auth.js, CTAs de marketing
- Aislar errores de integración para que una API Mailoo caída no dé 500 a todo el sitio (ver más abajo)
Host: los errores de integración no deben tumbar la aplicación
Trata Mailoo como una función, no como dependencia dura de cada página.
- Los helpers de layout/RSC del paquete no deben lanzar cuando la API no está disponible: devuelven
null,{ ok: false, status }o JSON BFF 503 / 502. - El host debe comprobar ese resultado antes de renderizar y mostrar un error solo de esa función (catálogo vacío, «no se pudieron cargar los posts», omitir el shell de chat).
- No dejes un
await fetch(...)desnudo en un Server Component o en el layout raíz: un rechazo de red lanza antes de comprobarres.oky puede dar 500 a toda la respuesta. - No llames a Mailoo desde el
layout.tsxraíz salvo que uses un helper que nunca lance (resolveMailooChatHostConfiges el patrón de referencia). - HTTP 404 (artículo inexistente) no es lo mismo que API caída --- usa
notFound()solo para not-found reales; los fallos de upstream deben quedarse en HTTP 200 con UI de error local a la función. - En producción,
MAILOO_*_APIdebe ser el origen público de la API (p. ej.https://api.mailoo.app), nolocalhost.
Instalar desde el registro de paquetes de GitLab
Los paquetes se publican en el registro de paquetes de GitLab de este proyecto (npm), alcance @mailoo.
Añade un .npmrc en el proyecto consumidor (reemplaza host / id del proyecto y usa un token con read_api o read_package_registry):
@mailoo:registry=https://<gitlab-host>/api/v4/projects/<project-id>/packages/npm/
//<gitlab-host>/api/v4/projects/<project-id>/packages/npm/:_authToken=<TOKEN>
Luego instala los paquetes que necesites (por ejemplo con pnpm):
pnpm add @mailoo/next-core @mailoo/blog
# y/o @mailoo/forms @mailoo/chat @mailoo/images @mailoo/market
@mailoo/next-core
import {
readMailooIntegrationConfig,
proxyMailooWebhookJsonPost,
jsonError,
} from '@mailoo/next-core'
import { getRequestOriginBaseUrl } from '@mailoo/next-core/request-origin'
readMailooIntegrationConfig({ prefix })--- lee{PREFIX}_API,_API_KEY,_PROJECT_UID,_ID/_INTEGRATION_IDproxyMailooWebhookJsonPost--- POST JSON conX-API-KeyyOrigin/Refererdel navegador para CORS upstreamgetRequestOriginBaseUrl(@mailoo/next-core/request-origin) --- origen absoluto desde cabeceras de solicitud (RSC → BFF)
@mailoo/blog
Entorno (solo servidor):
MAILOO_BLOG_API=
MAILOO_BLOG_API_KEY=
MAILOO_BLOG_PROJECT_UID=
MAILOO_BLOG_INTEGRATION_ID=
Rutas BFF
// app/api/v1/blog/route.ts
import { createBlogListHandler } from '@mailoo/blog/routes'
export const GET = createBlogListHandler()
// app/api/v1/blog/slug/[slug]/route.ts
import { createBlogSlugHandler } from '@mailoo/blog/routes'
export const GET = createBlogSlugHandler()
// app/api/v1/blog/slug/[slug]/json-ld/route.ts
import { createBlogJsonLdHandler } from '@mailoo/blog/routes'
export const GET = createBlogJsonLdHandler()
También disponibles: createBlogCategoriesHandler, createBlogCategoryDescriptionHandler, o createMailooBlogRoutes() (list, slug, jsonLd, categories, categoryDescription).
Cliente de servidor / RSC
import {
createMailooBlogClient,
getMailooBlogConfigFromEnv,
fetchBlogPostBySlug,
firstEmbedImageUrlFromArticle,
} from '@mailoo/blog/server'
createMailooBlogClient(config)---listPosts,getPostBySlug,getPostJsonLd,listCategories,getCategoryDescription(API directa de Mailoo; usar desde RSC / sitemap para que K8s/Docker nunca haga loopback al BFF del host). Los fetches usannext: { revalidate: 60 }por defecto; pasa{ revalidate: 0 }(u otro número) como último argumento.listPostsdevuelvedata(array de posts) máspaginationeimagePublicEmbedBaseUrl(mismo envelope que la API de lista externa; envelope incompleto →ok: false).fetchBlogPostBySlug/fetchBlogPostJsonLd--- fetch mediante el BFF del mismo origen del host solamente
Componentes
import { BlogArticleBody } from '@mailoo/blog/client/article-body'
import { BlogLinkedLinksDisplay } from '@mailoo/blog/client/linked-links'
import { BlogPrintableButtons } from '@mailoo/blog/client/printable'
import { BlogArticleShare } from '@mailoo/blog/client/share'
O el barrel @mailoo/blog/client. Prefiere subrutas cuando quieras evitar importar peers opcionales (p. ej. react-markdown) en cada import. BlogArticleShare muestra controles X / Facebook / LinkedIn / copiar enlace; pasa text, etiquetas i18n y url absoluto opcional.
Ejemplos de página paso a paso: blog-nextjs-example{.interpreted-text role="doc"}. Resumen del producto: blog-headless-cms{.interpreted-text role="doc"}.
@mailoo/forms
Entorno
- Boletín (FORM):
MAILOO_CONTACT_FORM_INTEGRATION_{API,API_KEY,PROJECT_UID,ID}(o_INTEGRATION_ID). Omiteprefixen el controlador de formulario para usar este valor por defecto. - Comentarios (CONTACT_FORM):
MAILOO_FEEDBACK_INTEGRATION_{API,API_KEY,PROJECT_UID,ID}Opcionales (mismo prefijo):MAILOO_FEEDBACK_INTEGRATION_FORM_NAMESPACE,MAILOO_FEEDBACK_INTEGRATION_SITE_ID,MAILOO_FEEDBACK_INTEGRATION_ALLOWED_FORM_KEYS - Ciclo de vida (registro / activación en la app):
MAILOO_LIFECYCLE_{API,API_KEY,PROJECT_UID,ID}(o_INTEGRATION_ID). Usado poringestMailooLifecycleEventy los controladores BFF de ciclo de vida. - Múltiples integraciones: pasa
prefix(ogetConfig) por ruta BFF, p. ej.createFeedbackSubmitHandler({ prefix: 'MAILOO_TEAM_SUBSCRIPTION_FORM' }). Helper de configuración preferido:getMailooFormsIntegrationConfig({ prefix }). Obsoletos:getMailooFormIntegrationConfig,getMailooFeedbackIntegrationConfig,isMailooFeedbackIntegrationConfigured.
Exportaciones
@mailoo/forms/routes---createFormSubmitHandler,createFeedbackSubmitHandler,createLifecycleEventHandler,createLifecycleStatusHandler@mailoo/forms/hooks---useMailooFormSubmit,useMailooFeedbackSubmit,useMailooLifecycleEvent(los hooks devuelven{ submit|emit, pending })@mailoo/forms/events--- catálogo cerradoMAILOO_LIFECYCLE_EVENTS(user.registered,user.activated) y validadores de claves de atributos@mailoo/forms/server---ingestMailooLifecycleEventpara emitir solo en el servidor
Los cuerpos de envío FORM están tipados (email, opcionales name / content / ...). Las respuestas de ya suscrito tienen éxito sin messageId. eventName y las claves de atributos del ciclo de vida son cerrados --- valores desconocidos fallan de inmediato (400).
Rutas y hooks
// app/api/v1/webhooks/forms/submit/route.ts
import { createFormSubmitHandler } from '@mailoo/forms/routes'
export const POST = createFormSubmitHandler()
// app/api/v1/webhooks/feedback/submit/route.ts
import { createFeedbackSubmitHandler } from '@mailoo/forms/routes'
export const POST = createFeedbackSubmitHandler()
// Segunda integración CONTACT_FORM (prefijo de entorno personalizado)
import { createFeedbackSubmitHandler } from '@mailoo/forms/routes'
export const POST = createFeedbackSubmitHandler({
prefix: 'MAILOO_TEAM_SUBSCRIPTION_FORM',
})
'use client'
import { useMailooFormSubmit, useMailooFeedbackSubmit } from '@mailoo/forms/hooks'
// Apunta los hooks a la ruta BFF que vincula la integración deseada
Consulta también website-forms-nextjs-example{.interpreted-text role="doc"}, feedback-form-nextjs-example{.interpreted-text role="doc"} y lifecycle-events{.interpreted-text role="doc"}.
@mailoo/chat
Entorno
MAILOO_CHAT_INTEGRATION_API=
MAILOO_CHAT_INTEGRATION_API_KEY=
MAILOO_CHAT_INTEGRATION_PROJECT_UID=
MAILOO_CHAT_INTEGRATION_ID=
# Opcional: origen público de la API para WSS cuando difiere del destino BFF
MAILOO_CHAT_WS_ORIGIN=
MAILOO_CHAT_WELCOME_MESSAGE=
El navegador abre WebSocket directamente a Mailoo (/api/v1/chat/visitor-ws). El ingress debe soportar Upgrade. Consulta chat-websocket-production{.interpreted-text role="doc"}.
Rutas, hooks y widget
El paquete no depende de Auth.js. Opcionalmente inyecta el correo de sesión:
import {
createChatSubmitHandler,
createChatMessagesHandler,
createChatStatusHandler,
} from '@mailoo/chat/routes'
export const POST = createChatSubmitHandler({
resolveSender: async () => {
const session = await auth()
return session?.user?.email
? { email: session.user.email, name: session.user.name ?? undefined }
: null
},
})
// app/api/v1/webhooks/chat/messages/route.ts
export const GET = createChatMessagesHandler()
// app/api/v1/webhooks/chat/status/route.ts
export const GET = createChatStatusHandler()
import { useMailooChatSession } from '@mailoo/chat/hooks'
import { MailooSiteChatWidget } from '@mailoo/chat/client'
<MailooSiteChatWidget
apiOrigin={wsOrigin}
projectUid={projectUid}
integrationId={integrationId}
welcomeMessage={welcome}
locale={locale}
isAuthenticated={isAuthenticated}
/>
Helpers de configuración: getMailooChatIntegrationConfig, getMailooChatWebSocketOrigin, fetchMailooChatIntegrationStatus desde @mailoo/chat.
Indisponibilidad / rate limit (unavailableAction, renderUnavailable, fault): chat-widget-nextjs-example{.interpreted-text role="doc"}.
Tutorial de UI: chat-widget-nextjs-example{.interpreted-text role="doc"}.
@mailoo/images
Proxy de embed público (la clave API permanece en el servidor):
// app/api/mailoo-image/route.ts
import { createPublicImageContentHandler } from '@mailoo/images/routes'
export const GET = createPublicImageContentHandler()
Por defecto usa MAILOO_BLOG_API / MAILOO_BLOG_API_KEY (HTTP 503 si no están establecidos). Pasa getApiBaseUrl / getApiKey para anular.
Reescribe URLs públicas absolutas de Mailoo en HTML/Markdown antes del renderizado:
import { rewriteMailooPublicImageUrls } from '@mailoo/images'
rewriteMailooPublicImageUrls(html, {
canonicalPrefixWithIdEquals:
'https://api.mailoo.app/api/v1/images/public/content?id=',
proxyPrefixWithIdEquals: 'https://yoursite.com/api/mailoo-image?id=',
})
Para portada, HTML y Markdown de un artículo: rewriteMailooBlogMediaUrls. O createMailooBlogClient(config, { imageProxyPrefixWithIdEquals }) --- el prefijo canónico sale de imagePublicEmbedBaseUrl. La clave necesita image.external-read. og:image debe ser la URL del host, no la de Mailoo.
BFF estilo panel de control con Bearer: createImagesLibraryHandlers({ getApiBaseUrl, getAuthHeader, resolveAuthHeader }). Detalles: images{.interpreted-text role="doc"}.
@mailoo/market
Entorno (solo servidor):
MAILOO_MARKET_API=
MAILOO_MARKET_API_KEY=
MAILOO_MARKET_PROJECT_UID=
MAILOO_MARKET_INTEGRATION_ID=
Rutas BFF (entorno faltante → 503; upstream caído → 502):
// app/api/v1/market/products/route.ts
import { createMarketProductsHandler } from '@mailoo/market/routes'
export const GET = createMarketProductsHandler()
// app/api/v1/market/orders/route.ts
import { createMarketOrdersHandler } from '@mailoo/market/routes'
export const POST = createMarketOrdersHandler()
Cliente de servidor:
import {
createMailooMarketClient,
getMailooMarketConfigFromEnv,
} from '@mailoo/market/server'
const config = getMailooMarketConfigFromEnv()
if (config) {
const list = await createMailooMarketClient(config).listProducts({
locale: 'es',
})
if (!list.ok) {
// error local a la función — no lanzar
}
}
Los asistentes offer-generator aún no están en este esqueleto --- consulta offer-generator-market{.interpreted-text role="doc"}. Resumen del producto: market-catalog-external-api{.interpreted-text role="doc"}, market-order-email-checkout{.interpreted-text role="doc"}.
Notas de seguridad
- Nunca expongas claves API ni URLs base de Mailoo mediante
NEXT_PUBLIC_*. - Prefiere rutas BFF del mismo origen; el navegador llama solo a tu aplicación Next.js.
- Claves API restringidas: usa los alcances documentados para cada integración (p. ej.
blog.external-read,webhook.form-submission,chat.send-message,image.external-read,market.external-read,market.order.submit).
Páginas relacionadas
blog-nextjs-example{.interpreted-text role="doc"}website-forms-nextjs-example{.interpreted-text role="doc"}feedback-form-nextjs-example{.interpreted-text role="doc"}lifecycle-events{.interpreted-text role="doc"}chat-widget-nextjs-example{.interpreted-text role="doc"}images{.interpreted-text role="doc"}market-catalog-external-api{.interpreted-text role="doc"}social-networks{.interpreted-text role="doc"}