Newsroom extras: product cards, sources, corrections, reminders and card badges
These are the pieces a market-news blog uses around its posts. Each works on its own, and each also works on the release fields of a drop post (see A release newsroom on a blog). They are:
the product card, a live card for a product sold elsewhere;
the sources block, which lists your citations and checks them for dead links;
the corrections block, a dated history of updates to the post;
follow a tag and Remind me, which send readers email;
the card badge, which prints a post field on the generated social card.
The three blocks can go in any space type, not only a blog.
1. Product card
A product card shows one product from another site. It has a picture, a name, a price, the live asks and bids from a marketplace, and Buy and Track buttons.
In the editor, type /product card. In Markdown, write:
:::productCard{url="https://shop.example.com/p/air-max-1" endpoint="https://api.example.com/cards/air-max-1"}:::As block JSON:
{ "type": "productCard", "attrs": { "url": "https://shop.example.com/p/air-max-1", "endpoint": "https://api.example.com/cards/air-max-1" } }Attribute | What it does |
|---|---|
| The product page. The card links to it. Required. |
| Your JSON endpoint for the live data (contract below). Optional. |
| Your own words and picture. They are used when the live answer has none. |
| A fallback retail price, for example |
|
|
|
|
| Your own words for the button labels. |
Where the data comes from
Your readers' browsers never contact your endpoint. While the page renders, our server fetches the data and puts the card into the HTML. This means:
The card is in the HTML before any script runs, so crawlers see it and nothing moves on the page as it loads.
The fetch goes through our SSRF-safe fetcher. Redirects are not followed, the timeout is 4 seconds and the answer can be at most 256 KB. An address on a private network is refused.
The product picture is served through our image proxy, so a reader never requests it from your host.
The answer is cached for 60 seconds by default, because asks and bids change quickly. To change this, send
maxAgein your JSON, or aCache-Control: max-ageheader. Values outside 15–900 seconds are clamped to that range.If a refresh fails, the card keeps the last good answer and adds the note "Prices may be out of date". We try the endpoint again 30 seconds later. If no fetch has ever succeeded, the card shows the block's own
name,imageUrlandprice. If those are empty too, it shows a plain link to the product.
If you don't give an endpoint, the card reads the product page's own preview tags (Open Graph) instead: its title, description and picture.
The endpoint contract
We call your endpoint with GET and Accept: application/json. Only name is required. Keys we don't use are ignored, so your endpoint can also serve your own app.
{
"name": "Air Max 1 “Big Bubble”",
"url": "https://shop.example.com/p/air-max-1",
"image": "https://cdn.example.com/am1.png",
"brand": "Nike",
"sku": "DZ4549-110",
"description": "The 1987 original returns.",
"price": { "amount": 190, "currency": "USD" },
"availability": "preorder",
"market": {
"currency": "USD",
"lowestAsk": 250,
"highestBid": 230,
"lastSale": 241,
"asks": 12,
"bids": 30,
"updatedAt": "2026-10-21T10:00:00Z"
},
"buttons": [
{ "kind": "buy", "label": "Buy now", "url": "https://shop.example.com/buy/am1" },
{ "kind": "track", "label": "Track price", "url": "https://shop.example.com/track/am1" }
],
"maxAge": 60
}Key | Type |
|---|---|
| string, 1–200 characters. Required. |
| absolute http(s) URLs |
| strings (up to 120 and 80 characters) |
| string, up to 500 characters |
|
|
|
|
|
|
| up to 4 buttons, each |
| seconds, 0–3600, clamped to 15–900 |
If an answer doesn't match the contract, it isn't used. To see why, open the block in the editor and choose Fetch preview, or call POST /api/orgs/{orgId}/newsroom/product-cards/preview with {"url": "…", "endpoint": "…"}. The response has error.issues, which names each path that is wrong, such as price.currency: An ISO 4217 currency code such as USD.
Structured data
A blog post adds a schema.org Product to its JSON-LD for each card. The Product has an Offer for the retail price and availability, and an AggregateOffer for the asks: lowPrice, highPrice and offerCount. It is built from the same answer the card shows.
Theme keys (theme.newsroom.productCard)
These keys follow the usual order: the space's value wins over the site's, and the site's wins over the default. A block's own attributes always win over all of them.
Key | Values |
|---|---|
|
|
|
|
|
|
| 0–48 (px) |
| hex colours. |
{ "newsroom": { "productCard": { "layout": "vertical", "accent": "#0B835B", "radius": 16 } } }2. Sources
The sources block lists the citations a story stands on, in the same format every time: publisher, title (as a link), date and an optional note.
In the editor, type /sources. In Markdown, the block is one line with the attributes in a URL-encoded JSON data payload. Exporting a page writes this line for you:
:::sources{data="%7B%22items%22%3A%5B%7B%22publisher%22%3A%22Nike%20News%22%2C%22title%22%3A%22Air%20Max%20Day%22%2C%22url%22%3A%22https%3A%2F%2Fnews.example.com%2Fam-day%22%2C%22date%22%3A%222026-03-20%22%7D%5D%7D"}:::As block JSON:
{
"type": "sources",
"attrs": {
"items": [
{ "publisher": "Nike News", "title": "Air Max Day", "url": "https://news.example.com/am-day", "date": "2026-03-20" }
],
"style": "numbered"
}
}styleisnumbered,listorcompact.headingreplaces the "Sources" heading. Set it to""to hide the heading.markDeadis described below.
A row without a URL or a title isn't shown, so you can save a half-written row. The sources also go into the post's JSON-LD as citation.
The dead-link check
Every time a page or post is published or edited, we record the URLs its sources blocks cite. Once an hour, a scheduled check visits them:
a URL that has never been checked is visited straight away;
a URL that failed its last check is visited again after an hour;
a URL that worked is visited again after a day.
We use a HEAD request, and fall back to GET when that fails. These answers count as alive: any 2xx or 3xx, and 401, 403 and 429 (paywalls and bot walls — the page is there). These count as failed: 404, 410 and any 5xx, a name that doesn't resolve, a refused or timed-out connection, and an address on a private network.
One failure isn't enough to call a link dead. A link is marked dead after two failed checks in a row, and it stops being dead as soon as a check succeeds. A URL cited by ten stories is checked once.
Who sees dead links:
Staff always see them. In the editor, each source row shows Dead link with the HTTP status, and has a Check now button. On the live page, a member who sees the staff bar sees a red Dead link mark.
Readers see dead links only if you turn it on. Set
theme.newsroom.sources.markDead: true, or set the block's ownmarkDead. The link is then struck through, followed by "(link may be broken)" in the page's language. A block'smarkDead: falseturns this off for that block, even when the theme turns it on.
Related API calls:
GET /api/orgs/{orgId}/spaces/{spaceId}/newsroom/source-links?status=deadlists a space's cited links, dead ones first.POST /api/orgs/{orgId}/newsroom/source-links/statuswith{"urls": […]}returns the verdict for each URL.POST /api/orgs/{orgId}/newsroom/source-links/checkwith{"url": "…"}checks a URL again now.
To set the default style for every sources block, use theme.newsroom.sources.style.
3. Corrections
The corrections block is the post's visible "Updated" history. Each entry is a date, your note ("Release moved from Nov 7 to Nov 14.") and an optional summary of what changed, which readers can open. Entries are shown newest first.
{
"type": "corrections",
"attrs": {
"items": [
{ "date": "2026-11-01", "note": "Release moved from Nov 7 to Nov 14.", "diff": "- Releases November 7.\n+ Releases November 14." }
],
"style": "banner"
}
}In Markdown it is :::corrections{data="…"}:::, written the same way as sources.
styleislist(a rule down the side) orbanner(a tinted box).headingreplaces the "Updates" heading.theme.newsroom.corrections.stylesets the default style.
Drafting from the last edit. A blog post already keeps its revisions: each edit saves the text it replaced, along with the editor's reason for the edit. In the post editor, choose Draft from the last edit. This adds an entry dated today. The note is the reason from the last edit, and the summary is what that edit changed, as - old and + new lines. The corrections and sources blocks themselves are left out of the summary.
The same draft is available from GET /api/orgs/{orgId}/spaces/{spaceId}/blog/{topicId}/corrections/suggest. It writes nothing. Field values, such as releaseDate, aren't part of a post's revisions, so if a release date moved, say so in the note.
Structured data. The post's dateModified is never earlier than its newest correction. If you date a correction today, crawlers are told the post changed today.
4. Follow a tag
Readers can follow one tag of a blog. They then get an email each time a post with that tag is published.
On a tag page (
/b/{org}/{space}/tag/{tag}, or{blog}/tag/{tag}on your domain). The page shows a follow control if the blog's front page includes asubscribeband or a subscribe menu (asubscribeband withmenu: trueandfrontDoor.subscribeStyle: "menu"or"email-popover"; see the Blog guide). On a tag page, both are scoped to that tag by default. A front page without either shows only the RSS link.Through the API:
POST /api/public/orgs/{orgId}/spaces/{spaceId}/blog/subscribewith{"email": "…", "scope": {"tags": ["sneakers"]}}.
Following a tag works like the rest of the blog's newsletter. The reader confirms by email before anything is sent (double opt-in), every email has a one-click unsubscribe link (RFC 8058), and the confirmation email is sent in the language of the page the reader subscribed from.
If a reader who already follows one tag follows another, they get both. A stranger who knows their address can add tags but can't take any away.
Every way of publishing sends the email to the tag's followers, along with the blog's other subscribers:
POST …/blog/{id}/publish;POST …/blog/{id}/statewith{"state": "PUBLISHED"}(the studio's list view);POST …/items/{id}/publish;a scheduled publish.
Each subscriber gets at most one email per post. To publish without sending email, send notify: false to the first two. …/state returns mailed, the number of emails it queued.
5. Remind me
When a post's release date is still ahead, a Remind me box appears under the post. A reader gives their email address, confirms it, and we email them a set number of hours before the release.
Key ( | Default | What it does |
|---|---|---|
|
| Set it to |
|
| The field that holds the release date. A dotted path, such as |
|
| How many hours before the release the email goes out (1–720). |
The field can hold a date (2026-11-14, which means the start of that day in UTC), a full timestamp, or an object with a date key.
Through the API: POST /api/public/orgs/{orgId}/spaces/{spaceId}/blog/{topicId}/remind with {"email": "…", "locale": "de"}.
If the org has a captcha set up, include
captchaToken.A signed-in reader with a verified address is set up straight away. The response has
confirmed: true.If the post has no release date in the future, the response is 422. The check uses the database clock.
A reminder is a newsletter subscription to that one post, so it uses the newsletter's double opt-in, its 7-day confirmation window and its one-click unsubscribe. A sweep runs every 10 minutes and claims each reminder before sending it. If the release date moves, readers get a new reminder for the new date. If the date stays the same, nobody gets a second reminder.
The two emails are Confirm a release reminder (blog.reminder-confirm) and Release reminder (blog.release-reminder). You can change them under Settings → Email templates, including a version for each language. Their variables are post.title, post.excerpt, release.date (written in the reader's language) and link.
6. The card badge
A generated social card, the …/social-card.png image shown when a post is shared, can print one post field as a badge. For example, it can show the release date or the price.
To set the badge for a whole space, add badge to the card template. In the studio this is in SEO → Card badge. Through the API, call PUT /api/orgs/{orgId}/spaces/{spaceId}/seo:
{
"socialCard": {
"badge": { "field": "releaseDate", "label": "Drops", "position": "bottom-right", "color": "#0B835B" }
}
}Key | What it does |
|---|---|
| The post field to print, such as |
| A word in front of the value, such as "Drops" or "Retail". |
|
|
| The currency for a field that is a plain number. |
| The language dates and prices are written in. The default is English. |
|
|
| The badge's colour and its text colour. By default they are the card's accent and background colours. |
Send "badge": null to remove the badge.
A post can override the space's badge with customization.socialBadge, which is in the post's Style settings in the studio:
{ "customization": { "socialBadge": { "text": "Sold out" } } }You can use field to print a different field, text to print your own words, label to change the label, or hide: true to remove the badge from this post's card. Send null to go back to the space's badge. If the field is empty, the card has no badge rather than an empty one.
The card URL changes whenever the post is edited, so if a release date moves, sites that show the card fetch the new picture. The card's fonts come with the API image, so the badge text appears on every card it renders.
In the API
Every operation below is also reachable from the other surfaces without a dedicated command. forumulate api <Operation> calls it from the CLI, the JavaScript SDK has a typed method for each one (the last column names it; the Python and Go clients are generated from the same spec), and an agent reaches the organization-scoped ones through the MCP call_api tool. Where a CLI command or MCP tool exists too, the last column names it. Operation names link to the API reference.
Newsroom
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| Preview the product card |
| |
| Create the status in an organization (newsroom source links) |
| |
| Create the check in an organization |
| |
| List source links in a space |
| |
| Get the suggest in a space |
|
Public Newsroom
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| Resolve the newsroom |
| |
| Create the remind in a space |
|
Space Settings seo
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| List SEO settings in a space |
| |
| Replace SEO settings in a space |
|