{
  "version": "https://jsonfeed.org/version/1.1",
  "title": "A Node on the Web — plume",
  "home_page_url": "https://rmendes.net/categories/plume/",
  "feed_url": "https://rmendes.net/categories/plume/feed.json",
  "hubs": [
    {
      "type": "WebSub",
      "url": "https://websubhub.com/hub"
    }
  ],
  "description": "Posts tagged with \"plume\" on A Node on the Web",
  "language": "en",
  "authors": [
    {
      "name": "Ricardo Mendes",
      "url": "https://rmendes.net/"
    }
  ],
  "_textcasting": {
    "version": "1.0",
    "about": "https://textcasting.org/"
  },
  "items": [
    {
      "id": "https://rmendes.net/articles/2026/05/20/building-plume/",
      "url": "https://rmendes.net/articles/2026/05/20/building-plume/",
      "title": "Building Plume",
      "content_html": "<p>I got annoyed.</p>\n<p>Every time I wanted to bookmark a page or jot down a note, I’d open the Indiekit admin UI in a new tab, paste a URL, and click around. Small friction. But small friction repeats forever. So two days ago I started a brainstorm with Claude on what it would take to build a browser extension that posts to my Micropub blog directly from the toolbar.</p>\n<p>Two days later, Plume is live on the Chrome Web Store and Mozilla AMO. Source on GitHub. No telemetry, two browsers, 127 unit tests if you’re counting.</p>\n<p>Here’s how it happened.</p>\n<h2 id=\"what-plume-does\" tabindex=\"-1\"><a class=\"header-anchor\" href=\"https://rmendes.net/articles/2026/05/20/building-plume/#what-plume-does\">What Plume does</a></h2>\n<p>It’s a Micropub client that lives in your browser’s toolbar. Click the feather icon and you get a composer for notes, articles, bookmarks, replies, likes, reposts, quotes, and photos. Multi-account if you run more than one blog. Right-click any page, link, image, or text selection to post about it with the right fields already filled in. Drafts auto-save while you type. Failed posts queue up and retry in the background. There’s a Markdown toolbar with a live preview, and the composer can pop out to a tab if you need desk-width room for an article. The only network requests Plume makes are to your own Micropub endpoint.</p>\n<p>Standard IndieWeb stuff, just packaged so it’s always one keystroke away. Default shortcut: <code>Alt+Shift+P</code>.</p>\n<h2 id=\"how-we-built-it\" tabindex=\"-1\"><a class=\"header-anchor\" href=\"https://rmendes.net/articles/2026/05/20/building-plume/#how-we-built-it\">How we built it</a></h2>\n<p>I’d already written a Micropub MCP server — same family as Plume — so Claude could post to my blog directly. That codebase had everything Plume needed minus the browser layer: HTTP client for Micropub create/update/delete/query/upload, IndieAuth + PKCE, endpoint discovery. The work was wrapping it in something that runs inside Chrome and Firefox.</p>\n<p>Stack: WXT for the extension framework (Vite-based, generates Chrome and Firefox manifests from one config), Preact for the UI, TypeScript, Bun. Preact is small and feels like React minus the dependency cost. Bun runs TypeScript directly so there’s no build step for tests.</p>\n<p>The actual workflow was three skills stacked. First, a brainstorming skill that takes a one-line idea, walks me through clarifying questions, and produces a design document. Then a planning skill that turns the design into an implementation plan with exact files, code, and commit boundaries. Then a subagent-driven execution skill that dispatches a fresh agent per task, one per file or feature, with a review loop after each. Each agent only saw what it needed. I wasn’t managing thirty open files. That last part is the actual win — agentic coding only works if the human’s context stays clear too.</p>\n<p>Some things didn’t go to plan. Biome, the linter we picked first, crashed with SIGABRT twice in a row, so we migrated to ESLint + Prettier mid-build. Chrome Web Store rejected the first upload because we’d included a <code>manifest.key</code> field, which CWS refuses on initial submission (it assigns its own ID). Mozilla AMO then rejected v1.0.3 because Firefox extensions now have to declare data collection permissions in the manifest. Each one felt like a setback at the time. Each one resolved in under thirty minutes.</p>\n<p>Three decisions I’m happy with:</p>\n<p>The Markdown preview lazy-loads snarkdown and DOMPurify only when you click the preview button. Users who never preview pay nothing for the parser. Eager-loaded, the popup chunk was 47 kB. Lazy, it’s 21 kB with 27 kB of optional chunks. The popup feels instant.</p>\n<p>The composer pop-out is the same <code>popup.html</code> opened in a tab with a <code>?popout=1</code> flag. The component code doesn’t know or care which target it’s rendering into — only the layout cares. Toolbar popup at 360 px, tab view at 480–720 px. One file, two contexts.</p>\n<p>For IndieAuth, <code>chrome.identity.launchWebAuthFlow</code> handles the OAuth dance. The catch is that Chrome’s flow uses a per-extension callback URL on <code>*.chromiumapp.org</code>. To make the same extension work in dev and prod, we embed the Chrome Web Store’s production public key as <code>manifest.key</code> in dev mode. The dev install then derives the same extension ID as the published one, so one redirect URI declaration covers both.</p>\n<p>I also patched the upstream Indiekit endpoint-auth package so it can fetch a <code>client_id</code> URL and parse <code>&lt;link rel=&quot;redirect_uri&quot;&gt;</code> tags with wildcard subdomain matching. Plume’s redirect URI lives on <code>chromiumapp.org</code>, not on <code>rmendes.net</code>, and the existing validation rejected cross-host redirects.</p>\n<h2 id=\"releases\" tabindex=\"-1\"><a class=\"header-anchor\" href=\"https://rmendes.net/articles/2026/05/20/building-plume/#releases\">Releases</a></h2>\n<p>v1.0.0 shipped on May 18. By the end of the next day we were at v1.2.0. The dot releases in between were mostly the store-publishing learning curve. 1.0.1 fixed a missing file picker on the photo tab and a context-menu race condition. 1.0.2 made <code>manifest.key</code> dev-only. 1.0.3 pinned the dev key to the CWS production key so callback URLs stayed stable. 1.0.4 added Firefox data collection permissions. Then 1.1.0 brought the MediaPicker (browse existing media on the server), the pop-out composer, server-side extension detection via <code>?q=post-types</code> property scanning, the keyboard shortcut, and live-updating queue and draft lists. 1.2.0 added the Markdown toolbar and preview.</p>\n<p>Shipping a tagged release every couple of hours is unreasonably motivating. Each version landed, I tested it on <a href=\"http://rmendes.net/\">rmendes.net</a>, found the next gap, and the next one shipped in an hour or two. The release workflow on GitHub Actions extracts the changelog section by tag and drafts a GitHub release with the Chrome and Firefox zips attached.</p>\n<h2 id=\"how-to-use-it\" tabindex=\"-1\"><a class=\"header-anchor\" href=\"https://rmendes.net/articles/2026/05/20/building-plume/#how-to-use-it\">How to use it</a></h2>\n<p>Install from the Chrome Web Store or Mozilla AMO. Click the feather icon in your toolbar. The first time, it’ll say “No Micropub account connected.” Click “Open Plume settings,” paste your blog URL, click Authorize. Your IndieAuth server opens a consent page in a small Chrome window. Approve. Plume gets a token and stores it locally.</p>\n<p>From there: toolbar feather to write. Right-click a page to bookmark it. Right-click selected text to reply or quote with the passage as a Markdown blockquote. Right-click an image to post a photo. Settings any time to switch accounts, drain the retry queue, or recover a draft.</p>\n<h2 id=\"whats-next\" tabindex=\"-1\"><a class=\"header-anchor\" href=\"https://rmendes.net/articles/2026/05/20/building-plume/#whats-next\">What’s next</a></h2>\n<p>A few things on the list. Sending <code>content</code> as <code>{markdown: &quot;...&quot;}</code> explicitly instead of relying on Indiekit’s default markdown handling. Better cross-browser end-to-end coverage — right now E2E only runs on Chromium. Maybe a Firefox-specific design pass; the popup feels slightly tight there compared to Chrome.</p>\n<p>But the core thing is done. Plume is the tool I wanted: friction-free Micropub posting from any page, with my own blog as the destination. If you run Indiekit or another spec-compliant Micropub server, it should just work.</p>\n<p>Source: <a href=\"https://github.com/rmdes/plume\">github.com/rmdes/plume</a><br />\nLanding page: <a href=\"https://rmdes.github.io/plume/\">rmdes.github.io/plume</a></p>\n",
      "content_text": "I got annoyed. Every time I wanted to bookmark a page or jot down a note, I’d open the Indiekit admin UI in a new tab, paste a URL, and click around. Small friction. But small friction repeats forever. So two days ago I started a brainstorm with Claude on what it would take to build a browser extension that posts to my Micropub blog directly from the toolbar. Two days later, Plume is live on the Chrome Web Store and Mozilla AMO. Source on GitHub. No telemetry, two browsers, 127 unit tests if you’re counting. Here’s how it happened. What Plume does It’s a Micropub client that lives in your browser’s toolbar. Click the feather icon and you get a composer for notes, articles, bookmarks, replies, likes, reposts, quotes, and photos. Multi-account if you run more than one blog. Right-click any page, link, image, or text selection to post about it with the right fields already filled in. Drafts auto-save while you type. Failed posts queue up and retry in the background. There’s a Markdown toolbar with a live preview, and the composer can pop out to a tab if you need desk-width room for an article. The only network requests Plume makes are to your own Micropub endpoint. Standard IndieWeb stuff, just packaged so it’s always one keystroke away. Default shortcut: Alt+Shift+P. How we built it I’d already written a Micropub MCP server — same family as Plume — so Claude could post to my blog directly. That codebase had everything Plume needed minus the browser layer: HTTP client for Micropub create/update/delete/query/upload, IndieAuth + PKCE, endpoint discovery. The work was wrapping it in something that runs inside Chrome and Firefox. Stack: WXT for the extension framework (Vite-based, generates Chrome and Firefox manifests from one config), Preact for the UI, TypeScript, Bun. Preact is small and feels like React minus the dependency cost. Bun runs TypeScript directly so there’s no build step for tests. The actual workflow was three skills stacked. First, a brainstorming skill that takes a one-line idea, walks me through clarifying questions, and produces a design document. Then a planning skill that turns the design into an implementation plan with exact files, code, and commit boundaries. Then a subagent-driven execution skill that dispatches a fresh agent per task, one per file or feature, with a review loop after each. Each agent only saw what it needed. I wasn’t managing thirty open files. That last part is the actual win — agentic coding only works if the human’s context stays clear too. Some things didn’t go to plan. Biome, the linter we picked first, crashed with SIGABRT twice in a row, so we migrated to ESLint + Prettier mid-build. Chrome Web Store rejected the first upload because we’d included a manifest.key field, which CWS refuses on initial submission (it assigns its own ID). Mozilla AMO then rejected v1.0.3 because Firefox extensions now have to declare data collection permissions in the manifest. Each one felt like a setback at the time. Each one resolved in under thirty minutes. Three decisions I’m happy with: The Markdown preview lazy-loads snarkdown and DOMPurify only when you click the preview button. Users who never preview pay nothing for the parser. Eager-loaded, the popup chunk was 47 kB. Lazy, it’s 21 kB with 27 kB of optional chunks. The popup feels instant. The composer pop-out is the same popup.html opened in a tab with a ?popout=1 flag. The component code doesn’t know or care which target it’s rendering into — only the layout cares. Toolbar popup at 360 px, tab view at 480–720 px. One file, two contexts. For IndieAuth, chrome.identity.launchWebAuthFlow handles the OAuth dance. The catch is that Chrome’s flow uses a per-extension callback URL on *.chromiumapp.org. To make the same extension work in dev and prod, we embed the Chrome Web Store’s production public key as manifest.key in dev mode. The dev install then derives the same extension ID as the published one, so one redirect URI declaration covers both. I also patched the upstream Indiekit endpoint-auth package so it can fetch a client_id URL and parse &lt;link rel=&quot;redirect_uri&quot;&gt; tags with wildcard subdomain matching. Plume’s redirect URI lives on chromiumapp.org, not on rmendes.net, and the existing validation rejected cross-host redirects. Releases v1.0.0 shipped on May 18. By the end of the next day we were at v1.2.0. The dot releases in between were mostly the store-publishing learning curve. 1.0.1 fixed a missing file picker on the photo tab and a context-menu race condition. 1.0.2 made manifest.key dev-only. 1.0.3 pinned the dev key to the CWS production key so callback URLs stayed stable. 1.0.4 added Firefox data collection permissions. Then 1.1.0 brought the MediaPicker (browse existing media on the server), the pop-out composer, server-side extension detection via ?q=post-types property scanning, the keyboard shortcut, and live-updating queue and draft lists. 1.2.0 added the Markdown toolbar and preview. Shipping a tagged release every couple of hours is unreasonably motivating. Each version landed, I tested it on rmendes.net, found the next gap, and the next one shipped in an hour or two. The release workflow on GitHub Actions extracts the changelog section by tag and drafts a GitHub release with the Chrome and Firefox zips attached. How to use it Install from the Chrome Web Store or Mozilla AMO. Click the feather icon in your toolbar. The first time, it’ll say “No Micropub account connected.” Click “Open Plume settings,” paste your blog URL, click Authorize. Your IndieAuth server opens a consent page in a small Chrome window. Approve. Plume gets a token and stores it locally. From there: toolbar feather to write. Right-click a page to bookmark it. Right-click selected text to reply or quote with the passage as a Markdown blockquote. Right-click an image to post a photo. Settings any time to switch accounts, drain the retry queue, or recover a draft. What’s next A few things on the list. Sending content as {markdown: &quot;...&quot;} explicitly instead of relying on Indiekit’s default markdown handling. Better cross-browser end-to-end coverage — right now E2E only runs on Chromium. Maybe a Firefox-specific design pass; the popup feels slightly tight there compared to Chrome. But the core thing is done. Plume is the tool I wanted: friction-free Micropub posting from any page, with my own blog as the destination. If you run Indiekit or another spec-compliant Micropub server, it should just work. Source: github.com/rmdes/plume Landing page: rmdes.github.io/plume",
      "date_published": "2026-05-20T07:42:02Z",
      "date_modified": "2026-05-20T08:33:57Z"
    }
  ]
}
