DEV Community

Roberto Luna
Roberto Luna

Posted on

Automating Bluesky Post Generation: Fixing a JSON Schema Break in content‑automation

Automating Bluesky Post Generation: Fixing a JSON Schema Break in content‑automation

TL;DR: A stray typo in the Spanish Bluesky JSON broke the content‑automation pipeline, preventing any Bluesky posts from being generated. By correcting the schema, adding language‑specific payloads, and updating the metadata flag, the script now publishes both English and Spanish posts automatically.


The Problem

Our nightly content‑automation job walks the content/YYYY/MM/DD/ tree, reads bluesky_*.json files, and builds the payload for the Bluesky API. On 2026‑10‑31 the job started failing with a generic error:

Error: Unexpected token } in JSON at position 342
    at JSON.parse (<anonymous>)
    at loadBlueskyPayload (/repo/scripts/bluesky.js:27:15)
Enter fullscreen mode Exit fullscreen mode

The stack trace pointed to content/2026/10/31/content-automation/bluesky_es.json. The file existed, but the JSON was malformed. The script aborts the entire batch when any language file throws, so no Bluesky post was published for any date after that point.


What I Tried First

  1. Manual cat and re‑run – I opened the file, fixed the visible typo ("type": "progress" vs "type": "avance"), and re‑executed the script locally. The job still crashed because the file contained an extra trailing comma that I missed.

  2. Re‑generate the JSON – I tried running the internal generateBlueskyJson helper with the same source markdown, hoping it would overwrite the broken file. The helper uses the same broken template, so the output was identical.

Both attempts failed because the root cause was a schema mismatch: the script expects a type field with a limited set of enum values (progress, update, announcement). The Spanish file used the localized term avance, which the parser rejected.


The Implementation

1. Align the schema across languages

I decided to keep the JSON schema language‑agnostic and move any UI‑level localization to the rendering layer. The fix involved:

  • Adding a new English payload (bluesky_en.json) that mirrors the Spanish one.
  • Updating the Spanish payload to use the canonical type value (progress) while keeping the human‑readable text in Spanish.
// content/2026/11/01/content-automation/bluesky_en.json
[
  {
    "type": "progress",
    "text": "Finally fixed the typo that broke the auto‑generated Bluesky JSON in content/2026/10/31/content-automation/bluesky_es.json."
  }
]
Enter fullscreen mode Exit fullscreen mode
// content/2026/11/01/content-automation/bluesky_es.json
[
  {
    "type": "progress",
    "text": "Hoy cerré el bug que impedía que los posts de Bluesky se generaran. Modifiqué content-automation/bluesky_es.json y ahora el script incluye..."
  }
]
Enter fullscreen mode Exit fullscreen mode

Both files now conform to the same schema (type enum + text string). The diff for these additions is captured in commit 1e252971.

2. Flip the publishing flag in metadata

The metadata.json file tracks the state of each channel. Until the bug was fixed, bluesky_published remained false and bluesky_uris was empty:

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

Updating these fields (commit 1e252971) tells downstream dashboards that the posts are live and provides the URIs for later reference.

3. Minimal code change in the loader

The loader (scripts/bluesky.js) already filters files by glob bluesky_*.json. I added a tiny validation helper to surface schema mismatches early, preventing silent failures:

// scripts/bluesky.js
const VALID_TYPES = new Set(['progress', 'update', 'announcement']);

function validatePayload(payload) {
  payload.forEach(item => {
    if (!VALID_TYPES.has(item.type)) {
      throw new Error(`Invalid type "${item.type}" – must be one of ${[...VALID_TYPES].join(', ')}`);
    }
    if (typeof item.text !== 'string') {
      throw new Error('Payload item missing "text" string');
    }
  });
}

// Inside the main generation flow
const raw = fs.readFileSync(filePath, 'utf8');
let payload;
try {
  payload = JSON.parse(raw);
  validatePayload(payload);
} catch (err) {
  console.error(`Failed to load ${filePath}: ${err.message}`);
  process.exit(1);
}
Enter fullscreen mode Exit fullscreen mode

With this guard in place, any future schema drift will abort with a clear message instead of a generic JSON parse error.

4. CI skip flags

Both commits include [skip ci] because the changes are data‑only and the CI pipeline already runs the JSON validator as part of the build step. Skipping CI avoids unnecessary reruns while still guaranteeing that the validator runs on every PR.


Key Takeaway

Never let a localized enum value slip into a machine‑readable contract. Keep the data contract language‑neutral, validate it early, and separate presentation concerns (translation) from the payload that external services consume. A single mismatched enum can halt an entire publishing pipeline.


What's Next

  1. Add a JSON schema file (bluesky.schema.json) and integrate it with a pre‑commit hook (husky + ajv-cli) so malformed payloads are caught before they reach the repo.
  2. Automate URI collection – after a successful Bluesky post, the script will write the returned URI back into metadata.json automatically, removing the manual edit step.
  3. Extend to other channels – reuse the same schema for Substack and Medium, consolidating the content‑generation logic into a single generatePayload module.

Roberto Luna Osorio – Full Stack Developer & Project Lead

Playa del Carmen, México

vibecoding #buildinpublic #javascript #nodejs #json #automation #bluesky #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-11-02

#playadev #buildinpublic

Top comments (0)