Next.js integrator packages (`@mailoo/*`)

Обновлено: Jul 28, 2026Раздел: General

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 and Link
  • 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_ID
  • proxyMailooWebhookJsonPost --- POST JSON with X-API-Key and browser Origin / Referer for upstream CORS
  • getRequestOriginBaseUrl --- 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).
  • 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"}