[{"data":1,"prerenderedAt":74},["ShallowReactive",2],{"doc-blog":3},{"html":4,"headings":5},"\u003Cp>Write posts in the panel's editor and publish them on a hosted blog at\n\u003Ccode>\u002Fblog\u002F&lt;public key&gt;\u003C\u002Fcode>, inside your own site with one script tag, in an RSS feed,\nand in your subscribers' inboxes. Readers react to posts and comment on them.\nMuch of what Ghost does is here: standalone pages, featured and members-only\nposts, a designed front page with navigation and search, what search engines\nsee, a sitemap, publish-and-email, and an import from Ghost.\nPanel: \u003Cstrong>Project → Blog\u003C\u002Fstrong> (Posts, Pages, Comments, Design, Settings); the native\napp has the same screens.\u003C\u002Fp>\n\u003Cp>Everything here is in both editions. In HelpBites Cloud, emailing a post to\nsubscribers is a campaign, so it needs a plan with campaigns (Pro); the blog,\ncomments, reactions and RSS are on every plan.\u003C\u002Fp>\n\u003Ch2 id=\"setup\">Setup\u003Ca class=\"heading-anchor\" href=\"#setup\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Col>\n\u003Cli>\u003Cstrong>Project → Blog → Settings → Public blog\u003C\u002Fstrong> on. Until then nothing is public,\nnot even published posts.\u003C\u002Fli>\n\u003Cli>Optionally name it and describe it (the name shows at the top and in the\nfeed; it defaults to &quot;&lt;project&gt; blog&quot;).\u003C\u002Fli>\n\u003Cli>Pick what readers may do: \u003Cstrong>Reactions\u003C\u002Fstrong> and \u003Cstrong>Comments\u003C\u002Fstrong> (both on by default),\nand who is emailed about new comments.\u003C\u002Fli>\n\u003Cli>If your posts also live on your own site (rendered from the API), set\n\u003Cstrong>Post address on your site\u003C\u002Fstrong>, for example \u003Ccode>{siteUrl}\u002Fblog\u002F{slug}\u003C\u002Fcode>. The feed,\nthe newsletter and the hosted page's canonical link then point there.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Design\u003C\u002Fstrong>: the hosted blog's layout, typeface, colours, logo, cover,\nnavigation, social links and footer (below).\u003C\u002Fli>\n\u003Cli>Moving from Ghost? \u003Cstrong>… → Import from Ghost\u003C\u002Fstrong> (below).\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2 id=\"writing\">Writing\u003Ca class=\"heading-anchor\" href=\"#writing\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>The editor is Nuxt UI's (TipTap): headings, bold, italic, strikethrough, code,\nlists, quotes, code blocks, dividers, links and images. Type \u003Ccode>\u002F\u003C\u002Fcode> for blocks;\nselect text for the formatting bubble. Images are uploaded to the project's\nstorage (8 MB each, PNG, JPEG, GIF or WebP); they count against the workspace's\nstorage. See \u003Ca href=\"#smaller-images-and-modern-formats\">Smaller images\u003C\u002Fa> to send them\nsmaller.\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cstrong>Saved as Markdown.\u003C\u002Fstrong> That is what the API takes and returns as \u003Ccode>body\u003C\u002Fcode>. It is\nrendered once on save into \u003Ccode>bodyHtml\u003C\u002Fcode>, through an allowlist: no scripts,\nforms, inline styles or event handlers, and no frames except a YouTube or\nVimeo player (sandboxed; in the newsletter it becomes a link to the video). An\n\u003Ccode>h1\u003C\u002Fcode> in the body becomes an \u003Ccode>h2\u003C\u002Fcode> (the title is the page's \u003Ccode>h1\u003C\u002Fcode>).\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Or HTML.\u003C\u002Fstrong> A post imported from Ghost keeps its HTML (\u003Ccode>bodyFormat: &quot;html&quot;\u003C\u002Fcode>);\nthe editor edits it as HTML, and it goes through the same allowlist. The API\ntakes \u003Ccode>bodyFormat: &quot;html&quot;\u003C\u002Fcode> too.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Drafts save themselves\u003C\u002Fstrong> a moment after you stop typing. A \u003Cstrong>live or\nscheduled\u003C\u002Fstrong> post keeps your edits until you press \u003Cstrong>Update\u003C\u002Fstrong>, so half a\nsentence never goes public. ⌘S \u002F Ctrl+S saves either way.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Write with AI.\u003C\u002Fstrong> The \u003Cstrong>Write with AI\u003C\u002Fstrong> button (web and the native app) reads\nthe project's connected GitHub repos (read-only: README, manifests and file\nlist, three repos at most) and the post as it stands, and proposes Markdown:\n\u003Cstrong>write\u003C\u002Fstrong> a whole post from a one-line brief, \u003Cstrong>continue\u003C\u002Fstrong> it, \u003Cstrong>improve\u003C\u002Fstrong> its\ntext, or suggest a \u003Cstrong>title, excerpt, search description and tags\u003C\u002Fstrong>. Nothing is\nsaved until you apply the proposal to the editor, where it saves like any\nedit. In the web editor it is also a block: type \u003Ccode>\u002F\u003C\u002Fcode> and pick \u003Cstrong>Write with AI\u003C\u002Fstrong>\nto describe a passage for the cursor, or select text and press the sparkle in\nthe formatting bubble to rewrite just that (say how, or leave it empty to tidy it). It runs on the workspace's AI provider (or the server's gateway, in AI\ncredits), is told not to invent features the repos do not show, and is not\noffered for a post kept as HTML. See \u003Ca href=\"#write-with-ai-over-the-api\">over the API\u003C\u002Fa>.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Settings beside the editor\u003C\u002Fstrong>: status, publish date, address (\u003Ccode>slug\u003C\u002Fcode>),\n\u003Cstrong>Featured\u003C\u002Fstrong>, \u003Cstrong>Who can read it\u003C\u002Fstrong> (everyone, or members only), excerpt, cover\nimage and its alt text, up to 10 tags, author, whether the post takes\ncomments, and \u003Cstrong>Search and social\u003C\u002Fstrong>: a title and description for search\nengines (with a preview), the image link previews show, and a canonical\naddress for a post that first appeared elsewhere. Each falls back to the\npost's own when empty.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2 id=\"smaller-images-and-modern-formats\">Smaller images and modern formats\u003Ca class=\"heading-anchor\" href=\"#smaller-images-and-modern-formats\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>An upload can be re-encoded on the way in (\u003Cstrong>Image uploads\u003C\u002Fstrong> in the panel, next to\nthe cover and design pickers and in the editor; remembered in the browser, smaller\nby default). Over the API it is off unless asked, with \u003Ccode>POST \u002Fv1\u002Fadmin\u002Fblog\u002Fimages\u003C\u002Fcode>:\u003C\u002Fp>\n\u003Cdiv class=\"table-wrap\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Field (or query parameter)\u003C\u002Fth>\n\u003Cth>\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>\u003Ccode>compress=true\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>One file: WebP at quality 80, scaled down to 2560 px when larger. A GIF is kept as sent, and so is any file that would not come out smaller.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>formats=avif,webp\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>A set for a \u003Ccode>&lt;picture&gt;\u003C\u002Fcode>: the formats asked for that came out smaller than the fallback (AVIF, then WebP), and the \u003Cstrong>fallback\u003C\u002Fstrong> every browser, app and mail client reads: a JPEG for a JPEG, a PNG for anything else (so transparency survives). Implies \u003Ccode>compress\u003C\u002Fcode>.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Cp>The answer adds \u003Ccode>compressed\u003C\u002Fcode> and \u003Ccode>originalSize\u003C\u002Fcode>; with \u003Ccode>formats\u003C\u002Fcode>, \u003Ccode>url\u003C\u002Fcode> is the\nfallback, \u003Ccode>variants\u003C\u002Fcode> lists every stored file (most efficient first, the fallback\nlast) and \u003Ccode>picture\u003C\u002Fcode> is a ready-made \u003Ccode>&lt;picture&gt;\u003C\u002Fcode>. Every file counts against the\nstorage; the 8 MB limit is on what was sent.\u003C\u002Fp>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-bash\">\u003Ccode>\u003Cspan class=\"shj-syn-func\">curl\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -s\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -X\u003C\u002Fspan> POST https:\u002F\u002FYOUR-SUPPORT-HOST\u002Fv1\u002Fadmin\u002Fblog\u002Fimages \\\n \u003Cspan class=\"shj-syn-kwd\"> -H\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">\"Authorization: Bearer \u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">$HELPBITES_ADMIN_KEY\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\"\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -F\u003C\u002Fspan> \u003Cspan class=\"shj-syn-var\">project\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>PROJECT_ID\u003Cspan class=\"shj-syn-kwd\"> -F\u003C\u002Fspan> \u003Cspan class=\"shj-syn-var\">formats\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>avif,webp\u003Cspan class=\"shj-syn-kwd\"> -F\u003C\u002Fspan> \u003Cspan class=\"shj-syn-var\">file\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>@shot.png\n\u003Cspan class=\"shj-syn-cmnt\"># { \"url\": \"...\u002Fx.png\", \"variants\": [{ \"format\": \"avif\", \"url\": \"...\u002Fx.avif\", … }, { \"format\": \"webp\", … }, { \"format\": \"png\", … }],\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-cmnt\">#   \"picture\": \"&lt;picture&gt;&lt;source type=\\\"image\u002Favif\\\" srcset=\\\"...\u002Fx.avif\\\"&gt;&lt;source type=\\\"image\u002Fwebp\\\" …&gt;&lt;img src=\\\"...\u002Fx.png\\\" …&gt;&lt;\u002Fpicture&gt;\", … }\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Cp>A post's body allows \u003Ccode>&lt;picture&gt;\u003C\u002Fcode> and \u003Ccode>&lt;source&gt;\u003C\u002Fcode>, so paste \u003Ccode>picture\u003C\u002Fcode> into an HTML\npost (or into Markdown, which passes it through the same filter) and readers get\nthe smallest format their browser reads. The editor's own uploads always send one\nsmaller file. The newsletter keeps only the fallback \u003Ccode>&lt;img&gt;\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Ch2 id=\"your-own-data-on-a-post\">Your own data on a post\u003Ca class=\"heading-anchor\" href=\"#your-own-data-on-a-post\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>A post can carry a JSON object of your own, \u003Ccode>data\u003C\u002Fcode> (up to 200,000 characters as\nJSON), for a site that renders its posts itself from the API: steps, a gallery,\nsettings, anything the Markdown body can't say. It is saved with \u003Ccode>POST\u003C\u002Fcode> \u002F\n\u003Ccode>PATCH \u002Fv1\u002Fadmin\u002Fblog\u002Fposts\u003C\u002Fcode>, \u003Ccode>null\u003C\u002Fcode> clears it, and it comes back as \u003Ccode>data\u003C\u002Fcode> on\nthe post (admin) and on \u003Ccode>GET \u002Fv1\u002Fblog\u002F&lt;slug&gt;\u003C\u002Fcode> (public, with \u003Ccode>bodyHtml\u003C\u002Fcode>; not in\nthe list, and not on a post that is \u003Ccode>locked\u003C\u002Fcode>). The hosted blog, the embed, the\nfeed and the newsletter ignore it. Keep the body readable on its own: that is\nwhat they show.\u003C\u002Fp>\n\u003Ch2 id=\"publishing\">Publishing\u003Ca class=\"heading-anchor\" href=\"#publishing\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cdiv class=\"table-wrap\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>\u003C\u002Fth>\n\u003Cth>\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>Publish\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>Live now. The date is now unless you set one.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>Schedule\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>Publish with a later date: it goes live on its own at that moment (the hosted page, the API and the feed all wait for it).\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>Publish and email\u003C\u002Fstrong> \u002F \u003Cstrong>Schedule and email\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>The arrow beside Publish: publishes (or schedules) and opens the newsletter. A scheduled post's newsletter waits for it.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>Unpublish\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>Back to a draft (… menu). The date stays for when it comes back. A newsletter scheduled with it goes back to a draft too.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Cp>The \u003Ccode>slug\u003C\u002Fcode> comes from the title (&quot;Cómo empezar&quot; → \u003Ccode>como-empezar\u003C\u002Fcode>). A slug another\npost has gets \u003Ccode>-2\u003C\u002Fcode>, \u003Ccode>-3\u003C\u002Fcode>…; \u003Ccode>comments\u003C\u002Fcode>, \u003Ccode>rss\u003C\u002Fcode>, \u003Ccode>feed\u003C\u002Fcode>, \u003Ccode>tags\u003C\u002Fcode> and \u003Ccode>subscribe\u003C\u002Fcode> are\nreserved. Changing the slug of a live post breaks links to the old one.\u003C\u002Fp>\n\u003Cp>The \u003Cstrong>excerpt\u003C\u002Fstrong> is the lead-in on the list, in the feed and in link previews. Leave\nit empty and it is taken from the text (headings left out), and follows the text\nas you edit.\u003C\u002Fp>\n\u003Cp>A \u003Cstrong>featured\u003C\u002Fstrong> post is shown first: large above the rest on the magazine layout,\nmarked on the others; \u003Ccode>?featured=true\u003C\u002Fcode> on the API lists only those.\u003C\u002Fp>\n\u003Ch2 id=\"pages\">Pages\u003Ca class=\"heading-anchor\" href=\"#pages\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>\u003Cstrong>Pages\u003C\u002Fstrong> (the tab next to Posts) are for what stands on its own: About, Contact,\na reading list. They are written and published like posts and live at the same\naddresses (\u003Ccode>\u002Fblog\u002F&lt;pk&gt;\u002Fabout\u003C\u002Fcode>), but stay out of the list, the feed, the\nnewsletter and webhooks, and take no comments, reactions, tags or author. Link\nthem from the blog's navigation (\u003Cstrong>Design → Navigation\u003C\u002Fstrong>, \u003Ccode>\u002Fabout\u003C\u002Fcode>). The API\nlists them with \u003Ccode>?type=page\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Ch2 id=\"members-only-posts\">Members-only posts\u003Ca class=\"heading-anchor\" href=\"#members-only-posts\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>Set \u003Cstrong>Who can read it → Members only\u003C\u002Fstrong> and everyone sees the title, excerpt and\ncover, while the rest is for members: a user your app signed in (an identity\ntoken, as for comments) or a reader who confirmed their email with the 6-digit\ncode (needs \u003Cstrong>Allow email-only users\u003C\u002Fstrong>). On the hosted page the post shows its\nlead-in and a sign-in; once the reader is in, the rest appears in place. The API\nreturns such a post \u003Ccode>locked\u003C\u002Fcode>, without \u003Ccode>bodyHtml\u003C\u002Fcode>, until then; the feed carries\nonly its excerpt and a link; its comments are for members too. Search engines are\ntold it is behind a sign-in (\u003Ccode>isAccessibleForFree: false\u003C\u002Fcode>), so the gate is not\nread as cloaking.\u003C\u002Fp>\n\u003Cp>There are no paid tiers (yet): members-only means signed in, not paying.\u003C\u002Fp>\n\u003Ch2 id=\"readers-reactions-and-comments\">Readers: reactions and comments\u003Ca class=\"heading-anchor\" href=\"#readers-reactions-and-comments\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>Reading is open to everyone. Reacting and commenting need to know who the reader\nis, in one of two ways:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cstrong>Your user, signed in on your site.\u003C\u002Fstrong> Hand the embed the same identity token\nthe chat uses (\u003Ca href=\"\u002Fdocs\u002Fconnect\">CONNECTING.md\u003C\u002Fa>): your server signs it with the\nproject secret. Comments then carry your user's name and avatar. The \u003Ccode>role: &quot;admin&quot;\u003C\u002Fcode> claim marks your own staff, who also moderate.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Just an email address.\u003C\u002Fstrong> With \u003Cstrong>Project → Settings → Allow email-only users\u003C\u002Fstrong> on,\nreaders of the hosted blog (or of an embed without \u003Ccode>getToken\u003C\u002Fcode>) sign in with\ntheir address; after a few writes they confirm it with a 6-digit code.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>Reactions are ❤️ 👍 🎉 😄 🚀 👀, one of each per reader, toggled. Comments are\nMarkdown, up to 4,000 characters, rendered with a strict allowlist (no raw HTML,\nimages or headings, at most 6 links). Replies are one level deep: a reply to a\nreply joins the thread of the comment it answers.\u003C\u002Fp>\n\u003Cp>Rules:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>Five comments per ten minutes per reader; the same text twice in a row on one\npost is refused (409).\u003C\u002Fli>\n\u003Cli>The author edits and deletes their own comment. A deleted comment's words are\ngone; replies to it stay.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Moderation\u003C\u002Fstrong>: \u003Cstrong>Project → Blog → Comments\u003C\u002Fstrong> lists every comment, newest first.\nHide (only the team then sees it), show, delete, or reply: the team's comments\nare marked \u003Cstrong>Team\u003C\u002Fstrong>.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Close a thread\u003C\u002Fstrong> per post (Comments on this post), or turn comments off for\nthe whole blog. Old comments stay readable.\u003C\u002Fli>\n\u003Cli>A person erased with \u003Ccode>DELETE \u002Fv1\u002Fend-users\u002F{externalId}\u003C\u002Fcode> (or from their contact)\nloses their comments and reactions too.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2 id=\"newsletter\">Newsletter\u003Ca class=\"heading-anchor\" href=\"#newsletter\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>Any published post can go to your subscribers: \u003Cstrong>Send as newsletter\u003C\u002Fstrong> beside the\neditor (admins). It becomes a campaign (\u003Ca href=\"https:\u002F\u002Fgithub.com\u002Felrumo\u002Fhelp-bites-oss\u002Fblob\u002Fmain\u002Fdocs\u002FMARKETING.md\" target=\"_blank\" rel=\"noopener\">MARKETING.md\u003C\u002Fa>), so it has\neverything campaigns have: one message per subscribed contact with an email,\ntopics, segments, test sends, pause and cancel, the delivery log and its numbers,\nunder \u003Cstrong>Email → Campaigns\u003C\u002Fstrong>.\u003C\u002Fp>\n\u003Cul>\n\u003Cli>The email is built from the post when sending starts: your logo, the title,\ndate and reading time, the cover, the body (styled for mail clients), a\n\u003Cstrong>Read it on the blog\u003C\u002Fstrong> button and the footer with the unsubscribe link, the\npreferences link and your postal address.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Postal address required\u003C\u002Fstrong> (Email → Settings), as for every campaign.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Audience\u003C\u002Fstrong>: subscribed contacts with an email. \u003Cstrong>Topic\u003C\u002Fstrong> (default: the blog's\n\u003Cstrong>Default topic\u003C\u002Fstrong>) leaves out whoever opted out of it; a \u003Cstrong>segment\u003C\u002Fstrong> narrows it.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Send now\u003C\u002Fstrong>, \u003Cstrong>Schedule\u003C\u002Fstrong>, or \u003Cstrong>Save as a draft\u003C\u002Fstrong> to send a test from the\ncampaign first. For a scheduled post, \u003Cstrong>Send when published\u003C\u002Fstrong> schedules the\nemail for the moment the post goes live, and a later time is fine, an earlier\none is not; reschedule the post later and the email follows.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>A post goes out once.\u003C\u002Fstrong> A draft campaign is reused if you send again; only a\ncanceled one can be replaced. Editing the post afterwards changes the blog, not\nthe email that went.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>Readers subscribe through the sign-up form under each hosted post (\u003Cstrong>Sign-up\nform under posts\u003C\u002Fstrong> in Settings), any other sign-up form of yours\n(\u003Ca href=\"https:\u002F\u002Fgithub.com\u002Felrumo\u002Fhelp-bites-oss\u002Fblob\u002Fmain\u002Fdocs\u002FMARKETING.md\" target=\"_blank\" rel=\"noopener\">MARKETING.md\u003C\u002Fa>), or your own app.\u003C\u002Fp>\n\u003Ch2 id=\"rss\">RSS\u003Ca class=\"heading-anchor\" href=\"#rss\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>\u003Ccode>GET \u002Fv1\u002Fprojects\u002F&lt;public key&gt;\u002Fblog.rss\u003C\u002Fcode>: the latest 30 live posts, newest first,\nas RSS 2.0 with their full HTML (\u003Ccode>content:encoded\u003C\u002Fcode>), cover, author and tags. A\nmembers-only post carries its excerpt and a link instead of its body; pages are\nleft out. The hosted pages link to it for feed readers.\u003C\u002Fp>\n\u003Ch2 id=\"the-hosted-blog\">The hosted blog\u003Ca class=\"heading-anchor\" href=\"#the-hosted-blog\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>\u003Ccode>\u002Fblog\u002F&lt;public key&gt;\u003C\u002Fcode> is server-rendered HTML, readable without JavaScript and\nmeant to be indexed. \u003Cstrong>Project → Blog → Design\u003C\u002Fstrong> sets how it looks:\u003C\u002Fp>\n\u003Cdiv class=\"table-wrap\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>\u003C\u002Fth>\n\u003Cth>\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>Layout\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>List\u003C\u002Fstrong> (one column), \u003Cstrong>Grid\u003C\u002Fstrong> (cards), or \u003Cstrong>Magazine\u003C\u002Fstrong> (the newest featured post, or the newest post, large above a grid).\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>Typeface\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>Sans serif or serif, for the whole blog.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>Colour scheme\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>Follow the reader's system, or always light or dark.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>Accent colour\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>Links, buttons and tags; the project's accent unless you pick one.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>Logo\u003C\u002Fstrong> and \u003Cstrong>cover image\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>Beside the name, and a wide image across the top of the front page.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>Navigation\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>Up to 8 links in the header: \u003Ccode>\u002F\u003C\u002Fcode> (the blog), \u003Ccode>\u002F&lt;page slug&gt;\u003C\u002Fcode> (a page), or any address.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>Social links\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>X, Bluesky, Mastodon (with \u003Ccode>rel=&quot;me&quot;\u003C\u002Fcode>, so Mastodon verifies it), GitHub, LinkedIn, Instagram, YouTube, a website.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>Footer\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>A line at the bottom of every page.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Cp>Beyond the look, every page has a search box (\u003Ccode>?q=\u003C\u002Fcode> finds words in titles,\nexcerpts and tags; result pages are \u003Ccode>noindex\u003C\u002Fcode>), tag pages (\u003Ccode>?tag=\u003C\u002Fcode>), and paging.\nA post has its date, author and reading time, its tags, \u003Cstrong>Newer\u003C\u002Fstrong> and \u003Cstrong>Older\u003C\u002Fstrong>\nlinks, and up to three \u003Cstrong>Keep reading\u003C\u002Fstrong> posts that share a tag. For search\nengines and link previews: the title, description, image and canonical address\nfrom \u003Cstrong>Search and social\u003C\u002Fstrong>, Open Graph and Twitter cards, and structured data\n(\u003Ccode>Blog\u003C\u002Fcode>, \u003Ccode>BlogPosting\u003C\u002Fcode>, \u003Ccode>WebPage\u003C\u002Fcode>). The sitemap is\n\u003Ccode>\u002Fblog\u002F&lt;public key&gt;\u002Fsitemap.xml\u003C\u002Fcode> (shown in Settings), listing the front page and\nevery live post and page whose canonical copy is the hosted one: hand it to\nGoogle Search Console.\u003C\u002Fp>\n\u003Cp>The design is the hosted blog's. The embed takes your page's look instead, and\nthe layout (\u003Ccode>layout\u003C\u002Fcode> below).\u003C\u002Fp>\n\u003Ch2 id=\"put-it-on-your-site\">Put it on your site\u003Ca class=\"heading-anchor\" href=\"#put-it-on-your-site\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>One script tag draws the blog inside your page, no iframe, so it takes your\npage's width and fonts. The list opens each post in place, with its reactions,\ncomments and the sign-up form; the open post is kept in the URL (\u003Ccode>?post=slug\u003C\u002Fcode>),\nso links and the Back button work.\u003C\u002Fp>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-html\">\u003Ccode>\u003Cspan class=\"shj-syn-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">div\u003C\u002Fspan> \u003Cspan class=\"shj-syn-class\">id\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\"blog\"\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&gt;\u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&lt;\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">div\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&gt;\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">script\u003C\u002Fspan> \u003Cspan class=\"shj-syn-class\">src\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\"https:\u002F\u002FYOUR-SUPPORT-HOST\u002Fblog.js\"\u003C\u002Fspan> \u003Cspan class=\"shj-syn-class\">data-project\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\"pk_…\"\u003C\u002Fspan> \u003Cspan class=\"shj-syn-class\">data-target\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\"#blog\"\u003C\u002Fspan> \u003Cspan class=\"shj-syn-class\">defer\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&gt;\u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&lt;\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">script\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&gt;\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Cp>To let your signed-in users react and comment:\u003C\u002Fp>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-html\">\u003Ccode>\u003Cspan class=\"shj-syn-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">script\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&gt;\u003C\u002Fspan>\n  window\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-syn-class\">HelpBitesBlog\u003C\u002Fspan> \u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan> window\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-syn-class\">HelpBitesBlog\u003C\u002Fspan> \u003Cspan class=\"shj-syn-oper\">||\u003C\u002Fspan> []\n  \u003Cspan class=\"shj-syn-class\">HelpBitesBlog\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-syn-func\">push\u003C\u002Fspan>([\u003Cspan class=\"shj-syn-str\">'init'\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">,\u003C\u002Fspan> {\n    getToken\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> () \u003Cspan class=\"shj-syn-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-syn-func\">fetch\u003C\u002Fspan>(\u003Cspan class=\"shj-syn-str\">'\u002Fapi\u002Fsupport\u002Ftoken'\u003C\u002Fspan>)\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-syn-func\">then\u003C\u002Fspan>(r \u003Cspan class=\"shj-syn-kwd\">=&gt;\u003C\u002Fspan> r\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>ok \u003Cspan class=\"shj-syn-oper\">?\u003C\u002Fspan> r\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-syn-func\">json\u003C\u002Fspan>() \u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-syn-num\">null\u003C\u002Fspan>)\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-syn-func\">then\u003C\u002Fspan>(r \u003Cspan class=\"shj-syn-kwd\">=&gt;\u003C\u002Fspan> r \u003Cspan class=\"shj-syn-oper\">&#38;&#38;\u003C\u002Fspan> r\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>token)\u003Cspan class=\"shj-syn-oper\">,\u003C\u002Fspan>\n    onSignIn\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> () \u003Cspan class=\"shj-syn-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-syn-func\">openYourLoginModal\u003C\u002Fspan>()   \u003Cspan class=\"shj-syn-cmnt\">\u002F\u002F a signed-out reader tried to write\n\u003C\u002Fspan>  }])\n  \u003Cspan class=\"shj-syn-cmnt\">\u002F\u002F After your user signs in or out:\n\u003C\u002Fspan>  \u003Cspan class=\"shj-syn-class\">HelpBitesBlog\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-syn-func\">push\u003C\u002Fspan>([\u003Cspan class=\"shj-syn-str\">'refresh'\u003C\u002Fspan>])\n\u003Cspan class=\"shj-syn-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&lt;\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">script\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&gt;\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Cp>Already rendering posts yourself (from the API, for SEO)? Add only what goes\nunder one:\u003C\u002Fp>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-html\">\u003Ccode>\u003Cspan class=\"shj-syn-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">div\u003C\u002Fspan> \u003Cspan class=\"shj-syn-class\">id\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\"extras\"\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&gt;\u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&lt;\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">div\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&gt;\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">script\u003C\u002Fspan> \u003Cspan class=\"shj-syn-class\">src\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\"https:\u002F\u002FYOUR-SUPPORT-HOST\u002Fblog.js\"\u003C\u002Fspan> \u003Cspan class=\"shj-syn-class\">data-project\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\"pk_…\"\u003C\u002Fspan> \u003Cspan class=\"shj-syn-class\">data-view\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\"extras\"\u003C\u002Fspan> \u003Cspan class=\"shj-syn-class\">data-post\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\"my-post-slug\"\u003C\u002Fspan> \u003Cspan class=\"shj-syn-class\">data-target\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\"#extras\"\u003C\u002Fspan> \u003Cspan class=\"shj-syn-class\">defer\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&gt;\u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&lt;\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">script\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&gt;\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Ch3 id=\"options\">Options\u003Ca class=\"heading-anchor\" href=\"#options\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh3>\n\u003Cp>As \u003Ccode>data-*\u003C\u002Fcode> attributes or in \u003Ccode>init\u003C\u002Fcode>:\u003C\u002Fp>\n\u003Cdiv class=\"table-wrap\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Option\u003C\u002Fth>\n\u003Cth>\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>\u003Ccode>project\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>The public key. Required.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>target\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>A selector; without it the blog is drawn where the script tag is.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>view\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>blog\u003C\u002Fcode> (the list; the default), \u003Ccode>post\u003C\u002Fcode> (one post with everything) or \u003Ccode>extras\u003C\u002Fcode> (reactions, comments and sign-up only).\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>post\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>The slug, for \u003Ccode>post\u003C\u002Fcode> and \u003Ccode>extras\u003C\u002Fcode>.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>tag\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Only posts with this tag.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>q\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>A search, as if typed in the search box.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>layout\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>list\u003C\u002Fcode>, \u003Ccode>grid\u003C\u002Fcode> or \u003Ccode>magazine\u003C\u002Fcode> (the first post across the top); defaults to the hosted blog's.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>body\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>With \u003Ccode>view=&quot;extras&quot;\u003C\u002Fcode> under your own copy of a members-only post: a selector the post's HTML goes into once the reader may read it.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>limit\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Posts per page (10).\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>theme\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>system\u003C\u002Fcode>, \u003Ccode>light\u003C\u002Fcode> or \u003Ccode>dark\u003C\u002Fcode>; defaults to a \u003Ccode>dark\u003C\u002Fcode>\u002F\u003Ccode>light\u003C\u002Fcode> class on your \u003Ccode>&lt;html&gt;\u003C\u002Fcode>.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>accent\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>A colour; defaults to the project's.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>radius\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Corner radius in px (14).\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>font\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>inherit\u003C\u002Fcode> (your page's; the default), \u003Ccode>system\u003C\u002Fcode>, \u003Ccode>serif\u003C\u002Fcode> or \u003Ccode>mono\u003C\u002Fcode>.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>show\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Which parts: \u003Ccode>cover,excerpt,tags,author,date,badges,search,reactions,comments,subscribe\u003C\u002Fcode>.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>deep-link-param\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>The URL parameter an open post is kept in (\u003Ccode>post\u003C\u002Fcode>).\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>host\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Where HelpBites is, when you serve \u003Ccode>blog.js\u003C\u002Fcode> from your own site (it is MIT).\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>labels\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>init\u003C\u002Fcode> only: any of the words, e.g. \u003Ccode>{ back: 'Back to the blog', post: 'Send' }\u003C\u002Fcode>.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Cp>Calls: \u003Ccode>HelpBitesBlog.push(['open', 'slug'])\u003C\u002Fcode>, \u003Ccode>(['back'])\u003C\u002Fcode>, \u003Ccode>(['refresh'])\u003C\u002Fcode>.\nEvery reader action is a \u003Ccode>helpbites:blog\u003C\u002Fcode> event on \u003Ccode>window\u003C\u002Fcode>, with\n\u003Ccode>detail: { type: 'open' | 'react' | 'comment' | 'subscribe', slug }\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Ch3 id=\"restyle-it\">Restyle it\u003Ca class=\"heading-anchor\" href=\"#restyle-it\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh3>\n\u003Cp>It is drawn in a shadow root, so your CSS does not leak in by accident. Change it\non purpose with variables on \u003Ccode>.help-bites-blog\u003C\u002Fcode>:\u003C\u002Fp>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-css\">\u003Ccode>\u003Cspan class=\"shj-syn-var\">.help-bites-blog\u003C\u002Fspan> {\n  --\u003Cspan class=\"shj-syn-var\">hb-blog-accent\u003C\u002Fspan>: \u003Cspan class=\"shj-syn-num\">#0f766e\u003C\u002Fspan>;\n  --\u003Cspan class=\"shj-syn-var\">hb-blog-on-accent\u003C\u002Fspan>: \u003Cspan class=\"shj-syn-num\">#fff\u003C\u002Fspan>;\n  --\u003Cspan class=\"shj-syn-var\">hb-blog-bg\u003C\u002Fspan>: \u003Cspan class=\"shj-syn-num\">transparent\u003C\u002Fspan>;\n  --\u003Cspan class=\"shj-syn-var\">hb-blog-card\u003C\u002Fspan>: \u003Cspan class=\"shj-syn-num\">#fff\u003C\u002Fspan>;\n  --\u003Cspan class=\"shj-syn-var\">hb-blog-text\u003C\u002Fspan>: \u003Cspan class=\"shj-syn-num\">#111\u003C\u002Fspan>;\n  --\u003Cspan class=\"shj-syn-var\">hb-blog-muted\u003C\u002Fspan>: \u003Cspan class=\"shj-syn-num\">#666\u003C\u002Fspan>;\n  --\u003Cspan class=\"shj-syn-var\">hb-blog-line\u003C\u002Fspan>: \u003Cspan class=\"shj-syn-num\">#e5e5e5\u003C\u002Fspan>;\n  --\u003Cspan class=\"shj-syn-var\">hb-blog-radius\u003C\u002Fspan>: \u003Cspan class=\"shj-syn-num\">8\u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">px\u003C\u002Fspan>\u003Cspan class=\"shj-syn-num\">\u003C\u002Fspan>;\n  --\u003Cspan class=\"shj-syn-var\">hb-blog-font\u003C\u002Fspan>: \u003Cspan class=\"shj-syn-num\">inherit\u003C\u002Fspan>;\n  --\u003Cspan class=\"shj-syn-var\">hb-blog-width\u003C\u002Fspan>: \u003Cspan class=\"shj-syn-num\">720\u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">px\u003C\u002Fspan>\u003Cspan class=\"shj-syn-num\">\u003C\u002Fspan>;\n}\n\u003Cspan class=\"shj-syn-var\">.help-bites-blog\u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">::part\u003C\u002Fspan>(\u003Cspan class=\"shj-syn-kwd\">card\u003C\u002Fspan>) { \u003Cspan class=\"shj-syn-var\">box-shadow\u003C\u002Fspan>: \u003Cspan class=\"shj-syn-num\">0\u003C\u002Fspan> \u003Cspan class=\"shj-syn-num\">1\u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">px\u003C\u002Fspan>\u003Cspan class=\"shj-syn-num\">\u003C\u002Fspan> \u003Cspan class=\"shj-syn-num\">2\u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">px\u003C\u002Fspan>\u003Cspan class=\"shj-syn-num\">\u003C\u002Fspan> \u003Cspan class=\"shj-syn-func\">rgb\u003C\u002Fspan>(\u003Cspan class=\"shj-syn-num\">0\u003C\u002Fspan> \u003Cspan class=\"shj-syn-num\">0\u003C\u002Fspan> \u003Cspan class=\"shj-syn-num\">0\u003C\u002Fspan> \u002F \u003Cspan class=\"shj-syn-num\">0.06\u003C\u002Fspan>); }\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Cp>Parts: \u003Ccode>header\u003C\u002Fcode>, \u003Ccode>search\u003C\u002Fcode>, \u003Ccode>list\u003C\u002Fcode>, \u003Ccode>card\u003C\u002Fcode>, \u003Ccode>cover\u003C\u002Fcode>, \u003Ccode>title\u003C\u002Fcode>, \u003Ccode>meta\u003C\u002Fcode>, \u003Ccode>excerpt\u003C\u002Fcode>,\n\u003Ccode>tag\u003C\u002Fcode>, \u003Ccode>badge\u003C\u002Fcode>, \u003Ccode>post\u003C\u002Fcode>, \u003Ccode>body\u003C\u002Fcode>, \u003Ccode>gate\u003C\u002Fcode>, \u003Ccode>reactions\u003C\u002Fcode>, \u003Ccode>reaction\u003C\u002Fcode>, \u003Ccode>comments\u003C\u002Fcode>,\n\u003Ccode>comment\u003C\u002Fcode>, \u003Ccode>composer\u003C\u002Fcode>, \u003Ccode>button\u003C\u002Fcode>, \u003Ccode>subscribe\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Cp>A members-only post shows its excerpt and a \u003Cstrong>This post is for members\u003C\u002Fstrong> box\n(the \u003Ccode>gate\u003C\u002Fcode> part) with the sign-in: your \u003Ccode>onSignIn\u003C\u002Fcode>, or the email form with its\ncode. With \u003Ccode>getToken\u003C\u002Fcode> handing over a signed-in user, it simply opens.\u003C\u002Fp>\n\u003Ch2 id=\"in-a-swift-app\">In a Swift app\u003Ca class=\"heading-anchor\" href=\"#in-a-swift-app\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>A list of posts that opens each one, with its reactions, read with plain\n\u003Ccode>URLSession\u003C\u002Fcode> against the public API. Pass \u003Ccode>token\u003C\u002Fcode> (your user's identity token,\nfrom your server) and readers can react; leave it out and the view is read-only.\u003C\u002Fp>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-plain\">\u003Ccode>\u002F\u002F BlogView.swift. MIT. A HelpBites blog: posts, one post, its reactions.\nimport SwiftUI\n\nstruct BlogPost: Decodable, Identifiable, Hashable {\n  struct Reaction: Decodable, Hashable { let key: String; let count: Int; let reacted: Bool? }\n  let id, slug, title, excerpt, publishedAt, authorName: String\n  let coverImageUrl: String\n  let readingMinutes: Int\n  var reactions: [Reaction]\n  var bodyHtml: String?\n}\n\nstruct BlogView: View {\n  let host: URL\n  let projectKey: String\n  var token: String? = nil\n  var accent: Color = .accentColor\n  var cornerRadius: CGFloat = 12\n  @State private var posts: [BlogPost] = []\n\n  var body: some View {\n    List(posts) { post in\n      NavigationLink(value: post) {\n        VStack(alignment: .leading, spacing: 6) {\n          if let url = URL(string: post.coverImageUrl), !post.coverImageUrl.isEmpty {\n            AsyncImage(url: url) { $0.resizable().scaledToFill() } placeholder: { Color.secondary.opacity(0.1) }\n              .frame(height: 140).clipShape(.rect(cornerRadius: cornerRadius))\n          }\n          Text(post.title).font(.headline)\n          Text(post.excerpt).font(.subheadline).foregroundStyle(.secondary).lineLimit(3)\n        }\n      }\n    }\n    .navigationTitle(\u003Cspan class=\"shj-syn-str\">\"Blog\"\u003C\u002Fspan>)\n    .navigationDestination(for: BlogPost.self) { BlogPostView(api: api, slug: $0.slug, accent: accent) }\n    .task {\n      struct Page: Decodable { let posts: [BlogPost] }\n      if let page: Page = try? await api.get(\u003Cspan class=\"shj-syn-str\">\"blog\"\u003C\u002Fspan>) { posts = page.posts }\n    }\n  }\n\n  private var api: BlogAPI { BlogAPI(host: host, projectKey: projectKey, token: token) }\n}\n\nstruct BlogPostView: View {\n  let api: BlogAPI\n  let slug: String\n  let accent: Color\n  @State private var post: BlogPost?\n  @State private var text = AttributedString()\n  private let emoji = [\u003Cspan class=\"shj-syn-str\">\"heart\"\u003C\u002Fspan>: \u003Cspan class=\"shj-syn-str\">\"❤️\"\u003C\u002Fspan>, \u003Cspan class=\"shj-syn-str\">\"+1\"\u003C\u002Fspan>: \u003Cspan class=\"shj-syn-str\">\"👍\"\u003C\u002Fspan>, \u003Cspan class=\"shj-syn-str\">\"hooray\"\u003C\u002Fspan>: \u003Cspan class=\"shj-syn-str\">\"🎉\"\u003C\u002Fspan>, \u003Cspan class=\"shj-syn-str\">\"laugh\"\u003C\u002Fspan>: \u003Cspan class=\"shj-syn-str\">\"😄\"\u003C\u002Fspan>, \u003Cspan class=\"shj-syn-str\">\"rocket\"\u003C\u002Fspan>: \u003Cspan class=\"shj-syn-str\">\"🚀\"\u003C\u002Fspan>, \u003Cspan class=\"shj-syn-str\">\"eyes\"\u003C\u002Fspan>: \u003Cspan class=\"shj-syn-str\">\"👀\"\u003C\u002Fspan>]\n\n  var body: some View {\n    ScrollView {\n      if let post {\n        VStack(alignment: .leading, spacing: 12) {\n          Text(post.title).font(.largeTitle.bold())\n          Text(\u003Cspan class=\"shj-syn-str\">\"\\(post.authorName) · \\(post.readingMinutes) min read\"\u003C\u002Fspan>).font(.subheadline).foregroundStyle(.secondary)\n          Text(text)\n          HStack {\n            ForEach([\u003Cspan class=\"shj-syn-str\">\"heart\"\u003C\u002Fspan>, \u003Cspan class=\"shj-syn-str\">\"+1\"\u003C\u002Fspan>, \u003Cspan class=\"shj-syn-str\">\"hooray\"\u003C\u002Fspan>, \u003Cspan class=\"shj-syn-str\">\"laugh\"\u003C\u002Fspan>, \u003Cspan class=\"shj-syn-str\">\"rocket\"\u003C\u002Fspan>, \u003Cspan class=\"shj-syn-str\">\"eyes\"\u003C\u002Fspan>], id: \\.self) { key in\n              let reaction = post.reactions.first { $0.key == key }\n              Button { Task { await react(key) } } label: {\n                Text(\u003Cspan class=\"shj-syn-str\">\"\\(emoji[key] ?? \"\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\")\\(reaction.map { \"\u003C\u002Fspan> \\($0.count)\u003Cspan class=\"shj-syn-str\">\" } ?? \"\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\")\"\u003C\u002Fspan>)\n              }\n              .buttonStyle(.bordered).tint(reaction?.reacted == true ? accent : .secondary)\n              .disabled(api.token == nil)\n            }\n          }\n        }\n        .padding()\n      }\n    }\n    .task {\n      struct One: Decodable { let post: BlogPost }\n      guard let one: One = try? await api.get(\u003Cspan class=\"shj-syn-str\">\"blog\u002F\\(slug)\"\u003C\u002Fspan>) else { return }\n      post = one.post\n      \u002F\u002F The body is sanitised HTML; this keeps its headings, lists and links.\n      if let data = (one.post.bodyHtml ?? \u003Cspan class=\"shj-syn-str\">\"\"\u003C\u002Fspan>).data(using: .utf8),\n         let html = try? NSAttributedString(data: data, options: [.documentType: NSAttributedString.DocumentType.html, .characterEncoding: String.Encoding.utf8.rawValue], documentAttributes: nil),\n         let rendered = try? AttributedString(html, including: \\.uiKit) {\n        text = rendered\n      }\n    }\n  }\n\n  private func react(_ key: String) async {\n    struct Reactions: Decodable { let reactions: [BlogPost.Reaction] }\n    if let answer: Reactions = try? await api.post(\u003Cspan class=\"shj-syn-str\">\"blog\u002F\\(slug)\u002Freactions\"\u003C\u002Fspan>, body: [\u003Cspan class=\"shj-syn-str\">\"reaction\"\u003C\u002Fspan>: key]) { post?.reactions = answer.reactions }\n  }\n}\n\nstruct BlogAPI {\n  let host: URL\n  let projectKey: String\n  let token: String?\n\n  func get&lt;T: Decodable&gt;(_ path: String) async throws -&gt; T {\n    try await send(URLRequest(url: url(path)))\n  }\n\n  func post&lt;T: Decodable&gt;(_ path: String, body: [String: String]) async throws -&gt; T {\n    var request = URLRequest(url: url(path))\n    request.httpMethod = \u003Cspan class=\"shj-syn-str\">\"POST\"\u003C\u002Fspan>\n    request.setValue(\u003Cspan class=\"shj-syn-str\">\"application\u002Fjson\"\u003C\u002Fspan>, forHTTPHeaderField: \u003Cspan class=\"shj-syn-str\">\"Content-Type\"\u003C\u002Fspan>)\n    request.httpBody = try JSONEncoder().encode(body)\n    return try await send(request)\n  }\n\n  private func url(_ path: String) -&gt; URL {\n    \u002F\u002F Signed in, the token names the project; signed out, the public key does.\n    var components = URLComponents(url: host.appending(path: \u003Cspan class=\"shj-syn-str\">\"v1\u002F\\(path)\"\u003C\u002Fspan>), resolvingAgainstBaseURL: false)!\n    if token == nil { components.queryItems = [URLQueryItem(name: \u003Cspan class=\"shj-syn-str\">\"pk\"\u003C\u002Fspan>, value: projectKey)] }\n    return components.url!\n  }\n\n  private func send&lt;T: Decodable&gt;(_ request: URLRequest) async throws -&gt; T {\n    var request = request\n    if let token { request.setValue(\u003Cspan class=\"shj-syn-str\">\"Bearer \\(token)\"\u003C\u002Fspan>, forHTTPHeaderField: \u003Cspan class=\"shj-syn-str\">\"Authorization\"\u003C\u002Fspan>) }\n    let (data, _) = try await URLSession.shared.data(for: request)\n    return try JSONDecoder().decode(T.self, from: data)\n  }\n}\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Cp>Use it inside a \u003Ccode>NavigationStack\u003C\u002Fcode>: \u003Ccode>BlogView(host: URL(string: &quot;https:\u002F\u002FYOUR-SUPPORT-HOST&quot;)!, projectKey: &quot;pk_…&quot;)\u003C\u002Fcode>.\n\u003Ccode>accent\u003C\u002Fcode> and \u003Ccode>cornerRadius\u003C\u002Fcode> set the colours of the reactions and the shape of the\ncovers; fonts come from your app's environment. The HTML conversion uses UIKit;\non macOS use \u003Ccode>\\.appKit\u003C\u002Fcode> instead of \u003Ccode>\\.uiKit\u003C\u002Fcode>. It is your file, so change anything:\ncomments are \u003Ccode>GET\u003C\u002Fcode>\u002F\u003Ccode>POST v1\u002Fblog\u002F&lt;slug&gt;\u002Fcomments\u003C\u002Fcode>, below.\u003C\u002Fp>\n\u003Ch2 id=\"your-own-ui\">Your own UI\u003Ca class=\"heading-anchor\" href=\"#your-own-ui\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>Everything the hosted page and the embed draw is public JSON, open to any origin,\nfor a blog you build yourself (a Nuxt or Next site rendering posts for SEO, an app\nscreen):\u003C\u002Fp>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-bash\">\u003Ccode>\u003Cspan class=\"shj-syn-cmnt\"># The live posts, newest first (no bodies), and the blog's name, layout and options\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-func\">curl\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -s\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">\"https:\u002F\u002FYOUR-SUPPORT-HOST\u002Fv1\u002Fblog?pk=PROJECT_PUBLIC_KEY&#38;limit=20&#38;tag=engineering\"\u003C\u002Fspan>\n\n\u003Cspan class=\"shj-syn-cmnt\"># A search; the featured posts; the pages\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-func\">curl\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -s\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">\"https:\u002F\u002FYOUR-SUPPORT-HOST\u002Fv1\u002Fblog?pk=PROJECT_PUBLIC_KEY&#38;q=offline\"\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-func\">curl\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -s\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">\"https:\u002F\u002FYOUR-SUPPORT-HOST\u002Fv1\u002Fblog?pk=PROJECT_PUBLIC_KEY&#38;featured=true&#38;limit=1\"\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-func\">curl\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -s\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">\"https:\u002F\u002FYOUR-SUPPORT-HOST\u002Fv1\u002Fblog?pk=PROJECT_PUBLIC_KEY&#38;type=page\"\u003C\u002Fspan>\n\n\u003Cspan class=\"shj-syn-cmnt\"># One post, with its sanitised HTML: safe to put straight into your page\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-func\">curl\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -s\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">\"https:\u002F\u002FYOUR-SUPPORT-HOST\u002Fv1\u002Fblog\u002Fmy-post-slug?pk=PROJECT_PUBLIC_KEY\"\u003C\u002Fspan>\n\n\u003Cspan class=\"shj-syn-cmnt\"># Its comments, oldest first (replies have parentId)\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-func\">curl\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -s\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">\"https:\u002F\u002FYOUR-SUPPORT-HOST\u002Fv1\u002Fblog\u002Fmy-post-slug\u002Fcomments?pk=PROJECT_PUBLIC_KEY\"\u003C\u002Fspan>\n\n\u003Cspan class=\"shj-syn-cmnt\"># As a signed-in reader (an identity token): react, comment, reply\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-func\">curl\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -s\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -X\u003C\u002Fspan> POST https:\u002F\u002FYOUR-SUPPORT-HOST\u002Fv1\u002Fblog\u002Fmy-post-slug\u002Freactions \\\n \u003Cspan class=\"shj-syn-kwd\"> -H\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">\"Authorization: Bearer \u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">$IDENTITY_TOKEN\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\"\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -H\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">'Content-Type: application\u002Fjson'\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -d\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">'{\"reaction\":\"heart\"}'\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-func\">curl\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -s\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -X\u003C\u002Fspan> POST https:\u002F\u002FYOUR-SUPPORT-HOST\u002Fv1\u002Fblog\u002Fmy-post-slug\u002Fcomments \\\n \u003Cspan class=\"shj-syn-kwd\"> -H\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">\"Authorization: Bearer \u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">$IDENTITY_TOKEN\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\"\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -H\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">'Content-Type: application\u002Fjson'\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -d\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">'{\"body\":\"Nice post!\"}'\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Cp>A reader's request with a token is never cached; an anonymous one may be cached\nfor a minute. 404 means the project's blog is off, or the post is a draft or\nscheduled. Each post carries \u003Ccode>type\u003C\u002Fcode>, \u003Ccode>featured\u003C\u002Fcode>, \u003Ccode>visibility\u003C\u002Fcode>, \u003Ccode>locked\u003C\u002Fcode> (a\nmembers-only post this viewer may not read: no \u003Ccode>bodyHtml\u003C\u002Fcode>), the SEO fields\n(\u003Ccode>metaTitle\u003C\u002Fcode>, \u003Ccode>metaDescription\u003C\u002Fcode>, \u003Ccode>ogImageUrl\u003C\u002Fcode>, \u003Ccode>canonicalUrl\u003C\u002Fcode>, each empty when\nthe post's own applies) and \u003Ccode>coverImageAlt\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Ch2 id=\"managing-posts-over-the-api\">Managing posts over the API\u003Ca class=\"heading-anchor\" href=\"#managing-posts-over-the-api\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>With an admin key (\u003Ccode>ak_…\u003C\u002Fcode>) or an operator session, scoped to the key's\nworkspace (and projects):\u003C\u002Fp>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-bash\">\u003Ccode>\u003Cspan class=\"shj-syn-cmnt\"># Write a post (Markdown) and publish it now; tags and a cover are optional\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-func\">curl\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -s\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -X\u003C\u002Fspan> POST https:\u002F\u002FYOUR-SUPPORT-HOST\u002Fv1\u002Fadmin\u002Fblog\u002Fposts \\\n \u003Cspan class=\"shj-syn-kwd\"> -H\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">\"Authorization: Bearer \u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">$HELPBITES_ADMIN_KEY\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\"\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -H\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">'Content-Type: application\u002Fjson'\u003C\u002Fspan> \\\n \u003Cspan class=\"shj-syn-kwd\"> -d\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">'{\"project\":\"PROJECT_ID\",\"title\":\"Release 3.0\",\"body\":\"## What is new\\n\\nA lot.\",\"tags\":[\"Releases\"],\"status\":\"published\"}'\u003C\u002Fspan>\n\n\u003Cspan class=\"shj-syn-cmnt\"># Schedule a draft for tomorrow morning\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-func\">curl\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -s\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -X\u003C\u002Fspan> PATCH https:\u002F\u002FYOUR-SUPPORT-HOST\u002Fv1\u002Fadmin\u002Fblog\u002Fposts\u002FPOST_ID \\\n \u003Cspan class=\"shj-syn-kwd\"> -H\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">\"Authorization: Bearer \u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">$HELPBITES_ADMIN_KEY\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\"\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -H\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">'Content-Type: application\u002Fjson'\u003C\u002Fspan> \\\n \u003Cspan class=\"shj-syn-kwd\"> -d\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">'{\"status\":\"published\",\"publishedAt\":\"2026-11-02T08:00:00Z\"}'\u003C\u002Fspan>\n\n\u003Cspan class=\"shj-syn-cmnt\"># An About page (pages stay out of the list and the feed)\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-func\">curl\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -s\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -X\u003C\u002Fspan> POST https:\u002F\u002FYOUR-SUPPORT-HOST\u002Fv1\u002Fadmin\u002Fblog\u002Fposts \\\n \u003Cspan class=\"shj-syn-kwd\"> -H\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">\"Authorization: Bearer \u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">$HELPBITES_ADMIN_KEY\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\"\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -H\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">'Content-Type: application\u002Fjson'\u003C\u002Fspan> \\\n \u003Cspan class=\"shj-syn-kwd\"> -d\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">'{\"project\":\"PROJECT_ID\",\"type\":\"page\",\"title\":\"About\",\"slug\":\"about\",\"body\":\"We are three people.\",\"status\":\"published\"}'\u003C\u002Fspan>\n\n\u003Cspan class=\"shj-syn-cmnt\"># Members only, featured, with its own search title\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-func\">curl\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -s\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -X\u003C\u002Fspan> PATCH https:\u002F\u002FYOUR-SUPPORT-HOST\u002Fv1\u002Fadmin\u002Fblog\u002Fposts\u002FPOST_ID \\\n \u003Cspan class=\"shj-syn-kwd\"> -H\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">\"Authorization: Bearer \u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">$HELPBITES_ADMIN_KEY\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\"\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -H\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">'Content-Type: application\u002Fjson'\u003C\u002Fspan> \\\n \u003Cspan class=\"shj-syn-kwd\"> -d\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">'{\"visibility\":\"members\",\"featured\":true,\"metaTitle\":\"Release 3.0: what is new\"}'\u003C\u002Fspan>\n\n\u003Cspan class=\"shj-syn-cmnt\"># Upload an image for the body or the cover\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-func\">curl\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -s\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -X\u003C\u002Fspan> POST https:\u002F\u002FYOUR-SUPPORT-HOST\u002Fv1\u002Fadmin\u002Fblog\u002Fimages \\\n \u003Cspan class=\"shj-syn-kwd\"> -H\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">\"Authorization: Bearer \u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">$HELPBITES_ADMIN_KEY\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\"\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -F\u003C\u002Fspan> \u003Cspan class=\"shj-syn-var\">project\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>PROJECT_ID\u003Cspan class=\"shj-syn-kwd\"> -F\u003C\u002Fspan> \u003Cspan class=\"shj-syn-var\">file\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>@shot.png\n\n\u003Cspan class=\"shj-syn-cmnt\"># Email it to the \"blog\" topic's subscribers now (a plan with campaigns)\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-func\">curl\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -s\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -X\u003C\u002Fspan> POST https:\u002F\u002FYOUR-SUPPORT-HOST\u002Fv1\u002Fadmin\u002Fblog\u002Fposts\u002FPOST_ID\u002Fnewsletter \\\n \u003Cspan class=\"shj-syn-kwd\"> -H\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">\"Authorization: Bearer \u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">$HELPBITES_ADMIN_KEY\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\"\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -H\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">'Content-Type: application\u002Fjson'\u003C\u002Fspan> \\\n \u003Cspan class=\"shj-syn-kwd\"> -d\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">'{\"send\":\"now\",\"audience\":{\"topic\":\"blog\"}}'\u003C\u002Fspan>\n\n\u003Cspan class=\"shj-syn-cmnt\"># Every comment, for moderation; hide one\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-func\">curl\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -s\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">\"https:\u002F\u002FYOUR-SUPPORT-HOST\u002Fv1\u002Fadmin\u002Fblog\u002Fcomments?project=PROJECT_ID\"\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -H\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">\"Authorization: Bearer \u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">$HELPBITES_ADMIN_KEY\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\"\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-func\">curl\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -s\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -X\u003C\u002Fspan> PATCH https:\u002F\u002FYOUR-SUPPORT-HOST\u002Fv1\u002Fblog\u002Fcomments\u002FCOMMENT_ID \\\n \u003Cspan class=\"shj-syn-kwd\"> -H\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">\"Authorization: Bearer \u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">$HELPBITES_ADMIN_KEY\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\"\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -H\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">'Content-Type: application\u002Fjson'\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -d\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">'{\"status\":\"hidden\"}'\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Cp>The settings are \u003Ccode>PATCH \u002Fv1\u002Fadmin\u002Fprojects\u002F{id}\u003C\u002Fcode> with \u003Ccode>{ blog: { … } }\u003C\u002Fcode> (admins;\nonly the keys sent change): \u003Ccode>enabled\u003C\u002Fcode>, \u003Ccode>title\u003C\u002Fcode>, \u003Ccode>description\u003C\u002Fcode>, \u003Ccode>comments\u003C\u002Fcode>,\n\u003Ccode>reactions\u003C\u002Fcode>, \u003Ccode>commentNotify\u003C\u002Fcode>, \u003Ccode>postUrl\u003C\u002Fcode>, \u003Ccode>newsletterTopic\u003C\u002Fcode>, \u003Ccode>subscribeForm\u003C\u002Fcode> and\n\u003Ccode>design\u003C\u002Fcode> (\u003Ccode>layout\u003C\u002Fcode>, \u003Ccode>font\u003C\u002Fcode>, \u003Ccode>colorScheme\u003C\u002Fcode>, \u003Ccode>accent\u003C\u002Fcode>, \u003Ccode>logoUrl\u003C\u002Fcode>, \u003Ccode>coverImageUrl\u003C\u002Fcode>,\n\u003Ccode>navigation\u003C\u002Fcode>, \u003Ccode>social\u003C\u002Fcode>, \u003Ccode>footer\u003C\u002Fcode>; partial too, and \u003Ccode>social\u003C\u002Fcode> merges per network,\n\u003Ccode>&quot;&quot;\u003C\u002Fcode> removing one). The whole reference is in the OpenAPI document at\n\u003Ccode>\u002F_docs\u002Fscalar\u003C\u002Fcode>, under \u003Cstrong>Blog\u003C\u002Fstrong>.\u003C\u002Fp>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-bash\">\u003Ccode>\u003Cspan class=\"shj-syn-cmnt\"># The magazine layout, serif, with an About link\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-func\">curl\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -s\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -X\u003C\u002Fspan> PATCH https:\u002F\u002FYOUR-SUPPORT-HOST\u002Fv1\u002Fadmin\u002Fprojects\u002FPROJECT_ID \\\n \u003Cspan class=\"shj-syn-kwd\"> -H\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">\"Authorization: Bearer \u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">$HELPBITES_ADMIN_KEY\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\"\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -H\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">'Content-Type: application\u002Fjson'\u003C\u002Fspan> \\\n \u003Cspan class=\"shj-syn-kwd\"> -d\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">'{\"blog\":{\"design\":{\"layout\":\"magazine\",\"font\":\"serif\",\"navigation\":[{\"label\":\"About\",\"url\":\"\u002Fabout\"}]}}}'\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Ch3 id=\"preview-over-the-api\">Preview over the API\u003Ca class=\"heading-anchor\" href=\"#preview-over-the-api\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh3>\n\u003Cp>\u003Ccode>POST \u002Fv1\u002Fadmin\u002Fblog\u002Fpreview\u003C\u002Fcode> is what the editor's \u003Cstrong>Preview\u003C\u002Fstrong> and \u003Cstrong>Live preview\u003C\u002Fstrong>\ncall. Send the fields of a post (\u003Ccode>title\u003C\u002Fcode>, \u003Ccode>body\u003C\u002Fcode>, \u003Ccode>bodyFormat\u003C\u002Fcode>, \u003Ccode>excerpt\u003C\u002Fcode>,\n\u003Ccode>coverImageUrl\u003C\u002Fcode>, \u003Ccode>tags\u003C\u002Fcode>, \u003Ccode>authorName\u003C\u002Fcode>, \u003Ccode>publishedAt\u003C\u002Fcode>, \u003Ccode>type\u003C\u002Fcode>) and get \u003Ccode>{ html }\u003C\u002Fcode>:\nthe whole page in the blog's design, as readers will see it. Nothing is read from\nor saved to a stored post, so it works for a draft that was never saved; reactions,\ncomments and the sign-up form are left out. Show it in an \u003Ccode>iframe\u003C\u002Fcode> with \u003Ccode>sandbox=&quot;&quot;\u003C\u002Fcode>\n(\u003Ccode>srcdoc\u003C\u002Fcode>); a Live preview is that call repeated as you type.\u003C\u002Fp>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-bash\">\u003Ccode>\u003Cspan class=\"shj-syn-func\">curl\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -X\u003C\u002Fspan> POST https:\u002F\u002Fyour-helpbites.example\u002Fv1\u002Fadmin\u002Fblog\u002Fpreview \\\n \u003Cspan class=\"shj-syn-kwd\"> -H\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">\"Authorization: Bearer ak_…\"\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -H\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">\"Content-Type: application\u002Fjson\"\u003C\u002Fspan> \\\n \u003Cspan class=\"shj-syn-kwd\"> -d\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">'{ \"project\": \"PROJECT_ID\", \"title\": \"Offline mode\", \"body\": \"It **works** on a plane.\" }'\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Ch3 id=\"write-with-ai-over-the-api\">Write with AI over the API\u003Ca class=\"heading-anchor\" href=\"#write-with-ai-over-the-api\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh3>\n\u003Cp>\u003Ccode>POST \u002Fv1\u002Fadmin\u002Fblog\u002Fai\u003C\u002Fcode> is what the editor's button calls. Nothing is saved: you\nget a proposal and send what you keep to the create or update endpoint above.\u003C\u002Fp>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-bash\">\u003Ccode>\u003Cspan class=\"shj-syn-func\">curl\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -X\u003C\u002Fspan> POST https:\u002F\u002Fyour-helpbites.example\u002Fv1\u002Fadmin\u002Fblog\u002Fai \\\n \u003Cspan class=\"shj-syn-kwd\"> -H\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">\"Authorization: Bearer ak_…\"\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -H\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">\"Content-Type: application\u002Fjson\"\u003C\u002Fspan> \\\n \u003Cspan class=\"shj-syn-kwd\"> -d\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">'{ \"project\": \"PROJECT_ID\", \"mode\": \"write\",\u003C\u002Fspan>\n        \u003Cspan class=\"shj-syn-str\">\"instruction\"\u003C\u002Fspan>: \u003Cspan class=\"shj-syn-str\">\"Announce our new offline mode, for a technical audience.\"\u003C\u002Fspan> \u003Cspan class=\"shj-syn-oper\">}\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">'\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-cmnt\"># { \"result\": { \"title\": \"…\", \"excerpt\": \"…\", \"metaDescription\": \"…\", \"tags\": [\"…\"],\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-cmnt\">#               \"body\": \"## …\", \"notes\": \"…\", \"repos\": [{ \"repo\": \"you\u002Fapp\", \"read\": true }] } }\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Cul>\n\u003Cli>\u003Ccode>mode\u003C\u002Fcode>: \u003Ccode>write\u003C\u002Fcode> (needs \u003Ccode>instruction\u003C\u002Fcode> or \u003Ccode>title\u003C\u002Fcode>), \u003Ccode>continue\u003C\u002Fcode>, \u003Ccode>improve\u003C\u002Fcode> and \u003Ccode>meta\u003C\u002Fcode>\n(these need \u003Ccode>body\u003C\u002Fcode>, the Markdown so far) and \u003Ccode>insert\u003C\u002Fcode> (needs \u003Ccode>instruction\u003C\u002Fcode>: a\npassage for the cursor). \u003Ccode>continue\u003C\u002Fcode> and \u003Ccode>insert\u003C\u002Fcode> return only the text to add.\nSend \u003Ccode>selection\u003C\u002Fcode> (Markdown) with \u003Ccode>improve\u003C\u002Fcode> to rewrite only that passage; \u003Ccode>body\u003C\u002Fcode> is\nthen its replacement and \u003Ccode>instruction\u003C\u002Fcode> says how.\u003C\u002Fli>\n\u003Cli>\u003Ccode>repos\u003C\u002Fcode>: \u003Ccode>owner\u002Fname\u003C\u002Fcode> of linked repos to read; all of the project's when left out.\n\u003Ccode>GET \u002Fv1\u002Fadmin\u002Fcms\u002Frepos?project=…\u003C\u002Fcode> lists them.\u003C\u002Fli>\n\u003Cli>Fields a mode does not produce are empty. Errors: \u003Ccode>400\u003C\u002Fcode> nothing to go on, \u003Ccode>501\u003C\u002Fcode>\nno AI provider configured, \u003Ccode>502\u003C\u002Fcode> the provider failed, \u003Ccode>429\u003C\u002Fcode> six a minute per operator.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2 id=\"webhooks\">Webhooks\u003Ca class=\"heading-anchor\" href=\"#webhooks\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>With the project's event webhook set (\u003Cstrong>Email → Settings\u003C\u002Fstrong>; see\n\u003Ca href=\"https:\u002F\u002Fgithub.com\u002Felrumo\u002Fhelp-bites-oss\u002Fblob\u002Fmain\u002Fdocs\u002FMARKETING.md\" target=\"_blank\" rel=\"noopener\">MARKETING.md\u003C\u002Fa>), the blog sends, signed and retried like every\nevent:\u003C\u002Fp>\n\u003Cdiv class=\"table-wrap\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Event\u003C\u002Fth>\n\u003Cth>When\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>\u003Ccode>blog.post.published\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>A post went public: published now, or its scheduled moment came.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>blog.post.updated\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>A public post was changed.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>blog.post.unpublished\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>A public post stopped being public: unpublished, or rescheduled into the future.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>blog.post.deleted\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>A public post was deleted.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Cp>\u003Ccode>data\u003C\u002Fcode> is \u003Ccode>{ post: { id, slug, title, visibility, publishedAt, url } }\u003C\u002Fcode>. Rebuild\na static site, post to social media, or ping a search engine on them. Pages and\nimports send nothing.\u003C\u002Fp>\n\u003Ch2 id=\"moving-from-ghost\">Moving from Ghost\u003Ca class=\"heading-anchor\" href=\"#moving-from-ghost\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>\u003Cstrong>Project → Blog → … → Import from Ghost\u003C\u002Fstrong> takes the file Ghost makes under\n\u003Cstrong>Settings → Advanced → Import\u002FExport → Export content\u003C\u002Fstrong> (a \u003Ccode>.json\u003C\u002Fcode>):\u003C\u002Fp>\n\u003Cul>\n\u003Cli>Posts and pages, with their HTML (kept as HTML), slug, excerpt, cover and its\nalt text, tags (Ghost's internal \u003Ccode>#tags\u003C\u002Fcode> left out), first author, publish date,\nfeatured flag, meta title and description, social image and canonical URL.\u003C\u002Fli>\n\u003Cli>Published posts stay published with their dates, scheduled ones stay scheduled,\ndrafts stay drafts. Email-only posts (\u003Ccode>sent\u003C\u002Fcode>) come in as drafts.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Visibility\u003C\u002Fstrong>: \u003Ccode>public\u003C\u002Fcode> stays public; \u003Ccode>members\u003C\u002Fcode> is members-only; \u003Ccode>paid\u003C\u002Fcode> and\n\u003Ccode>tiers\u003C\u002Fcode> become members-only too (there are no paid tiers here), and the import\nsays how many.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Your Ghost site's address\u003C\u002Fstrong>: the export writes its own links and images as\n\u003Ccode>__GHOST_URL__\u002F…\u003C\u002Fcode>; give the address and they point there again. The images stay\non that site, so keep it up until you have moved them (upload them here and\nchange the posts), or the posts will show broken images.\u003C\u002Fli>\n\u003Cli>Importing the same file again skips what came in before (matched by Ghost's\nid); \u003Cstrong>Overwrite posts imported before\u003C\u002Fstrong> replaces them with the file's copy.\nNothing is emailed and no webhook fires. Up to 2,000 posts per file.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Members\u003C\u002Fstrong> are a separate CSV in Ghost (\u003Cstrong>Members → Export\u003C\u002Fstrong>). Import it under\n\u003Cstrong>Contacts → Import\u003C\u002Fstrong>: \u003Ccode>email\u003C\u002Fcode>, \u003Ccode>name\u003C\u002Fcode>, \u003Ccode>labels\u003C\u002Fcode> (as tags) and\n\u003Ccode>subscribed_to_emails\u003C\u002Fcode> (as newsletter consent) are picked up; unset the \u003Ccode>id\u003C\u002Fcode>\ncolumn, which is Ghost's, not your app's.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>Over the API:\u003C\u002Fp>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-bash\">\u003Ccode>\u003Cspan class=\"shj-syn-func\">curl\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -s\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -X\u003C\u002Fspan> POST https:\u002F\u002FYOUR-SUPPORT-HOST\u002Fv1\u002Fadmin\u002Fblog\u002Fimport \\\n \u003Cspan class=\"shj-syn-kwd\"> -H\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">\"Authorization: Bearer \u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">$HELPBITES_ADMIN_KEY\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\"\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -H\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">'Content-Type: application\u002Fjson'\u003C\u002Fspan> \\\n \u003Cspan class=\"shj-syn-kwd\"> -d\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">\"{\\\"project\\\":\\\"PROJECT_ID\\\",\\\"siteUrl\\\":\\\"https:\u002F\u002Fblog.example.com\\\",\\\"export\\\":\u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">$(cat my-blog.ghost.json)\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">}\"\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-cmnt\"># → { \"created\": 41, \"updated\": 0, \"skipped\": 0, \"warnings\": [\"2 paid or tier-only posts are members-only here: …\"] }\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Ch2 id=\"limits\">Limits\u003Ca class=\"heading-anchor\" href=\"#limits\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cdiv class=\"table-wrap\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>\u003C\u002Fth>\n\u003Cth>\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>Title\u003C\u002Ftd>\n\u003Ctd>200 characters\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Body\u003C\u002Ftd>\n\u003Ctd>200,000 characters (Markdown or HTML)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>data\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>200,000 characters as JSON\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Search and social\u003C\u002Ftd>\n\u003Ctd>title 300, description 500, alt text 300 characters\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Navigation\u003C\u002Ftd>\n\u003Ctd>8 links\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Ghost import\u003C\u002Ftd>\n\u003Ctd>2,000 posts per file\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Excerpt\u003C\u002Ftd>\n\u003Ctd>300 characters\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Write with AI\u003C\u002Ftd>\n\u003Ctd>2,000 characters of instruction, 3 repos, 6 requests a minute per operator\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Tags\u003C\u002Ftd>\n\u003Ctd>10 per post, 40 characters each\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Images\u003C\u002Ftd>\n\u003Ctd>8 MB, PNG, JPEG, GIF or WebP\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Comments\u003C\u002Ftd>\n\u003Ctd>4,000 characters, 6 links, five per ten minutes per reader\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Reactions\u003C\u002Ftd>\n\u003Ctd>60 a minute per reader\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Public reads\u003C\u002Ftd>\n\u003Ctd>1,200 a minute per IP\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Feed\u003C\u002Ftd>\n\u003Ctd>the latest 30 posts\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\n",[6,10,13,16,19,22,25,28,31,34,37,40,43,47,50,53,56,59,62,65,68,71],{"id":7,"text":8,"level":9},"setup","Setup",2,{"id":11,"text":12,"level":9},"writing","Writing",{"id":14,"text":15,"level":9},"smaller-images-and-modern-formats","Smaller images and modern formats",{"id":17,"text":18,"level":9},"your-own-data-on-a-post","Your own data on a post",{"id":20,"text":21,"level":9},"publishing","Publishing",{"id":23,"text":24,"level":9},"pages","Pages",{"id":26,"text":27,"level":9},"members-only-posts","Members-only posts",{"id":29,"text":30,"level":9},"readers-reactions-and-comments","Readers: reactions and comments",{"id":32,"text":33,"level":9},"newsletter","Newsletter",{"id":35,"text":36,"level":9},"rss","RSS",{"id":38,"text":39,"level":9},"the-hosted-blog","The hosted blog",{"id":41,"text":42,"level":9},"put-it-on-your-site","Put it on your site",{"id":44,"text":45,"level":46},"options","Options",3,{"id":48,"text":49,"level":46},"restyle-it","Restyle it",{"id":51,"text":52,"level":9},"in-a-swift-app","In a Swift app",{"id":54,"text":55,"level":9},"your-own-ui","Your own UI",{"id":57,"text":58,"level":9},"managing-posts-over-the-api","Managing posts over the API",{"id":60,"text":61,"level":46},"preview-over-the-api","Preview over the API",{"id":63,"text":64,"level":46},"write-with-ai-over-the-api","Write with AI over the API",{"id":66,"text":67,"level":9},"webhooks","Webhooks",{"id":69,"text":70,"level":9},"moving-from-ghost","Moving from Ghost",{"id":72,"text":73,"level":9},"limits","Limits",1792677543076]