DEV Community

Roberto Luna
Roberto Luna

Posted on

Adding Bluesky Support to Content‑Automation: Updating Metadata and File Generation

Adding Bluesky Support to Content‑Automation: Updating Metadata and File Generation


TL;DR:

I extended the content‑automation repo to support Bluesky by adding bluesky_uris arrays to every metadata.json and creating empty bluesky_{lang}.json files. This change lets the publisher script track Bluesky URLs per language without breaking existing workflows.

The Problem

Our automation pipeline only tracked Medium and Substack posts. When we started publishing to Bluesky, the script crashed because the metadata schema didn’t include a place to store Bluesky URLs or a flag to indicate publication status. The error stack was:

TypeError: Cannot read property 'bluesky_uris' of undefined
    at publishPost (/usr/src/content-automation/scripts/publish.js:112:23)
Enter fullscreen mode Exit fullscreen mode

The symptom was a silent failure: posts would be generated for Medium/Substack, but Bluesky would never get a URI, and the metadata remained stale.

What I Tried First

I first added a top‑level bluesky_published boolean to the existing metadata.json:

{
  "bluesky_published": false,
  "bluesky_uris": {}
}
Enter fullscreen mode Exit fullscreen mode

But the script still failed because it expected bluesky_uris to be an object with language keys. Adding a default {} didn’t solve the problem, and the code would later try to push into bluesky_uris.es or bluesky_uris.en, causing a Cannot read property 'push' of undefined error.

Next, I tried to patch the publishing function to create the language keys on the fly. That worked for the first run but left the metadata in an inconsistent state after subsequent runs, because the script would overwrite the whole bluesky_uris object, losing previously stored URLs.

The Implementation

1. Normalizing metadata.json

I updated every metadata.json under content/2026/07/30/VS/ and content/2026/07/30/content-automation/ to include a fully‑structured bluesky_uris object:

{
  "medium_generated": false,
  "substack_generated": false,
  "bluesky_published": false,
  "bluesky_uris": {
    "es": [],
    "en": []
  },
  "craft_..."
}
Enter fullscreen mode Exit fullscreen mode

This change is reflected in the diff:

@@ -17,6 +17,9 @@
   "medium_generated": false,
   "substack_generated": false,
   "bluesky_published": false,
-  "bluesky_uris": {},
+  "bluesky_uris": {
+    "es": [],
+    "en": []
+  },
   "craft_
Enter fullscreen mode Exit fullscreen mode

The nested arrays allow us to push multiple URLs per language without mutating the top‑level object.

2. Adding Empty Bluesky Payload Files

For each language, I added a new file bluesky_{lang}.json that starts as an empty array. These files are the payloads that the publisher script will later fill with the post content for Bluesky.

[added] content/2026/07/30/VS/bluesky_en.json (+1/-0)
@@ -0,0 +1 @@
+[]
\ No newline at end of file
[added] content/2026/07/30/VS/bluesky_es.json (+1/-0)
@@ -0,0 +1 @@
+[]
\ No newline at end of file
Enter fullscreen mode Exit fullscreen mode

The empty arrays prevent the script from throwing when it tries to read or write to the payload.

3. Generating the Dev.to Article Metadata

I also added a fully‑fledged metadata.json for the new Dev.to article:

{
  "repo": "VS",
  "date": "2026-07-30",
  "languages": ["es", "en"],
  "topics": ["Productivity", "Docker", "Networking"],
  "commits": 4,
  "bluesky_published": false,
  "bluesky_uris": {"es": [], "en": []},
  "medium_generated": false,
  "substack_generated": false
}
Enter fullscreen mode Exit fullscreen mode

This file lives in content/2026/07/30/VS/metadata.json. The script now reads this file to decide which platforms to publish to and where to store the resulting URLs.

4. Updating the Publisher Script (Pseudo‑code)

// publish.js
const meta = loadMetadata(filePath);
const langs = meta.languages;

langs.forEach(lang => {
  // Medium
  if (!meta.medium_generated) {
    const mediumUrl = publishMedium(post, lang);
    meta.medium_uris[lang] = mediumUrl;
    meta.medium_generated = true;
  }

  // Substack
  if (!meta.substack_generated) {
    const substackUrl = publishSubstack(post, lang);
    meta.substack_uris[lang] = substackUrl;
    meta.substack_generated = true;
  }

  // Bluesky
  if (!meta.bluesky_published) {
    const blueskyPost = buildBlueskyPayload(post, lang);
    fs.writeFileSync(`bluesky_${lang}.json`, JSON.stringify(blueskyPost, null, 2));
    // Assume we get a URI after posting
    const blueskyUrl = publishBluesky(blueskyPost);
    meta.bluesky_uris[lang].push(blueskyUrl);
    meta.bluesky_published = true;
  }
});

saveMetadata(meta);
Enter fullscreen mode Exit fullscreen mode

The key change is the meta.bluesky_uris[lang].push(blueskyUrl) line, which now works because the arrays are pre‑initialized.

5. Commit History

  • bbb3689c: Added bluesky_uris arrays to existing metadata files.
  • 32f8df51: Added new bluesky_{lang}.json files, a new Dev.to article metadata.json, and placeholder markdown files for Medium/Substack/Bluesky.

Key Takeaway

When extending a metadata schema for multi‑platform publishing, always pre‑define nested objects and arrays per language. This prevents runtime errors and keeps the schema forward‑compatible. A small structural change in metadata.json can save hours of debugging when new platforms are added.

What's Next

  1. Implement the actual Bluesky API integration: Build a publishBluesky function that sends the

Part of my Build in Public series — sharing the real process of building SaaS projects from Playa del Carmen, México.

Repo: zaerohell/content-automation · 2026-07-31

#playadev #buildinpublic

Top comments (1)

Collapse
 
vic_xie_9bed0062d5fd73d12 profile image
vic xie

Nice write-up! For devs who deal with messy copied text, TextStow might help — it's a Mac menu bar tool combining clipboard history with prompt templates and text cleanup. Free: textstow.com