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. A409comes back asRiplloErrorwithcode: '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
- Marketing campaigns — announcing a new blog post via email.
- API → Blog — full HTTP reference.