Mailoo публикует headless npm-пакеты для приложений Next.js App Router. Они предоставляют фабрики BFF-маршрутов, серверные клиенты и опциональные нестилизованные компоненты. Хост-приложение сохраняет за собой маршрутизацию по локалям, i18n, тему, авторизацию и макеты страниц.
Внешние хосты Next.js используют те же паттерны: серверные учётные данные, BFF того же домена, без NEXT_PUBLIC_* API-ключей.
Локальный пример --- tests/integrator-demo (порт 3080): запуск из Dev Manager Optional или pnpm run dev:integrator-demo. См. tests/integrator-demo/README.md.
Полная справка по API: https://api.mailoo.app/docs/v1
Список пакетов
Пакет Назначение
@mailoo/next-core Общие хелперы: конфигурация окружения, JSON-прокси вебхуков, URL происхождения запроса для RSC
@mailoo/blog Фабрики BFF для списка/slug/json-ld/категорий блога, серверный клиент, UI-компоненты статей
@mailoo/forms BFF отправки FORM (рассылка) и CONTACT_FORM (обратная связь) + клиентские хуки; события жизненного цикла (./events, ./server)
@mailoo/chat BFF чата посетителя JSBOX, headless-хуки и плавающий виджет (WebSocket к API Mailoo)
@mailoo/images Фабрика прокси-маршрута для публичных изображений + rewriteMailooPublicImageUrls
@mailoo/market Фабрики BFF каталога Market + email-checkout и серверный клиент (каркас; offer-generator --- отдельный follow-up)
SOCIAL --- без npm-пакета: compose/publish только в dashboard Mailoo (Bearer). Внешний сайт не монтирует SOCIAL runtime --- см. social-networks{.interpreted-text role="doc"}.
Версии совпадают с VERSION монорепозитория (публикуются вместе). Peer-зависимость от @mailoo/next-core для функциональных пакетов.
Что входит в пакет, а что в хост
Внутри пакета
- Вызов Mailoo с
X-API-Key/ базовым URL из серверного окружения - Нет env на BFF → HTTP 503 JSON (не необработанный throw)
- Сбой upstream / сеть →
{ ok: false },nullили HTTP 502 JSON - Типы для полезных нагрузок API
- Опциональное представление с
className/ label пропсами (без next-intl)
Хост-приложение
- Монтирование обработчиков маршрутов в
app/api/... - Маршрутизация
[locale]иLink - Переводы, тема Tailwind / типографика, Auth.js, маркетинговые CTA
- Изоляция ошибок интеграции, чтобы недоступный API Mailoo не давал 500 всему сайту (см. ниже)
Хост: ошибки интеграции не должны валить приложение
Считайте Mailoo фичей, а не жёсткой зависимостью каждой страницы.
- Хелперы layout/RSC пакета не должны бросать при недоступном API: возвращают
null,{ ok: false, status }или BFF JSON 503 / 502. - Хост обязан проверить результат до рендера и показать ошибку только этой фичи.
- Не оставляйте голый
await fetch(...)в Server Component / root layout: сеть бросает до проверкиres.okи может дать 500 всему ответу. - Не вызывайте Mailoo из root
layout.tsxбез never-throw хелпера (эталон ---resolveMailooChatHostConfig). - HTTP 404 (нет статьи) ≠ API down ---
notFound()только для настоящего not-found; сбой upstream --- HTTP 200 с ошибкой фичи. - В проде
MAILOO_*_API--- публичный origin API (напримерhttps://api.mailoo.app), неlocalhost.
Установка из реестра пакетов GitLab
Пакеты опубликованы в реестре пакетов GitLab (npm) этого проекта, область @mailoo.
Добавьте .npmrc в проект-потребитель (замените хост / id проекта и используйте токен с read_api или 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>
Затем установите нужные пакеты (например, с pnpm):
pnpm add @mailoo/next-core @mailoo/blog
# и/или @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 })--- читает{PREFIX}_API,_API_KEY,_PROJECT_UID,_ID/_INTEGRATION_IDproxyMailooWebhookJsonPost--- POST JSON сX-API-Keyи браузернымOrigin/Refererдля upstream CORSgetRequestOriginBaseUrl(@mailoo/next-core/request-origin) --- абсолютный origin из заголовков запроса (RSC → BFF)
@mailoo/blog
Окружение (только сервер):
MAILOO_BLOG_API=
MAILOO_BLOG_API_KEY=
MAILOO_BLOG_PROJECT_UID=
MAILOO_BLOG_INTEGRATION_ID=
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()
Также доступны: createBlogCategoriesHandler, createBlogCategoryDescriptionHandler или createMailooBlogRoutes() (list, slug, jsonLd, categories, categoryDescription).
Серверный клиент / RSC
import {
createMailooBlogClient,
getMailooBlogConfigFromEnv,
fetchBlogPostBySlug,
firstEmbedImageUrlFromArticle,
} from '@mailoo/blog/server'
createMailooBlogClient(config)---listPosts,getPostBySlug,getPostJsonLd,listCategories,getCategoryDescription(прямой API Mailoo; используйте из RSC / карты сайта, чтобы K8s/Docker не зацикливался на хост BFF). Запросы используютnext: { revalidate: 60 }по умолчанию; передайте{ revalidate: 0 }(или другое число) последним аргументом.listPostsвозвращаетdata(массив записей) плюсpaginationиimagePublicEmbedBaseUrl(тот же конверт, что и внешний API списка; неполный конверт →ok: false).fetchBlogPostBySlug/fetchBlogPostJsonLd--- запрос через BFF того же домена хоста
Компоненты
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'
Или бочковой экспорт @mailoo/blog/client. Предпочитайте подпути, чтобы не подтягивать опциональные peer-зависимости (например, react-markdown) в каждый импорт. BlogArticleShare --- кнопки X / Facebook / LinkedIn / копировать ссылку; передайте text, подписи и необязательный абсолютный url.
Пошаговые примеры страниц: blog-nextjs-example{.interpreted-text role="doc"}. Обзор продукта: blog-headless-cms{.interpreted-text role="doc"}.
@mailoo/forms
Окружение
- Рассылка (FORM):
MAILOO_CONTACT_FORM_INTEGRATION_{API,API_KEY,PROJECT_UID,ID}(или_INTEGRATION_ID). Не указывайтеprefixу обработчика формы для использования по умолчанию. - Обратная связь (CONTACT_FORM):
MAILOO_FEEDBACK_INTEGRATION_{API,API_KEY,PROJECT_UID,ID}Необязательно (тот же префикс):MAILOO_FEEDBACK_INTEGRATION_FORM_NAMESPACE,MAILOO_FEEDBACK_INTEGRATION_SITE_ID,MAILOO_FEEDBACK_INTEGRATION_ALLOWED_FORM_KEYS - Жизненный цикл (регистрация / активация в приложении):
MAILOO_LIFECYCLE_{API,API_KEY,PROJECT_UID,ID}(или_INTEGRATION_ID). ИспользуетсяingestMailooLifecycleEventи BFF-обработчиками lifecycle. - Несколько интеграций: передайте
prefix(илиgetConfig) для каждого BFF-маршрута, напримерcreateFeedbackSubmitHandler({ prefix: 'MAILOO_TEAM_SUBSCRIPTION_FORM' }). Рекомендуемый хелпер:getMailooFormsIntegrationConfig({ prefix }). Устаревшие:getMailooFormIntegrationConfig,getMailooFeedbackIntegrationConfig,isMailooFeedbackIntegrationConfigured.
Экспорты
@mailoo/forms/routes---createFormSubmitHandler,createFeedbackSubmitHandler,createLifecycleEventHandler,createLifecycleStatusHandler@mailoo/forms/hooks---useMailooFormSubmit,useMailooFeedbackSubmit,useMailooLifecycleEvent(хуки возвращают{ submit|emit, pending })@mailoo/forms/events--- закрытый каталогMAILOO_LIFECYCLE_EVENTS(user.registered,user.activated) и валидаторы ключей атрибутов@mailoo/forms/server---ingestMailooLifecycleEventдля серверной отправки
Тела отправки FORM типизированы (email, опционально name / content / ...). Ответы «уже подписан» успешны без messageId. eventName и ключи атрибутов lifecycle закрыты --- неизвестные значения завершаются ошибкой (400).
Маршруты и хуки
// 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()
// Вторая интеграция CONTACT_FORM (пользовательский префикс окружения)
import { createFeedbackSubmitHandler } from '@mailoo/forms/routes'
export const POST = createFeedbackSubmitHandler({
prefix: 'MAILOO_TEAM_SUBSCRIPTION_FORM',
})
'use client'
import { useMailooFormSubmit, useMailooFeedbackSubmit } from '@mailoo/forms/hooks'
// Направьте хуки на BFF-путь, привязанный к нужной интеграции
См. также website-forms-nextjs-example{.interpreted-text role="doc"}, feedback-form-nextjs-example{.interpreted-text role="doc"} и lifecycle-events{.interpreted-text role="doc"}.
@mailoo/chat
Окружение
MAILOO_CHAT_INTEGRATION_API=
MAILOO_CHAT_INTEGRATION_API_KEY=
MAILOO_CHAT_INTEGRATION_PROJECT_UID=
MAILOO_CHAT_INTEGRATION_ID=
# Необязательно: публичный API origin для WSS, если отличается от целевого BFF
MAILOO_CHAT_WS_ORIGIN=
MAILOO_CHAT_WELCOME_MESSAGE=
Браузер открывает WebSocket напрямую к Mailoo (/api/v1/chat/visitor-ws). Ingress должен поддерживать Upgrade. См. chat-websocket-production{.interpreted-text role="doc"}.
Маршруты, хуки и виджет
Пакет не зависит от Auth.js. Опционально внедрите e-mail сессии:
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}
/>
Хелперы конфигурации: getMailooChatIntegrationConfig, getMailooChatWebSocketOrigin, fetchMailooChatIntegrationStatus из @mailoo/chat.
Недоступность / rate limit (unavailableAction, renderUnavailable, fault): chat-widget-nextjs-example{.interpreted-text role="doc"}.
Описание интерфейса: chat-widget-nextjs-example{.interpreted-text role="doc"}.
@mailoo/images
Публичный прокси встраивания (API-ключ остаётся на сервере):
// app/api/mailoo-image/route.ts
import { createPublicImageContentHandler } from '@mailoo/images/routes'
export const GET = createPublicImageContentHandler()
По умолчанию использует MAILOO_BLOG_API / MAILOO_BLOG_API_KEY (HTTP 503 если не задано). Передайте getApiBaseUrl / getApiKey для переопределения.
Перезапись абсолютных URL публичных изображений Mailoo в HTML/Markdown перед рендером:
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=',
})
Обложка, HTML и Markdown статьи: rewriteMailooBlogMediaUrls. Или createMailooBlogClient(config, { imageProxyPrefixWithIdEquals }) --- канонический префикс берётся из imagePublicEmbedBaseUrl. Ключу нужен image.external-read. og:image --- URL хоста, не URL Mailoo.
Bearer BFF в стиле панели управления: createImagesLibraryHandlers({ getApiBaseUrl, getAuthHeader, resolveAuthHeader }). Подробности: images{.interpreted-text role="doc"}.
@mailoo/market
Env (только сервер):
MAILOO_MARKET_API=
MAILOO_MARKET_API_KEY=
MAILOO_MARKET_PROJECT_UID=
MAILOO_MARKET_INTEGRATION_ID=
BFF (нет env → 503; upstream down → 502):
import { createMarketProductsHandler } from '@mailoo/market/routes'
export const GET = createMarketProductsHandler()
import { createMarketOrdersHandler } from '@mailoo/market/routes'
export const POST = createMarketOrdersHandler()
Серверный клиент: createMailooMarketClient / getMailooMarketConfigFromEnv из @mailoo/market/server --- проверяйте ok до рендера. Offer-generator в этом каркасе ещё нет --- offer-generator-market{.interpreted-text role="doc"}.
Примечания по безопасности
- Никогда не передавайте API-ключи или базовые URL Mailoo через
NEXT_PUBLIC_*. - Предпочитайте BFF-маршруты того же домена; браузер вызывает только ваше приложение Next.js.
- Ограниченные API-ключи: используйте области действия, документированные для каждой интеграции (например,
blog.external-read,webhook.form-submission,chat.send-message,image.external-read,market.external-read,market.order.submit).
Связанные страницы
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"}