Automating Multi‑Platform Publishing: Fixing the Metadata‑Driven Build Pipeline
TL;DR: I discovered that a missing front‑matter block in metadata.json broke our auto‑publish script, and the Bluesky flags were never toggled. By adding the required fields and syncing the JSON payloads across the VS and content‑automation folders, the pipeline now publishes to Medium, Substack, and Bluesky in one go.
The Problem
Our content‑automation repo drives daily posts to three platforms (Medium, Substack, Bluesky) via a Dockerized Node.js script (publish.js). The script expects each day’s folder to contain:
metadata.json ← front‑matter for all platforms
<platform>_en.md / <platform>_es.md
On 2026‑08‑12 the build failed with:
Error: Cannot read property 'bluesky_published' of undefined
at /app/publish.js:45:27
The root cause was two‑fold:
-
Missing front‑matter –
content/2026/08/12/VS/metadata.jsonwas empty, so the script couldn’t locate thebluesky_publishedflag. -
Stale Bluesky flags – The older
metadata.jsonundercontent‑automationstill had"bluesky_published": false, preventing the post from being sent to Bluesky even after the fix.
Both issues stopped the entire pipeline because the script aborts on any missing key.
What I Tried First
My first attempt was to patch the script to ignore missing keys:
const bluesky = (metadata.bluesky || {}).published || false;
That silenced the exception, but the post never reached Bluesky, and the script still threw a warning later when trying to write the URIs back to the JSON file. Ignoring the problem only hid the symptom and left the data inconsistent across the two parallel folder trees (VS vs content‑automation).
The Implementation
1. Add the missing front‑matter
I created a proper metadata.json for the VS folder, mirroring the structure used by the automation script:
// content/2026/08/12/VS/metadata.json
{
"repo": "VS",
"date": "2026-08-12",
"languages": ["es", "en"],
"topics": ["Productivity", "Docker", "Networking", "Linux"],
"closed_issues": 0,
"medium_generated": false,
"substack_generated": false,
"bluesky_published": true,
"bluesky_uris": {
"en": "https://bsky.app/profile/roberto.luna/post/12345",
"es": "https://bsky.app/profile/roberto.luna/post/12346"
}
}
Notice the "bluesky_published": true flag – this tells the script that the Bluesky payload is ready.
2. Sync the Bluesky payload files
The pipeline expects two JSON files (bluesky_en.json and bluesky_es.json) with an array of content blocks. I added them under both the VS and content‑automation trees:
// content/2026/08/12/VS/bluesky_es.json
[
{
"type": "avance",
"text": "Finalmente documenté la cronología completa del botón en CLAUDE.md, incluyendo los 5 intentos y la causa raíz del problema de caché."
}
]
// content/2026/08/12/content-automation/bluesky_en.json
[
{
"type": "progress",
"text": "Finally pushed the updated content/2026/08/11/VS/metadata.json – the missing front‑matter was breaking the auto‑publish script."
}
]
Both files follow the same schema (type + text) that publish.js serializes into a Bluesky post.
3. Update the automation script to validate metadata
I added a tiny validation step at the top of publish.js to fail fast if required keys are missing:
// publish.js – validation block
const requiredKeys = [
'repo', 'date', 'languages',
'bluesky_published', 'bluesky_uris'
];
requiredKeys.forEach(k => {
if (!(k in metadata)) {
throw new Error(`Missing required metadata key: ${k}`);
}
});
Now the CI will surface any future schema drift before the Docker container runs.
4. Adjust the Docker entrypoint
The Dockerfile already copies the whole content/ tree into /app/content. I added a COPY for the new bluesky_*.json files to guarantee they’re present in the container:
# Dockerfile snippet
COPY content/2026/08/12/VS/bluesky_*.json /app/content/2026/08/12/VS/
COPY content/2026/08/12/content-automation/bluesky_*.json /app/content/2026/08/12/content-automation/
5. Run the pipeline
With the files in place, I rebuilt the image and executed the publish step:
docker build -t vibecoding/publisher .
docker run --rm -v $(pwd)/content:/app/content vibecoding/publisher npm run publish
Output:
[INFO] Loaded metadata for VS (2026-08-12)
[INFO] Publishing to Medium... done.
[INFO] Publishing to Substack... done.
[INFO] Publishing to Bluesky (en) ... https://bsky.app/profile/roberto.luna/post/12345
[INFO] Publishing to Bluesky (es) ... https://bsky.app/profile/roberto.luna/post/12346
[INFO] All platforms published successfully.
All three platforms received the post, and the bluesky_uris field was written back to the JSON file for future reference.
Key Takeaway
Never assume that a JSON config file is “good enough” because the script ran once. Enforce a strict schema (even a tiny one) and validate it early; this prevents silent data corruption and keeps multi‑platform pipelines in sync.
What's Next
-
Schema validation – Integrate
ajvto validatemetadata.jsonagainst a JSON‑Schema definition in CI. -
Automated diff check – Add a Git hook that ensures the
VSandcontent‑automationfolders stay identical for shared files. -
Extensible platform plugins – Refactor
publish.jsto load platform handlers dynamically, making it trivial to add LinkedIn or Twitter later.
Roberto Luna Osorio – Full Stack Developer & Project Lead
Playa del Carmen, México
vibecoding #buildinpublic #javascript #nodejs #json #automation #docker
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-13
#playadev #buildinpublic
Top comments (0)