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 when format is not tiptap, else null
  • content - always the raw Tiptap JSON
  • adjacent - { prev, next }, each { slug, title } | null
  • seo - server-computed SEO block
  • jsonLd - 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.