DEV Community

Roberto Luna
Roberto Luna

Posted on

Adding Robust Language‑Aware Bluesky URIs to Content‑Automation Metadata

Adding Robust Language‑Aware Bluesky URIs to Content‑Automation Metadata


TL;DR:

I patched the metadata.json schema to include per‑language arrays for Bluesky URIs, fixing runtime errors during publishing. The change allows the automation pipeline to track URLs for each locale, improving reliability across English and Spanish posts.

The Problem

When running the nightly content‑automation job, the script that pushes posts to Bluesky would crash with a TypeError:

TypeError: Cannot read property 'push' of undefined
Enter fullscreen mode Exit fullscreen mode

The error occurred on the line that attempted to append the newly created Bluesky URI to metadata.bluesky_uris[lang]. In the original schema, bluesky_uris was an empty object:

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

Because the script expected an array for each language (en, es), accessing metadata.bluesky_uris[lang] returned undefined, leading to the crash. This manifested only for new posts where the metadata had not been manually updated, making the bug hard to reproduce during local tests.

What I Tried First

My first instinct was to add a fallback array inline in the publishing function:

const uris = metadata.bluesky_uris[lang] || [];
uris.push(newUri);
metadata.bluesky_uris[lang] = uris;
Enter fullscreen mode Exit fullscreen mode

This prevented the crash, but it introduced silent data loss: every time the script ran, it would overwrite the existing array with a new one containing only the latest URI. The result was that only the most recent post’s URI survived in the metadata, breaking the tracking logic used by downstream analytics.

Another attempt was to patch the JSON schema with a single array:

"bluesky_uris": []
Enter fullscreen mode Exit fullscreen mode

However, the rest of the pipeline was built around language‑specific keys, so this change broke the type assumptions in multiple modules, leading to further errors.

The Implementation

1. Schema Update

The definitive fix was to modify the metadata.json schema to explicitly declare per‑language arrays. The diff in commit 701a0fcc shows the change:

-  "bluesky_uris": {},
+  "bluesky_uris": {
+    "es": [],
+    "en": []
+  },
Enter fullscreen mode Exit fullscreen mode

This update ensures that the publishing script always has an array to push to, regardless of whether a post has been published in that language yet.

2. Adding Empty Language Files

To support the new schema, I added placeholder JSON files for each language in content-automation:

touch content/2026/08/01/content-automation/bluesky_en.json
touch content/2026/08/01/content-automation/bluesky_es.json
Enter fullscreen mode Exit fullscreen mode

Both files contain an empty array:

[]
Enter fullscreen mode Exit fullscreen mode

These files act as a source of truth for the initial state and are used by the automation script to seed the bluesky_uris arrays when a new language is introduced.

3. Publishing Logic

Below is the core snippet from publish.js that handles Bluesky URIs:

const metadata = require('./metadata.json');
const langs = ['en', 'es'];

langs.forEach(lang => {
  const postPath = `content/${lang}/latest.md`;
  if (!fs.existsSync(postPath)) return;

  const { url } = publishToBluesky(postPath, lang); // returns the new URI
  metadata.bluesky_uris[lang].push(url);
});

metadata.bluesky_published = true;
fs.writeFileSync('./metadata.json', JSON.stringify(metadata, null, 2));
Enter fullscreen mode Exit fullscreen mode

Key decisions:

  • Explicit language array: metadata.bluesky_uris[lang] is guaranteed to be an array, preventing undefined errors.
  • Idempotent push: The script appends the new URL to the existing array, preserving history.
  • Centralized update: After publishing all languages, the flag bluesky_published is set, making the state machine clear.

4. Validation Hook

To catch similar schema regressions in the future, I added a small validation step at the start of the script:

function validateMetadata(meta) {
  const langs = ['en', 'es'];
  if (!Array.isArray(meta.bluesky_uris)) throw new Error('bluesky_uris must be an object');
  langs.forEach(lang => {
    if (!Array.isArray(meta.bluesky_uris[lang])) {
      throw new Error(`bluesky_uris[${lang}] must be an array`);
    }
  });
}

validateMetadata(metadata);
Enter fullscreen mode Exit fullscreen mode

This guard provides an early fail‑fast mechanism during CI runs.

Key Takeaway

Define your data schema with explicit defaults and validate it before use.

In dynamic content pipelines, missing or malformed fields can surface as hard‑to‑debug runtime errors. By pre‑defining the structure (e.g., language‑specific arrays) and validating it upfront, you avoid silent data loss and make the pipeline more maintainable.

What's Next

  • Automated Tests: Add unit tests that load metadata.json, run the publishing logic, and assert that URIs are appended correctly.
  • CI Integration: Hook the validation step into GitHub Actions to fail builds on schema violations.
  • Analytics Dashboard: Expose the bluesky_uris array in a lightweight dashboard to visualize post reach per language.

Tags: #vibecoding #buildinpublic #automation #nodejs #javascript #content-automation #bluesky #medium #substack #newsletter #developer


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

#playadev #buildinpublic

Top comments (0)