Next.js-Integrator-Pakete (`@mailoo/*`)

Zuletzt aktualisiert: Oct 12, 2026Abschnitt: Integrationen

Mailoo veröffentlicht headless npm-Pakete für Next.js-App-Router-Anwendungen. Sie bieten BFF-Routenfabriken, serverseitige Clients und optionale ungestylte Komponenten. Die Host-Anwendung behält Locale-Routing, i18n, Theme, Auth und Seitenlayouts.

Externe Next.js-Hosts verwenden dieselben Muster: ausschließlich serverseitige Zugangsdaten, Same-Origin-BFF, keine NEXT_PUBLIC_*-API-Schlüssel.

Ein lokales Beispiel liegt unter tests/integrator-demo (Port 3080): Start über Dev Manager Optional oder pnpm run dev:integrator-demo. Siehe tests/integrator-demo/README.md.

Vollständige API-Referenz: https://api.mailoo.app/docs/v1

Paketliste


Paket Funktion


@mailoo/next-core Gemeinsame Helfer: Umgebungskonfiguration, Webhook-JSON-Proxy, Request-Origin-URL für RSC

@mailoo/blog Blog-Listen-/Slug-/JSON-LD-/Kategorien-BFF-Fabriken, Server-Client, Artikel-UI-Teile

@mailoo/forms FORM-(Newsletter-) und CONTACT_FORM-(Feedback-)Submit-BFF + Client-Hooks; Lifecycle-Ereignisse (./events, ./server)

@mailoo/chat JSBOX-Besucher-Chat-BFF, Headless-Hooks und schwebendes Widget (WebSocket zu Mailoo-API)

@mailoo/images Öffentliche Bild-Proxy-Routenfabrik + rewriteMailooPublicImageUrls

@mailoo/market Market-Katalog- und E-Mail-Checkout-Bestell-BFF-Fabriken sowie Server-Client (Gerüst; Offer-Generator-UI folgt separat)

SOCIAL hat kein npm-Paket: Compose/Publish nur im Mailoo-Dashboard (Bearer). Externe Sites mounten keinen SOCIAL-Runtime --- siehe social-networks{.interpreted-text role="doc"}.

Versionen stimmen mit dem Monorepo-VERSION überein (gemeinsam veröffentlicht). Peer-Abhängigkeit auf @mailoo/next-core für die Feature-Pakete.

Was das Paket besitzt vs. der Host

Im Paket

  • Mailoo mit X-API-Key / Basis-URL aus serverseitiger Umgebung aufrufen
  • Fehlende Umgebung am BFF → HTTP-503-JSON (kein ungefangener Throw)
  • Upstream-/Netzfehler → { ok: false }, null oder HTTP-502-JSON
  • Typen für API-Nutzlasten
  • Optionale Darstellung mit className-/Label-Props (kein next-intl)

Host-Anwendung

  • Routenhandler unter app/api/... einbinden
  • [locale]-Routing und Link
  • Übersetzungen, Tailwind-/Typografie-Theme, Auth.js, Marketing-CTAs
  • Integrationsfehler isolieren, damit eine down Mailoo-API nicht die ganze Site mit 500 trifft (siehe unten)

Host: Integrationsfehler dürfen die App nicht abreißen

Behandeln Sie Mailoo als Feature, nicht als harte Abhängigkeit jeder Seite.

  • Layout-/RSC-Helfer des Pakets dürfen nicht werfen, wenn die API unerreichbar ist: sie liefern null, { ok: false, status } oder BFF-JSON 503 / 502.
  • Der Host muss dieses Ergebnis vor dem Rendern prüfen und einen Fehler nur für dieses Feature zeigen (leerer Katalog, „Beiträge konnten nicht geladen werden", Chat-Shell weglassen).
  • Lassen Sie kein nacktes await fetch(...) in einem Server Component oder Root-Layout: eine Netzverweigerung wirft vor der Prüfung von res.ok und kann die ganze Antwort mit 500 beenden.
  • Rufen Sie Mailoo nicht aus dem Root-layout.tsx auf, außer Sie nutzen einen Never-Throw-Helfer (resolveMailooChatHostConfig ist das Referenzmuster).
  • HTTP 404 (fehlender Artikel) ist nicht dasselbe wie API down --- notFound() nur für echtes Not-Found; Upstream-Fehler bleiben HTTP 200 mit feature-lokaler Fehler-UI.
  • In Produktion muss MAILOO_*_API der öffentliche API-Origin sein (z. B. https://api.mailoo.app), nicht localhost.

Installation über GitLab Package Registry

Pakete werden in der GitLab Package Registry dieses Projekts (npm) veröffentlicht, Scope @mailoo.

Fügen Sie eine .npmrc im Consumer-Projekt hinzu (Host / Projekt-ID ersetzen und ein Token mit read_api oder read_package_registry verwenden):

@mailoo:registry=https://<gitlab-host>/api/v4/projects/<project-id>/packages/npm/
//<gitlab-host>/api/v4/projects/<project-id>/packages/npm/:_authToken=<TOKEN>

Dann installieren Sie die benötigten Pakete (z. B. mit pnpm):

pnpm add @mailoo/next-core @mailoo/blog
# und/oder @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 }) --- liest {PREFIX}_API, _API_KEY, _PROJECT_UID, _ID / _INTEGRATION_ID
  • proxyMailooWebhookJsonPost --- POST-JSON mit X-API-Key und Browser- Origin / Referer für Upstream-CORS
  • getRequestOriginBaseUrl (@mailoo/next-core/request-origin) --- absoluter Origin aus Request-Headern (RSC → BFF)

@mailoo/blog

Umgebung (nur serverseitig):

MAILOO_BLOG_API=
MAILOO_BLOG_API_KEY=
MAILOO_BLOG_PROJECT_UID=
MAILOO_BLOG_INTEGRATION_ID=

BFF-Routen

// 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()

Ebenfalls verfügbar: createBlogCategoriesHandler, createBlogCategoryDescriptionHandler oder createMailooBlogRoutes() (list, slug, jsonLd, categories, categoryDescription).

Server-Client / RSC

import {
  createMailooBlogClient,
  getMailooBlogConfigFromEnv,
  fetchBlogPostBySlug,
  firstEmbedImageUrlFromArticle,
} from '@mailoo/blog/server'
  • createMailooBlogClient(config) --- listPosts, getPostBySlug, getPostJsonLd, listCategories, getCategoryDescription (direkte Mailoo-API; aus RSC / Sitemap verwenden, damit K8s/Docker niemals zum Host-BFF zurückschleifen). Fetches verwenden next: { revalidate: 60 } standardmäßig; übergeben Sie { revalidate: 0 } (oder eine andere Zahl) als letztes Argument. listPosts gibt data (Posts-Array) plus pagination und imagePublicEmbedBaseUrl zurück (gleiche Hülle wie die externe Listen-API; unvollständige Hülle → ok: false).
  • fetchBlogPostBySlug / fetchBlogPostJsonLd --- Fetch über das Same-Origin-BFF des Hosts

Komponenten

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'

Oder das Barrel @mailoo/blog/client. Bevorzugen Sie Unterpfade, um optionale Peers (z. B. react-markdown) nicht in jeden Import zu ziehen. BlogArticleShare rendert X- / Facebook- / LinkedIn- / Link-kopieren-Steuerelemente; übergeben Sie text, i18n-labels und optional absolute url.

Schritt-für-Schritt-Seitenbeispiele: blog-nextjs-example{.interpreted-text role="doc"}. Produktübersicht: blog-headless-cms{.interpreted-text role="doc"}.

@mailoo/forms

Umgebung

  • Newsletter (FORM): MAILOO_CONTACT_FORM_INTEGRATION_{API,API_KEY,PROJECT_UID,ID} (oder _INTEGRATION_ID). Ohne prefix beim Form-Handler wird dieses Standardpräfix verwendet.
  • Feedback (CONTACT_FORM): MAILOO_FEEDBACK_INTEGRATION_{API,API_KEY,PROJECT_UID,ID} Optional (gleiches Präfix): MAILOO_FEEDBACK_INTEGRATION_FORM_NAMESPACE, MAILOO_FEEDBACK_INTEGRATION_SITE_ID, MAILOO_FEEDBACK_INTEGRATION_ALLOWED_FORM_KEYS
  • Lifecycle (App-Registrierung / -Aktivierung): MAILOO_LIFECYCLE_{API,API_KEY,PROJECT_UID,ID} (oder _INTEGRATION_ID). Genutzt von ingestMailooLifecycleEvent und Lifecycle-BFF-Handlern.
  • Mehrere Integrationen: prefix (oder getConfig) pro BFF-Route übergeben, z. B. createFeedbackSubmitHandler({ prefix: 'MAILOO_TEAM_SUBSCRIPTION_FORM' }). Bevorzugter Konfigurationshelfer: getMailooFormsIntegrationConfig({ prefix }). Veraltet: getMailooFormIntegrationConfig, getMailooFeedbackIntegrationConfig, isMailooFeedbackIntegrationConfigured.

Exports

  • @mailoo/forms/routes --- createFormSubmitHandler, createFeedbackSubmitHandler, createLifecycleEventHandler, createLifecycleStatusHandler
  • @mailoo/forms/hooks --- useMailooFormSubmit, useMailooFeedbackSubmit, useMailooLifecycleEvent (Hooks liefern { submit|emit, pending })
  • @mailoo/forms/events --- geschlossener Katalog MAILOO_LIFECYCLE_EVENTS (user.registered, user.activated) und Attributschlüssel-Validatoren
  • @mailoo/forms/server --- ingestMailooLifecycleEvent für serverseitiges Emit

FORM-Submit-Bodies sind typisiert (email, optional name / content / ...). Bereits-abonniert-Antworten sind erfolgreich ohne messageId. Lifecycle- eventName und Attributschlüssel sind geschlossen --- unbekannte Werte scheitern fail-fast (400).

Routen und 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()
// Zweite CONTACT_FORM-Integration (eigenes Umgebungspräfix)
import { createFeedbackSubmitHandler } from '@mailoo/forms/routes'
export const POST = createFeedbackSubmitHandler({
  prefix: 'MAILOO_TEAM_SUBSCRIPTION_FORM',
})
'use client'
import { useMailooFormSubmit, useMailooFeedbackSubmit } from '@mailoo/forms/hooks'
// Hooks auf den BFF-Pfad richten, der die gewünschte Integration bindet

Siehe auch website-forms-nextjs-example{.interpreted-text role="doc"}, feedback-form-nextjs-example{.interpreted-text role="doc"} und lifecycle-events{.interpreted-text role="doc"}.

@mailoo/chat

Umgebung

MAILOO_CHAT_INTEGRATION_API=
MAILOO_CHAT_INTEGRATION_API_KEY=
MAILOO_CHAT_INTEGRATION_PROJECT_UID=
MAILOO_CHAT_INTEGRATION_ID=
# Optional: öffentlicher API-Origin für WSS wenn abweichend vom BFF-Ziel
MAILOO_CHAT_WS_ORIGIN=
MAILOO_CHAT_WELCOME_MESSAGE=

Der Browser öffnet WebSocket direkt zu Mailoo (/api/v1/chat/visitor-ws). Der Ingress muss Upgrade unterstützen. Siehe chat-websocket-production{.interpreted-text role="doc"}.

Routen, Hooks und Widget

Das Paket hängt nicht von Auth.js ab. Optional Session-E-Mail injizieren:

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

Konfigurationshelfer: getMailooChatIntegrationConfig, getMailooChatWebSocketOrigin, fetchMailooChatIntegrationStatus aus @mailoo/chat.

Nicht verfügbar / Rate-Limit (unavailableAction, renderUnavailable, fault): chat-widget-nextjs-example{.interpreted-text role="doc"}.

UI-Anleitung: chat-widget-nextjs-example{.interpreted-text role="doc"}.

@mailoo/images

Öffentlicher Einbettungs-Proxy (API-Schlüssel bleibt auf dem Server):

// app/api/mailoo-image/route.ts
import { createPublicImageContentHandler } from '@mailoo/images/routes'

export const GET = createPublicImageContentHandler()

Standardmäßig MAILOO_BLOG_API / MAILOO_BLOG_API_KEY (HTTP 503 wenn nicht gesetzt). Übergeben Sie getApiBaseUrl / getApiKey zum Überschreiben.

Absolute Mailoo-Public-URLs in HTML/Markdown vor dem Rendern umschreiben:

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=',
})

Artikel-Cover, HTML und Markdown: rewriteMailooBlogMediaUrls. Oder createMailooBlogClient(config, { imageProxyPrefixWithIdEquals }) --- das kanonische Präfix kommt aus imagePublicEmbedBaseUrl. Der Schlüssel braucht image.external-read. og:image ist die Host-URL, nicht die Mailoo-URL.

Bearer-Dashboard-BFF: createImagesLibraryHandlers({ getApiBaseUrl, getAuthHeader, resolveAuthHeader }). Details: images{.interpreted-text role="doc"}.

@mailoo/market

Umgebung (nur serverseitig):

MAILOO_MARKET_API=
MAILOO_MARKET_API_KEY=
MAILOO_MARKET_PROJECT_UID=
MAILOO_MARKET_INTEGRATION_ID=

BFF-Routen (fehlende Umgebung → 503; Upstream down → 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()

Server-Client:

import {
  createMailooMarketClient,
  getMailooMarketConfigFromEnv,
} from '@mailoo/market/server'

const config = getMailooMarketConfigFromEnv()
if (config) {
  const list = await createMailooMarketClient(config).listProducts({
    locale: 'de',
  })
  if (!list.ok) {
    // feature-lokaler Fehler — nicht werfen
  }
}

Offer-Generator-Assistenten sind in diesem Gerüst noch nicht enthalten --- siehe offer-generator-market{.interpreted-text role="doc"}. Produktübersicht: market-catalog-external-api{.interpreted-text role="doc"}, market-order-email-checkout{.interpreted-text role="doc"}.

Sicherheitshinweise

  • Mailoo-API-Schlüssel oder Basis-URLs niemals über NEXT_PUBLIC_* offenlegen.
  • Same-Origin-BFF-Routen bevorzugen; der Browser ruft nur Ihre Next.js-App auf.
  • Beschränkte API-Schlüssel: die für jede Integration dokumentierten Berechtigungen verwenden (z. B. blog.external-read, webhook.form-submission, chat.send-message, image.external-read, market.external-read, market.order.submit).

Verwandte Seiten

  • 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"}