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)
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));
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',
};
}
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'] },
},
};
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"
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 });
And the metadata schema now looks like:
{
"medium_generated": true,
"substack_generated": true,
"bluesky_generated": true,
"bluesky_published": false,
"bluesky_uris": {}
}
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',
+ };
+}
+// src/publish/validateBluesky.js (extended schema)
+required: ['type', 'text', 'date', 'language'],
+// .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
+// 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"
+ }
+]
+// content/2026/11/13/content-automation/metadata.json (updated flags)
+ "medium_generated": true,
+ "substack_generated": true,
+ "bluesky_generated": true,
+ "bluesky_published": false,
+ "bluesky_uris": {}
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)