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
Top comments (0)