Blog spaces

Embed
Preview of the card
<a href="https://forumulate.com/guides/blog"><img src="https://forumulate.com/guides/_api/public/orgs/046a9bc0-d530-43ae-9bae-af7945008cda/spaces/36aa4df0-1e95-4cef-8ae9-795169441345/pages/01d9451e-c011-4444-bf89-49a59865a9d9/social-card.svg?v=3e7ae33eec8b" alt="Blog spaces" width="600" height="315"></a>

A Blog space is dated, authored articles — a company blog, release notes with prose, an engineering journal. Its public prefix is /b/, and on a custom domain its posts are addressed from the root.

Everything below is the API. The studio writes the same fields through the same doors.

Write a post

curl -sS -A 'my-app/1.0' -X POST \
  -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' \
  https://forumulate.com/api/orgs/$ORG/spaces/$SPACE/blog \
  -d '{
    "title": "Rebuilding search in a week",
    "slug": "rebuilding-search",
    "publishedAt": "2026-05-25T09:00:00.000Z",
    "markdown": "We rebuilt search and made it four times faster.",
    "socialImageUrl": "https://cdn.example.com/og/search.png"
  }'
  • slug is the address. Ask for one and you get it or a 409 — never a silent -2 suffix, because an import told to keep /blog/welcome has not kept it if it lands on /blog/welcome-2. Renaming a post leaves the old address answering a 308 to the new one at once (both addresses are dropped from every cache on the rename); GET …/blog/{id}/slug/history lists what it used to answer to.

  • publishedAt is the post's real date, which is what makes a migration possible.

  • markdown is the body, and content (block JSON) is the same body in the other spelling. They are alternatives: send one. Sending both is a 422 naming markdown — nothing silently wins, because a client that sent the wrong field would otherwise see a 201 and the wrong body. sections writes named slots — see required sections.

  • socialImageUrl is the link-preview image. It is accepted both at the top level, as shown above, and as seo.socialImageUrl; both spellings reach the same place, the post's declared fields, so the studio, /items and this door all read one value. (It used to be accepted at the top level and silently dropped — Crossly C-19.)

  • series, tags, excerpt, coverUrl, coverAlt (the cover's alt text, also its og:image:alt), furtherReading and commentsEnabled are all settable here and on PATCH …/blog/{id}. An empty string clears series, coverAlt or socialImageUrl; omitting a key never clears anything.

PATCH takes the same body, plus editReason, which rides onto the revision.

Required sections, and what publishing refuses

A space's shape says what a piece of writing here must contain. The Blog type ships two sections, both required: Standfirst (the line that appears in listings and link previews) and The post.

POST …/blog/{id}/publish refuses a post that has those sections with a required one left empty — 422, naming the slot, because the fix is one sentence of writing and the message should say which one. /items/{id}/publish has refused the same thing for longer; the two doors now agree (Crossly C-20).

One thing it does not refuse, and why:

  • A post written as prose, with no section blocks at all — which is how markdown and most imports arrive. It has no slots to be missing, and refusing it would make every post written before its space grew a shape unpublishable. It still comes back in the publish response's warnings array, so the answer never pretends the post is finished.

A post with no body at all is refused, and this page said the opposite until the rule moved. An empty post is not the prose carve-out above: having a body is the whole of the contract for prose, and a post with no body meets neither half. It is judged as what it is — every declared slot, empty — by the same 422, so the outline the studio opens for an untouched post and the same emptiness sent over HTTP now get the same answer.

Comments

Comments are replies to a published post, written by readers at /b/… (or on your own domain). They are off until you say otherwise, and there are three separate switches.

Per post

curl -sS -A 'my-app/1.0' -X POST -H "Authorization: Bearer $TOKEN" \
  -H 'content-type: application/json' \
  https://forumulate.com/api/orgs/$ORG/spaces/$SPACE/blog/$POST/comments \
  -d '{"enabled": false}'

Closing a post hides its existing comments and refuses new ones with a 403. (It used to do only the first.)

Per space

PATCH /orgs/{orgId}/spaces/{spaceId}/blog/settings carries the space's community settings. GET the same path to read them.

Field

Default

What it decides

allowAnonymous

off

Whether a reader with no account may comment at all. Opt-in — set it explicitly, either way.

moderateAnonymous

on

Whether a comment from somebody with no account waits for review.

moderateComments

off

Whether a comment from a member waits for review. Trusted members are exempt.

whoCanPost

members

Who may start a piece here. members also means an account is needed to comment.

reactionsEnabled

on

Whether readers may react to posts.

defaultSort

latest

The order the index runs in.

defaultByline

—

The name new posts are published under when the writer gives none. Stamped at write time, so changing it never rewrites a published post.

commentOrder

oldest

The thread's order: oldest first reads as a conversation, newest first puts the latest on top.

closeCommentsAfterDays

0

Close a post's comments this many days after it was published (0–3650; 0 never closes). The thread stays readable; the form goes and the comment door answers 403. Judged by the database's clock.

commentDepth

1

How deep replies nest, 1–6. 1 keeps the thread flat; a reply at the limit sits beside its parent.

commentsPerPage

0

Split long threads into pages of 10, 20, 50 or 100 (any 0–200 over the API); 0 is all on one page.

commentNotify

author

Who hears about a new comment: the post's author always does; editors adds owners, admins and maintainers.

defaultFormat

standard

The format a new post starts in (see Formats). Stamped when the post is written.

defaultTags

[]

Up to 10 tags a new post starts with when its writer gives none.

Two of those deserve spelling out, because they were the spam inlet a customer reported (Crossly C-21 / F-32):

  • allowAnonymous defaults to off for comments. A space nobody has configured refuses a stranger's comment with 401 auth.unauthenticated, and the refusal names the field and the door that turns it on. (It says 401 and not 403 because the answer is "sign in or be allowed in", not "your account may not" — a signed-in account that may not reply is the 403.) The composer is not even rendered: the reader waits for the answer to "may this person comment" before drawing a comment box, so a crawler, a feed reader and a reader with JavaScript off are never served an open "Your name" field. It is the same switch that governs anonymous reactions, and the two give the same answer: an unconfigured blog offers a logged-out reader no write path at all, and GET …/blog/t/{slug} reports reactions.enabled: false so the buttons are not drawn either. Send null to clear the choice and go back to following your organization's default.

  • moderateAnonymous defaults to on. With allowAnonymous on, a stranger's comment is accepted with status: "pending" and does not appear on the page. Approve or hide it with POST …/blog/posts/{postId}/status (visible, pending, hidden). GET …/blog/flags is the flag queue.

whoCanPost has an organization default, community.who_can_post, that applies to every space which never chose one; the space's own field wins where it is set.

The other comment door: comment-settings

GET PUT /orgs/{orgId}/spaces/{spaceId}/comment-settings is a different door to the switches above, and the one people reach for first because of its name. It carries enabled and moderated for comments a reader leaves on a page — a docs page, a legal document, a careers posting. A blog post's comments are governed by the per-post switch and …/blog/settings, never by this.

It is a partial update. Both fields are optional: a field you leave out keeps the value it has, and the reply is the whole setting as it now reads rather than the fragment you sent. So PUT {"enabled": true} turns page comments on and does not touch moderation, PUT {"moderated": true} holds comments for review without answering whether they are on, and a body carrying both still sets both. It used to require both fields, which made the obvious request the wrong one — PUT {"enabled": true} answered 422, naming moderated — and a caller who added the second field to get past it could turn moderation off by accident (Crossly E-40).

Captcha

A comment from a reader with no account is challenged whenever a captcha is configured, with the organization's own key:

curl -sS -A 'my-app/1.0' -X PUT -H "Authorization: Bearer $TOKEN" \
  -H 'content-type: application/json' \
  https://forumulate.com/api/orgs/$ORG/captcha \
  -d '{"provider": "turnstile", "siteKey": "0x4AAA…", "secret": "0x4AAA…"}'

provider is turnstile or hcaptcha. The secret is write-only — GET /orgs/{orgId}/captcha answers configured: true and never the key. The reader renders the widget from GET /public/orgs/{orgId}/captcha, which serves the site key only, and the comment door then requires a solved token (captchaToken) from anybody it cannot name. A reader who signed in is not challenged; they have proved more than a captcha would.

With no captcha configured there is no challenge, and the protection on an anonymous comment is the moderation hold above. If you turn moderateAnonymous off, configure a captcha first.

Posting one

curl -sS -A 'my-app/1.0' -X POST -H 'content-type: application/json' \
  https://forumulate.com/api/public/orgs/$ORG/spaces/$SPACE/blog/t/$SLUG/comment \
  -d '{"authorName": "Ada", "content": {"type": "doc", "content": []}, "captchaToken": "…"}'

parentPostId threads a reply under another comment. A signed-in reader comments as themselves and authorName is ignored — asking a known person to type their name into a box is how a comment section forgets who its readers are.

Front page

A blog's front page is a composed document. Until you edit it, it is generated from the index preset: the intro band, the search band and the posts feed (in the order you set), then subscribe and pagination. A blog feed sorts by latest, top or oldest and filters by tags, author, pinned, featured and section. A post's pinned (Pin to top) keeps it at the head of the default order; featured (Feature) puts it in a feed filtered to featured and makes it the opener of a magazine feed. Both are set with PATCH …/blog/{id}, and the public index takes featured, pinned and sort=oldest as query parameters.

Which layer draws the index

Three settings sound as if they all change the blog's index. They do not overlap: each owns a different part of the blog, and the order is fixed.

  1. A stored front page wins outright. Once you save a front page (PUT …/front-page, or the studio's front-page builder), the index draws that document and nothing else. The preset is no longer read for the index, and neither is frontDoor.sectionOrder (the order of the movable bands in the generated page). DELETE …/front-page goes back to the generated one.

  2. No stored page: the preset generates one. The index preset (PUT …/preset, Design → layout: feed, grid or magazine) is only the layout of the generated posts feed: title, intro, then search and the posts feed (a spaceFeed {controls: true, paginate: true, layout: <preset>}) in frontDoor.sectionOrder, then subscribe and pagination. Any other preset reads as feed.

  3. Inside a feed, the block's own attrs win. A spaceFeed in a stored page that names no layout is feed, whatever the preset says. Its card (card, lead, columns, fields, …) is whatever the block says, and unset keys are the card's defaults.

theme.blog.look is not one of these layers. Its patches set post.* keys (the post page) and blog.* keys (the hub pages: archive, series, authors, tags, search, a tag's head). They never reach the index or a front-page feed. So look: "magazine" and the magazine preset are different things that share a name: the look styles the post page and the hubs, and the preset arranges the index's generated feed. For a magazine blog throughout, set both, or set the look and compose the index yourself.

What does carry across: frontDoor switches such as showArchive, showRss and badgeStyle hold for a stored page and a generated one alike, and theme.blog.readTime.wpm scales every "N min read" on the index. A tag's landing page with no composed page of its own reuses the first feed of whichever front page is in force, stored or generated (see Categories on a blog).

Posts in other languages

Declare the languages first, as for docs: POST /orgs/{orgId}/spaces/{spaceId}/docs/locales with { "code": "es", "label": "Español" } (Studio → Languages). Then each post can have one translation per declared language — its title, excerpt and body — at …/blog/{topicId}/translations/{locale}:

  • PUT writes or corrects one by hand (title, excerpt, and content as blocks or markdown); it is published unless you send published: false.

  • POST …/{locale}/auto translates with your organization's own DeepL key. It never replaces a translation a person wrote unless you send force: true, and it stays unpublished until you publish it.

  • GET …/translations lists every declared language with exists, origin (human or machine), published and stale — stale means the original changed after the translation was made.

  • DELETE …/{locale} removes one.

A translation lives at the post's own address in that language — the language is a path segment after the blog's address (/blog/es/my-post), the original has none. The index lists each post under its translated title where one is published and keeps the rest as written, so a half-translated blog still shows every post. Both reader reads answer locale (the language served; default is the original) and locales (each declared language, available when something on the page is published in it), and the reader draws a language row from them. Writing, replacing or removing a translation purges the post's page and the index. These are the same translations, rows and shapes a changelog release has.

Every language has its own feeds and markdown. /blog/es/rss.xml, /blog/es/feed.xml and /blog/es/feed.json carry each post in Spanish where it has a published translation (linking to /blog/es/<post>) and the rest as written (linking to the original), with the feed's language set. /blog/es/<post>.md is the post's translation as markdown, or the original when there is none. The same holds for a forum (/forum/es/rss.xml, latest.rss, top.rss, category, tag and <topic>/posts.rss feeds — replies stay as written), a changelog (rss.xml, atom.xml, feed.json) and a docs page's .md (the page's twin in that language). Each index page advertises its language's feed and names every other published language's feed with <link rel="alternate" hreflang>. On the API these are ?locale= on the feed and .md reads.

What a reader gets

GET /public/orgs/{orgId}/spaces/{spaceId}/blog is the index; …/blog/t/{slug} is one post with its comments, and …/blog/t/{slug}.md is the same post as Markdown. There are feeds at rss.xml, feed.xml and feed.json, an archive by month, a series list, and tags.

Who can read a post, and paid memberships

A post's access is public (unset), members, paid or password, and previewBlocks says how much of a gated post a stranger reads. The cut is made by the API on every door that hands out a body — the post read, its .md, the media zip, the feeds, search, oEmbed, AMP and the podcast feed. Paid memberships (monthly, yearly, founding, trials, group seats, gifts, offer codes, complimentary memberships), the paywall card, password posts and tips are in Blog memberships, paid posts and password posts.

Post formats

format says how a post is drawn, not what it says. standard (unset) is every post as it always was.

format

How it reads

standard

Title, standfirst, body.

aside, status

A short note: no title on its card; the words are the card.

quote

The standfirst set as a pull quote.

link

The title goes where the post points.

image, gallery, video, audio

The media leads: a larger cover, the title under it.

The blog's defaultFormat and defaultTags (above) are what a new post starts with. An audio post with audioUrl gets a player, and is an episode of the podcast when that is on.

The blog's look: theme.blog

theme.post is how one post reads. theme.blog is everything around the posts: the hub pages (archive, series, authors, tags, search, a tag's head), the addresses, the membership portal, AMP, recommendations and reading time. Set it with PATCH /orgs/{orgId}/spaces/{spaceId}/theme like any theme key; it is strict, so a misspelled key is a 422 naming its path, and every key unset is the blog exactly as it was. In the studio it is Blog: look and hub pages in the Design panel, beside Post page: every setting for theme.post.

Looks are patches

theme.blog.look picks one of five looks. paper (unset) is the blog as it was. Each of the other four is a patch: a handful of independent post.* and blog.* keys laid under yours, never stored. A key you set yourself wins under any look, and any key of one look can be mixed with another look, because each key means one thing on its own. The layers run theme → space → post: a post's own customization.theme.post beats both.

look

post keys it sets

blog keys it sets

paper

none

none

editorial

header.kicker: firstTag, header.dateStyle: mono-caps, dropCap: {style: sunk, lines: 3}, cover: {width: wide, aspect: 16:9}, byline: {avatars: true, role: true}

hubs.width: text, series.numbering: zero-pad, tagHead: {align: center, titleSize: l}, author.layout: centered

magazine

layout: split-cover, header: {kicker: firstTag, tagStyle: eyebrow}, cover.aspect: 3:2, suggested: {style: cards, count: 3}

hubs: {width: wide, cardStyle: grid}, tagsIndex: {layout: cards, showDescriptions: true}, search.resultStyle: cards, tagHead: {coverAspect: 16:9, titleSize: l}

minimal

cover.show: false, share.style: link, header: {kicker: none, tagsPlacement: hidden}, suggested.style: list, byline.avatars: false

hubs: {width: text, cardStyle: compact, covers: false}, tagsIndex.layout: list, tagHead: {coverAspect: none, eyebrow: false}, author.avatarSize: 48

newsletter

header.width: text, cover.width: text, byline.avatars: true, share.position: bottom

hubs.width: text, search.resultStyle: list, tagsIndex: {layout: list, showDescriptions: true}

Three combinations no look ships, each one PATCH …/theme body:

{ "blog": { "look": "magazine", "hubs": { "width": "text", "cardStyle": "compact" } } }

A magazine post page (split cover, eyebrow tags, card suggestions) with quiet, compact hub lists on the reading measure.

{ "blog": { "look": "minimal" }, "post": { "cover": { "show": true, "width": "wide", "aspect": "16:9" } } }

Minimal's bare words and compact hubs, but every post still opens on a wide cover.

{ "blog": { "look": "newsletter", "tagsIndex": { "layout": "cards" }, "series": { "numbering": "plain" } },
  "post": { "dropCap": { "style": "raised", "lines": 2 } } }

The newsletter column, with editorial's drop cap (raised, not sunk), a tags page of cards and plain part numbers.

Hub pages

Key

Values (default first)

What it does

hubs.width

list | text | wide | frame

The hub column's width.

hubs.align

start | center

center centres the hub's head.

hubs.cardStyle

feed | grid | compact

How every hub lists its posts: one column, a two-up grid, or title-and-meta rows.

hubs.covers

true | false

false: no covers on hub cards (the index keeps its own).

hubs.phone

{cardStyle: full|compact, hideCovers}

On a phone: title and meta only, and/or no covers.

archive.group

month | year

/archive by month, or by year with its months under it.

archive.showCounts

true | false

The post count beside each month.

archive.listPosts

off

Each month's post titles under it on /archive itself.

archive.heading

"Archive"

The archive's heading.

archive.calendar

off

A month calendar on the archive and each month's page; every day with posts links to that day.

series.numbering

zero-pad | plain | none

A part's number: 01, 1 or none.

series.showCount / series.eyebrow

shown

"4 parts, in reading order", and the small "A series" line.

author.avatarSize

80

The portrait, 24–200 px.

author.layout

row | centered

Portrait beside the name, or above it with everything centred.

author.showLinks / showRole / showHandle / showCount

shown

The author's links, job title, @handle and post count.

tagsIndex.layout

pills | cards | list

/tags as wrapped pills, cards with the tag's cover and description, or a ruled list.

tagsIndex.showCounts

shown

Each tag's post count.

tagsIndex.showDescriptions

hidden on pills, shown on cards and list

Each tag's description.

search.resultStyle

list | cards

A hit as a title line or a card with its cover.

search.showDate / search.showCover

shown

The date under a hit; the cover beside it (cards).

tagHead.show

shown

false hides a tag landing's head (a composed page brings its own).

tagHead.eyebrow / tagHead.count

shown

The "Tagged" line and "12 articles".

tagHead.coverAspect

3:1 | 2:1 | 16:9 | 3:2 | none

The tag cover's proportion.

tagHead.align / tagHead.titleSize

start / m

center; s, m, l.

readTime.wpm

200

Reading speed, 80–600 words a minute; every "N min read" scales from it.

Composed hub pages. Every hub can also be composed from blocks, like the front page — studio → Hub pages, or PUT /orgs/{orgId}/spaces/{spaceId}/blog/hubs/{hub}/{key}/page with {"doc": …}. {hub} is archive, month, seriesIndex, series, authors, author, tags or search; {key} is one instance (2026-10, a series name, an author key) or _ for the page every instance of that hub wears unless it has its own. A spaceBand title places the hub's own head, a spaceFeed of this blog with controls is the hub's own list, and any other feed of this blog is narrowed to the hub (its author, its series). GET reads it, DELETE puts the generated page back. A tag's landing page keeps its own door (…/blog/tags/{slug}/page, under Blog and Changelog spaces).

Addresses and pages a blog has

Under the blog's address (/b/<org>/<space> on the platform, the blog's mount on your own domain):

Address

What it is

/<slug>

A post (or /2026/<slug>, /2026/10/<slug>, /2026/10/19/<slug> — see Permalinks).

/<slug>.md

The post as Markdown.

/archive, /archive/2026, /archive/2026-10, /archive/2026-10-19

The archive, a year, a month, a day. A period nobody published in is a 404.

/series, /series/<slug>

Every series; one series in reading order.

/author, /author/<name>

Every author; one author's page.

/tags, /tag/<tag>

Every tag; a tag's landing page. A child tag's posts are found on its parent's page too.

/search

Search.

/recommendations

The publications you recommend (below).

/membership

The plans, gifts, the redeem box and the reader's own membership (Blog memberships).

/subscribe

The newsletter's own page; /subscribe?embed=1 is the bare signup form for an iframe on another site.

/rss.xml, /feed.xml, /atom.xml, /feed.json

The blog's feeds.

/tag/<tag>/rss.xml, /tag/<tag>/atom.xml, /tag/<tag>/feed.json

One tag's feeds; the same three under /author/<name>/ and /series/<slug>/.

/podcast.xml

The podcast feed, when the podcast is on.

/amp/<slug>

A post's AMP copy, when AMP is on.

/indexnow.txt

The IndexNow key, when IndexNow is on.

Each language a post is translated into has the same feeds under its language segment (/es/rss.xml), as above.

theme.blog.permalinks chooses the addresses:

Key

Default

What it does

structure

slug

slug is /<slug>; year, month and day put the publication date in front (in UTC): /2026/<slug>, /2026/10/<slug>, /2026/10/19/<slug>.

tagBase

tag

A word of yours for the tag pages: "topics" serves /topics/<tag>. Lower-case letters, digits and dashes, up to 30.

authorBase

author

The same for author pages.

seriesBase

series

The same for series pages.

The canonical address follows your choice, and every other spelling keeps answering, so no link already out there breaks — a move from /<slug> to dated addresses costs nothing.

Feeds

The front door's feed settings (PUT …/preset with frontDoor, or the front door's settings in the studio) apply to every feed of the blog:

frontDoor key

Default

What it does

feedItems

50

How many posts a feed carries: 10, 20, 50 or 100.

feedContent

full

full (the whole post) or excerpt. A gated post is always its excerpt.

feedCover

on

The cover in every format (it used to ride only in the JSON Feed).

feedTitle

the blog's name

The feed's title.

feedDescription

the blog's description

The feed's description (up to 240 characters).

emptyText

the reader's default

What an empty list — the index, or a hub with nothing in it — says.

subscribeStyle

link

The subscribe control a subscribe band with menu: true draws.

oEmbed and AMP

Each post page advertises an oEmbed endpoint (GET /public/orgs/{orgId}/spaces/{spaceId}/oembed?slug=<slug>, format=json only, maxwidth 120–2000): pasting a post's address into a tool that reads oEmbed gives a rich card of plain markup — title, summary, picture, link — that runs nothing of ours on the page it lands in. An email-only post has none.

theme.blog.amp: true gives every public post an AMP copy at /amp/<slug>, linked from the post with rel="amphtml" and canonical to the post. It is off unless you choose it, because an AMP page loads the AMP runtime from Google's CDN — a third party you opt your readers into. Gated posts have no AMP copy.

Recommendations

Studio → Recommendations, or PUT /orgs/{orgId}/spaces/{spaceId}/blog-recommendations: up to 30 publications you recommend, each {name, url, description, imageUrl} (imageUrl https). They are listed at /recommendations, and theme.blog.recommendations shapes the page and where it is offered:

Key

Default

What it does

heading

"Recommendations"

The page's heading.

showInNav

shown when there are any

A link beside the archive's in the index nav.

layout

list

list (a reading list) or cards (with pictures).

afterSubscribe

off

Offer them right after a reader subscribes.

The same body takes discoveryBlurb: one line about your blog for the public blog directory (below). Its old discovery field was an opt-in to a directory that was not shown yet; it is still accepted and no longer decides anything.

The public blog directory

Forumulate keeps a public directory of the blogs published with it, at forumulate.com/blogs, linked from the footer of our own site. It is server-rendered, in our sitemap, and has one canonical address, so search engines can find it and, through it, your blog.

Every public blog is listed by default. You can leave it out two ways:

  • This blog: Studio → Recommendations → untick List this blog in the Forumulate blog directory. By API it is the space's listed switch (settings.discovery.listed, default true): PUT /orgs/{orgId}/spaces/{spaceId}/discovery with { "listed": false }. GET on the same address reads it back.

  • Every blog of your organization: the organization setting discovery.blog_directory (Settings → SEO, List our public blogs in the Forumulate blog directory), on by default. Turned off, it wins over every blog in the organization, whatever each blog's own switch says.

Some blogs are never listed, whatever either switch says:

  • a private space;

  • a members-only blog (one behind a reader sign-in or password gate);

  • a paid-only blog (one where every published post is for members, paying members or password holders, so a reader would find nothing to read);

  • a blog that has published nothing yet;

  • a blog marked noindex (Studio → SEO → hide from search engines);

  • a blog on a network allow-list (the space's own, or one on its custom domain).

Each entry shows the blog's name, its organization, your one-line discoveryBlurb and a link to the blog's canonical address (your custom domain when it has one). The most recently active blogs come first. The list is refreshed every 30 minutes, so a change takes up to half an hour to show. GET /public/discover/blogs answers the same list as JSON.

Podcast

Studio → Podcast, or PUT /orgs/{orgId}/spaces/{spaceId}/podcast. Off unless switched on; a blog that never set it has no podcast feed.

Key

Default

What it does

enabled

false

Publish /podcast.xml.

title, author, description

empty

The show's name, author and description (up to 4,000 characters).

ownerEmail

empty

The owner address Apple asks for. It is in the feed, so use a role address.

category

empty

An Apple Podcasts category ("Technology").

explicit

false

The show's explicit flag.

imageUrl

empty

Square artwork, 1400–3000 px, https.

type

episodic

episodic (newest first) or serial (in order, by each post's episode).

Every public post with an audioUrl is an episode, with audioDuration, audioBytes and episode filling the iTunes tags Apple Podcasts and Spotify read. A members-only or paid episode is left out of the feed.

More on the post page

These are theme.post keys like the ones in Blog and Changelog spaces, so each also works under post.byTag and on one post's own theme. Unset is the page as it was.

Key

Values

What it does

furtherReading

{show, title, style: list|cards}

The post's own Further reading links, after the words. Shown when the post has some.

adjacent

{show, style: titles|cards}

The previous and next post.

comments

{heading, avatars, avatarSize: 16–64, layout: stack|boxed, formPosition: top|bottom, width: text|wide}

The comments' look. heading: "" hides the heading. What the thread does lives in the community settings.

paywall

{heading, body, button, style: card|band|minimal}

The card where a gated post's preview stops (Blog memberships).

sponsor

{show, url, label, body}

A "Sponsor this blog" box; with tips on, one-off tips too.

marks

{claps, highlights, notes, restacks, streak}

Readers' marks, each off unless on: claps (up to 50 per reader), highlights and the post's top highlight above the body, private notes on a passage, restacks with a line of the reader's own, and the signed-in reader's reading streak. Counts and the top highlight are public; a note comes back only to its writer; every mark needs a signed-in reader.

helpful

{show, prompt, comment}

"Was this helpful?" at the foot; comment (on unless false) offers a box to say why. Answers land in Studio → Readers.

print

{showUrl, showCover, showComments}

The post on paper: its address and cover are printed unless false; the comments only with showComments: true.

share.selection

boolean

Select words and a small bar offers to share or copy the quote with a link back. Nothing third-party is asked anything until the reader picks a network.

suggested.placement

after | left | none

Where "more to read" goes. The Suggested layout is now this key (left), so every suggested key works under every layout.

toc.show

boolean

The on-this-page column. The Contents layout is now this key; a post's own onThisPage still wins.

Newsletter

Every blog has a mailing list (double opt-in). Studio → Newsletter, or PUT /orgs/{orgId}/spaces/{spaceId}/newsletter (partial; GET reads it), makes it a publication. Every key defaults to what the list did before, so a space that sets nothing mails exactly as it did.

Key

Default

What it does

name, description

empty

What the list is called on its signup form, its /subscribe page and in the studio.

fromName

the organization's

The display name on the From line. No quotes or angle brackets.

fromAddress

empty

The From address. Used once your organization sends through its own mail provider (Settings → Email); until then your fromName goes on our address, because a platform-sent mail cannot claim your domain.

replyTo

the From address

Where a reply goes.

headerImage

none

A banner at the top of every issue (https only).

titleAlign

left

left or center.

showCover

false

The post's cover in the issue.

showExcerpt

true

The post's excerpt.

showAuthor

false

"By <author>" under the title.

body

excerpt

excerpt (the excerpt and a link) or full (the whole post; a gated post still sends only its excerpt).

buttonLabel

"Read the article"

The button's words.

footer

empty

Small print at the foot of every issue and broadcast.

lists

[]

Up to 10 named newsletters, {id, name, description}, that a reader picks between when subscribing. Empty is the one list every space had.

notifyTags

[]

Who a publish mails when the post names nobody: subscribers with any of these labels. Empty is everyone.

referrals

{enabled: false, threshold: 3, months: 1}

A referral programme: every issue carries the subscriber's own link, and every threshold confirmed signups through it earn them months (1, 3, 6 or 12) of membership, mailed as a gift code.

The design keys build the issue's layout. It sits under any template you wrote in Email templates, which still wins.

Per post. A post's delivery (both, web, email), newsletters (list ids), emailTags (labels) and emailNote (a paragraph only the email carries) decide where it goes and who gets it; see Per-article fields. An email-only post is unlisted: it has no web page, feed entry, sitemap entry or search hit.

Test sends. POST …/newsletter/test-issue with {"topicId"} mails you the issue a post would send (blog.author); POST …/newsletter/test-broadcast with {subject, body} mails you a draft broadcast. Both are 10 a minute and record nothing. In the studio: Email me this post as a test and Send me a test.

Broadcasts to part of the list. POST …/blog/broadcasts takes lists (only the subscribers of those newsletters), segment (subscribedAfter, subscribedBefore, tags — any of them) and test: true (to you only).

Importing subscribers. POST …/newsletter/import with a CSV whose header names email (and optionally tags, name):

curl -sS -A 'my-app/1.0' -X POST \
  -H "Authorization: Bearer $FORUMULATE_TOKEN" -H 'content-type: application/json' \
  https://forumulate.com/api/orgs/$ORG/spaces/$SPACE/newsletter/import \
  -d '{"csv": "email,name\nada@example.com,Ada\n", "mode": "optin", "tags": ["imported"], "lists": ["weekly"]}'

mode: "optin" (the default) sends each address the confirmation email. mode: "subscribed" adds them as subscribed and requires consent: true, your statement that these people agreed elsewhere — skipping the double opt-in is an act, not a default. An address that unsubscribed here is never re-added. The answer counts added, updated, skipped and invited, with each bad row and why.

Where signups come from. The subscribe body takes ref (a page's ?ref=, a campaign or a referral code) and name (with theme.blog.portal.nameField). Each subscriber records its source, and GET …/blog/subscribers.csv exports name, tags and source with the address.

The signup form elsewhere. /subscribe?embed=1 is the bare form, sized for an iframe on your own site; Newsletter → Signup form for other sites has the snippet. The newsletter's own page is the same address without ?embed=1.

Opens and clicks. Only with the organization setting community.newsletter_tracking on (off by default) does an issue carry an open pixel and rewritten links; they are counted per post in Readers. Whether you need your readers' consent for that is yours to manage.

Readers

Studio → Readers is each post's numbers over the last 30 days (1–365 with ?days=): views, unique readers, call-to-action clicks, helpful and unhelpful votes, comments, and — with tracking on — email opens and clicks, plus subscriber growth. GET /orgs/{orgId}/spaces/{spaceId}/engagement reads it, …/engagement/export.csv downloads it, and …/engagement/comments lists what readers wrote under "Was this helpful?". A changelog has the same panel.

Publishing

Review before publishing. An author sends a draft for review (POST …/blog-review/{topicId}/review, Send for review); a publisher approves it (…/review/approve) or asks for changes with a note (…/review/request-changes); DELETE …/review withdraws it. With the space setting requireReview: true, only an approved post publishes — off unless you choose it, and a blog does not take the changelog's organization default. Webhooks: blog.review_requested, blog.reviewed.

Publishing pings. Studio → Publishing pings, or PUT /orgs/{orgId}/spaces/{spaceId}/publish-pings. All off until switched on:

Key

Default

What it does

indexNow

false

Tell Bing, Yandex, Seznam and Naver about a new address at once. The key file is served at /indexnow.txt under the space's address.

updateServices

[]

Up to 10 XML-RPC weblogUpdates.ping endpoints (Ping-o-Matic).

sendWebmentions

false

Tell every page a post links to, where it accepts webmentions.

acceptWebmentions

false

Receive other sites' mentions of your posts (POST /public/orgs/{orgId}/spaces/{spaceId}/webmention with source and target, advertised on each post with <link rel="webmention">).

autoApproveMentions

false

Show received mentions without review. Unset, each waits: GET …/publish-pings/mentions lists them and PUT …/publish-pings/mentions/{mentionId} approves or rejects one. Approved ones are drawn under the post.

A changelog takes the same settings.

Post by email. GET …/blog-email-in (studio: Publishing pings → Post by email) gives the blog a private address. Mail to it from an organization member's address becomes a draft — the subject is the title, the message the body, attachments the pictures; mail from anyone else is dropped. Keep the address private.

GitHub sync and backup. Through the space's Git connection (the one docs sync uses), POST …/blog-github/sync reads every *.md under the connection's path — front matter title, slug, date, tags, excerpt, cover, draft — and writes each as a post (new slugs created, existing ones updated, nobody mailed). POST …/blog-github/export writes every published post back as <path>/<slug>.md with the same front matter, one commit per changed file.

Stock photos and GIFs. Studio → Stock media takes your own Unsplash and GIPHY keys (PUT …/media-search/keys with unsplash, giphy; null removes one). The editor then searches both; choosing an Unsplash photo tells Unsplash, as its terms require. Keys are stored encrypted and never sent back.

Importing. WordPress, Ghost, Substack and Medium exports, or one article by its address, come in through Studio → Import (POST …/blog-import): each post keeps its title, address, date, tags, author, cover and canonical URL; a post whose address the blog already has is skipped, and nobody is mailed. Every importer is described in Importers.

Federation. A blog as an ActivityPub actor is covered in ActivityPub federation.

Tags in depth

On top of the display fields in Blog and Changelog spaces, a tag (PUT …/blog/tags/{slug}) takes:

  • parent — the tag it sits under (hierarchical categories). The parent's page lists its children, and a post tagged with the child is found on the parent's page too. null is top level; a tag under itself or its own child is a 422. Studio: Sits under.

  • translations — {"de": {"label": "KI", "description": "…"}}, the label and description per language, up to 30. The reader shows the one for the page's language. Authors take translations too, with role and bio. Studio: In other languages.

  • internal: true — an internal tag: it groups posts for you and is never shown to a reader.

Categories on a blog

A blog has no separate "category" object. Its tags are its categories: a tag has a display name, colours, a gradient, a cover, an icon, a description, a parent and a page of its own, which is everything a category needs. Use a handful of top-level tags as sections ("Engineering", "Design", "News") and give each one a look:

PUT /orgs/{orgId}/spaces/{spaceId}/blog/tags/engineering
{ "label": "Engineering",
  "color": "#0f766e", "textColor": "white",
  "colorDark": "#5eead4", "textColorDark": "#042f2e",
  "gradient": ["#0f766e", "#2563eb"],
  "icon": "code",
  "coverUrl": "/api/assets/…",
  "description": "How we build it, and what broke on the way.",
  "pageScheme": "dark", "pagePalette": { "primary": "#0f766e" } }

Key

What it is

Where a reader sees it

label

The display name; the slug stays the id.

Every chip, the tags band, /tags, the tag page.

color, textColor

The chip's fill and ink (a #hex or CSS colour name).

Chips on cards and posts, the tags band's pills (the active pill takes the tag's colour), pills-filled tabs.

colorDark, textColorDark

The same in a dark scheme. Unset: color/textColor in both.

As above, when the page is dark.

gradient

2–3 #hex stops, drawn through the tag's words.

The text and eyebrow tag styles and a card's firstTag kicker. A chip or an outline keeps color.

icon

A glyph name (any name in GET /icons) or an image URL.

Before the label on chips, kickers and the tags band's pills, on the blog and the forum.

coverUrl

An image.

The head of the tag's page (theme.blog.tagHead.coverAspect, 3:1 unset) and its card on /tags when tagsIndex.layout is cards (3:1).

description

Up to 500 characters.

The tag page's head and meta description, /tags cards and list, the chip's tooltip.

pageScheme, pagePalette

The scheme and palette slots the tag's pages wear.

The tag's page.

parent

The tag it sits under.

The parent's page lists its children and finds their posts.

The order categories appear in is yours where it matters: the tags band and a feed's tag tabs list the tags you name, in that order (unset, the band shows the most-used eight, or limit of them). /tags lists every tag by post count, most first.

Recipes for a magazine front page

Each is a PUT /orgs/{orgId}/spaces/{spaceId}/front-page body; every key below is in GET /docs/block-schema.

Category tabs over the index. The tags band is the row of categories (style: pills, pills-filled, tabs, text or segmented). Each tab wears its tag's colours and icon, links to the tag's page, and the current one is marked there:

{ "doc": { "type": "doc", "content": [
  { "type": "spaceBand", "attrs": { "band": "title" } },
  { "type": "spaceBand", "attrs": { "band": "tags", "style": "pills-filled",
      "tags": ["engineering", "design", "news"], "showAll": true, "overflow": "scroll" } },
  { "type": "spaceFeed", "attrs": { "layout": "grid", "controls": true, "paginate": true } }
] } }

A featured opener, then one row per category. lead.source: "featured" opens the magazine on the newest featured post wherever it is — however old, whatever limit is — and fills the rest with the newest others (nothing featured: the newest post opens). dedupe: true keeps everything the magazine drew out of the rows under it. Each category row is a feed filtered to one tag, with the tag as its kicker:

{ "doc": { "type": "doc", "content": [
  { "type": "spaceFeed", "attrs": { "layout": "magazine", "limit": 4,
      "lead": { "source": "featured", "style": "split", "titleSize": "xl" } } },
  { "type": "spaceFeed", "attrs": { "heading": "Engineering", "layout": "grid",
      "columns": { "sm": 1, "md": 3, "lg": 3 }, "limit": 3, "dedupe": true,
      "filter": { "tags": ["engineering"] },
      "card": { "kicker": "firstTag", "tagStyle": "eyebrow" },
      "moreLink": { "label": "All engineering" } } },
  { "type": "spaceFeed", "attrs": { "heading": "Design", "layout": "grid",
      "columns": { "sm": 1, "md": 3, "lg": 3 }, "limit": 3, "dedupe": true,
      "filter": { "tags": ["design"] },
      "card": { "kicker": "firstTag", "tagStyle": "eyebrow" } } }
] } }

Give moreLink an href to the tag's page; with none it links to the blog itself. A row can cover several tags: filter.tags takes up to ten, joined by tagMatch (any, unset, or all), and filter.excludeTags leaves some out.

One feed, the categories as tabs over it. tabs.tags filters the feed in place instead of leaving the page:

{ "type": "spaceFeed", "attrs": { "layout": "magazine", "limit": 7,
    "tabs": { "tags": ["engineering", "design", "news"], "allLabel": "Everything",
              "style": "segmented" } } }

Category pages that match. A tag's page (/tag/<slug>) opens with the tag's head (name, description, colour, cover; theme.blog.tagHead shapes it) and then the first feed of the front page in force, narrowed to the tag and paginated. So a magazine front page gives magazine category pages without composing anything. To design one category's page, compose it: PUT …/blog/tags/{slug}/page (see Blog and Changelog spaces). A /tags page of category cards with covers and descriptions is theme.blog.tagsIndex.layout: "cards", which look: "magazine" also sets.

One post's own look

A post's customization.theme may set the token keys (colours, palettes, fonts, typography, colorScheme, components, widths, layout, motion) and the post namespace — every key on this page and in How a post reads, including paywall, sponsor, marks and comments. Another type's namespace (changelog, docs, forum, …) is refused with a 422 naming it, and so is blog: the hubs, addresses and looks are the space's alone. In the studio it is This piece's theme in the post's details.

{ "customization": { "theme": {
  "post": { "layout": "split-cover", "paywall": { "style": "band", "heading": "The rest is for supporters" } }
} } }

Outlets

A theme component can place HTML in these blog outlets. None wraps; the post outlets carry post.title and post.slug.

Outlet

Pages

Where

blog-index-above-list

index

The blog index, above the list of posts.

blog-index-below-list

index

Below the list and its pager.

blog-card-footer

index, hub

The foot of every post card (repeats; post.title, post.slug).

blog-hub-above-list

hub

Archive, an author, a series, a tag, search — above the list.

blog-hub-below-list

hub

The same pages, below the list.

blog-post-before

post

Above a post's body.

blog-post-after

post

After its body.

blog-post-end

post

After everything that follows the post (suggested posts, the piece page), before the comments; as wide as post.after.width.

Where it is in the studio

The blog's studio sections: Posts, Languages, Comments and community (the settings table above, default format and tags), Hub pages, Newsletter, Readers, Import, Recommendations, Podcast, Stock media, Publishing pings (with post by email), Paid memberships, Moderation and Comments on a post. A post's own fields — access, preview length, format, audio, delivery, newsletters — are in its details. The look is in the Design panel: Blog: look and hub pages and Post page: every setting.

What you need to set up

  • Paid posts, memberships, gifts and tips: your organization's Stripe, under Settings → Payments (Payments and the shop). Until then readers of a paid post see the preview and the card with no plans. Details in Blog memberships.

  • A newsletter From address on your domain: your organization's own mail provider (Settings → Email). Until then issues come from our address under your fromName.

  • Open and click counts: the organization setting community.newsletter_tracking. Until then the Readers panel shows no email numbers.

  • Stock photos and GIFs: your own Unsplash and/or GIPHY key under Studio → Stock media. Until then the picker says where to add one.

  • GitHub sync and backup: a Git connection on the space.

  • Post by email: inbound mail on Forumulate's side; while it is not available the address reads empty (null).

  • AMP, IndexNow, update services, webmentions, the podcast, recommendations: each is a switch of yours, off until you turn it on.

  • The public blog directory: on by default; untick it per blog, or turn off discovery.blog_directory for the whole organization (above).

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.

Blog Comments

Operation

Request

What it does

Shortcuts

BlogComments_vote

POST /orgs/{orgId}/spaces/{spaceId}/blog/posts/{postId}/vote

Create the vote in a space (blog posts)

client.blog.comments.vote()

BlogComments_react

POST /orgs/{orgId}/spaces/{spaceId}/blog/posts/{postId}/react

Create the react in a space (blog posts)

client.blog.comments.react()

BlogComments_editPost

PATCH /orgs/{orgId}/spaces/{spaceId}/blog/posts/{postId}

Update the post (blog)

client.blog.comments.editPost()

BlogComments_deletePost

DELETE /orgs/{orgId}/spaces/{spaceId}/blog/posts/{postId}

Delete the post (blog)

client.blog.comments.deletePost()

BlogComments_revisions

GET /orgs/{orgId}/spaces/{spaceId}/blog/posts/{postId}/revisions

List revisions in a space (blog posts)

client.blog.comments.revisions()

BlogComments_revisionDiff

GET /orgs/{orgId}/spaces/{spaceId}/blog/posts/{postId}/revisions/{revision}

Get the revision (blog)

client.blog.comments.revisionDiff()

BlogComments_flag

POST /orgs/{orgId}/spaces/{spaceId}/blog/posts/{postId}/flag

Flag the post (blog)

client.blog.comments.flag()

BlogComments_setPostStatus

POST /orgs/{orgId}/spaces/{spaceId}/blog/posts/{postId}/status

Create the status in a space (blog posts)

client.blog.comments.setPostStatus()

BlogComments_recoverPost

POST /orgs/{orgId}/spaces/{spaceId}/blog/posts/{postId}/recover

Create the recover in a space (blog posts)

client.blog.comments.recoverPost()

Blog Email In

Operation

Request

What it does

Shortcuts

BlogEmailIn_address

GET /orgs/{orgId}/spaces/{spaceId}/blog-email-in

Read a blog’s post-by-email address

client.blog.emailIn.address()

Blog Engagement

Operation

Request

What it does

Shortcuts

BlogEngagement_perPost

GET /orgs/{orgId}/spaces/{spaceId}/engagement

Read each post’s reader numbers

client.blog.engagement.perPost()

BlogEngagement_csv

GET /orgs/{orgId}/spaces/{spaceId}/engagement/export.csv

Download each post’s reader numbers as CSV

client.blog.engagement.csv()

BlogEngagement_nps

GET /orgs/{orgId}/spaces/{spaceId}/engagement/nps

Read the changelog widget’s NPS

client.blog.engagement.nps()

BlogEngagement_comments

GET /orgs/{orgId}/spaces/{spaceId}/engagement/comments

Read the feedback readers left on posts

client.blog.engagement.comments()

Blog Github

Operation

Request

What it does

Shortcuts

BlogGithub_sync

POST /orgs/{orgId}/spaces/{spaceId}/blog-github/sync

Bring Markdown posts in from the connected GitHub repository

client.blog.github.sync()

BlogGithub_export

POST /orgs/{orgId}/spaces/{spaceId}/blog-github/export

Back up published posts to the connected GitHub repository

client.blog.github.export()

Operation

Request

What it does

Shortcuts

BlogMediaSearch_status

GET /orgs/{orgId}/spaces/{spaceId}/media-search/keys

Which stock-media providers have a key

client.blog.mediaSearch.status()

BlogMediaSearch_setKeys

PUT /orgs/{orgId}/spaces/{spaceId}/media-search/keys

Set or remove the stock-media providers’ keys

client.blog.mediaSearch.setKeys()

BlogMediaSearch_search

GET /orgs/{orgId}/spaces/{spaceId}/media-search

Search stock photos or GIFs

client.blog.mediaSearch.search()

BlogMediaSearch_use

POST /orgs/{orgId}/spaces/{spaceId}/media-search/use

Record that a stock photo was used

client.blog.mediaSearch.use()

Blog Moderation

Operation

Request

What it does

Shortcuts

BlogModeration_flags

GET /orgs/{orgId}/spaces/{spaceId}/blog/flags

List flags in a space (blog)

client.blog.moderation.flags()

BlogModeration_reviewFlag

POST /orgs/{orgId}/spaces/{spaceId}/blog/flags/{flagId}/review

Create the review in a space (blog flags)

client.blog.moderation.reviewFlag()

BlogModeration_resolveFlag

POST /orgs/{orgId}/spaces/{spaceId}/blog/flags/{flagId}/resolve

Resolve the flag (blog)

client.blog.moderation.resolveFlag()

Blog Newsletter

Operation

Request

What it does

Shortcuts

BlogNewsletter_subscribers

GET /orgs/{orgId}/spaces/{spaceId}/blog/subscribers

List subscribers in a space

client.blog.newsletter.subscribers()

BlogNewsletter_subscribersCsv

GET /orgs/{orgId}/spaces/{spaceId}/blog/subscribers.csv

File of subscribers

client.blog.newsletter.subscribersCsv()

BlogNewsletter_broadcasts

GET /orgs/{orgId}/spaces/{spaceId}/blog/broadcasts

List broadcasts in a space (blog)

client.blog.newsletter.broadcasts()

BlogNewsletter_sendBroadcast

POST /orgs/{orgId}/spaces/{spaceId}/blog/broadcasts

Create a broadcast in a space (blog)

client.blog.newsletter.sendBroadcast()

BlogNewsletter_setSubscriberTags

PUT /orgs/{orgId}/spaces/{spaceId}/blog/subscribers/{subscriberId}/tags

Replace tags in a space

client.blog.newsletter.setSubscriberTags()

BlogNewsletter_subscriberTags

GET /orgs/{orgId}/spaces/{spaceId}/blog/subscriber-tags

List subscriber tags in a space

client.blog.newsletter.subscriberTags()

Blog Publish Pings

Operation

Request

What it does

Shortcuts

BlogPublishPings_read

GET /orgs/{orgId}/spaces/{spaceId}/publish-pings

Read who a space tells when it publishes

client.blog.publishPings.read()

BlogPublishPings_write

PUT /orgs/{orgId}/spaces/{spaceId}/publish-pings

Change who a space tells when it publishes

client.blog.publishPings.write()

BlogPublishPings_mentions

GET /orgs/{orgId}/spaces/{spaceId}/publish-pings/mentions

List the webmentions received

client.blog.publishPings.mentions()

BlogPublishPings_moderate

PUT /orgs/{orgId}/spaces/{spaceId}/publish-pings/mentions/{mentionId}

Approve or reject a received webmention

client.blog.publishPings.moderate()

Blog Recommendations

Operation

Request

What it does

Shortcuts

BlogRecommendations_read

GET /orgs/{orgId}/spaces/{spaceId}/blog-recommendations

Read a blog’s recommendations

client.blog.recommendations.read()

BlogRecommendations_write

PUT /orgs/{orgId}/spaces/{spaceId}/blog-recommendations

Change a blog’s recommendations

client.blog.recommendations.write()

Blog Review

Operation

Request

What it does

Shortcuts

BlogReview_request

POST /orgs/{orgId}/spaces/{spaceId}/blog-review/{topicId}/review

Send a draft blog post for review

client.blog.review.request()

BlogReview_withdraw

DELETE /orgs/{orgId}/spaces/{spaceId}/blog-review/{topicId}/review

Take a blog post out of review

client.blog.review.withdraw()

BlogReview_approve

POST /orgs/{orgId}/spaces/{spaceId}/blog-review/{topicId}/review/approve

Approve a blog post under review

client.blog.review.approve()

BlogReview_requestChanges

POST /orgs/{orgId}/spaces/{spaceId}/blog-review/{topicId}/review/request-changes

Ask for changes to a blog post under review

client.blog.review.requestChanges()

Blog Taxonomy

Operation

Request

What it does

Shortcuts

BlogTaxonomy_getHubPage

GET /orgs/{orgId}/spaces/{spaceId}/blog/hubs/{hub}/{key}/page

Get the page in a space (hubs)

client.blog.taxonomy.getHubPage()

BlogTaxonomy_setHubPage

PUT /orgs/{orgId}/spaces/{spaceId}/blog/hubs/{hub}/{key}/page

Replace the page in a space (hubs)

client.blog.taxonomy.setHubPage()

BlogTaxonomy_removeHubPage

DELETE /orgs/{orgId}/spaces/{spaceId}/blog/hubs/{hub}/{key}/page

Delete the page in a space (hubs)

client.blog.taxonomy.removeHubPage()

BlogTaxonomy_listTags

GET /orgs/{orgId}/spaces/{spaceId}/blog/tag-info

Get the tag info in a space (blog)

client.blog.taxonomy.listTags()

BlogTaxonomy_setTag

PUT /orgs/{orgId}/spaces/{spaceId}/blog/tags/{slug}

Replace the tag

client.blog.taxonomy.setTag()

BlogTaxonomy_clearTag

DELETE /orgs/{orgId}/spaces/{spaceId}/blog/tags/{slug}

Delete the tag

client.blog.taxonomy.clearTag()

BlogTaxonomy_getTagPage

GET /orgs/{orgId}/spaces/{spaceId}/blog/tags/{slug}/page

Get the page in a space (tags)

client.blog.taxonomy.getTagPage()

BlogTaxonomy_setTagPage

PUT /orgs/{orgId}/spaces/{spaceId}/blog/tags/{slug}/page

Replace the page in a space (tags)

client.blog.taxonomy.setTagPage()

BlogTaxonomy_removeTagPage

DELETE /orgs/{orgId}/spaces/{spaceId}/blog/tags/{slug}/page

Delete the page in a space (tags)

client.blog.taxonomy.removeTagPage()

BlogTaxonomy_listAuthors

GET /orgs/{orgId}/spaces/{spaceId}/blog/authors

List authors in a space (blog)

client.blog.taxonomy.listAuthors()

BlogTaxonomy_putAuthor

PUT /orgs/{orgId}/spaces/{spaceId}/blog/authors/{key}

Replace the author (blog)

client.blog.taxonomy.putAuthor()

BlogTaxonomy_removeAuthor

DELETE /orgs/{orgId}/spaces/{spaceId}/blog/authors/{key}

Delete the author (blog)

client.blog.taxonomy.removeAuthor()

Public Blog Feeds

Operation

Request

What it does

Shortcuts

PublicBlogFeeds_rss

GET /public/orgs/{orgId}/spaces/{spaceId}/blog/rss.xml

RSS feed of the blog (rss.xml)

forumulate reader blog rss<br>client.blog.publicFeeds.rss()

PublicBlogFeeds_rssAlias

GET /public/orgs/{orgId}/spaces/{spaceId}/blog/feed.xml

RSS feed of the blog (feed.xml)

forumulate reader blog feed<br>client.blog.publicFeeds.rssAlias()

PublicBlogFeeds_atom

GET /public/orgs/{orgId}/spaces/{spaceId}/blog/atom.xml

The blog's posts as an Atom feed

client.blog.publicFeeds.atom()

PublicBlogFeeds_json

GET /public/orgs/{orgId}/spaces/{spaceId}/blog/feed.json

JSON feed of the blog

forumulate reader blog json-feed<br>client.blog.publicFeeds.json()

PublicBlogFeeds_tagRss

GET /public/orgs/{orgId}/spaces/{spaceId}/blog/tag/{tag}/rss.xml

One tag's posts as an RSS feed

client.blog.publicFeeds.tagRss()

PublicBlogFeeds_tagAtom

GET /public/orgs/{orgId}/spaces/{spaceId}/blog/tag/{tag}/atom.xml

One tag's posts as an Atom feed

client.blog.publicFeeds.tagAtom()

PublicBlogFeeds_tagJson

GET /public/orgs/{orgId}/spaces/{spaceId}/blog/tag/{tag}/feed.json

One tag's posts as a JSON Feed

client.blog.publicFeeds.tagJson()

PublicBlogFeeds_authorRss

GET /public/orgs/{orgId}/spaces/{spaceId}/blog/author/{ref}/rss.xml

One author's posts as an RSS feed

client.blog.publicFeeds.authorRss()

PublicBlogFeeds_authorAtom

GET /public/orgs/{orgId}/spaces/{spaceId}/blog/author/{ref}/atom.xml

One author's posts as an Atom feed

client.blog.publicFeeds.authorAtom()

PublicBlogFeeds_authorJson

GET /public/orgs/{orgId}/spaces/{spaceId}/blog/author/{ref}/feed.json

One author's posts as a JSON Feed

client.blog.publicFeeds.authorJson()

PublicBlogFeeds_seriesRss

GET /public/orgs/{orgId}/spaces/{spaceId}/blog/series/{name}/rss.xml

One series' posts as an RSS feed

client.blog.publicFeeds.seriesRss()

PublicBlogFeeds_seriesAtom

GET /public/orgs/{orgId}/spaces/{spaceId}/blog/series/{name}/atom.xml

One series' posts as an Atom feed

client.blog.publicFeeds.seriesAtom()

PublicBlogFeeds_seriesJson

GET /public/orgs/{orgId}/spaces/{spaceId}/blog/series/{name}/feed.json

One series' posts as a JSON Feed

client.blog.publicFeeds.seriesJson()

Public Blog Newsletter

Operation

Request

What it does

Shortcuts

PublicBlogNewsletter_info

GET /public/orgs/{orgId}/spaces/{spaceId}/newsletter

Read a public space’s newsletter (name and lists)

client.blog.publicNewsletter.info()

Public Blog News Sitemap

Operation

Request

What it does

Shortcuts

PublicBlogNewsSitemap_newsSitemap

GET /public/orgs/{orgId}/spaces/{spaceId}/blog/news-sitemap.xml

The blog’s Google News sitemap

client.blog.publicNewsSitemap.newsSitemap()

Public Blog Podcast

Operation

Request

What it does

Shortcuts

PublicBlogPodcast_feed

GET /public/orgs/{orgId}/spaces/{spaceId}/blog/podcast.xml

The blog’s podcast feed

client.blog.publicPodcast.feed()

Public Blog Recommendations

Operation

Request

What it does

Shortcuts

PublicBlogRecommendations_list

GET /public/orgs/{orgId}/spaces/{spaceId}/blog-recommendations

Read a public blog’s recommendations

client.blog.publicRecommendations.list()

PublicBlogRecommendations_discover

GET /public/discover/blogs

List the publications in the platform directory

client.blog.publicRecommendations.discover()

Public Captcha

Operation

Request

What it does

Shortcuts

PublicCaptcha_get

GET /public/orgs/{orgId}/captcha

Get the captcha in an organization (public reader)

client.public.captcha.get()

Public Faq Channels

Operation

Request

What it does

Shortcuts

PublicFaqChannels_rss

GET /public/orgs/{orgId}/spaces/{spaceId}/faq/rss.xml

An FAQ's new and changed answers as RSS (?tag= narrows)

client.faq.publicChannels.rss()

PublicFaqChannels_atom

GET /public/orgs/{orgId}/spaces/{spaceId}/faq/atom.xml

An FAQ's new and changed answers as Atom (?tag= narrows)

client.faq.publicChannels.atom()

PublicFaqChannels_json

GET /public/orgs/{orgId}/spaces/{spaceId}/faq/feed.json

An FAQ's new and changed answers as JSON Feed (?tag= narrows)

client.faq.publicChannels.json()

PublicFaqChannels_markdown

GET /public/orgs/{orgId}/spaces/{spaceId}/faq/index.md

The whole FAQ, every question and answer, as one markdown document

client.faq.publicChannels.markdown()

PublicFaqChannels_widget

GET /public/orgs/{orgId}/spaces/{spaceId}/faq/widget

The FAQ help widget's questions, with those suggested for a page

client.faq.publicChannels.widget()

Public Publish Pings

Operation

Request

What it does

Shortcuts

PublicPublishPings_receive

POST /public/orgs/{orgId}/spaces/{spaceId}/webmention

Send a webmention to one of this space’s posts

client.public.publishPings.receive()

PublicPublishPings_mentions

GET /public/orgs/{orgId}/spaces/{spaceId}/mentions/{slug}

List a post’s approved webmentions

client.public.publishPings.mentions()

PublicPublishPings_key

GET /public/orgs/{orgId}/spaces/{spaceId}/indexnow-key

This space’s IndexNow key

client.public.publishPings.key()

Public Space Authors

Operation

Request

What it does

Shortcuts

PublicSpaceAuthors_authorList

GET /public/orgs/{orgId}/spaces/{spaceId}/authors

List authors in a space (public reader)

client.spaces.publicAuthors.authorList()

PublicSpaceAuthors_author

GET /public/orgs/{orgId}/spaces/{spaceId}/authors/{ref}

Get the author

client.spaces.publicAuthors.author()

Space Authors

Operation

Request

What it does

Shortcuts

SpaceAuthors_listAuthors

GET /orgs/{orgId}/spaces/{spaceId}/authors

List authors in a space

client.spaces.authors.listAuthors()

SpaceAuthors_putAuthor

PUT /orgs/{orgId}/spaces/{spaceId}/authors/{key}

Replace the author

client.spaces.authors.putAuthor()

SpaceAuthors_removeAuthor

DELETE /orgs/{orgId}/spaces/{spaceId}/authors/{key}

Delete the author

client.spaces.authors.removeAuthor()

Space Settings comment settings

Operation

Request

What it does

Shortcuts

SpaceSettings_getCommentSettings

GET /orgs/{orgId}/spaces/{spaceId}/comment-settings

List comment settings in a space

client.spaces.settings.getCommentSettings()

SpaceSettings_setCommentSettings

PUT /orgs/{orgId}/spaces/{spaceId}/comment-settings

Turn reader comments on, and whether they are moderated

client.spaces.settings.setCommentSettings()

The Forumulate teamUpdated