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": {}
}
When the publishing script (scripts/publish_bluesky.js) tried to push a new URI it did:
metadata.bluesky_uris[lang].push(uri);
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)
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);
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);
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_
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": []
}
}
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));
}
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);
});
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)