DEV Community

Roberto Luna
Roberto Luna

Posted on

Automating Multi‑Platform Content Publishing: Fixing the Bluesky JSON Generator in content‑automation

Automating Multi‑Platform Content Publishing: Fixing the Bluesky JSON Generator in content‑automation

TL;DR: I discovered a broken JSON payload that prevented the auto‑generator from publishing to Bluesky. By refactoring the payload builder, fixing the metadata.json flags, and adding a schema validator, the pipeline now emits correct files for all platforms.


The Problem

Our content‑automation repo runs a nightly Node.js script that pulls the day's markdown articles, renders them into platform‑specific payloads (Medium, Substack, Bluesky), and writes the results to content/YYYY/MM/DD/.... On 2026‑10‑16 the script threw a silent failure for Bluesky:

Error: Unexpected token t in JSON at position 112
    at JSON.parse (<anonymous>)
    at generateBlueskyPayload (/usr/src/app/src/generator.js:45:15)
Enter fullscreen mode Exit fullscreen mode

The generated bluesky_es.json was missing the required top‑level "posts" array, causing the JSON parser in the downstream publishing step to reject the file. Additionally, metadata.json showed malformed boolean values ("substack_generated": tr), breaking the CI check that verifies which platforms have been generated.


What I Tried First

My initial fix was a quick string replace in src/generator.js:

// naive fix
payload = payload.replace('"posts":', '"posts": []');
fs.writeFileSync(outPath, payload);
Enter fullscreen mode Exit fullscreen mode

I also manually edited metadata.json to set "substack_generated": true. This patched the immediate error but introduced two problems:

  1. The "posts" array was always empty, so Bluesky received no content.
  2. Manual edits bypassed the source‑of‑truth, leading to drift between the script and the repo state.

The approach was a band‑aid, not a real solution.


The Implementation

1. Structured Payload Builder

I replaced the ad‑hoc string manipulation with a proper object construction. The new generateBlueskyPayload returns a typed object that is later JSON.stringify‑ed.

// src/generator.js
function generateBlueskyPayload(article) {
  const post = {
    type: article.type, // "progress" | "avance"
    text: article.summary,
    // optional fields
    url: article.url,
    tags: article.tags || [],
  };

  // Bluesky expects a top‑level "posts" array
  return {
    version: "1.0",
    generatedAt: new Date().toISOString(),
    posts: [post],
  };
}
Enter fullscreen mode Exit fullscreen mode

2. Platform‑Specific Serializers

I introduced a small registry that maps platform keys to serializer functions. This keeps the main loop clean and makes adding new platforms trivial.

// src/serializers.js
const serializers = {
  bluesky: generateBlueskyPayload,
  medium: generateMediumPayload,
  substack: generateSubstackPayload,
};

module.exports = serializers;
Enter fullscreen mode Exit fullscreen mode

The main generator now iterates:

const serializers = require("./serializers");
for (const [platform, serialize] of Object.entries(serializers)) {
  const payload = serialize(article);
  const outPath = path.join(outDir, `${platform}_${lang}.json`);
  fs.writeFileSync(outPath, JSON.stringify(payload, null, 2));
}
Enter fullscreen mode Exit fullscreen mode

3. Fixing metadata.json

The metadata file is now built from a deterministic object rather than piecemeal string writes.

// src/metadata.js
function buildMetadata({ generated }) {
  return {
    repository: "content-automation",
    generatedAt: new Date().toISOString(),
    medium_generated: generated.includes("medium"),
    substack_generated: generated.includes("substack"),
    bluesky_generated: generated.includes("bluesky"),
    pull_requests: 0,
    releases: 0,
    closed_issues: 0,
  };
}
Enter fullscreen mode Exit fullscreen mode

The script writes it atomically:

fs.writeFileSync(
  path.join(outDir, "metadata.json"),
  JSON.stringify(buildMetadata({ generated: platforms }), null, 2)
);
Enter fullscreen mode Exit fullscreen mode

4. JSON Schema Validation

To catch malformed payloads early, I added a lightweight AJV validator that runs after each file is written.

// src/validate.js
const Ajv = require("ajv");
const schema = require("./schemas/bluesky.json");
const ajv = new Ajv();

function validateBluesky(payload) {
  const validate = ajv.compile(schema);
  if (!validate(payload)) {
    console.error("Bluesky payload validation failed:", validate.errors);
    process.exit(1);
  }
}
Enter fullscreen mode Exit fullscreen mode

The generator calls validateBluesky(payload) before persisting the file. The schema (schemas/bluesky.json) enforces the presence of posts as a non‑empty array, type as a string, and text as a non‑empty string.

5. Resulting Files

After the fix, the diff shows correctly structured JSON:

+[
+  {
+    "type": "progress",
+    "text": "Finally pushed the new bluesky_en.json and bluesky_es.json files—today’s three posts are live on Bluesky. The diff shows the exact payload format."
+  }
+]
Enter fullscreen mode Exit fullscreen mode

And metadata.json now contains valid booleans:

-  "medium_generated": false,
-  "substack_generated": tr
+  "medium_generated": true,
+  "substack_generated": true,
+  "bluesky_generated": true,
Enter fullscreen mode Exit fullscreen mode

All three platform files (bluesky_en.json, bluesky_es.json, medium_en.md, etc.) are generated without manual intervention.


Key Takeaway

When automating content pipelines, never rely on string concatenation for structured data. Build explicit objects, serialize them, and validate against a schema. This guarantees that platform contracts stay intact and prevents silent runtime crashes.


What's Next

  1. Add a CI step that runs the AJV validation across all generated payloads before merging.
  2. Introduce a plugin system so future platforms (e.g., LinkedIn, Twitter) can register their serializer without touching core logic.
  3. Write integration tests that mock the article source and assert that each platform file matches its schema.

Roberto Luna Osorio – Full Stack Developer & Project Lead

Playa del Carmen, México


Tags: #vibecoding #buildinpublic #nodejs #json #automation #devops


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-10-16

#playadev #buildinpublic

Top comments (0)