Blog (Headless CMS) Integration
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
localeis set), category (id, slug, name or null), featured, readTimeMinutes, tags (array, default []). Pagination includespage,limit,total,totalPages,hasNextPage,hasPrevPage. Inline images use URLs from the Mailoo image API (seeimages{.interpreted-text role="doc"}); there is no separate top-level articleimagefield.
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
- Go to Dashboard → Projects → [Your Project]
- Click Create New Integration
- Select Blog (Headless CMS)
- Set a name and status (e.g. Active)
- 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 (
descriptionPostIdis 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 (slugportfolio). Existing Blog integrations receive the same rows via database migration (idempotent). - Default list:
GET .../blog/{projectUid}/integrations/{integrationId}with nocategoryand nocategoryIdomits published articles whose linked category slug isaboutorportfolio. Articles that only use the legacy string fieldcategory(nocategoryId) are omitted when that string matches those slugs (case-insensitive). - Explicit filter: Pass
category(slug or legacy string) orcategoryIdto include those articles --- for example?category=portfolioor?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/blogQuery:page,limit,category,categoryId,search,locale,featured,tag(optional); filters combine with logical AND. WithoutcategoryandcategoryId, articles in reserved slugsaboutandportfolioare excluded (see System categories above).tagis an exact match on one value in the articletagsarray (case-sensitive as stored). Returns{ success, data: [...], pagination }withhasNextPage,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/categoriesReturns{ success, data: Category[] }(id, slug, name). -
Get category description by slug:
GET /api/v1/blog/categories/slug/{slug}/descriptionQuery:locale(optional). Returns{ success, data: { description, descriptionHtml } }. If category has no description article, both fields are empty strings. If category slug does not exist, returns404. -
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}/descriptionQuery: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"}.