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) 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); I also manually edited metadata.json to set "substack_generated": true. This patched the immediate error but introduced two problems:
- The
"posts"array was always empty, so Bluesky received no content. - 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], }; } 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; 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)); } 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, }; } The script writes it atomically:
fs.writeFileSync( path.join(outDir, "metadata.json"), JSON.stringify(buildMetadata({ generated: platforms }), null, 2) ); 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); } } 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." + } +] And metadata.json now contains valid booleans:
- "medium_generated": false, - "substack_generated": tr + "medium_generated": true, + "substack_generated": true, + "bluesky_generated": true, 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
- Add a CI step that runs the AJV validation across all generated payloads before merging.
- Introduce a plugin system so future platforms (e.g., LinkedIn, Twitter) can register their serializer without touching core logic.
- 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
![Why Object.keys(user) Returns string[], Not (keyof User)[]: TypeScript Means It](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2q4w48b9bzs43tynyhsf.png)
