Blog API: Next.js Example

Última actualización: May 6, 2026Sección: General

Blog API: Next.js Example

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

This page shows how to integrate Mailoo's public Blog API into a Next.js app. The reference implementation is the blog in this repository at en/blog: list via BFF, article by slug via BFF, no API key on the client.

Prerequisites

  • A Mailoo project with a Blog integration and at least one published article
  • The project UID, integration ID, and an API key (dashboard → project → API Keys)

The exact values for MAILOO_BLOG_PROJECT_UID and MAILOO_BLOG_INTEGRATION_ID are shown on the integration settings page in the dashboard (Connection (External API) block). Use an API key with Blog (external read) scope.

Environment (server-only)

Add to .env.local:

MAILOO_BLOG_API=<same value as API_BASE_URL>
MAILOO_BLOG_API_KEY=your-api-key-here
MAILOO_BLOG_PROJECT_UID=<from dashboard integration page>
MAILOO_BLOG_INTEGRATION_ID=<from dashboard integration page>

Use server-side only; do not use NEXT_PUBLIC_* for API URL or key.

List Page (e.g. app/blog/page.tsx)

Using BFF routes (same app exposes /api/v1/blog):

export default async function BlogPage() {
  const res = await fetch('/api/v1/blog?limit=20', { next: { revalidate: 60 } })
  if (!res.ok) return <div>Failed to load blog</div>
  const json = await res.json()
  const posts = json.data || []
  return (
    <div>
      <h1>Blog</h1>
      <ul>
        {posts.map((p: { id: string; title: string; slug: string }) => (
          <li key={p.id}><a href={`/blog/${p.slug}`}>{p.title}</a></li>
        ))}
      </ul>
    </div>
  )
}

Optional: Categories (e.g. for filters)

To show category filters or links, fetch categories from the BFF:

const res = await fetch('/api/v1/blog/categories', { next: { revalidate: 60 } })
const json = await res.json()
const categories = json.data || []  // [{ id, slug, name }, ...]

Use categoryId in the list query when filtering: /api/v1/blog?categoryId=... (see blog-headless-cms{.interpreted-text role="doc"}).

Article Page (e.g. app/blog/[slug]/page.tsx)

Use BFF route /api/v1/blog/slug/[slug] or call the API with X-API-Key server-side. Return 404 if not found. For localized article content, pass the current locale in the query (e.g. ?locale=en); see blog-headless-cms{.interpreted-text role="doc"}.

import { notFound } from 'next/navigation'

export default async function BlogPostPage({ params }: { params: Promise<{ slug: string; locale: string }> }) {
  const { slug, locale } = await params
  const res = await fetch(`/api/v1/blog/slug/${encodeURIComponent(slug)}?locale=${encodeURIComponent(locale)}`, { next: { revalidate: 60 } })
  if (!res.ok) notFound()
  const json = await res.json()
  const post = json.data
  if (!post?.id) notFound()
  const links = Array.isArray(post.linkedLinks) ? post.linkedLinks : []
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.excerpt}</p>
      <div dangerouslySetInnerHTML={{ __html: post.htmlContent || post.content }} />
      {links.length > 0 && (
        <section aria-labelledby="related-links">
          <h2 id="related-links">Related links</h2>
          {['internal_article', 'update_announcement', 'external_resource'].map((type) => {
            const group = links.filter((l: { type: string }) => l.type === type)
            if (!group.length) return null
            return (
              <div key={type}>
                <h3 className="text-sm uppercase text-gray-500">{type}</h3>
                <ul>
                  {group
                    .sort((a: { sortOrder: number }, b: { sortOrder: number }) => a.sortOrder - b.sortOrder)
                    .map((item: { id: string; label: string; url: string; intro?: string | null; date?: string | null }) => (
                      <li key={item.id}>
                        <a href={item.url.startsWith('/blog/') ? `/${locale}${item.url}` : item.url}>
                          {item.label}
                        </a>
                        {item.date && <p><time dateTime={item.date}>{new Date(item.date).toLocaleDateString()}</time></p>}
                        {item.intro && <p>{item.intro}</p>}
                      </li>
                    ))}
                </ul>
              </div>
            )
          })}
        </section>
      )}
    </article>
  )
}

Response shape and errors

For full response fields and error codes (e.g. 404 for unknown slug), see the API reference.