SDK reference
@floggy/cms is a thin, fully typed client for your Floggy content. It fetches your posts, tags, SEO metadata, JSON-LD, and feeds and returns them fully typed. Zero runtime dependencies - it uses the platform fetch.
createClient(config)
import { createClient } from "@floggy/cms";
const floggy = createClient({
project: "projecta", // required: the Floggy username / project
key: process.env.FLOGGY_API_KEY, // optional: flg_ key for drafts / private content
site: { // optional: used by feeds.*
url: "blog.example.com", // public origin; protocol auto-added if missing
name: "Example", // defaults to the project name
description: "My blog", // defaults to "<name>'s blog"
locale: "en", // defaults to "en"
},
fetch: customFetch, // optional: custom fetch implementation
});
project is required. key is only needed for non-public content; leave it out for a purely public blog. Returns a FloggyClient with four namespaces: posts, tags, seo, feeds.
posts
posts.list(options?) -> Promise<{ posts, pageInfo }>
Lists published posts. Always paginated (the SDK always sends page/perPage, so you always get the envelope).
| Option | Type | Default |
|---|---|---|
page |
number |
1 |
perPage |
number |
10 (capped at 50) |
tag |
string |
- |
sort |
"publishedAt:desc" | "publishedAt:asc" |
"publishedAt:desc" |
format |
"tiptap" | "html" | "markdown" |
"tiptap" |
pageInfo is { total, page, perPage, hasMore }.
posts.get(slug, options?) -> Promise<PostDetail>
Fetches one post. PostDetail is the post fields plus:
rendered- html/markdown string whenformatis nottiptap, elsenullcontent- always the raw Tiptap JSONadjacent-{ prev, next }, each{ slug, title } | nullseo- server-computed SEO blockjsonLd- array of JSON-LD nodes
Options: { format?: "tiptap" | "html" | "markdown" }.
posts.search(query, options?) -> Promise<Post[]>
Full-text search across published posts (title + excerpt). Returns a flat array. Options: { format? }.
tags
tags.list() -> Promise<TagWithCount[]>
Returns [{ name, slug, count }], sorted by count descending.
seo
seo.meta(post) -> Metadata
Reshapes a post's seo block into a plain object compatible with Next.js Metadata and TanStack head builders: title, description, keywords, alternates.canonical, robots, openGraph, twitter. Framework-agnostic, no framework import. Synchronous.
seo.jsonLd(post) -> JsonLd[]
Returns the post's JSON-LD nodes, ready to inject as <script type="application/ld+json">. Synchronous.
feeds
Driven by site in the client config.
feeds.sitemap() -> Promise<string>
Fetches all published posts and returns a sitemap.xml string.
feeds.rss() -> Promise<string>
Returns an RSS 2.0 feed string (most recent 20 posts).
feeds.robots() -> string
Returns a robots.txt string pointing at the sitemap and feed. Synchronous.
Errors
Every non-2xx response throws a typed error.
import { FloggyError, RateLimitError } from "@floggy/cms";
try {
await floggy.posts.get("missing");
} catch (err) {
if (err instanceof RateLimitError) {
console.log("retry after", err.retryAfter, "seconds");
} else if (err instanceof FloggyError) {
console.log(err.status, err.message, err.code);
}
}
FloggyError carries status, message, code, url. RateLimitError (status 429) adds retryAfter (seconds, parsed from the Retry-After header).
Next.js (App Router)
A complete post page with metadata, rendered HTML, and JSON-LD.
// app/[slug]/page.tsx
import { createClient } from "@floggy/cms";
const floggy = createClient({ project: "projecta" });
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await floggy.posts.get(slug);
return floggy.seo.meta(post); // title, description, openGraph, twitter, alternates.canonical, robots
}
export default async function PostPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await floggy.posts.get(slug, { format: "html" });
return (
<article>
<h1>{post.title}</h1>
{floggy.seo.jsonLd(post).map((node, i) => (
<script
key={i}
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(node) }}
/>
))}
<div dangerouslySetInnerHTML={{ __html: post.rendered ?? "" }} />
</article>
);
}
Drop-in feed routes
// app/sitemap.xml/route.ts
import { createClient } from "@floggy/cms";
const floggy = createClient({
project: "projecta",
site: { url: "blog.example.com", name: "Example", description: "My blog" },
});
export async function GET() {
return new Response(await floggy.feeds.sitemap(), {
headers: { "Content-Type": "application/xml" },
});
}
// app/rss.xml/route.ts
import { floggy } from "@/lib/floggy"; // a shared client with `site` set
export async function GET() {
return new Response(await floggy.feeds.rss(), {
headers: { "Content-Type": "application/rss+xml" },
});
}
// app/robots.txt/route.ts
import { floggy } from "@/lib/floggy";
export function GET() {
return new Response(floggy.feeds.robots(), {
headers: { "Content-Type": "text/plain" },
});
}
Reuse one client across routes by exporting it from lib/floggy.ts with site configured, so the feed builders know your public origin.
TanStack Start
seo.meta returns a plain object, so it slots straight into a route's head.
// src/routes/$slug.tsx
import { createFileRoute } from "@tanstack/react-start";
import { createClient } from "@floggy/cms";
const floggy = createClient({ project: "projecta" });
export const Route = createFileRoute("/$slug")({
loader: ({ params }) => floggy.posts.get(params.slug, { format: "html" }),
head: ({ loaderData: post }) => {
const meta = floggy.seo.meta(post);
return {
meta: [
{ title: meta.title },
{ name: "description", content: meta.description },
{ property: "og:title", content: meta.openGraph.title },
{ property: "og:image", content: meta.openGraph.images[0]?.url },
],
links: [{ rel: "canonical", href: meta.alternates.canonical }],
};
},
component: Post,
});
function Post() {
const post = Route.useLoaderData();
return <article dangerouslySetInnerHTML={{ __html: post.rendered ?? "" }} />;
}
Feed routes work the same way: a server route handler that returns await floggy.feeds.sitemap() / rss() / floggy.feeds.robots() with the right Content-Type.