CmsService class (accessed via client.cms) reads a store’s editorial content — blog posts and shoppable lookbooks — and records views against the shopper’s session, so a sale can later be attributed to the content that led to it.
Blog Posts
listPosts
Retrieve a paginated list of published blog posts, newest first. This is ideal for building a
“News” or “Magazine” section on your storefront. Only published posts are returned — there is no
status filter. Filter by category with the category slug (category: 'buying-guides').
Response
excerpt, featured_image, video (an optional cover video; featured_image stays its poster and fallback), slug, and
product_count (how many products a shoppable post links to). For the full content, use getPost.
?fields= on the listing accepts the summary fields above. The article body (content) and its
embedded products and lookbooks are fetched per post and belong to getPost; asking for one of
them here returns 400 naming the field rather than omitting it silently.
Each post carries two timestamps. created_at is when it was first written; updated_at is when it
was last edited, by anyone and in any way — a merchant revising the text, or an automated change such
as a regenerated cover image. An article page that shows a revision date reads as current when the
text has been kept up to date, so rendering “Updated ” where updated_at is later than the
publication date is usually worth doing. updated_at is always set: on a post that has never been
revised it matches created_at.
getPost
Retrieve the full content of a specific published blog post by its slug, including any linked
products for shoppable posts (each entry in the products array carries the product’s name,
sku, price, images, and display_type). Returns 404 if no published post matches the slug.
An embedded product also carries variant_id — pass that to client.cartWishlist.addToCart,
which is keyed by variant rather than product, so a shoppable post can add to a basket without a
second call. embed_id identifies the placement rather than the product. A productEmbed block in
content is matched to its entry on product_id, which equals the block’s productId; block_id is the
id of the content block an embed is anchored to and is null for embeds placed in the post editor. An
embed whose product is no longer available is omitted, so a post can return fewer products than were
authored.
content is an Editor.js document: { time, version, blocks }, or null for a
post with no body. Each block is { id, type, data }, and the fields in data depend on type.
Headings are typed
header. In a list, items holds plain strings in older posts and
{ content, items, meta } objects in current ones, where content is inline HTML and items nests a
sub-list; both forms occur. A renderer skips any block whose type it does not recognise, because new
block types can be added without a change to the API version.
Inline HTML is limited to <b>, <i>, <u>, <a href>, <mark>, <code> and <br>. Content is
returned as stored and is not sanitised on read, so a storefront sanitises inline HTML to that list
before inserting it into a page, and accepts only http, https and mailto URLs in href. The
example below uses DOMPurify for that step.
Response
getRelatedPosts
Posts related to a given one, ranked on the server. The ranking combines what the writing is about
with the topics two posts share and how recently each was published, and nudges a post from a
different category up between otherwise equal results. Returns 404 if no published post matches
the slug.
Render reason, not score. The list is ordered by a rank that includes the category nudge, so
the scores are not always decreasing — a post scoring 0.458 can sit above one scoring 0.511. To
a reader that looks like a sorting fault, while reason (“Closely related subject”) is something
they can act on.
The list is topped up to at least three entries when few posts qualify, since a shelf of one reads
as a fault rather than a short list. Topped-up entries score strictly below the weakest genuine match
and carry the reason “More from this blog”. degraded is true when the post has no stored
relevance data yet — most often because it was just published — and the ranking used topics and
recency alone; the list is still returned.
Response
getTrendingPosts
The posts being read most, ranked by reads per day over a recent window.
The window widens — 7 days, then 21, 45 and 90 — until enough posts qualify, and the one that was
used comes back as window_days. A blog that publishes occasionally has quiet weeks, and a list
that is empty half the time is less useful than one describing a longer period honestly. Show the
period beside the list rather than calling everything “this week”.
reads_per_day is normalised by the window, so the figure stays comparable as the window widens. It
orders the list; it is not a view counter and reads oddly if shown as one.
Response
Attributing content views
getPost and getLookbook both accept a visitor and a session identifier, and record the view
against them. That is what lets the merchant see which posts and lookbooks drive sales rather than
just traffic — and it is the first half of the content funnel: pass the same content_id (with
content_type) in the order’s attribution at checkout and the sale is credited back to the
content the shopper came from.
Both are optional. When you omit them the view still counts, but it is attributed to a coarse
fallback derived from the request, so repeat views by one shopper may not group together.
Lookbooks
Lookbooks are curated collections of lifestyle images linked to specific products, perfect for “Shop the Look” features.listLookbooks
Retrieve a list of published lookbooks, newest first. Only published lookbooks are returned —
there is no status filter.
Response
created_at and updated_at on both the listing and the detail response, with the
same meaning they have on a post: when it was created, and when it was last changed.
The gallery (images, with its shoppable hotspots) and collection_id are fetched per lookbook and
belong to getLookbook; asking for one of them on the listing returns 400.
getLookbook
Retrieve the full details of a published lookbook by its slug, including its ordered image gallery
and the shoppable hotspots placed on each image. Returns 404 if no published lookbook matches
the slug.
Each lookbook exposes status (always "published" from the public API) and collection_id — the
product collection that backs the lookbook, or null if it is standalone. The gallery lives under
images[], where each image has a url and a hotspots[] array tagging the products featured at
specific points in the photo (your “Shop the Look” markers).
A hotspot gives x and y as percentages of the image’s width and height from the top-left, so the
marker lands correctly at any display size, plus an optional label and the resolved product. The
product carries variant_id — pass that to client.cartWishlist.addToCart, which is keyed by
variant rather than product, so a marker can be added to a basket without a second call. Send a shopper
who wants a different size or colour to the product page instead. A hotspot whose product is no longer
available is omitted from the response, so an image can return fewer hotspots than were authored.
Response
status is always "published" from the public API. collection_id is the backing product
collection, or null when the lookbook is standalone (as above). Each images[] entry carries a
hotspots[] array — empty here, otherwise one marker per tagged product.Newsletter
subscribeNewsletter
Capture a newsletter signup from your storefront. The email address is the identity, so this works for both anonymous visitors and signed-in shoppers (a signed-in storefront prefills the email field). When the merchant has connected a marketing tool, subscribers are synced to their chosen list automatically.
Response
200 with already_subscribed: true and never creates a duplicate; a brand-new signup returns 201. This endpoint accepts both publishable and secret keys (it’s a storefront write, like cart), so you can call it directly from the browser.
Response Codes
The read endpoints (posts, lookbooks) areGET; subscribeNewsletter is a POST. All accept both publishable and secret keys. Slug validation is enforced — empty or whitespace-only slugs return 400 without hitting the database.

