A release newsroom on a blog

Embed
Preview of the card
<a href="https://forumulate.com/guides/release-newsroom"><img src="https://forumulate.com/guides/_api/public/orgs/046a9bc0-d530-43ae-9bae-af7945008cda/spaces/36aa4df0-1e95-4cef-8ae9-795169441345/pages/cf099c10-a1a6-4fee-a640-ddcc4ff029da/social-card.svg?v=3e7ae33eec8b" alt="A release newsroom on a blog" width="600" height="315"></a>

A release newsroom is a blog whose posts are drops: a sneaker, a console, a vinyl pressing, a game. Each one has a release day that is not the day you publish the post, a price, a style code, the shops selling it and the regions it reaches, and readers come back to see what is out now and what is coming.

Forumulate does this on an ordinary Blog space. Nothing here is a separate product: the release facts are declared fields of a post kind, so the studio, the API, markdown front matter and folder sync all handle them the way they handle any other field.

The quickest start is the Dropline template (Studio → Templates → Dropline), which sets up everything on this page with sample drops.


1. Make posts into drops: the drop kind

A post's kind says which declared fields it carries. Two kinds ship for blogs:

Kind

What it is

drop

A product release, with the release fields below.

roundup

A post that lists the releases in a window and keeps itself up to date.

Give a post a kind in either of these ways:

  • One post at a time. In the studio, open the post's settings and choose Kind of post. Through the API, send kind on the blog door: PATCH /api/orgs/{org}/spaces/{space}/blog/{id} with {"kind": "drop"}. A kind the space does not offer is refused with 422, and the error lists the kinds it does offer. Send {"kind": null} to take the kind off.

  • Every post in the space. Make drop the default kind with PUT /api/orgs/{org}/spaces/{space}/shape/default-kind and {"kind": "drop"}, or apply the preset's fields to the space's own shape.

The release fields

Field

Type

What it holds

releaseDate

date (YYYY-MM-DD)

The day it drops, in the blog's time zone. This is separate from the publish date.

releaseWindow

text

A window when the day isn't known yet ("Holiday 2026"), or alongside the day.

releaseStatus

upcoming · released · delayed · cancelled · restocked

Where the release stands, as you set it. "Out now" is worked out for you (below).

retailPrice

money ({min, currency})

The retail price. min is the price and currency is an ISO 4217 code. It is never converted.

sku

text

The style code or SKU.

colorway

text

The colourway.

releaseCategory

text

Sneakers, Apparel, Consoles and so on. The calendar and feeds can filter by it.

retailers

links ([{label, url, note?}])

Where to buy: a name and an http(s) address for each shop, plus an optional note ("Raffle", "10am ET"). Up to 30.

regions

several of WW US CA MX LATAM UK EU EUROPE JP KR CN HK TW SEA AU NZ IN MENA AFRICA

The regions it releases in. Leave it empty if you don't want to say.

regionNote

text

What readers in other regions are told.

retailers uses the links field type, a short list of named links. You can use it on any space type, for example for a product's stockists or a talk's slides and recording.

Setting the fields

All the usual ways of setting a field work:

  • Studio: the post's settings list the fields of its kind.

  • The blog door: send fields on POST or PATCH …/blog. The values you send are merged with the stored ones, and null clears a field. Values are checked against the post's kind exactly as /items checks them, so a wrong value is a 422 that names the field.

    {
      "title": "Air Runner 84 “Varsity Red”",
      "kind": "drop",
      "fields": {
        "releaseDate": "2026-11-14",
        "releaseStatus": "upcoming",
        "retailPrice": { "min": 180, "currency": "USD" },
        "sku": "AR8401-600",
        "colorway": "Varsity Red/Black-Sail",
        "releaseCategory": "Sneakers",
        "retailers": [
          { "label": "Nike SNKRS", "url": "https://www.nike.com/launch", "note": "10am ET" },
          { "label": "END. Launches", "url": "https://launches.endclothing.com", "note": "Raffle" }
        ],
        "regions": ["US", "EU", "UK"],
        "regionNote": "Not releasing where you are? END. ships to most countries."
      }
    }
  • /items: PATCH …/items/{id}/fields and params on POST …/items.

  • Markdown front matter and folder sync (forumulate publish): put kind: drop in a post's front matter, and write each field under its own key. The CLI reads the kind's fields and sends them.

    ---
    title: Air Runner 84 “Varsity Red”
    kind: drop
    releaseDate: 2026-11-14
    releaseStatus: upcoming
    retailPrice: { min: 180, currency: USD }
    sku: AR8401-600
    retailers:
      - { label: Nike SNKRS, url: "https://www.nike.com/launch" }
    regions: [US, EU, UK]
    ---

2. What readers see on a drop

The "Release details" block

A drop gets a Release details block. It shows the release day (with the window, if there is one), a status badge, the price in the reader's own number format, the style code, the colourway, the category, the regions, and a button for each retailer. It is rendered on the server, so it is part of the HTML a crawler reads and nothing moves around after the page loads.

"Out now", worked out for you

The badge is calculated each time the page is rendered, from the release day and the blog's own clock (theme.blog.release.timeZone, which defaults to UTC):

What you set

The badge says

cancelled

Cancelled

restocked

Restocked

released, or any status once the day has come

Out now

delayed, before the (new) day

Delayed

a release day in the future

Upcoming

no day yet (only a window)

Date TBA

You never have to flip a status at midnight. A drop dated the 14th reads "Out now" on the 14th, in the blog's time zone. The same badge appears on the drop's card in lists and feeds.

Regions

A drop that names regions lists them. Readers in another region see the drop's regionNote (or the theme's regions.note). The reader's country comes from our own edge, Cloudflare's CF-IPCountry header, and never from a geolocation service. Pages are cached at the edge for everyone, so a reader who isn't signed in gets the answer from a small same-origin request after the page loads (GET /api/public/geo, which is never cached). All three wordings sit in the same reserved space, so nothing shifts when the answer arrives. Signed-in readers have the answer rendered into the page directly. Set regions.personalise: false to show every reader the same note.

Structured data

Each drop adds a schema.org Product to the page's JSON-LD, next to its BlogPosting. It includes sku, color, category, releaseDate, and an Offer for each retailer with the price, currency, availability and availabilityStarts. To emit a SaleEvent on the release day instead, set structuredData: "event". A cancelled drop becomes an EventCancelled and a delayed one an EventRescheduled. Set "none" to emit neither. If a drop has no price, no offers are emitted, because an offer without a price is rejected by search engines.


3. The release calendar

The calendar lives at /<blog>/calendar, and in each language at /<blog>/<locale>/calendar. It has three views:

  • Month: six week rows (so moving between months never shifts what's below the grid). Each release in a day's cell links to its post. The day's number jumps to that day in the list under the grid, which is also what phones see.

  • Week: the seven days with every release.

  • List: the upcoming releases, soonest first, grouped by day.

Tag pills filter the calendar (?tag=sneakers). The address holds the state, so every view can be linked to: ?view=week&week=2026-11-09, ?month=2026-12.

The calendar is on once the blog has a published release with a date, or straight away if you set calendar.enabled: true. With enabled: false it is off, and the address goes to a post whose slug is calendar, if there is one. Turning the calendar on never hides a post that was already published at that address. While it is on, a Calendar link appears next to Archive on the blog's index, and the page is listed in the blog's sitemap.

The calendar is a blog hub page, so you can compose it like the archive: PUT …/blog/hubs/calendar/_/page.


4. Sorting and filtering by release

The blog index's public read (GET /api/public/orgs/{org}/spaces/{space}/blog) accepts these parameters:

Parameter

Meaning

sort=release

Soonest release day first (release-desc for latest first). Posts with no day come last.

release

upcoming (not out yet), out (out now or restocked) or any (every post with release facts).

releaseFrom, releaseTo

A window of release days, inclusive.

releaseToday

What "today" is for upcoming and out. The reader sends the blog's own day; if it is left out, the UTC day is used.

releaseStatus

Comma-separated statuses, as you set them.

releaseCategory

One category. Case doesn't matter.

A value that can't be used gets a 422 that names the parameter. It never silently matches everything. The blog's own index shows Upcoming and Out now tabs next to Latest and Top once it has releases.

GET /api/public/orgs/{orgId}/spaces/{spaceId}/blog/releases returns the releases in a window, soonest first, with the same parameters plus tag. This is what the calendar reads.

On a front page

A spaceFeed that reads a blog can sort and filter by release:

{
  "type": "spaceFeed",
  "attrs": {
    "sort": "release",
    "filter": { "release": { "when": "upcoming", "category": "Sneakers" } },
    "layout": "grid",
    "fields": ["release", "cover", "title", "standfirst"]
  }
}

filter.release takes when (upcoming, out or any), range (below), from/to, status (a list) and category. The release card field is the drop's badge, day and price.


5. Roundups that assemble themselves

A roundup lists the releases whose day falls in a window, and it works out the window again each time the page is rendered. A "This week's drops" post you write once lists the right drops every week.

  • As a post: give it the roundup kind and set roundupRange. The other fields are roundupFrom/roundupTo (for custom), roundupTag, roundupCategory and roundupLimit. The list is drawn under the post's body.

  • As a band: {"type": "spaceBand", "attrs": {"band": "releaseRoundup", "releaseRange": "next-month"}}. The band also takes releaseFrom, releaseTo, releaseCategory, tags (only the first one is used), limit and heading.

  • As a feed: filter.release.range.

The ranges are this-week, next-week, this-month, next-month, next-7-days, next-30-days, past-7-days, upcoming (from today onward, with no end) and custom (between from and to). Weeks start on the calendar's weekStart day.

The calendar is also available as a band: {"band": "releaseCalendar", "releaseView": "week"}, which takes releaseView (month, week or list), releaseCategory, tags, limit and heading. It shows the grid with a link to the full calendar.


6. Every look is a key: theme.blog.release

Set these on the space theme (PATCH /api/orgs/{org}/spaces/{space}/theme) or on the site theme. The space theme wins over the site's, and the site's wins over the defaults. Each one is also in the studio's keys panel under Blog → release.

{
  "blog": {
    "release": {
      "timeZone": "America/New_York",
      "structuredData": "product",
      "details": {
        "position": "top",
        "layout": "grid",
        "frame": "rule",
        "heading": "Release details",
        "parts": ["date", "status", "price", "sku", "colorway", "regions", "retailers"],
        "dateStyle": "long",
        "priceStyle": "symbol",
        "retailers": "buttons",
        "accent": "#ff4d00"
      },
      "badge": {
        "style": "tag",
        "onCards": true,
        "dateOnCards": true,
        "colors": { "out": "#00b37e", "upcoming": "#ff4d00", "cancelled": "#b91c1c" },
        "labels": { "out": "Live now" }
      },
      "calendar": {
        "enabled": true,
        "view": "month",
        "weekStart": "monday",
        "width": "wide",
        "upcomingLimit": 12,
        "covers": true,
        "prices": true,
        "tagFilter": true,
        "inNav": true,
        "showCancelled": true,
        "todayColor": "#ff4d00"
      },
      "regions": { "style": "chips", "personalise": true },
      "roundup": { "layout": "grid", "groupBy": "day", "covers": true }
    }
  }
}

Key

Default

What it does

timeZone

UTC

The blog's clock, used to decide "today" for Out now, the calendar and roundups.

structuredData

product

product, event or none.

details.show / position

on / top

Whether to show the block, and where: top, bottom (after the body) or rail (the post's side rail).

details.layout / frame

table / card

table, grid or inline; framed as a card, between rules, or none.

details.parts

every part the post has

Which parts to show, and in what order.

details.retailers

buttons

buttons, list or chips.

badge.style

pill

pill, tag, ribbon or text.

badge.colors / labels

green / brand / amber / red

A colour and a word for each state.

calendar.*

see above

Whether it's on, its view, week start, width, list length, covers, prices, tag filter, nav link, cancelled releases and today's colour.

regions.style / note / personalise

chips / built-in / true

How regions are shown, the note when a post has none of its own, and whether to tell readers if it's available where they are.

roundup.layout / groupBy

list / day

list, grid or table; grouped by day, week or none.

Unknown keys get a 422 that names them, as with every theme object. The words the reader shows (the badge states, "Release details", the calendar's buttons) come in every language the reader supports, and you can replace them with the labels keys.

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.

Public Blog Releases

Operation

Request

What it does

Shortcuts

PublicBlogReleases_releases

GET /public/orgs/{orgId}/spaces/{spaceId}/blog/releases

List releases in a space

client.blog.publicReleases.releases()

Public Geo

Operation

Request

What it does

Shortcuts

PublicGeo_country

GET /public/geo

Get the geo

client.public.geo.country()

The Forumulate teamUpdated