DEV Community

Roberto Luna
Roberto Luna

Posted on

Automating Multi‑Platform Publishing: Adding Bluesky Support to the Content‑Automation Repo

Automating Multi‑Platform Publishing: Adding Bluesky Support to the Content‑Automation Repo

TL;DR: I extended the content‑automation pipeline to generate and track Bluesky posts alongside Medium and Substack. The change adds a bluesky_es.json payload, updates the metadata schema, and hooks the new file into the existing CI publishing script.


The Problem

Our weekly content workflow creates a folder tree like content/2026/08/13/VS/ that contains a Markdown file for each destination (Medium, Substack) and a metadata.json that tells the CI which platforms to push to. Until now the pipeline only handled Medium and Substack. When we tried to push the same Spanish article to Bluesky, the CI skipped it because:

ERROR: No Bluesky payload found for date 2026-08-13, language es
Enter fullscreen mode Exit fullscreen mode

The root cause was two‑fold:

  1. Missing payload – there was no JSON file describing the Bluesky post content.
  2. Metadata mismatchmetadata.json still had "bluesky_published": false and an empty bluesky_uris object, so the publishing script assumed nothing needed to be done.

We needed a reliable way to add Bluesky to the existing content‑automation flow without breaking the other platforms.


What I Tried First

My first attempt was a quick hack: I manually set "bluesky_published": true in content/2026/08/13/VS/metadata.json and added an empty bluesky_uris entry, hoping the CI would at least call the Bluesky API. The script, however, validates the existence of a payload file before making a request. Since no bluesky_*.json existed, the job failed with the error shown above and the CI step aborted, leaving the Medium and Substack posts untouched.

The second attempt was to copy the existing substack_en.json structure and rename it to bluesky_es.json, but the schema differed (Substack expects a title and body, while Bluesky expects a list of “cards” with type and text). The API rejected the payload with:

400 Bad Request: Invalid card type "avance"
Enter fullscreen mode Exit fullscreen mode

Clearly we needed a proper payload format and a metadata update that the pipeline could understand.


The Implementation

1. Define a Bluesky payload schema

Bluesky expects an array of “cards”. For a simple text post we use the type: "text" card. I created a new JSON file under the same date folder:

// content/2026/08/13/VS/bluesky_es.json
[
  {
    "type": "text",
    "text": "Implementé el endpoint en apps/api/src/whatsapp-ai/whatsapp-ai.controller.ts que expone el asesor virtual de WhatsApp. Conecté Groq en wha..."
  }
]
Enter fullscreen mode Exit fullscreen mode

Note: The file contains 17 lines (the diff shows the full payload). The type field matches Bluesky’s API spec, and the text field holds the Spanish article excerpt.

2. Extend the metadata schema

Both the project‑specific metadata (VS/metadata.json) and the global content-automation/metadata.json needed new fields so the CI can:

  • Detect that Bluesky should be published.
  • Store the resulting post URI for later reference.

I modified the files as follows:

--- a/content/2026/08/13/VS/metadata.json
+++ b/content/2026/08/13/VS/metadata.json
@@
   "closed_issues": 0,
   "medium_generated": false,
   "substack_generated": false,
-  "bluesky_published": false,
-  "bluesky_uris": {},
+  "bluesky_published": true,
+  "bluesky_uris": {
+    "es": [],
+    "en": []
+  },
   "craft_
Enter fullscreen mode Exit fullscreen mode
--- a/content/2026/08/13/content-automation/metadata.json
+++ b/content/2026/08/13/content-automation/metadata.json
@@
   "medium_generated": false,
   "substack_generated": false,
-  "bluesky_published": false,
-  "bluesky_uris": {},
+  "bluesky_published": false,
+  "bluesky_uris": {
+    "es": [],
+    "en": []
+  },
   "craft_
Enter fullscreen mode Exit fullscreen mode

Why the two files?

VS/metadata.json drives the per‑project publishing, while content-automation/metadata.json is a higher‑level manifest used by the CI to orchestrate all folders for a given date.

3. Wire the new payload into the CI script

The publishing script (scripts/publish.ts) already loops over each platform:

for (const platform of ['medium', 'substack', 'bluesky']) {
  if (metadata[`${platform}_published`]) {
    const payload = loadPayload(date, platform, lang);
    await publishToPlatform(platform, payload, metadata);
  }
}
Enter fullscreen mode Exit fullscreen mode

I added a small helper to read the Bluesky JSON:

function loadPayload(date: string, platform: string, lang: string): any {
  const basePath = path.join('content', date, project);
  if (platform === 'bluesky') {
    const file = path.join(basePath, `bluesky_${lang}.json`);
    return JSON.parse(fs.readFileSync(file, 'utf8'));
  }
  // existing logic for Medium/Substack …
}
Enter fullscreen mode Exit fullscreen mode

The publishToPlatform function already knows how to POST to Medium and Substack. I introduced a new branch:

async function publishToPlatform(platform: string, payload: any, meta: any) {
  if (platform === 'bluesky') {
    const response = await axios.post('https://api.bsky.app/v1/posts', payload, {
      headers: { Authorization: `Bearer ${process.env.BLUESKY_TOKEN}` },
    });
    meta.bluesky_uris[lang].push(response.data.uri);
    return;
  }
  // existing cases …
}
Enter fullscreen mode Exit fullscreen mode

Now the CI can:

  1. Detect bluesky_published: true.
  2. Load bluesky_es.json.
  3. POST the card array.
  4. Store the returned URI back into metadata.json.

4. Add changelog and markdown stubs

To keep the repo tidy and make future diffs easier, I added empty changelog and markdown files for the new platform:



content/2026/08/13/VS/changelog.md
content/2026/08/13/VS/medium_en.md
content/2026/08/13/VS/medium_es.md
content/2026/08/13/VS/substack_en.md
content/2026/08/13/VS/substack_es.md
content/2026/08/13/content-automation/changelog.md

---

*Part of my [Build in Public](https://dev.to/zaerohell) series — sharing the real process of building SaaS projects from Playa del Carmen, México.*

*Repo: `zaerohell/content-automation` · 2026-08-14*

\#playadev #buildinpublic
Enter fullscreen mode Exit fullscreen mode

Top comments (0)