DEV Community

Roberto Luna
Roberto Luna

Posted on

Automating Multi‑Platform Publishing: Fixing the Metadata‑Driven Build Pipeline

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
Enter fullscreen mode Exit fullscreen mode

On 2026‑08‑12 the build failed with:

Error: Cannot read property 'bluesky_published' of undefined
    at /app/publish.js:45:27
Enter fullscreen mode Exit fullscreen mode

The root cause was two‑fold:

  1. Missing front‑mattercontent/2026/08/12/VS/metadata.json was empty, so the script couldn’t locate the bluesky_published flag.
  2. Stale Bluesky flags – The older metadata.json under content‑automation still 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;
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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é."
  }
]
Enter fullscreen mode Exit fullscreen mode
// 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."
  }
]
Enter fullscreen mode Exit fullscreen mode

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}`);
  }
});
Enter fullscreen mode Exit fullscreen mode

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/
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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

  1. Schema validation – Integrate ajv to validate metadata.json against a JSON‑Schema definition in CI.
  2. Automated diff check – Add a Git hook that ensures the VS and content‑automation folders stay identical for shared files.
  3. Extensible platform plugins – Refactor publish.js to 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)