I use FeedPress to handle this blog’s RSS feeds. It reads my statically-generated RSS feed and gives me subscriber stats, as well as the ability to send new posts to social media endpoints. But it lacks Mastodon integration, and I’m spending most of my time on Mastodon lately (find me at @ttscoff@nojack.easydns.ca). So I wanted my new posts on this blog to automatically post to Mastodon. The script in this post could be used with any blog that generates an RSS feed, but is mostly geared toward static blogs.

I got started with a post from Dr. Drang called “Announcing New Posts on Mastodon”. It included a Python script that I referenced to create a Ruby script for my needs. Thanks to the Doc for getting me started!

You can find the script here. See below for configuration and usage.

Configuration

To configure the script, at minimum, you need your Mastodon endpoint and an auth key. Your endpoint is generally your Mastodon instance URL with /api/v1/statuses appended to it, e.g. https://mastodon.social/api/v1/statuses. I’m not sure if this is ever not the case. The auth key can be generated by going to your Mastodon homepage, clicking the Settings gear icon, and choosing Development. Create a new application with any name, your blog url as the website, and leave the Redirect URI as is. Make sure it at least has permission to write:statuses. You’ll then see a client key, a client secret, and an access token. All you need is the access token for this script.

Set up the script by updating MASTODON_ENDPOINT (as described above) and MASTODON_AUTH (with your access token).

You’ll also need to configure the RSS feed. The script can parse JSON or XML feeds of your blog (local files or URLs) to find the latest post. If you want to use a JSON feed, set JSON_FEED to either a local JSON file (~ is fine for your HOME directory) or a JSON feed url. If you’re using an XML feed, set JSON_FEED to nil, and set RSS_FEED to either a local file or url. With FeedPress, my JSON is generated from my XML and both feeds can take a few minutes to update, and the most immediate list of posts I have is the local atom.xml file generated with my site, so that’s what I have mine set to. That way I can run this script immediately after a new deploy and still get the latest post.

The rest of the settings in the script are the template used for the status (POST_TEMPLATE), an optional query string that can be appended to the URL for campaign tracking, etc. (QUERY_STRING), and then a few options for updating front matter. I imagine most people won’t need the front matter bit, but the options are commented if you need them.

It records what it posts to a local JSON file (location set with TOOTS_DB), so it won’t post the same thing twice. Optionally, it can go back and add a mastodon: [TOOT_URL] key to the original Markdown for the post. I’m using this to render a Mastodon link on each post that links to the toot so that people can reply/share it from their Mastodon account (since Mastodon doesn’t/can’t offer a “Toot This” link the way Twitter can, as far as I know)1. For most people this step is probably unnecessary, and can be disabled entirely by setting the ADD_FRONT_MATTER constant at the top of the script to false.

Usage

All of the necessary config is commented, should be pretty easy to get running. Rather than rely on Ruby’s Net/HTTP library, it just uses curl to make the call, so you need a curl binary available (this is default on most systems).

To use the script, save it as toot.rb and run chmod a+x toot.rb on it. If properly configured, you can just run ./toot.rb to toot the latest post from your feed. If you want to test, use --debug (or just -d), and it will set the visibility=direct parameter that will make the resulting Mastodon post show up only for you (private).

You can run ./toot.rb --last-ten to toot the last 10 posts. Use ./toot.rb -h to see available options.

By default, when the script creates a new toot, it will notify you on STDERR. It will also output NEW_TOOT: [TOOT_URI] to STDOUT, which is what I use to detect whether a new toot was created and trigger a rebuild. You can disable all output with --silent (or -s).

Updating Front Matter

If you want to use the front matter updating “as is” in the script, it requires that your blog posts be named to match the url. It takes the url path, substitutes slashes for dashes, and removes index.html and any trailing slash from the url to create the slug, then adds .md. So if your post is at https://brettterpstra.com/2023/04/21/im-on-shrooms-like-right-now/, the resulting file it will look for is 2023-04-21-im-on-shrooms-like-right-now.md. If your naming scheme is anything else, it would require manual editing to work.

For reference, I have this script run as part of my Rakefile :deploy task. Once the site is fully deployed, it runs toot.rb and toots the latest post. If the script returns NEW_TOOT on STDOUT (meaning the post has never been tooted before and the front matter for the tooted post has been updated), it will re-render and deploy the site again so that the Mastodon link on the newest post goes live. Presumably, although that’s about to be tested when I publish this post. It’s a bit of a hacky workaround, but I can’t toot it until the post is live, and I can’t get the Mastodon link until it’s tooted, so it takes twice to get it right. It will only ever toot a post once, so updates and re-renders won’t be doubled.

task :generate_deploy do
  # [Deploy tasks...]
  toot
end

def toot
  exe = File.join(__dir__, 'toot.rb')
  res = `#{exe}`.strip
  Rake::Task[:generate_deploy].invoke if res =~ /^NEW_TOOT/
end

In Closing

I hope that FeedPress eventually adds Mastodon sharing to its social services, but my current setup really likes having the actual URL for the associated Mastodon post available, so that wouldn’t do much good in this scenario anyway. I hope this script is helpful to other people using static blog setups and wanting Mastodon integration.

If you do generate your own RSS feed and don’t currently get statistics from it, I do recommend checking out FeedPress. It’s affordable, and great for blogs and podcasts.

Again, you can find the script in this gist.

  1. I considered just using JavaScript to load the JSON record that this script creates and inject a Mastodon link on the front end (avoiding multiple renders), but for a few reasons decided I’d rather add a hardcoded link at render time.