RyanParsleyDotCom

I Finally Got POSSE Automated

Last updated on

Almost a year ago, I wrote about my game plan for POSSE. Like many automation projects, it sat on my back burner making me feel guilty every time I manually posted to social media. Today I finally built an automated workflow I’m proud to share.

Static sites are the bees knees, but…

My blog runs on Astro, which is great, but I wanted to syndicate my ephemera posts (notes in IndieWeb parlance) automatically to social media. The rub is you want bidirectional linking and SSG has a bit of a chicken and egg problem.

The gist is this, publishing content on your own site spawns the creation of a post elsewhere. The syndicated post links to the canonical post and the canonical post links to the syndication. The rub is, when you write markdown, there is no syndicated post to mention in the frontmatter. This is a little beyond the sweet spot of static sites, but not unachievable.

The old way was fine but not great

  • I’d write a post, publish it
  • Remember to copy the content to Mastodon
  • Do the same for Bluesky
  • Hope I didn’t mess up the formatting

It worked, but I aspired for a more seamless process. Often, I’d regress to simply using the mastodon app and only occasionally link up an ephemeral post after the fact.

The new hotness

I spent a weekend refactoring everything into a proper Astro integration. Here’s what the new system does:

  • Scans my ephemera folder automatically
  • Finds posts from the last 24 hours
  • Skips ones I’ve already shared
  • Posts to Mastodon and Bluesky
  • Handles images and links properly
  • Logs everything so I can see what happened

The best part? It runs every time I build the site. No extra steps, no remembering to do it.

My first Astro integration

Since my site is served via CloudCannon, my first instinct was to use their build hooks. I got a version working and it was fine but testing was manual and I accidentally spammed my mastodon account via a bug. This had me thinking about alternative implementation details.

Getting this working required diving deeper into Astro’s API. Not gonna lie, the idea of using an Astro Integration came from me using OpenCode as a rubber duck. I’ve wired up a few 3rd party integrations but making a custom one hadn’t occurred to me. I’m glad I went through the effort though because testing is way easier and using Astro hooks feels more right than the previous approach did.

What that looks like

Here’s the core integration structure:

// src/integrations/posse.ts
export default function posseIntegration(options: PosseOptions = {}) {
  const { mastodon, bluesky, dryRun = false, maxPosts = 3 } = options;

  return {
    name: "posse-syndication",
    hooks: {
      "astro:build:done": async ({ logger }) => {
        try {
          await runSyndication({ mastodon, bluesky, dryRun, maxPosts, logger });
          logger.info("POSSE: Syndication process completed successfully");
        } catch (error) {
          const errorMessage =
            error instanceof Error ? error.message : String(error);
          logger.error(`POSSE: Syndication process failed: ${errorMessage}`);
        }
      },
    },
  };
}

Now it just works. Every time I deploy, it syndicates new posts. No extra steps.

Code to post to the fediverse

This function handles content formatting for different platforms:

function generatePostContent(
  data: EphemeraData,
  canonicalUrl: string,
  body: string,
  platform: "mastodon" | "bluesky",
): string {
  const initialContent = body?.trim() ? cleanContentForSocial(body.trim()) : "";
  const content =
    !initialContent || initialContent.length < 10
      ? data.title || "New ephemera post"
      : initialContent;

  const maxLength = platform === "bluesky" ? 300 : 400;
  // Reserve space for "\n\n" + canonicalUrl
  const urlSuffix = `\n\n${canonicalUrl}`;
  // Add safety buffer for grapheme counting differences and JSON overhead
  const safetyBuffer = platform === "bluesky" ? 20 : 10;
  const availableContentLength = maxLength - urlSuffix.length - safetyBuffer;

  const finalContent =
    content.length > availableContentLength
      ? content.substring(0, availableContentLength - 3) + "..."
      : content;

  return `${finalContent}${urlSuffix}`;
}

More like a good start than a happy ending

This automation works and is useful today, but it’s a solid foundation for bigger goals. For now though, I’m thrilled that my posts finally syndicate themselves—no more manual copying and pasting, no more forgotten social media updates. I can write a silly thing in markdown once, and trust copies find their way to appropriate silos.

Continue the converstion elsewhere

Let's chat more on the platform of your choice.

Published by