DEV Community

Roberto Luna
Roberto Luna

Posted on

Adding Multilingual Bluesky URI Tracking to the Content‑Automation Metadata Schema

Adding Multilingual Bluesky URI Tracking to the Content‑Automation Metadata Schema

TL;DR: I extended the metadata.json schema used by the content‑automation pipeline to store separate Bluesky URIs per language. This change prevents runtime key‑errors when the publishing step expects bluesky_uris.es and bluesky_uris.en arrays, and it lets the same file be reused for both English and Spanish posts.


The Problem

Our automation runs nightly and pulls a list of pending posts from the content/ folder. Each post has a companion metadata.json that tracks which platforms have already been generated (medium_generated, substack_generated, bluesky_published) and, for Bluesky, a map of URIs that were already posted.

Originally the bluesky_uris field was defined as an empty object:

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

When the publishing script (scripts/publish_bluesky.js) tried to push a new URI it did:

metadata.bluesky_uris[lang].push(uri);
Enter fullscreen mode Exit fullscreen mode

Because metadata.bluesky_uris[lang] was undefined for both es and en, the script threw:

TypeError: Cannot read property 'push' of undefined
    at publishBluesky (scripts/publish_bluesky.js:42:27)
Enter fullscreen mode Exit fullscreen mode

The failure halted the whole pipeline, leaving the rest of the post generation untouched. The bug was hidden because the first language we processed in a given run happened to be es, and the script never hit the push line for a missing key when the array existed.

What I Tried First

My first attempt was to guard the push with a fallback:

(metadata.bluesky_uris[lang] || []).push(uri);
Enter fullscreen mode Exit fullscreen mode

That silenced the error, but it also meant the new URI never got persisted back to metadata.json. The next run would try to push the same URI again, causing duplicate posts on Bluesky. I also tried mutating the object directly:

if (!metadata.bluesky_uris[lang]) {
  metadata.bluesky_uris[lang] = [];
}
metadata.bluesky_uris[lang].push(uri);
Enter fullscreen mode Exit fullscreen mode

That worked locally, but the change never made it into the source files because the JSON files were version‑controlled and the script only writes back when bluesky_published flips to true. Since the flag stayed false after the failed push, the file never got updated, and the next CI run started from the same broken state.

The Implementation

The clean solution was to make the schema explicit: bluesky_uris should always contain an object with language keys (es, en) that map to empty arrays by default. I updated the two metadata files that are generated daily (content/2026/08/11/VS/metadata.json and content/2026/08/11/content-automation/metadata.json) and added the same structure to any new post template.

Diff – Adding language keys

@@ -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

Both files now start with a predictable shape, so the publishing script can safely push without extra guards.

Updating the post‑creation template

I also edited templates/metadata_template.json (not listed in the commit diff but part of the repo) to reflect the new schema:

{
  "repo": "{{repo}}",
  "date": "{{date}}",
  "languages": ["es", "en"],
  "topics": [],
  "commits": 0,
  "medium_generated": false,
  "substack_generated": false,
  "bluesky_published": false,
  "bluesky_uris": {
    "es": [],
    "en": []
  }
}
Enter fullscreen mode Exit fullscreen mode

Now every new post scaffold gets the correct structure automatically.

Adjusting the publishing script

With the schema fixed, I simplified scripts/publish_bluesky.js:

// scripts/publish_bluesky.js
const fs = require('fs');
const path = require('path');

function publishBluesky(metadataPath, lang, uri) {
  const raw = fs.readFileSync(metadataPath, 'utf8');
  const metadata = JSON.parse(raw);

  // No need for defensive checks – schema guarantees arrays exist
  metadata.bluesky_uris[lang].push(uri);
  metadata.bluesky_published = true;

  fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2));
}
Enter fullscreen mode Exit fullscreen mode

Because the JSON always contains es and en arrays, the code is now a single line of push followed by a write‑back.

Adding a unit test

To avoid regressions, I added a Jest test in tests/metadata.test.js:

// tests/metadata.test.js
const { readFileSync } = require('fs');
const path = require('path');

test('metadata.json always contains language arrays for bluesky_uris', () => {
  const metaPath = path.resolve(__dirname, '../content/2026/08/11/VS/metadata.json');
  const meta = JSON.parse(readFileSync(metaPath, 'utf8'));

  expect(meta.bluesky_uris).toHaveProperty('es');
  expect(meta.bluesky_uris).toHaveProperty('en');
  expect(Array.isArray(meta.bluesky_uris.es)).toBe(true);
  expect(Array.isArray(meta.bluesky_uris.en)).toBe(true);
});
Enter fullscreen mode Exit fullscreen mode

Running npm test now catches any accidental removal of the language keys.

CI integration

The CI pipeline (.github/workflows/ci.yml) already runs npm test before publishing. After the schema change, the pipeline passes, and the bluesky_published flag flips to true on the first successful run, ensuring the metadata is persisted.

Key Takeaway

Never rely on “optional” fields in JSON that your code mutates later. Define the full shape up‑front, even if some values are empty arrays. This eliminates runtime type errors, simplifies the code path, and makes unit testing straightforward.

What's Next

I plan to extend the schema to support future languages (e.g., fr, de) by generating the language list dynamically from the languages array in the same file. A small helper will iterate over metadata.languages and create matching keys in bluesky_uris during the scaffolding step, keeping the schema DRY and future‑proof.


Tags: #vibecoding #buildinpublic #javascript #json #automation #docker #networking


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-08-12

#playadev #buildinpublic

Top comments (0)