Next.js integrator packages (`@mailoo/*`)
Next.js integrator packages (@mailoo/*)
Mailoo publishes headless npm packages for Next.js App Router apps. They provide BFF route factories, server-side clients, and optional unstyled components. The host application keeps locale routing, i18n, theme, auth, and page layouts.
The reference consumer is apps/web in this repository (same patterns an external site would use: server-only credentials, same-origin BFF, no NEXT_PUBLIC_* API keys).
Full API reference: https://api.mailoo.app/docs/v1
Package list
Package Role
@mailoo/next-core Shared helpers: env config, webhook JSON proxy, request-origin URL for RSC
@mailoo/blog Blog list/slug/categories BFF factories, server client, article UI pieces
@mailoo/forms FORM (newsletter) and CONTACT_FORM (feedback) submit BFF + client hooks
@mailoo/chat JSBOX visitor chat BFF + floating widget (WebSocket to Mailoo API)
@mailoo/images Public image proxy route factory + rewriteMailooPublicImageUrls
Versions match the monorepo VERSION (published together). Peer dependency on @mailoo/next-core for the feature packages.
What the package owns vs the host
Inside the package
- Calling Mailoo with
X-API-Key/ base URL from server-only env - Fail-fast config validation (missing env → HTTP 503)
- Types for API payloads
- Optional presentation with
className/ label props (no next-intl)
Host application
- Mounting route handlers under
app/api/... [locale]routing andLink- Translations, Tailwind / typography theme, Auth.js, marketing CTAs
Install from GitLab Package Registry
Packages are published to this project's GitLab Package Registry (npm), scope @mailoo. CI uses templates/Pipelines/package-publish.yml.
Add an .npmrc in the consumer project (replace host / project id and use a token with read_api or 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>
Then:
pnpm add @mailoo/next-core @mailoo/blog
# and/or @mailoo/forms @mailoo/chat @mailoo/images
Inside the Mailoo monorepo, apps/web uses workspace:* so local source is linked without waiting for a registry publish.
@mailoo/next-core
import {
readMailooIntegrationConfig,
proxyMailooWebhookJsonPost,
getRequestOriginBaseUrl,
jsonError,
} from '@mailoo/next-core'
readMailooIntegrationConfig({ prefix })--- reads{PREFIX}_API,_API_KEY,_PROJECT_UID,_ID/_INTEGRATION_IDproxyMailooWebhookJsonPost--- POST JSON withX-API-Keyand browserOrigin/Refererfor upstream CORSgetRequestOriginBaseUrl--- absolute origin from request headers (RSC → BFF)
@mailoo/blog
Env (server-only):
MAILOO_BLOG_API=
MAILOO_BLOG_API_KEY=
MAILOO_BLOG_PROJECT_UID=
MAILOO_BLOG_INTEGRATION_ID=
BFF routes
// 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()
Also available: createBlogCategoriesHandler, createBlogCategoryDescriptionHandler, or createMailooBlogRoutes().
Server client / RSC
import {
createMailooBlogClient,
getMailooBlogConfigFromEnv,
fetchBlogPostBySlug,
firstEmbedImageUrlFromArticle,
} from '@mailoo/blog/server'
createMailooBlogClient(config)---listPosts,getPostBySlug,listCategories,getCategoryDescription(direct Mailoo API)fetchBlogPostBySlug--- fetch via the host's same-origin BFF
Components
import { BlogArticleBody } from '@mailoo/blog/client/article-body'
import { BlogLinkedLinksDisplay } from '@mailoo/blog/client/linked-links'
import { BlogPrintableButtons } from '@mailoo/blog/client/printable'
Or the barrel @mailoo/blog/client. Prefer subpaths when you want to avoid pulling optional peers (e.g. react-markdown) into every import.
Step-by-step page examples: blog-nextjs-example{.interpreted-text role="doc"}. Product overview: blog-headless-cms{.interpreted-text role="doc"}.
@mailoo/forms
Env
- Newsletter (FORM):
MAILOO_CONTACT_FORM_INTEGRATION_{API,API_KEY,PROJECT_UID,ID} - Feedback (CONTACT_FORM):
MAILOO_FEEDBACK_INTEGRATION_{API,API_KEY,PROJECT_UID,ID}Optional:MAILOO_FEEDBACK_INTEGRATION_FORM_NAMESPACE,MAILOO_FEEDBACK_INTEGRATION_SITE_ID,MAILOO_FEEDBACK_ALLOWED_FORM_KEYS
Routes and 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()
'use client'
import { useMailooFormSubmit, useMailooFeedbackSubmit } from '@mailoo/forms/hooks'
See also website-forms-nextjs-example{.interpreted-text role="doc"} and feedback-form-nextjs-example{.interpreted-text role="doc"}.
@mailoo/chat
Env
MAILOO_CHAT_INTEGRATION_API=
MAILOO_CHAT_INTEGRATION_API_KEY=
MAILOO_CHAT_INTEGRATION_PROJECT_UID=
MAILOO_CHAT_INTEGRATION_ID=
# Optional: public API origin for WSS when it differs from the BFF target
MAILOO_CHAT_WS_ORIGIN=
MAILOO_CHAT_WELCOME_MESSAGE=
The browser opens WebSocket directly to Mailoo (/api/v1/chat/visitor-ws). Ingress must support Upgrade. See chat-websocket-production{.interpreted-text role="doc"}.
Routes and widget
The package does not depend on Auth.js. Optionally inject session email:
import { createChatSubmitHandler, createChatMessagesHandler } 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()
import { MailooSiteChatWidget } from '@mailoo/chat/client'
<MailooSiteChatWidget
apiOrigin={wsOrigin}
projectUid={projectUid}
integrationId={integrationId}
welcomeMessage={welcome}
locale={locale}
isAuthenticated={isAuthenticated}
/>
Config helpers: getMailooChatIntegrationConfig, getMailooChatWebSocketOrigin from @mailoo/chat.
UI walkthrough: chat-widget-nextjs-example{.interpreted-text role="doc"}.
@mailoo/images
Public embed proxy (API key stays on the server):
// app/api/mailoo-image/route.ts
import { createPublicImageContentHandler } from '@mailoo/images/routes'
export const GET = createPublicImageContentHandler({
getApiBaseUrl: () => process.env.MAILOO_BLOG_API!,
getApiKey: () => process.env.MAILOO_BLOG_API_KEY!,
})
Rewrite absolute Mailoo public URLs in HTML/Markdown before render:
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=',
})
Bearer dashboard-style BFF: createImagesLibraryHandlers({ getApiBaseUrl, getAuthHeader, resolveAuthHeader }). Details: images{.interpreted-text role="doc"}.
Security notes
- Never expose Mailoo API keys or base URLs via
NEXT_PUBLIC_*. - Prefer same-origin BFF routes; the browser calls your Next.js app only.
- Restricted API keys: use the scopes documented for each integration (e.g.
blog.external-read,webhook.form-submission,chat.send-message,image.external-read).
Related pages
blog-nextjs-example{.interpreted-text role="doc"}website-forms-nextjs-example{.interpreted-text role="doc"}feedback-form-nextjs-example{.interpreted-text role="doc"}chat-widget-nextjs-example{.interpreted-text role="doc"}images{.interpreted-text role="doc"}