Fixing a JSON‑Schema Break in the Content‑Automation Pipeline for Bluesky Posts

TL;DR: The automation stopped publishing to Bluesky because a stray comma broke the JSON schema used for post payloads. I updated the schema, patched the generation script, and added proper validation, getting the pipeline back online.

The Problem

My daily content‑automation repo (content-automation) generates Markdown for Medium, Substack, and JSON payloads for Bluesky. On 2026‑11‑02 the pipeline threw a validation error:

Error: data.posts[0].text should NOT have additional properties at Ajv.validate (node_modules/ajv/dist/ajv.js:123:15) 

The root cause was a typo in the generated bluesky_es.json – an extra trailing comma after the last object made the file invalid JSON, causing the schema validator to reject the entire payload. The automation script silently skipped publishing, leaving Bluesky empty for the day.

What I Tried First

My first instinct was to manually edit the offending bluesky_es.json in the repo and re‑run the pipeline:

# Attempted quick fix sed -i 's/,]$/]/' content/2026/11/02/content-automation/bluesky_es.json npm run publish:bluesky 

The change removed the stray comma, and the script ran without throwing an exception, but the underlying issue remained: the generation step kept producing malformed JSON. Every new run would re‑introduce the error, so this “quick fix” was not sustainable.

I also tried disabling schema validation in the publish.js script:

// publish.js – temporary bypass // const valid = ajv.validate(schema, payload); // if (!valid) { console.warn('Skipping validation'); } 

Skipping validation let the broken payload slip through, but Bluesky’s API rejected the request with a 400 Bad Request because the JSON payload still contained the stray comma. The API error looked like:

Invalid JSON payload: Unexpected token ',' at position 342 

Both approaches were dead ends because they treated the symptom rather than the cause.

The Implementation

1. Define a strict JSON schema

I placed a schema file at schemas/bluesky-post.schema.json:

{ "$id": "https://vibecoding.com/schemas/bluesky-post.schema.json", "type": "array", "items": { "type": "object", "required": ["type", "text"], "properties": { "type": { "type": "string", "enum": ["progress", "avance"] }, "text": { "type": "string", "minLength": 1 } }, "additionalProperties": false } } 

This schema enforces that each post object contains only type and text, disallowing stray commas or extra fields.

2. Update the generation script

File: scripts/generate-bluesky.js

const fs = require('fs'); const path = require('path'); const Ajv = require('ajv'); const ajv = new Ajv({ allErrors: true }); const schema = require('../schemas/bluesky-post.schema.json'); const outputDir = path.join(__dirname, '..', 'content', '2026', '11', '02', 'content-automation'); // Helper to write JSON with deterministic formatting function writeJson(filePath, data) { const json = JSON.stringify(data, null, 2) + '\n'; fs.writeFileSync(filePath, json, 'utf8'); } // Build payload function buildPayload(lang) { const posts = [ { type: lang === 'en' ? 'progress' : 'avance', text: lang === 'en' ? 'Finally pushed the English version of today’s Bluesky post into content/2026/11/01/content-automation/bluesky_en.json.' : 'Finalmente resolví el bug que me impedía publicar en Bluesky: actualicé bluesky_es.json con el nuevo post de productividad.' } ]; return posts; } // Validate and write ['en', 'es'].forEach(lang => { const payload = buildPayload(lang); const valid = ajv.validate(schema, payload); if (!valid) { console.error('Schema validation failed:', ajv.errors); process.exit(1); } const fileName = `bluesky_${lang}.json`; writeJson(path.join(outputDir, fileName), payload); console.log(`✅ ${fileName} generated`); }); 

Key changes:

  • Strict validation – The script now aborts on any schema violation, preventing malformed files from being committed.
  • Deterministic JSON – JSON.stringify with a 2‑space indent guarantees no stray commas.
  • Language‑aware payload – The type field now matches the language (progress for English, avance for Spanish), aligning with the schema’s enum.

3. Adjust the CI step

ci/publish-bluesky.yml now runs the generator before attempting to push:

steps: - name: Generate Bluesky payloads run: npm run generate:bluesky - name: Publish to Bluesky if: success() run: npm run publish:bluesky 

If validation fails, the job stops, and the failure is visible in GitHub Actions, eliminating silent skips.

4. Commit the new files

The diff for the added JSON payloads (commit 8f74a93b) looks like:

+[ + { + "type": "progress", + "text": "Finally pushed the English version of today’s Bluesky post into content/2026/11/01/content-automation/bluesky_en.json. The diff shows the new payload." + } +] 
+[ + { + "type": "avance", + "text": "Finalmente resolví el bug que me impedía publicar en Bluesky: actualicé bluesky_es.json con el nuevo post de productividad. El diff muestra el nuevo payload." + } +] 

5. Update metadata.json

The metadata.json file tracks the state of each platform. The relevant change (commit 10cb2829) toggles the Bluesky flag:

- "bluesky_published": false, - "bluesky_uris": {}, + "bluesky_published": true, + "bluesky_uris": { + "en": "at://did:plc:.../app.bsky.feed.post/3kxyz", + "es": "at://did:plc:.../app.bsky.feed.post/3kabc" + }, 

Now the metadata reflects successful publication, which downstream tools (e.g., changelog generators) can consume.

Key Takeaway

Never rely on “happy‑path” scripts for data that crosses service boundaries. Enforcing a JSON schema at generation time catches structural bugs early, prevents silent failures, and makes CI failures explicit. A small schema file and a validation step add negligible overhead but dramatically improve reliability.

What's Next

  • Schema versioning – Publish the schema to a CDN and reference it via $id in downstream services to

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-11-03

#playadev #buildinpublic