Blog (Headless CMS) Integration

Last updated: Apr 27, 2026Section: General

Blog (Headless CMS) Integration

Full API reference: https://api.mailoo.app/docs/v1

Use Mailoo as a central headless CMS for your blog: create and manage articles in the dashboard and deliver them via an external API by project. External sites (or your own) can consume the same API.

Overview

The Blog integration works like other Mailoo integrations (e.g. Form): you add a Blog integration to a project. All articles for that blog belong to that integration. The API does not provide access to blog articles without authorization. Read API: list articles and get one by slug; requires X-API-Key with scope blog.external-read; access is keyed by project UID and integration ID.

Capabilities:

  • Create, edit, and delete articles in the Mailoo dashboard (per integration)
  • Publish as draft or published; optional excerpt, category, tags
  • Read API (X-API-Key required): list published articles and get one by slug, keyed by project UID and integration ID
  • Article response: id, title, slug, excerpt, content (Markdown, source of truth), htmlContent (HTML generated from content by the API; use for display), status, publishedAt, createdAt, updatedAt, author (id, name, email?, avatar, bio --- localized when locale is set), category (id, slug, name or null), featured, readTimeMinutes, tags (array, default []). Pagination includes page, limit, total, totalPages, hasNextPage, hasPrevPage. Inline images use URLs from the Mailoo image API (see images{.interpreted-text role="doc"}); there is no separate top-level article image field.

Author profiles (dashboard)

Each account can maintain user-owned author profiles (display name, slug, optional public email, avatar URL, bio, optional per-locale overrides). Articles store a live reference to the selected profile so updates propagate. Pseudonyms are supported (isAlias). One profile can be marked as your global default; you can also set a per--blog-integration default (your preference only --- not stored in shared integration config).

  • Authors tab on the Blog integration: list and create/edit profiles.
  • Connection & settings tab: Default author saves the per-integration override (falls back to the global default when unset).
  • New/Edit article: choose an author profile or leave integration / global default so the API resolves the author automatically.

Creating a Blog Integration

  1. Go to Dashboard → Projects → [Your Project]
  2. Click Create New Integration
  3. Select Blog (Headless CMS)
  4. Set a name and status (e.g. Active)
  5. After creation, open the integration to see the Articles list and Connection block

Managing Articles

On the integration page you can:

  • Articles table: Title, slug, status, date, and Edit for each article
  • Create article: Opens the new-article form (title, excerpt, content, status). Content is entered in Markdown (headings, lists, bold, links, images via the dashboard image panel or public embed URLs); the API converts it to HTML on save.
  • Edit: Opens the edit form for that article (save as draft, publish, or archive). Same Markdown editor for the default locale and for each translation (locales).

Content format: Article body is stored as content in Markdown. The API generates htmlContent from it. Consumers should use htmlContent for display to get correct structure and typography.

Links in htmlContent: Absolute http:// / https:// URLs and protocol-relative //… links include target="_blank" and rel="noopener noreferrer". Same-tab behavior applies to site-root paths (/path), explicit path-relative links (./…, ../…), in-page anchors (#section), and query-only URLs (?q=1).

Same-site links --- use a leading slash. In Markdown, [label](support/docs) becomes HTML href="support/docs". That is a path-relative URL: browsers resolve it per RFC 3986 against the directory of the page that displays the article, not against the site root. For example, on a post at https://example.com/en/blog/my-post, support/docs resolves to /en/blog/support/docs. To point at a site section from the root, write [label](/support/docs) (or the full https://… URL). Single-segment links without a slash (e.g. [other](other-slug)) are path-relative too, so they stay under the same directory as the post URL --- useful for sibling posts only when your public post URLs share that path prefix.

Mermaid in htmlContent: Fenced code blocks labeled mermaid are converted to static inline SVG (inside <figure class="mailoo-mermaid">). No browser-side Mermaid runtime is required; the same HTML is suitable for email-style rendering. Invalid Mermaid syntax causes the API to reject the save (articles, localized content, campaigns, etc.).

Managing Categories

A Categories block on the same integration page lets you manage blog categories (scoped to this integration). Use categories when creating or editing articles.

  • Add category: Enter name and slug (slug is auto-derived from name if left blank), then Add category
  • Edit: Change name or slug for a category, then Save or Cancel
  • Description (optional): Choose an article that will represent the category description in external clients. The API stores it as descriptionPostId.
  • Delete: Click Delete, then confirm with Confirm (or Cancel to keep the category). Articles keep their category link until you change it.
  • If a description article is deleted later, category descriptions are automatically cleared (descriptionPostId is set to null).

System categories (about / portfolio)

Mailoo reserves two category slugs per Blog integration: about and portfolio. Use them for standalone pages (about the site, portfolio) that should not appear in the default public article list.

  • Provisioning: Creating a Blog integration automatically creates categories About (slug about) and Portfolio (slug portfolio). Existing Blog integrations receive the same rows via database migration (idempotent).
  • Default list: GET .../blog/{projectUid}/integrations/{integrationId} with no category and no categoryId omits published articles whose linked category slug is about or portfolio. Articles that only use the legacy string field category (no categoryId) are omitted when that string matches those slugs (case-insensitive).
  • Explicit filter: Pass category (slug or legacy string) or categoryId to include those articles --- for example ?category=portfolio or ?categoryId=….
  • Single article by slug is unchanged: GET .../slug/{slug} still returns the article if it is published, including about/portfolio content.
  • Dashboard article list still shows all articles (no hidden categories).

The category query parameter filters by BlogCategory.slug or the legacy post category string (case-insensitive). If both category and categoryId are set, both conditions apply (logical AND).

Connection (External API)

The Connection block on the integration page shows the endpoints to use from your site or app. For web applications, the only recommended approach is BFF (API key on the server); direct API calls with X-API-Key are for server-to-server use. For Next.js, see blog-nextjs-example{.interpreted-text role="doc"}.

BFF routes (Next.js, same-origin) --- use env vars MAILOO_BLOG_PROJECT_UID, MAILOO_BLOG_INTEGRATION_ID, MAILOO_BLOG_API, MAILOO_BLOG_API_KEY:

  • List published articles: GET /api/v1/blog Query: page, limit, category, categoryId, search, locale, featured, tag (optional); filters combine with logical AND. Without category and categoryId, articles in reserved slugs about and portfolio are excluded (see System categories above). tag is an exact match on one value in the article tags array (case-sensitive as stored). Returns { success, data: [...], pagination } with hasNextPage, hasPrevPage.

  • Get one article by slug: GET /api/v1/blog/slug/{slug} Query: locale (optional). Returns { success, data: article }. Only published articles are returned.

  • List categories: GET /api/v1/blog/categories Returns { success, data: Category[] } (id, slug, name).

  • Get category description by slug: GET /api/v1/blog/categories/slug/{slug}/description Query: locale (optional). Returns { success, data: { description, descriptionHtml } }. If category has no description article, both fields are empty strings. If category slug does not exist, returns 404.

  • Public author profile (by id or slug): same-origin BFF is not required; call the API directly with X-API-Key:

    GET {baseUrl}/api/v1/blog/{projectUid}/integrations/{integrationId}/authors/{authorSlugOrId}

    Query: locale (optional). Returns localized name, bio, avatar, and email only when the profile exposes a public email (pseudonyms do not leak the account email).

Direct API calls --- require X-API-Key header and scope blog.external-read:

  • List: GET {baseUrl}/api/v1/blog/{projectUid}/integrations/{integrationId} Optional query: page, limit, category, categoryId, search, locale, featured, tag (same semantics as the BFF list route above).
  • Single: GET {baseUrl}/api/v1/blog/{projectUid}/integrations/{integrationId}/slug/{slug} Query: locale (optional).
  • Categories: GET {baseUrl}/api/v1/blog/{projectUid}/integrations/{integrationId}/categories
  • Category description: GET {baseUrl}/api/v1/blog/{projectUid}/integrations/{integrationId}/categories/slug/{slug}/description Query: locale (optional). Same behavior as BFF endpoint above.
  • Author profile: GET {baseUrl}/api/v1/blog/{projectUid}/integrations/{integrationId}/authors/{authorSlugOrId} Query: locale (optional). Public read of a project owner's author profile for bylines and author pages.

Base URL is your Mailoo API (e.g. https://api.mailoo.app). projectUid and integrationId are shown in the Connection block. Images in article bodies use the Mailoo image upload API and public embed URLs inside content / htmlContent (see images{.interpreted-text role="doc"} for storage, scopes, and how to read bytes with Bearer or X-API-Key). Tags are returned as an array; empty array when none. Articles used as category descriptions are excluded from the external article list, so they are not duplicated in regular feed results. The same default list also excludes about and portfolio category articles unless you pass an explicit category filter. For full response fields and error codes, see the API docs above.

For a full Next.js example, see blog-nextjs-example{.interpreted-text role="doc"}.