Currency

Blog

The blog namespace owns long-form storefront content — product launches, brand stories, how-to guides — in a single canonical place. Posts live on Ripllo and are served back through Storlaunch's /s/:merchant/blog and /s/:merchant/blog/:slug routes, but the source of truth is here. This page covers ripllo.blog on the Node SDK. For the HTTP surface, see API → Blog.

Namespace

ripllo.blog.list(params?)
ripllo.blog.get(id)
ripllo.blog.create(input)
ripllo.blog.update(id, patch)
ripllo.blog.delete(id)

ripllo.blog.publicList(accountId)
ripllo.blog.publicGet(accountId, slug)

Five merchant CRUD methods and two public reads. The public reads are unauthenticated (signed-out storefronts hit them via Storlaunch's SSR) and never return drafts.

Methods

blog.create

Signature. ripllo.blog.create(input): Promise<{ post: BlogPost }>

Creates a post in the calling workspace. The SDK auto-generates an Idempotency-Key header, but that header only feeds the HMAC string-to-sign — Ripllo stores no replay cache, so it does not make a retry safe on its own. What saves you here is the (accountId, slug) unique constraint: a retried create comes back 409 SLUG_TAKEN instead of minting a second post.

const { post } = await ripllo.blog.create({
  slug: 'how-we-source-cocoa',
  title: 'How we source our cocoa',
  excerpt: 'A short walk through our supply chain from Sulawesi.',
  body: '# How we source our cocoa\n\nIt starts in Polewali...',
  status: 'draft',
  tags: ['supply-chain', 'transparency'],
  metaTitle: 'How we source our cocoa | Brand',
  metaDescription: 'A walk through our supply chain.',
});

The (accountId, slug) pair is unique. Two posts in the same workspace can't share a slug. A 409 comes back as RiplloError with code: 'SLUG_TAKEN' — Ripllo error codes are SCREAMING_SNAKE, so a handler testing for 'slug_exists' never fires.

blog.list

Signature. ripllo.blog.list(params?): Promise<{ posts: BlogPost[] }>

Returns up to 200 posts in the workspace, newest first by publishedAt then createdAt. The status filter lets you fetch drafts only:

const { posts } = await ripllo.blog.list();                         // all
const { posts: drafts } = await ripllo.blog.list({ status: 'draft' });

Unlike most Ripllo list endpoints, blog isn't cursor-paginated — the assumption is a merchant has dozens of posts, not thousands. If you hit the 200 ceiling, file an issue; we'll page-segment in a later version.

blog.get

Signature. ripllo.blog.get(id): Promise<{ post: BlogPost }>

Fetches by ID — an opaque cuid, no bp_ prefix. Throws NOT_FOUND for missing or cross-workspace IDs.

const { post } = await ripllo.blog.get('<postId>');

blog.update

Signature. ripllo.blog.update(id, patch): Promise<{ post: BlogPost }>

PATCH semantics. Common flow: flip status from draft to published and let Ripllo stamp publishedAt:

await ripllo.blog.update('<postId>', {
  status: 'published',
  publishedAt: new Date().toISOString(),
});

You can also pre-schedule by setting status: 'published' with a future publishedAt — the post stays out of public reads until that time arrives. (The public-list endpoint filters on publishedAt <= now().)

blog.delete

Signature. ripllo.blog.delete(id): Promise<{ deleted: boolean }>

Hard delete — the row is removed. There's no archive flag. If you need an "unpublish but keep" flow, flip status to draft instead. Public storefront pages 301-redirect when a deleted slug is hit (Storlaunch's SlugRedirect table on the partner side; not Ripllo's concern).

blog.publicList

Signature. ripllo.blog.publicList(accountId): Promise<{ posts: SlimBlogPost[] }>

Anonymous read — returns only id, slug, title, excerpt, coverImage, authorName, tags, publishedAt. The body field is excluded to keep the SSR payload small; fetch the full post via publicGet.

const { posts } = await ripllo.blog.publicList('acc_<merchant>');

blog.publicGet

Signature. ripllo.blog.publicGet(accountId, slug): Promise<{ post: BlogPost }>

Anonymous slug lookup — the single-post page on the storefront.

const { post } = await ripllo.blog.publicGet('acc_<merchant>', 'how-we-source-cocoa');

Returns NOT_FOUND for missing slugs or drafts.

Types

type BlogPostStatus = 'draft' | 'published';

interface BlogPost {
  id: string;                // opaque cuid
  accountId: string;
  slug: string;
  title: string;
  excerpt: string | null;
  body: string;
  coverImage: string | null;
  status: BlogPostStatus;
  publishedAt: string | null;
  authorName: string | null;
  tags: string[];
  metaTitle: string | null;
  metaDescription: string | null;
  createdAt: string;
  updatedAt: string;
}

Common patterns

Draft → publish flow

const { post } = await ripllo.blog.create({ slug, title, body, status: 'draft' });
// ...preview in dashboard, get sign-off...
await ripllo.blog.update(post.id, {
  status: 'published',
  publishedAt: new Date().toISOString(),
});

Render the blog index in Storlaunch SSR

// app/s/[merchant]/blog/page.tsx
export default async function BlogIndex({ params }) {
  const { posts } = await ripllo.blog.publicList(params.merchant.accountId);
  return posts.map((p) => <Card key={p.id} {...p} />);
}

Bulk-tag

const { posts } = await ripllo.blog.list();
for (const p of posts) {
  if (p.body.includes('cocoa')) {
    await ripllo.blog.update(p.id, { tags: [...new Set([...p.tags, 'cocoa'])] });
  }
}

Errors

Ripllo error codes are SCREAMING_SNAKE. There is no ripllo:blog:* scope vocabulary — API keys carry read / write / admin, plus ripllo:platform:admin for partner keys, and the check is coarse: read covers GET, write covers every mutation, on every resource.

Code Status Cause
VALIDATION 400 Bad slug shape (must match ^[a-z0-9][a-z0-9-]*$), unknown status enum, marketingCampaignId outside this workspace.
SLUG_TAKEN 409 Duplicate (accountId, slug) on create or update.
NOT_FOUND 404 Post ID or slug doesn't exist (or is a draft on the public read).
NO_ACCOUNT 403 The principal carries no accountId.
INSUFFICIENT_SCOPE 403 API key lacks read (GETs) or write (mutations).

See API → Blog for the per-endpoint tables and Authentication for auth-layer codes.

Next

CurrencyRupiah is paid by transfer or QRIS; US dollars settle through PayPal.