Einen richtig coolen Blog bauen — in Minuten
Von npm install bis zum live deployten,
SEO-optimierten Headless-Blog mit Next.js, Astro oder Nuxt. Vollständig typisiert, zero dependencies.
// lib/publino.ts
import { PublinoClient } from "@publino/sdk";
// Der Secret Key (pub_live_…) gehört ausschließlich auf den Server.
// In Next.js wird diese Datei nur in Server Components / Route Handlers importiert.
export const publino = new PublinoClient({
apiKey: process.env.PUBLINO_API_KEY!,
siteId: process.env.PUBLINO_SITE_ID!,
// baseUrl ist optional – Standard: https://api.publino.de
}); SDK installieren & Keys hinterlegen
Das SDK funktioniert mit jedem Paketmanager. Den API-Key erzeugen Sie im Dashboard unter
API & MCP → Delivery Keys. Nutzen Sie den Secret Key (pub_live_…) ausschließlich serverseitig.
# npm
npm install @publino/sdk
# pnpm / yarn / bun
pnpm add @publino/sdk
yarn add @publino/sdk
bun add @publino/sdk # .env.local — niemals in den Browser bundeln!
PUBLINO_API_KEY=pub_live_xxxxxxxxxxxxxxxx
PUBLINO_SITE_ID=mein-blog Einen wiederverwendbaren Client anlegen
Eine zentrale lib/publino.ts kapselt Konfiguration und Keys.
So importieren Sie den Client überall mit einer Zeile — und halten Secrets serverseitig.
- • Vollständig typisiert — Autovervollständigung für alle Felder
- • Automatisches Retry & ETag-Caching
- • Tree-shakeable, keine Drittabhängigkeiten
// lib/publino.ts
import { PublinoClient } from "@publino/sdk";
// Der Secret Key (pub_live_…) gehört ausschließlich auf den Server.
// In Next.js wird diese Datei nur in Server Components / Route Handlers importiert.
export const publino = new PublinoClient({
apiKey: process.env.PUBLINO_API_KEY!,
siteId: process.env.PUBLINO_SITE_ID!,
// baseUrl ist optional – Standard: https://api.publino.de
}); Die Blog-Übersicht bauen
articles.list() liefert alle veröffentlichten Artikel
inklusive Titel, Auszug, Kategorie, Tags und Lesezeit. In einer React Server Component läuft der Abruf rein serverseitig — kein Loading-Spinner, kein Client-JS.
// app/blog/page.tsx — Next.js App Router (React Server Component)
import Link from "next/link";
import { publino } from "@/lib/publino";
export const metadata = {
title: "Blog",
description: "Aktuelle Artikel, Tutorials und News.",
};
export default async function BlogPage() {
// Nur veröffentlichte Artikel, neueste zuerst. list() gibt ein Array zurück.
const articles = await publino.articles.list({
locale: "de",
limit: 12,
});
return (
<main className="mx-auto max-w-3xl px-6 py-16">
<h1 className="mb-10 text-4xl font-bold">Blog</h1>
<div className="grid gap-8">
{articles.map((article) => (
<Link
key={article.id}
href={`/blog/${article.slug}-${article.id}`}
className="group block"
>
{article.category && (
<span className="text-sm font-semibold text-violet-600">
{article.category.name}
</span>
)}
<h2 className="mt-1 text-2xl font-semibold group-hover:underline">
{article.title}
</h2>
{article.excerpt && (
<p className="mt-2 text-gray-600">{article.excerpt}</p>
)}
<p className="mt-2 text-sm text-gray-400">
{article.readingMinutes} Min. Lesezeit
</p>
</Link>
))}
</div>
</main>
);
} Einzelartikel mit perfektem SEO
Hier entscheidet sich das Ranking. Publino liefert pro Artikel Meta-Description, Canonical-URL
und fertiges Schema.org-JSON-LD mit — Sie müssen es nur einsetzen. Mit generateMetadata bekommt jeder Artikel saubere Open-Graph-Tags.
// app/blog/[ref]/page.tsx — Einzelartikel mit perfektem SEO
import { notFound } from "next/navigation";
import type { Metadata } from "next";
import { publino } from "@/lib/publino";
type Props = { params: Promise<{ ref: string }> };
// 1. Meta-Tags pro Artikel — Title, Description, Canonical, Open Graph.
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { ref } = await params;
const article = await publino.articles.get(ref, { locale: "de" });
if (!article) return {};
const canonical =
article.canonicalUrl ?? `https://example.com/blog/${ref}`;
return {
title: article.title,
description: article.metaDescription ?? article.excerpt ?? undefined,
alternates: { canonical },
openGraph: {
title: article.title,
description: article.metaDescription ?? undefined,
type: "article",
url: canonical,
publishedTime: article.publishedAt ?? undefined,
},
};
}
export default async function ArticlePage({ params }: Props) {
const { ref } = await params;
const article = await publino.articles.get(ref, { locale: "de" });
if (!article) notFound();
return (
<article className="mx-auto max-w-2xl px-6 py-16">
{/* 2. Schema.org JSON-LD — Publino liefert fertiges BlogPosting-Markup mit. */}
{article.jsonLd && (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(article.jsonLd) }}
/>
)}
<h1 className="text-4xl font-bold">{article.title}</h1>
<p className="mt-3 text-sm text-gray-400">
{article.publishedAt &&
new Date(article.publishedAt).toLocaleDateString("de-DE")}
{" · "}
{article.readingMinutes} Min.
</p>
{/* 3. Body als sicheres, server-gerendertes HTML. */}
<div
className="prose mt-8"
dangerouslySetInnerHTML={{ __html: article.html }}
/>
</article>
);
} Für maximale Geschwindigkeit (und beste Core Web Vitals) generieren Sie die Artikel-Pfade vorab statisch:
// Statische Pfade vorab generieren (SSG) — blitzschnelle Seiten, top Core Web Vitals.
export async function generateStaticParams() {
const articles = await publino.articles.list({ locale: "de", limit: 100 });
return articles.map((a) => ({ ref: `${a.slug}-${a.id}` }));
} Filter & Navigation
Filtern Sie Artikel nach Kategorie- oder Tag-Slug und bauen Sie daraus eigene Übersichtsseiten — ideal für interne Verlinkung und Topic-Cluster, die das Ranking stärken.
categories.list() und tags.list() liefern alle Werte inkl. Artikel-Zähler für Ihre Navigation.
// Filter laufen über die Public-ID (das `id`-Feld von Kategorie/Tag).
const articles = await publino.articles.list({
locale: "de",
category: categoryId, // id aus categories.list()
tag: tagId, // id aus tags.list()
limit: 20,
});
// Alle Kategorien & Tags der Site (z. B. für die Navigation)
const categories = await publino.categories.list({ locale: "de" });
const tags = await publino.tags.list({ locale: "de" }); Caching, ISR & Echtzeit-Updates
Statisch ausgeliefert, aber immer aktuell: Mit Incremental Static Regeneration baut Next.js Seiten im Hintergrund neu. Per Publino-Webhook revalidieren Sie einzelne Seiten in Sekunden, sobald ein Artikel erscheint.
// app/blog/page.tsx
// ISR: Seite alle 5 Minuten im Hintergrund neu bauen — frische Inhalte ohne Redeploy.
export const revalidate = 300;
// Sofort revalidieren per Publino-Webhook (article.published / .updated):
// app/api/revalidate/route.ts
import { revalidatePath } from "next/cache";
export async function POST(req: Request) {
const { slug, articleId } = await req.json();
revalidatePath("/blog");
revalidatePath(`/blog/${slug}-${articleId}`);
return Response.json({ revalidated: true });
} Dasselbe SDK, jeder Stack
Das SDK ist framework-agnostisch. Ein Auszug für Astro und Nuxt:
---
// src/pages/blog/index.astro — Astro (komplett ohne Client-JS)
import { PublinoClient } from "@publino/sdk";
const publino = new PublinoClient({
apiKey: import.meta.env.PUBLINO_API_KEY,
siteId: "mein-blog",
});
const articles = await publino.articles.list({ locale: "de" });
---
<ul>
{articles.map((a) => (
<li><a href={`/blog/${a.slug}-${a.id}`}>{a.title}</a></li>
))}
</ul> // server/api/blog.get.ts — Nuxt 3 (Nitro server route)
import { PublinoClient } from "@publino/sdk";
const publino = new PublinoClient({
apiKey: process.env.PUBLINO_API_KEY!,
siteId: "mein-blog",
});
export default defineEventHandler(async () => {
return publino.articles.list({ locale: "de", limit: 12 });
}); Anderer Stack?
Auch für PHP und für reine HTML-Seiten ohne Backend gibt es einen direkten Weg zum Publino-Blog.