Tooting from the Blog Dark _ □ ✕
  • Posts
  • About
  • Debug
Post

Tooting from the Blog

Published
2026-08-11 00:00 UTC (precision: second)
Source feed
Heart Soul Machine [exclude this blog]
Link
https://heartsoulmachine.com/blog/2026/08-11-tooting-from-the-blog/
Canonical
https://heartsoulmachine.com/blog/2026/08-11-tooting-from-the-blog
Length
11569 runes
Extracted text
<p>I've been using <a href="https://echofeed.app/" target="_blank" rel="noopener noreferrer">EchoFeed</a> to crosspost from my blog to Mastodon for a while now. It's a solid little tool, but I've been slack and have been relying on someone else's free hosted service to keep my POSSE workflow running. I'm a massive fan of Robb and have been so appreciative of this tool, which came along at exactly the right time for me. But someone's generosity often hides the amount of effort that goes into it, and <a href="https://rknight.me/blog/shutting-down-echofeed/" target="_blank" rel="noopener noreferrer">now it's been shuttered </a> I need to look for a new solution. Thanks Robb for all the work that you've put into this service and the value it's given my tiny little corner of the web.</p> <p>With the news, I decided I needed to replace what Echofeed provided - posting to Mastodon is literally the only promotion I do for the site- and rather than migrate to another third-party service, I wondered if I could just use my existing stack instead. The result is a GitHub Action that reads my blog's RSS feed, works out which posts haven't been shared yet, and toots the new ones to Mastodon. I've set it up to also post to Bluesky as well. It runs as part of your existing deployment pipeline, keeps its own state in a JSON file committed to the repo, and requires no server, database, or any other subscription.</p> <blockquote> <p>Note: This was vibe-coded with Claude. While I appreciate the craft that goes into coding, I am not blessed with that skill. I know my way around web systems well enough to know what's possible - I just can't make the magic happen. If this puts you off - that's fine. If it inspires you to write something yourself - let me know. I'd be more than happy to swap out the AI slop with something hand-crafted.</p> </blockquote> <h2>How it works</h2> <p>Three pieces:</p> <ul> <li>A <strong>state file</strong> (<code>data/posted-guids.json</code>) that records which feed items have already been posted, so nothing gets duplicated.</li> <li>A <strong>script</strong> that fetches the feed, compares it against that state file, and posts anything new to Mastodon's API.</li> <li>A <strong>workflow</strong> that runs the script, either on a schedule or as a step in your existing build and deploy pipeline.</li> </ul> <p>On the very first run, it records everything currently in your feed as "already posted" without tooting any of it. That matters, otherwise your entire back catalogue lands on Mastodon in one go the moment you turn this on. From then on, only genuinely new items get posted.</p> <h2>The script</h2> <p>This is Node, using <code>rss-parser</code> to handle the feed and the built-in <code>fetch</code> for talking to both platforms' APIs. Each post gets a fixed prefix and the title, the feed's <code><summary></code> (Mastodon only, see below), the link, and hashtags built from any <code><category></code> elements on the item. If the item has an enclosure or <code><media:content></code> image, it downloads that once and attaches it natively wherever it's needed, rather than just linking to it.</p> <p>Mastodon and Bluesky post independently of each other. If one fails, the other still goes out, and only the failed one is retried on the next run.</p> <p>You can <a href="https://gist.github.com/timklapdor/54eeccd33dae2eeac307665872090f80" target="_blank" rel="noopener noreferrer">check out the script on GitHub</a>.</p> <h2>Adding hashtags</h2> <p>Hashtags come from RSS's own <code><category></code> element, which <code>rss-parser</code> already collects into <code>item.categories</code> with no extra parsing needed. Add a field to a post's front matter:</p> <pre><code class="language-yaml"> --- title: All in the Verbs date: 2026-04-10 mastodonTags: [ai, learningdesign] --- </code></pre> <p>Keeping it a separate field from whatever tags your site already uses for its own taxonomy or nav matters, since those often include values like <code>"posts"</code> or <code>"nav"</code> you wouldn't want turning into hashtags. Then in the feed template:</p> <pre><code class="language-liquid"> {% for post in collections.posts %} <item> <title>{{ post.data.title }}</title> <link>{{ post.url | url }}</link> <guid>{{ post.url | url }}</guid> <summary>{{ post.data.summary }}</summary> {% for tag in post.data.mastodonTags %} <category>{{ tag }}</category> {% endfor %} </item> {% endfor %} </code></pre> <p>Since it's a plain loop over an optional array, posts without <code>mastodonTags</code> just emit zero <code><category></code> tags, no conditional needed. A post with <code>[ai, learningdesign]</code> in front matter comes out as <code>#Ai #LearningDesign</code> in the toot.</p> <h2>The workflow</h2> <p>If you already build and deploy your site through a GitHub Actions workflow as I do, you can fold this in as a second job instead, gated with <code>needs:</code> so it only runs after a successful deploy:</p> <pre><code class="language-yaml"> toot: needs: deploy if: github.ref == 'refs/heads/main' && success() runs-on: ubuntu-22.04 permissions: contents: write steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: "24" - name: Wait for Pages CDN to catch up run: sleep 60 # The deploy job just pushed to gh-pages, but GitHub's CDN can take # a short while to actually serve the new content. Without this, # the feed fetch below can occasionally hit a stale cached version # of feed.xml, missing whatever changed in the most recent deploy. - name: Install dependencies run: npm install rss-parser - name: Check feed and post new items env: FEED_URL: ${{ vars.FEED_URL }} MASTODON_INSTANCE_URL: ${{ vars.MASTODON_INSTANCE_URL }} MASTODON_TOKEN: ${{ secrets.MASTODON_TOKEN }} BLUESKY_HANDLE: ${{ vars.BLUESKY_HANDLE }} BLUESKY_APP_PASSWORD: ${{ secrets.BLUESKY_APP_PASSWORD }} run: node scripts/toot-new-posts.mjs - name: Commit updated state file run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git add data/posted-guids.json git diff --staged --quiet || git commit -m "Update posted-guids state [skip ci]" git push </code></pre> <p>If you want, you could run this as a standalone version that polls every 30 minutes, independent of however your site actually gets built and deployed. That might be useful for you, but that doesn't suit the light touch workflow I have on my personal blog.</p> <h2>Setting it up</h2> <ol> <li><strong>Get a Mastodon access token.</strong> Settings → Development → New Application on your instance, tick <code>write:statuses</code>, create it, copy the token.</li> <li><strong>Add the two files</strong> to your repo: <code>scripts/toot-new-posts.mjs</code> and an empty <code>data/posted-guids.json</code> containing just <code>[]</code>, both at the repo root, plus whichever workflow file fits your setup.</li> <li><strong>Add repo secrets and variables</strong> (Settings → Secrets and variables → Actions): secret <code>MASTODON_TOKEN</code>, and variables <code>FEED_URL</code> and <code>MASTODON_INSTANCE_URL</code>.</li> <li><strong>Push, and check the Actions tab.</strong> The first run should complete without posting anything, just seeding the state file with your existing posts.</li> <li><strong>Publish something new</strong> to confirm it actually toots.</li> </ol> <p>If you want a different post format, hashtags, a different field from the feed, whatever, it's all in <code>buildStatusText()</code>. That's the only function you'd realistically need to touch.</p> <h2>Adding Bluesky</h2> <p>Bluesky (AT Protocol) is a different enough API that it's worth knowing the gaps before turning it on:</p> <ul> <li><strong>Auth is a handle + app password</strong>, not a static token. Generate one in Bluesky's Settings → App Passwords (not your main login password), and the script exchanges it for a short-lived session at the start of each run.</li> <li><strong>300 characters, not ~500.</strong> There isn't room for title, link, hashtags, <em>and</em> a summary, so the Bluesky post drops the summary entirely and just sends the prefix, title, link, and hashtags. If the title alone is long, it's truncated with an ellipsis so the link and hashtags always survive intact.</li> <li><strong>Links and hashtags need "facets."</strong> Unlike Mastodon, having a URL or <code>#tag</code> in the text doesn't make it clickable or searchable, AT Protocol needs an explicit byte-range annotation pointing at each one.</li> <li><strong>Images upload through a different endpoint</strong> (a blob upload, not a multipart form), producing a blob reference rather than a media ID.</li> </ul> <p>It's genuinely optional. Without both <code>BLUESKY_HANDLE</code> and <code>BLUESKY_APP_PASSWORD</code> set, Bluesky posting is skipped entirely and Mastodon behaves exactly as before. Turn it on later and it gets its own silent first-run seed, same as Mastodon did, so it won't try to post your whole archive the moment it's switched on. State entries are tracked per platform (<code>mastodon:<id></code>, <code>bluesky:<id></code>), so a failure on one never causes a duplicate or a skip on the other.</p> <h2>One gotcha: give your feed items a stable guid</h2> <p>The script tells "already posted" from "new" using each item's <code>guid</code>, falling back to its <code>link</code> if the feed doesn't set one:</p> <pre><code class="language-js">function itemId(item) { return item.guid || item.id || item.link; } </code></pre> <p>If your feed template doesn't set an explicit <code><guid></code>, most static site generators fall back to using the post's URL as the guid too, which is fine right up until that URL changes. Fix a typo in a slug, correct a post's date, move it between folders, and the URL changes, which means the script sees a "new" item it's never encountered before and toots it again, even though nothing about the content actually changed. I hit exactly this: correcting a post's date shifted its permalink by one day, and the next deploy dutifully re-tooted it as if it were brand new.</p> <p>Nothing breaks when this happens, you just get a duplicate toot linking to the same post under its corrected URL. Harmless, but avoidable.</p> <h2>Would this work on Codeberg?</h2> <p>Mostly, with caveats. Codeberg (running Forgejo) has two CI options: Woodpecker CI, which is the more established, production-ready offering, and Forgejo Actions, which is closer to GitHub Actions in syntax but still in public alpha there and comes with tighter resource and time limits on hosted runs. Forgejo Actions supports scheduled triggers via cron, same as GitHub, so the shape of this workflow translates.</p> <p>The friction is in the actions themselves. <code>actions/checkout</code> and <code>actions/setup-node</code> aren't guaranteed to exist as-is; Forgejo mirrors some GitHub Actions under its own namespace, but not the whole marketplace, and compatibility isn't guaranteed. You'd likely swap those for Forgejo-native equivalents or a plain shell script that does the checkout and Node setup manually. The script itself doesn't care where it runs, since it's just Node hitting two HTTP APIs, so the actual logic ports over unchanged. The wrapper around it is what would need adjusting.</p> <p>If your instinct is host-your-own generally, this would also run fine as a plain cron job on any server with Node installed, GitHub Actions isn't a requirement, just a convenient place to run it for free without maintaining a server yourself.</p>
Stored topics

Every stored assignment is listed, including rows below the current cutoff (0.75), so a missing tag can be explained.

Concept Code Score Meter Margin Origin Path
economy, business and finance economy-business-and-finance 0.99 4.39 ↑derived from 20000231 economy, business and finance
products and services products-and-services 0.99 4.39 ↑derived from 20000231 economy, business and finance > products and services
computing and information technology computing-and-information-technology 0.99 4.39 ↑derived from 20000231 economy, business and finance > products and services > computing and information technology
software and applications software-and-applications 0.99 4.39 direct economy, business and finance > products and services > computing and information technology > software and applications
science and technology science-and-technology 0.99 4.25 ↑derived from 20000763 science and technology
technology and engineering technology-and-engineering 0.99 4.25 ↑derived from 20000763 science and technology > technology and engineering
information technology and computer science information-technology-and-computer 0.99 4.25 direct science and technology > technology and engineering > information technology and computer science
Cryptography cryptography 0.99 4.24 direct science and technology > technology and engineering > information technology and computer science > Cryptography
arts, culture, entertainment and media arts-culture-entertainment-and-media 0.97 3.60 ↑derived from local:blogging arts, culture, entertainment and media
mass media mass-media 0.97 3.60 ↑derived from local:blogging arts, culture, entertainment and media > mass media
social media social-media 0.97 3.60 ↑derived from local:blogging arts, culture, entertainment and media > mass media > social media
Blogging blogging 0.97 3.60 direct arts, culture, entertainment and media > mass media > social media > Blogging
Software development software-development 0.92 2.46 direct economy, business and finance > products and services > computing and information technology > software and applications > Software development
Information security information-security 0.92 2.41 direct science and technology > technology and engineering > information technology and computer science > Information security
Open source open-source 0.89 2.13 direct economy, business and finance > products and services > computing and information technology > software and applications > Open source
lifestyle and leisure lifestyle-and-leisure 0.66 0.67 ↑derived from 20000550 lifestyle and leisure
leisure leisure 0.66 0.67 ↑derived from 20000550 lifestyle and leisure > leisure
hobby hobby 0.66 0.67 direct lifestyle and leisure > leisure > hobby
Self-hosting self-hosting 0.51 0.05 direct economy, business and finance > products and services > computing and information technology > Self-hosting
society society 0.46 -0.18 ↑derived from 20001300 society
fundamental rights fundamental-rights 0.46 -0.18 ↑derived from 20001300 society > fundamental rights
privacy privacy 0.46 -0.18 direct society > fundamental rights > privacy
crime, law and justice crime-law-and-justice 0.41 -0.38 ↑derived from 20000086 crime, law and justice
crime crime 0.41 -0.38 ↑derived from 20000086 crime, law and justice > crime
cyber crime cyber-crime 0.41 -0.38 direct crime, law and justice > crime > cyber crime
artificial intelligence artificial-intelligence 0.35 -0.63 direct science and technology > technology and engineering > information technology and computer science > artificial intelligence
media and entertainment industry media-and-entertainment-industry 0.22 -1.28 ↑derived from 20001293 economy, business and finance > products and services > media and entertainment industry
streaming service streaming-service 0.22 -1.28 direct economy, business and finance > products and services > media and entertainment industry > streaming service
books and publishing books-and-publishing 0.18 -1.53 direct economy, business and finance > products and services > media and entertainment industry > books and publishing
environment environment 0.14 -1.84 direct environment
health health 0.13 -1.88 ↑derived from 20000458 health
disease and condition disease-and-condition 0.13 -1.88 ↑derived from 20000458 health > disease and condition
mental health and disorder mental-health-and-disorder 0.13 -1.88 direct health > disease and condition > mental health and disorder
podcast podcast 0.12 -1.95 direct economy, business and finance > products and services > media and entertainment industry > podcast
arts and entertainment arts-and-entertainment 0.12 -2.00 ↑derived from 20000036 arts, culture, entertainment and media > arts and entertainment
visual arts visual-arts 0.12 -2.00 ↑derived from 20000036 arts, culture, entertainment and media > arts and entertainment > visual arts
photography photography 0.12 -2.00 direct arts, culture, entertainment and media > arts and entertainment > visual arts > photography
film industry film-industry 0.11 -2.11 direct economy, business and finance > products and services > media and entertainment industry > film industry
scientific research scientific-research 0.10 -2.16 ↑derived from 20000739 science and technology > scientific research
scientific exploration scientific-exploration 0.10 -2.16 ↑derived from 20000739 science and technology > scientific research > scientific exploration
space exploration space-exploration 0.10 -2.16 direct science and technology > scientific research > scientific exploration > space exploration
consumer goods consumer-goods 0.10 -2.21 ↑derived from 20001160 economy, business and finance > products and services > consumer goods
handicrafts handicrafts 0.10 -2.21 direct economy, business and finance > products and services > consumer goods > handicrafts
game game 0.09 -2.26 ↑derived from local:mmorpg lifestyle and leisure > leisure > game
video game video-game 0.09 -2.26 ↑derived from local:mmorpg lifestyle and leisure > leisure > game > video game
MMORPG mmorpg 0.09 -2.26 direct lifestyle and leisure > leisure > game > video game > MMORPG
politics and government politics-and-government 0.09 -2.33 direct politics and government
literature literature 0.08 -2.43 direct arts, culture, entertainment and media > arts and entertainment > literature
Tabletop gaming tabletop-gaming 0.07 -2.59 direct lifestyle and leisure > leisure > game > Tabletop gaming
natural science natural-science 0.07 -2.59 ↑derived from 20000719 science and technology > natural science
biology biology 0.07 -2.59 direct science and technology > natural science > biology
sport sport 0.06 -2.68 ↑derived from 20001183 sport
competition discipline competition-discipline 0.06 -2.68 ↑derived from 20001183 sport > competition discipline
eSports esports 0.06 -2.68 direct sport > competition discipline > eSports
television television 0.06 -2.72 direct arts, culture, entertainment and media > mass media > television
animation animation 0.05 -3.01 direct arts, culture, entertainment and media > arts and entertainment > animation
climate change climate-change 0.04 -3.16 direct environment > climate change
education education 0.04 -3.17 direct education
family family 0.04 -3.28 direct society > family
social sciences social-sciences 0.03 -3.43 direct science and technology > social sciences
labour labour 0.03 -3.45 direct labour
disaster, accident and emergency incident disaster-accident-and-emergency-incident 0.03 -3.45 direct disaster, accident and emergency incident
conflict, war and peace conflict-war-and-peace 0.03 -3.64 direct conflict, war and peace
exercise and fitness exercise-and-fitness 0.02 -3.72 direct lifestyle and leisure > wellness > exercise and fitness
wellness wellness 0.02 -3.72 ↑derived from 20001239 lifestyle and leisure > wellness
religion religion 0.02 -4.02 direct religion
health treatment and procedure health-treatment-and-procedure 0.02 -4.08 ↑derived from 20000465 health > health treatment and procedure
diet diet 0.02 -4.08 direct health > health treatment and procedure > diet
history history 0.02 -4.08 direct science and technology > social sciences > history
weather weather 0.01 -4.22 ↑derived from 20001128 weather
weather forecast weather-forecast 0.01 -4.22 direct weather > weather forecast
culture culture 0.01 -4.40 direct arts, culture, entertainment and media > culture
music music 0.01 -4.43 direct arts, culture, entertainment and media > arts and entertainment > music
human interest human-interest 0.01 -4.60 direct human interest
food and drink enthusiasm food-and-drink-enthusiasm 0.01 -4.61 direct lifestyle and leisure > leisure > hobby > food and drink enthusiasm
card game card-game 0.01 -4.71 direct lifestyle and leisure > leisure > game > card game
travel and tourism travel-and-tourism 0.01 -4.72 direct lifestyle and leisure > leisure > travel and tourism
award and prize award-and-prize 0.01 -4.79 direct human interest > award and prize
board game board-game 0.01 -4.82 direct lifestyle and leisure > leisure > game > board game
public health public-health 0.01 -4.89 direct health > public health
lifestyle lifestyle 0.00 -5.59 ↑derived from 20001257 lifestyle and leisure > lifestyle
house and home house-and-home 0.00 -5.59 ↑derived from 20001257 lifestyle and leisure > lifestyle > house and home
gardening gardening 0.00 -5.59 direct lifestyle and leisure > lifestyle > house and home > gardening

← Back to posts

feedtagger 0.1.0-dev model: modernbert-base-zeroshot-v2.0/int8@all-s256 2115 posts 0 unclassified min score 0.75