Build your website with your agent
Your newsletter deserves a home that looks like you wrote it, not like a template chose for you. Describe it to your agent and it writes the pages: plain HTML, CSS and JavaScript you own outright, hosted by Commune on your own domain, with your articles, your archive and your subscribe button already wired in. There is no editor and no builder, which also means there is no undo, so keep the site in a repository of your own.
Before you start
Commune keeps the page you last wrote and nothing else. There is no version history and no operation that returns a previous page, so keep your site in a git repository and write pages out of it. That repository is the only history there is, and it is what turns an overwrite from a loss into a diff.
Connect your agent once, then describe the site. It reads Commune's own reference for the page kinds and the URL grammar before it writes anything, so the first attempt is a working site rather than a round of refusals, and it writes every page as a draft you can look at before it goes live.
Connect once
Add Commune's MCP server to your agent, then approve the consent screen and pick your newsletter. Nothing else to install.
claude mcp add --transport http commune https://api.usecommune.com/mcpAsk
One prompt does the whole job. Paste it as it is, or change the parts that are yours.
Build a website for my Commune newsletter, in a new git repo in this folder.
Three pages: an article list at /, an article page at /posts/:article, and an about page at /about. Quiet and typographic, one column, serif headings, works on a phone. Use my newsletter's own articles, not placeholders.
Put them up as drafts first and give me the preview links. Once I say yes, publish them and point blog.example.org at the site. Commit after each page.
What it uses
Your agent picks these of Commune's tools to answer. You do not name them; they are here so you know where every part of the answer comes from.
build_my_websiteWrites one page: its kind, its path and its whole document.
newsletteris the article list,articleis a URL pattern that serves every issue, andpageis any fixed path. It answers with a signedpreview_urlfor a draft, which is the only way to look at one.what_is_on_my_websiteEverything the site is made of: each page's kind, path, state and weight, plus every hostname with its verification, its routing and which Commune origin answers for it. The tool for "is it live" and for "my domain is not loading", which are the same question from two ends.
commune_writeEverything build_my_website does not: publishing and unpublishing a page, editing one with the whole new document, minting the publishable key the page reads data with, and pointing a hostname at the site with
modeset tosite.commune_readReads a page's HTML back, for an agent that has lost its local copy, and reads your articles so the pages it writes talk about real issues.
What comes back
Your site is up. Three pages, all published:
| Page | Address | Serves |
|---|---|---|
| Article list | https://blog.example.org | your 42 issues, newest first |
| Article | /posts/:article | every issue, including future ones |
| About | /about | one page |
- blog.example.org is verified, routing and in
sitemode, so the name and avatar in Commune now link here. - I minted a publishable key (
cmn_pk_7Qa2Lk9x…) and put it in the article list's script tag. It is read-only and safe in page source. - Committed as
a1c93f0. If you want yesterday's homepage back, that isgit revertand one more write.
An example. Yours comes from your own newsletter.
Worth asking next
- Add a page for my paid archive, linking only the issues that are public.
- The article page is too wide on a phone. Make the measure 34rem and bump the line height.
- Which page is bringing in the most subscribers?
Tips
- Ask for drafts first. A draft has no public address at all: the signed preview link is the only way in, so nothing half-finished is ever reachable, and publishing is one more call.
- Keep the site in a repository and tell your agent to commit after every page. Commune stores no history, so the repository is the history, and an agent that overwrote the wrong page can put the previous commit back.
- Say what you want it to feel like, not what elements to use. There is no template to fight: the agent writes the markup and the CSS, so "quiet, typographic, one column" is a more useful instruction than a list of divs.
Four requests put a site up: look at what is there, write the article list, write the article template, and mint the key the pages read data with. A fifth points your own hostname at it. Every page is one document in one request, and there is no build step, so a page is live the moment it commits.
Before you start
- An API key with
content: write,settings: write, exported asCOMMUNE_API_KEY. - A hostname you own, if you want the site on your own domain. Add it and prove ownership in Commune under Settings, then the last step hands it to the website origin. Without one, the site serves at
<handle>.communepages.com. - A git repository for the pages. Commune keeps no history of a page, so this is the only history there is.
- Some steps need more than this. Each one lists it where it starts.
Run it in your client
Every request below, in order, as a collection. Import the file into Postman, Insomnia, Bruno, Yaak or Hoppscotch, set apiKey, and send them one by one.
Every request sends Authorization: Bearer $COMMUNE_API_KEY and Commune-Version: 2026-08-26.
- 1
See what is already there
The index of the site: every page with its kind, its path, its state and the weight of its document, drafts included. The documents are not in it, because a page holds up to 2MB and a site holds many.
A newsletter with no website answers with an empty list, which is the default rather than an error.
cURLcurl "https://api.usecommune.com/newsletters/example-letter/website-pages" \ -H "Authorization: Bearer $COMMUNE_API_KEY" \ -H "Commune-Version: 2026-08-26"Response 200{ "object": "list", "data": [], "pagination": { "has_more": false, "next_cursor": null } } - 2
Write the article list
kindisnewsletter: the article list, at most one per newsletter, and its path is where a reader who clicks your name or avatar inside Commune arrives. Writing it needssettings: writeas well ascontent: write, because it moves where Commune sends readers.The document is yours, byte for byte. Commune splices the title, the Open Graph tags, the canonical URL, JSON-LD and a preloaded
commune-datapayload in as it serves the page, never as it stores it, so reading the page back returns exactly what you sent.<commune-article-list>is filled from that payload, which means the list renders with no request and no key. Thedata-commune-keyattribute is for what the page asks for afterwards: a second page of the archive, another article.There is no undo
Commune keeps the document you last wrote and nothing else. Commit this HTML to your own repository before you send it: a second
POSTto the same path answers409, and aPATCHreplaces the document with no way back.cURLcurl -X POST "https://api.usecommune.com/newsletters/example-letter/website-pages" \ -H "Authorization: Bearer $COMMUNE_API_KEY" \ -H "Commune-Version: 2026-08-26" \ -H "Idempotency-Key: site-page-list-2026-10-12" \ -H "Content-Type: application/json" \ -d '{ "kind": "newsletter", "path": "/", "status": "published", "meta": { "title": "The Example Letter", "description": "A weekly letter about how newsletters and communities fit together." }, "html": "<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <link rel=\"stylesheet\" href=\"/styles.css\">\n <script type=\"module\" src=\"/.commune/commune.js\"\n data-commune-key=\"cmn_pk_7Qa2Lk9xBv3mNd8sTr4wYc6zQh1jFp5K\"></script>\n</head>\n<body>\n <header><h1>The Example Letter</h1></header>\n <main>\n <commune-article-list></commune-article-list>\n <commune-subscribe label=\"Get it by email\"></commune-subscribe>\n </main>\n</body>\n</html>" }'Response 201{ "object": "site_page", "id": "2b7f9c10-4d5e-4a6b-8c7d-9e0f1a2b3c4d", "newsletter": { "object": "newsletter", "id": "7d3f1c02-58a1-4a4e-9a0b-2f6d1c9e4411" }, "kind": "newsletter", "path": "/", "status": "published", "blocked": false, "meta": { "title": "The Example Letter", "description": "A weekly letter about how newsletters and communities fit together." }, "byte_size": 727, "url": "https://blog.example.org", "preview_url": null, "published_at": "2026-10-12T09:00:00Z", "created_at": "2026-10-12T09:00:00Z", "updated_at": "2026-10-12T09:00:00Z", "html": "<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <link rel=\"stylesheet\" href=\"/styles.css\">\n <script type=\"module\" src=\"/.commune/commune.js\"\n data-commune-key=\"cmn_pk_7Qa2Lk9xBv3mNd8sTr4wYc6zQh1jFp5K\"></script>\n</head>\n<body>\n <header><h1>The Example Letter</h1></header>\n <main>\n <commune-article-list></commune-article-list>\n <commune-subscribe label=\"Get it by email\"></commune-subscribe>\n </main>\n</body>\n</html>" } - 3
Write one page that serves every article
kindisarticleandpathis a URL pattern, not an address. One document serves every issue you have published and every issue you have not written yet.A token is a whole path segment.
:articleis required;:year,:month,:dayand:idare the rest, and there is nothing else: a token outside that list answers422, and two patterns that could both serve one URL answer409. A fixed path always beats a pattern, so/aboutand/:articlecoexist and/aboutis the page.This one is written as a
draft, which has no public address at all. Not a 404 page, not a placeholder: nothing is served at its path. The signedpreview_urlin the response opens that one page of that one newsletter and expires after a week.cURLcurl -X POST "https://api.usecommune.com/newsletters/example-letter/website-pages" \ -H "Authorization: Bearer $COMMUNE_API_KEY" \ -H "Commune-Version: 2026-08-26" \ -H "Idempotency-Key: site-page-article-2026-10-12" \ -H "Content-Type: application/json" \ -d '{ "kind": "article", "path": "/posts/:article", "html": "<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <link rel=\"stylesheet\" href=\"/styles.css\">\n <script type=\"module\" src=\"/.commune/commune.js\"></script>\n</head>\n<body>\n <main>\n <commune-article></commune-article>\n </main>\n <footer><a href=\"/\">All issues</a></footer>\n</body>\n</html>" }'Response 201{ "object": "site_page", "id": "4d9b1e32-6f70-4c8d-8e9f-1a2b3c4d5e6f", "newsletter": { "object": "newsletter", "id": "7d3f1c02-58a1-4a4e-9a0b-2f6d1c9e4411" }, "kind": "article", "path": "/posts/:article", "status": "draft", "blocked": false, "meta": {}, "byte_size": 414, "url": null, "preview_url": "https://blog.example.org/?preview=eyJwIjoiNGQ5YjFlMzIifQ.Zm9vYmFy", "published_at": null, "created_at": "2026-10-12T09:02:00Z", "updated_at": "2026-10-12T09:02:00Z", "html": "<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <link rel=\"stylesheet\" href=\"/styles.css\">\n <script type=\"module\" src=\"/.commune/commune.js\"></script>\n</head>\n<body>\n <main>\n <commune-article></commune-article>\n </main>\n <footer><a href=\"/\">All issues</a></footer>\n</body>\n</html>" } - 4
Put it live
Live immediately: there is no deploy step and no build.
published_atis stamped the first time and never moved again, so unpublishing and publishing a page keeps the date it first went up, andstatusis what says whether it is live now.From here on, editing the page is the only publish it needs. A
PATCHto a published page is live on commit.cURLcurl -X POST "https://api.usecommune.com/website-pages/4d9b1e32-6f70-4c8d-8e9f-1a2b3c4d5e6f/publish" \ -H "Authorization: Bearer $COMMUNE_API_KEY" \ -H "Commune-Version: 2026-08-26" \ -H "Idempotency-Key: site-page-article-publish-2026-10-12"Response 200{ "object": "site_page", "id": "4d9b1e32-6f70-4c8d-8e9f-1a2b3c4d5e6f", "newsletter": { "object": "newsletter", "id": "7d3f1c02-58a1-4a4e-9a0b-2f6d1c9e4411" }, "kind": "article", "path": "/posts/:article", "status": "published", "blocked": false, "meta": {}, "byte_size": 414, "url": null, "preview_url": null, "published_at": "2026-10-12T09:05:00Z", "created_at": "2026-10-12T09:02:00Z", "updated_at": "2026-10-12T09:05:00Z", "html": "<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <link rel=\"stylesheet\" href=\"/styles.css\">\n <script type=\"module\" src=\"/.commune/commune.js\"></script>\n</head>\n<body>\n <main>\n <commune-article></commune-article>\n </main>\n <footer><a href=\"/\">All issues</a></footer>\n</body>\n</html>" } - 5
Mint the key your pages read data with
A publishable key,
cmn_pk_-prefixed because it is meant to be public: it ships inside HTML anybody can read, and the prefix is what makes that recognisable to you, to a reviewer and to a secret scanner.What makes it safe is what it can reach, which is this one newsletter's already-published articles, from this newsletter's own website origins, inside a rate limit. Never an email address, never a draft, never an article restricted to a tag, and never another newsletter.
allowed_originsare extra origins, for a development machine. Your website's own origins are always accepted, which is why leaving it empty means "the real website and nowhere else" rather than "nowhere".The secret is in this response and nowhere else
Commune keeps only a digest of it. No later call returns it, including a list, so a key you have lost is a key you rotate.
cURLcurl -X POST "https://api.usecommune.com/newsletters/example-letter/publishable-keys" \ -H "Authorization: Bearer $COMMUNE_API_KEY" \ -H "Commune-Version: 2026-08-26" \ -H "Idempotency-Key: publishable-key-2026-10-12" \ -H "Content-Type: application/json" \ -d '{ "name": "Website", "allowed_origins": [ "http://localhost:5173" ] }'Response 201{ "object": "publishable_key", "id": "7081e165-92a3-4f10-9123-4d5e6f708192", "newsletter": { "object": "newsletter", "id": "7d3f1c02-58a1-4a4e-9a0b-2f6d1c9e4411" }, "name": "Website", "key_prefix": "cmn_pk_7Qa2Lk9x", "secret": "cmn_pk_7Qa2Lk9xBv3mNd8sTr4wYc6zQh1jFp5K", "allowed_origins": [ "http://localhost:5173" ], "origins": [ "https://blog.example.org", "https://example-letter.communepages.com" ], "rate_limit_per_minute": 600, "monthly_request_quota": null, "created_at": "2026-10-12T09:07:00Z", "last_used_at": null, "revoked_at": null } - 6
Point your own hostname at it
You'll need
- A hostname already added to this newsletter and verified. Adding one and proving ownership are not operations in this API.
A hostname serves one of two Commune origins and
modesays which.appis the Commune newsletter page, which is where every hostname starts and where readers sign in, chat and highlight.siteis your own pages.One host can carry a session or run your JavaScript, never both. The origin is the only security boundary a browser has: code on an origin that holds a session can act as whoever is reading. That is why this is a mode rather than two features on one hostname, and why switching to
sitemakes Commune clear the cookies and storage it left there.Nothing is reprovisioned. The certificate, the ownership records and the target your DNS points at are untouched: what changes is which origin answers.
cURLcurl -X PATCH "https://api.usecommune.com/domains/8f1c3a52-7b90-4d1e-9f2a-3b4c5d6e7f80" \ -H "Authorization: Bearer $COMMUNE_API_KEY" \ -H "Commune-Version: 2026-08-26" \ -H "Idempotency-Key: domain-site-mode-2026-10-12" \ -H "Content-Type: application/json" \ -d '{ "mode": "site" }'Response 200{ "object": "domain", "id": "8f1c3a52-7b90-4d1e-9f2a-3b4c5d6e7f80", "newsletter": { "object": "newsletter", "id": "7d3f1c02-58a1-4a4e-9a0b-2f6d1c9e4411" }, "domain": "blog.example.org", "mode": "site", "verification_status": "active", "ssl_status": "active", "dns_target": "origin.communepages.com", "verification_records": [], "verification_error": null, "routing_ok": true, "routing_checked_at": "2026-10-12T09:10:00Z", "verified_at": "2026-09-30T18:04:00Z", "last_checked_at": "2026-10-12T09:10:00Z", "created_at": "2026-09-30T17:58:00Z" } - 7
Read the data yourself, if the elements are not what you want
commune.jsis headless. The elements above are a convenience; the data is the contract, and every page Commune serves already has it.commune.articles.list()answers from the page when the page can answer, which on your article list is the first call and every call that fits inside it, so the common case costs no request at all.Chat and highlights on a website are not in v1. They need a visitor token, which identifies the person rather than the site, and the publishable key reads published articles only.
commune.thread()andcommune.highlights()refuse and say so.The whole SDK you are likely to need<script type="module" src="/.commune/commune.js" data-commune-key="cmn_pk_7Qa2Lk9x..."></script> <script type="module"> // Already in the page. No request, no key needed. const { articles, total, from } = await commune.articles.list(); console.log(from, total); // "page" 42 // The article this page was served with. const article = await commune.article(); document.querySelector("#body").innerHTML = article.body_html; // Past what the page carries: one request, with the key. const older = await commune.articles.list({ limit: 20, offset: 20 }); // From a click, so the browser knows a person asked. document.querySelector("#join").addEventListener("click", async () => { const { outcome } = await commune.subscribe(); if (outcome === "subscribed") location.hash = "#thanks"; }); </script>
All together
The whole thing as one program: write both pages out of a local directory, publish them, mint the key. Run it from the repository that holds your site, and commit before you run it.
import { readFile } from "node:fs/promises";
import { randomUUID } from "node:crypto";
const API = "https://api.usecommune.com";
const NEWSLETTER = "example-letter";
async function call(method, path, body) {
const res = await fetch(API + path, {
method,
headers: {
Authorization: `Bearer ${process.env.COMMUNE_API_KEY}`,
"Commune-Version": process.env.COMMUNE_VERSION,
"Idempotency-Key": randomUUID(),
...(body ? { "Content-Type": "application/json" } : {}),
},
...(body ? { body: JSON.stringify(body) } : {}),
});
const payload = await res.json();
if (!res.ok) throw new Error(`${method} ${path}: ${payload.error?.message ?? res.status}`);
return payload;
}
// One file per page, so the repository is the source and Commune is the host.
const pages = [
{ kind: "newsletter", path: "/", file: "index.html" },
{ kind: "article", path: "/posts/:article", file: "article.html" },
{ kind: "page", path: "/about", file: "about.html" },
];
for (const page of pages) {
const html = await readFile(new URL(page.file, import.meta.url), "utf8");
const written = await call("POST", `/newsletters/${NEWSLETTER}/website-pages`, {
kind: page.kind,
path: page.path,
html,
});
// Written as a draft, so look at it before anybody else can.
console.log(page.path, "->", written.preview_url);
await call("POST", `/website-pages/${written.id}/publish`);
}
const key = await call("POST", `/newsletters/${NEWSLETTER}/publishable-keys`, {
name: "Website",
});
// The only response that carries it. Put it in your pages' script tag.
console.log("publishable key:", key.secret);Next use cases
See all- API
Build an app every creator can connect
Ship an integration other newsletters install in a few clicks.
Read the use case - AgentAPI
Win back readers before they leave
See who is going quiet while there is still time to reach them.
Read the use case - AgentAPI
Plan your next issue from what readers said
Turn comments, replies and highlights into your next topic.
Read the use case