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 },nulloder 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 undLink- Ü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 vonres.okund kann die ganze Antwort mit 500 beenden. - Rufen Sie Mailoo nicht aus dem Root-
layout.tsxauf, außer Sie nutzen einen Never-Throw-Helfer (resolveMailooChatHostConfigist 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_*_APIder öffentliche API-Origin sein (z. B.https://api.mailoo.app), nichtlocalhost.
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_IDproxyMailooWebhookJsonPost--- POST-JSON mitX-API-Keyund Browser-Origin/Refererfür Upstream-CORSgetRequestOriginBaseUrl(@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 verwendennext: { revalidate: 60 }standardmäßig; übergeben Sie{ revalidate: 0 }(oder eine andere Zahl) als letztes Argument.listPostsgibtdata(Posts-Array) pluspaginationundimagePublicEmbedBaseUrlzurü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). Ohneprefixbeim 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 voningestMailooLifecycleEventund Lifecycle-BFF-Handlern. - Mehrere Integrationen:
prefix(odergetConfig) 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 KatalogMAILOO_LIFECYCLE_EVENTS(user.registered,user.activated) und Attributschlüssel-Validatoren@mailoo/forms/server---ingestMailooLifecycleEventfü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"}