DEV Community

Roberto Luna
Roberto Luna

Posted on

Automating Bluesky JSON Payload Generation for Content‑Automation

Automating Bluesky JSON Payload Generation for Content‑Automation

TL;DR: I fixed the bug that prevented the automatic creation of the Bluesky JSON payload and wired it into our daily content‑automation pipeline. The fix updates the payload schema, adds proper date handling, and syncs the metadata flags so the next publish runs without manual intervention.


The Problem

Our content‑automation repo publishes the same article to Medium, Substack, and Bluesky every day. While the Medium and Substack pipelines were working, the Bluesky step kept failing with the following error in the CI logs:

Error: JSON validation failed – property "date" is required
    at validatePayload (/usr/src/app/publish/bluesky.js:45:13)
Enter fullscreen mode Exit fullscreen mode

The bluesky_es.json file was either missing or contained an incomplete payload, causing the publish/bluesky.js script to abort. The symptom was a bluesky_published: false flag left untouched in metadata.json, which meant the article never appeared on Bluesky.

What I Tried First

My first attempt was to patch the missing fields directly in the generated JSON after the script ran:

// quick‑and‑dirty fix (did not commit)
const payload = JSON.parse(fs.readFileSync('bluesky_es.json'));
payload.date = new Date().toISOString();
fs.writeFileSync('bluesky_es.json', JSON.stringify(payload, null, 2));
Enter fullscreen mode Exit fullscreen mode

I added this snippet at the end of publish/bluesky.js and re‑ran the workflow. The script no longer threw a validation error, but the JSON file still lacked the required type and text fields for some articles, resulting in a partially published post. Moreover, the hack was brittle: any future schema change would break it again.

The Implementation

1. Refactor the payload builder

The core issue was that generateBlueskyPayload() assumed every article had a summary field, which is optional in our markdown front‑matter. When summary was missing, the function returned undefined for the text property, causing the validation error.

// src/publish/bluesky.js
function generateBlueskyPayload(mdMeta) {
  const text = mdMeta.summary
    ? mdMeta.summary
    : mdMeta.excerpt || 'No summary available';

  return {
    type: 'avance',               // <-- hard‑coded to match our content type
    text,
    date: new Date().toISOString(),
    language: mdMeta.lang || 'es',
  };
}
Enter fullscreen mode Exit fullscreen mode

I added a fallback to mdMeta.excerpt and finally a default string, guaranteeing that text is never undefined.

2. Update the JSON schema validator

The validator previously required date but not language. I extended it to enforce type, text, and language:

// src/publish/validateBluesky.js
const schema = {
  type: 'object',
  required: ['type', 'text', 'date', 'language'],
  properties: {
    type: { type: 'string', enum: ['avance', 'nota'] },
    text: { type: 'string', minLength: 1 },
    date: { type: 'string', format: 'date-time' },
    language: { type: 'string', enum: ['es', 'en'] },
  },
};
Enter fullscreen mode Exit fullscreen mode

Now the script fails fast with a clear message if any required field is missing.

3. Wire the generator into the daily run

The daily pipeline lives in .github/workflows/content.yml. I added a new step that runs the Bluesky generator after the Medium and Substack steps:

# .github/workflows/content.yml
- name: Generate Bluesky payload
  run: |
    node src/publish/bluesky.js \
      --input content/$(date +%Y/%m/%d)/content-automation/metadata.json \
      --output content/$(date +%Y/%m/%d)/content-automation/bluesky_es.json
- name: Commit Bluesky JSON
  uses: stefanzweifel/git-auto-commit-action@v4
  with:
    commit_message: "chore(bluesky): posts $(date +%Y-%m-%d) [skip ci]"
    file_pattern: "content/**/bluesky_es.json"
Enter fullscreen mode Exit fullscreen mode

This step guarantees the payload is generated before we attempt to publish.

4. Sync metadata flags

metadata.json tracks which channels have been generated. The previous commit only toggled bluesky_published after a successful post, leaving the generation flag out of sync. I added a bluesky_generated boolean that flips to true right after the JSON file is written:

// src/publish/bluesky.js (excerpt)
fs.writeFileSync(outputPath, JSON.stringify(payload, null, 2));
updateMetadata({ bluesky_generated: true });
Enter fullscreen mode Exit fullscreen mode

And the metadata schema now looks like:

{
  "medium_generated": true,
  "substack_generated": true,
  "bluesky_generated": true,
  "bluesky_published": false,
  "bluesky_uris": {}
}
Enter fullscreen mode Exit fullscreen mode

5. Verify with a diff of the changed files

Below are the relevant diffs that landed in the three commits:

+// src/publish/bluesky.js (new fallback logic)
+function generateBlueskyPayload(mdMeta) {
+  const text = mdMeta.summary
+    ? mdMeta.summary
+    : mdMeta.excerpt || 'No summary available';
+
+  return {
+    type: 'avance',
+    text,
+    date: new Date().toISOString(),
+    language: mdMeta.lang || 'es',
+  };
+}
Enter fullscreen mode Exit fullscreen mode
+// src/publish/validateBluesky.js (extended schema)
+required: ['type', 'text', 'date', 'language'],
Enter fullscreen mode Exit fullscreen mode
+// .github/workflows/content.yml (new step)
+- name: Generate Bluesky payload
+  run: |
+    node src/publish/bluesky.js \
+      --input content/$(date +%Y/%m/%d)/content-automation/metadata.json \
+      --output content/$(date +%Y/%m/%d)/content-automation/bluesky_es.json
Enter fullscreen mode Exit fullscreen mode
+// content/2026/11/13/content-automation/bluesky_es.json (generated)
+[
+  {
+    "type": "avance",
+    "text": "Finalmente resolví el bug que bloqueaba la generación automática del JSON para Bluesky. Ahora el archivo bluesky_es.json incluye los tres ...",
+    "date": "2026-11-14T08:12:00.000Z",
+    "language": "es"
+  }
+]
Enter fullscreen mode Exit fullscreen mode
+// content/2026/11/13/content-automation/metadata.json (updated flags)
+  "medium_generated": true,
+  "substack_generated": true,
+  "bluesky_generated": true,
+  "bluesky_published": false,
+  "bluesky_uris": {}
Enter fullscreen mode Exit fullscreen mode

Key Takeaway

Never assume optional front‑matter fields exist when building payloads for external APIs. Always provide deterministic fallbacks and validate the full schema before you hand the payload to the publishing client. This prevents silent data loss and makes CI failures easier to diagnose.

What's Next

  • Add unit tests for `generateBlues

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-14

#playadev #buildinpublic

Top comments (0)