Vollständige API-Referenz: https://api.mailoo.app/docs/v1
Diese Seite zeigt, wie Sie die öffentliche Blog-API von Mailoo in eine Next.js-App integrieren: Liste über BFF, Artikel nach Slug über BFF, kein API-Schlüssel auf dem Client. Verwenden Sie bevorzugt @mailoo/blog --- siehe nextjs-packages{.interpreted-text role="doc"}.
Voraussetzungen
- Ein Mailoo-Projekt mit einer Blog-Integration und mindestens einem veröffentlichten Artikel
- Die Projekt-UID, Integrations-ID und ein API-Schlüssel (Dashboard → Projekt → API Keys)
Die genauen Werte für MAILOO_BLOG_PROJECT_UID und MAILOO_BLOG_INTEGRATION_ID (und ein kopierbares .env-Beispiel) werden auf der Integrationseinstellungen-Seite im Dashboard angezeigt (Block Connection (External API)). Verwenden Sie einen API-Schlüssel mit der Berechtigung Blog (external read).
Umgebung (nur serverseitig)
Fügen Sie Folgendes zu .env.local hinzu:
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>
Nur serverseitig verwenden; kein NEXT_PUBLIC_* für API-URL oder Schlüssel.
Für die installierbaren @mailoo/blog- / @mailoo/next-core-Module (GitLab Package Registry, Routenfabriken, Komponenten) siehe nextjs-packages{.interpreted-text role="doc"}.
Listenseite (z. B. app/blog/page.tsx)
Mit BFF-Routen (dieselbe App stellt /api/v1/blog bereit):
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: Kategorien (z. B. für Filter)
Um Kategoriefilter oder -links anzuzeigen, rufen Sie Kategorien vom BFF ab:
const res = await fetch('/api/v1/blog/categories', { next: { revalidate: 60 } })
const json = await res.json()
const categories = json.data || [] // [{ id, slug, name }, ...]
Verwenden Sie categoryId in der Listenabfrage zum Filtern: /api/v1/blog?categoryId=... (siehe blog-headless-cms{.interpreted-text role="doc"}).
Artikelseite (z. B. app/blog/[slug]/page.tsx)
Verwenden Sie die BFF-Route /api/v1/blog/slug/[slug] oder rufen Sie die API serverseitig mit X-API-Key auf. Geben Sie 404 zurück wenn nicht gefunden. Für lokalisierten Artikelinhalt übergeben Sie das aktuelle Locale in der Abfrage (z. B. ?locale=en); siehe blog-headless-cms{.interpreted-text role="doc"}.
SEO-Metadaten: Verwenden Sie bevorzugt post.metaTitle ?? post.title, post.metaDescription ?? post.excerpt, post.ogImageUrl (sonst erstes Body-Bild) und post.canonicalUrl (sonst aus Site-Origin + Locale + Slug aufbauen) in generateMetadata (setzen Sie alternates.canonical). Optionales JSON-LD: GET /api/v1/blog/slug/{slug}/json-ld --- wenn Platzhalter verbleiben, ersetzen Sie {{canonicalUrl}} / {{origin}} / {{locale}}; wenn das Integrations-Canonical-Template bereits absolute URLs eingesetzt hat, betten Sie sie unverändert ein. Optionale Keywords: post.seoWords[].word und/oder post.tags verbinden. Konfigurieren Sie das Template in den Blog-Integrations-Einstellungen unter Connection & settings (publicBaseUrl + Pfad-Muster wie /{locale}/blog/{slug}).
IndexNOW: Um Suchmaschinen zu benachrichtigen, wenn Mailoo Artikel veröffentlicht oder aktualisiert, implementieren Sie den Integrator-Notify-Endpunkt und setzen Sie indexNow auf der Blog-Integration --- siehe indexnow-notify{.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>
)
}
Druckbare Zusammenfassungsblöcke
Wenn Artikel-Markdown einen mailoo-print-umzäunten Block enthält (siehe blog-headless-cms{.interpreted-text role="doc"}), enthält htmlContent <section data-mailoo-print="true">-Marker. Fügen Sie nach dem Artikelkörper eine kleine Client-Komponente hinzu, die Druckknöpfe einblendet:
'use client'
import { useEffect } from 'react'
function printMailooSection(section: HTMLElement) {
const body = section.querySelector('.mailoo-printable-body')
if (!body) return
const title = section.getAttribute('data-print-title')?.trim() || document.title
const win = window.open('', '_blank', 'noopener,noreferrer')
if (!win) return
win.document.write(`<!DOCTYPE html><html><head><meta charset="utf-8"><title>${title}</title></head><body>${body.innerHTML}</body></html>`)
win.document.close()
win.focus()
win.print()
}
export function BlogPrintableButtons() {
useEffect(() => {
document.querySelectorAll<HTMLElement>('[data-mailoo-print]:not([data-mailoo-print-enhanced])').forEach((section) => {
section.setAttribute('data-mailoo-print-enhanced', 'true')
const btn = document.createElement('button')
btn.type = 'button'
btn.textContent = 'Print summary'
btn.addEventListener('click', () => printMailooSection(section))
const actions = document.createElement('div')
actions.className = 'mailoo-printable-actions'
actions.appendChild(btn)
section.insertBefore(actions, section.firstChild)
})
}, [])
return null
}
Binden Sie <BlogPrintableButtons /> neben dem dangerouslySetInnerHTML-Artikelkörper-Wrapper ein.
Antwortstruktur und Fehler
Für vollständige Antwortfelder und Fehlercodes (z. B. 404 bei unbekanntem Slug) siehe die API-Referenz.