DEV Community

Roberto Luna
Roberto Luna

Posted on

Automating Multi‑Platform Content Publishing: Syncing Metadata and Generating Bluesky Payloads

Automating Multi‑Platform Content Publishing: Syncing Metadata and Generating Bluesky Payloads

TL;DR: I extended my content‑automation repo to keep a single source of truth for post metadata and to generate the JSON payloads required by Bluesky. The change eliminates manual copy‑pastes, fixes a JSON syntax error that broke the CI, and lets the same script publish to Medium, Substack and Bluesky in one go.


The Problem

My weekly workflow creates three separate drafts (Medium, Substack, Bluesky) and then runs a tiny Node script that copies the markdown files into platform‑specific JSON payloads. The metadata.json file is supposed to tell the script which platforms have already been generated:

{
  "pull_requests": 0,
  "releases": 0,
  "closed_issues": 0,
  "medium_generated": false,
  "substack_generated": false,
  "bluesky_generated": false
}
Enter fullscreen mode Exit fullscreen mode

Two issues kept surfacing:

  1. Stale flags – After I manually edited a file, the flags in metadata.json were out of sync, causing the script to skip regeneration for a platform that actually needed an update.
  2. Bluesky payload syntax error – The script writes bluesky_en.json and bluesky_es.json. A missing comma in the generated file broke the JSON parser, aborting the CI run with:
Error: Unexpected token } in JSON at position 123
Enter fullscreen mode Exit fullscreen mode

Both problems forced me to open the repo, edit the JSON by hand, and re‑run the pipeline—a clear productivity leak.


What I Tried First

My first attempt was to add a quick “touch” step in the CI workflow:

- name: Force metadata refresh
  run: echo '{}' > content/2026/11/10/content-automation/metadata.json
Enter fullscreen mode Exit fullscreen mode

This reset the flags to false every run. It worked for the flag‑sync issue but did not address the malformed Bluesky payload. Moreover, wiping the whole file erased useful counters (pull_requests, releases), so I reverted it.

Next, I tried a naïve string replace in the script:

payload = payload.replace('",\n]', '"]');
Enter fullscreen mode Exit fullscreen mode

It removed the stray comma that was causing the parse error, but it was brittle—any future change to the payload structure would break the regex. I needed a deterministic solution.


The Implementation

1. Centralising Metadata Updates

I refactored the script (scripts/generate.js) to treat metadata.json as a single source of truth. Instead of manually toggling flags, the script now:

  1. Reads the current metadata.
  2. Updates the flag for each platform after a successful write.
  3. Writes the whole object back in one atomic fs.writeFileSync call.
// scripts/generate.js
const fs = require('fs');
const path = require('path');

const META_PATH = path.join(__dirname, '..', 'content', '2026', '11', '10', 'content-automation', 'metadata.json');

function loadMeta() {
  return JSON.parse(fs.readFileSync(META_PATH, 'utf8'));
}

function saveMeta(meta) {
  // Pretty‑print to keep diffs readable
  fs.writeFileSync(META_PATH, JSON.stringify(meta, null, 2) + '\n');
}

// Example of updating the flag after Medium generation
function generateMedium() {
  // ... generate markdown, write files ...
  const meta = loadMeta();
  meta.medium_generated = true;
  saveMeta(meta);
}
Enter fullscreen mode Exit fullscreen mode

I also added a tiny helper to reset flags only for the current date, preserving the counters:

function resetDailyFlags(date) {
  const meta = loadMeta();
  meta.medium_generated = false;
  meta.substack_generated = false;
  meta.bluesky_generated = false;
  meta.last_run = date;
  saveMeta(meta);
}
Enter fullscreen mode Exit fullscreen mode

The diff for metadata.json now looks like this (excerpt):

@@ -12,8 +12,19 @@
   "closed_issues": 0,
-  "medium_generated": false,
-  "substack_generated": false,
+  "medium_generated": true,
+  "substack_generated": true,
+  "bluesky_generated": true,
+  "last_run": "2026-11-12",
+  "pull_requests": 0,
+  "releases": 0,
+  "closed_issues": 0,
+  "total_posts": 3
Enter fullscreen mode Exit fullscreen mode

2. Robust Bluesky Payload Generation

The Bluesky payload is a simple JSON array with objects that contain type and text. The previous implementation built the string manually, which left a trailing comma when the array had more than one entry.

I switched to native array construction and let JSON.stringify handle commas and escaping:

// scripts/bluesky.js
function buildBlueskyPayload(entries) {
  // entries = [{type: 'progress', text: '...'}, ...]
  return JSON.stringify(entries, null, 2);
}

// Usage
const payload = buildBlueskyPayload([
  {
    type: "progress",
    text: "Finally committed the Bluesky payload file (content/2026/11/10/content-automation/bluesky_en.json). The diff was a tiny JSON tweak, but it now passes validation."
  }
]);

fs.writeFileSync(
  path.join(__dirname, '..', 'content', '2026', '11', '10', 'content-automation', 'bluesky_en.json'),
  payload + '\n'
);
Enter fullscreen mode Exit fullscreen mode

The new bluesky_en.json file (first 5 lines) is now generated automatically:

[
  {
    "type": "progress",
    "text": "Finally committed the Bluesky payload file (content/2026/11/10/content-automation/bluesky_en.json). The diff was a tiny JSON tweak, but it now passes validation."
  }
]
Enter fullscreen mode Exit fullscreen mode

The same logic is used for the Spanish version (bluesky_es.json). The diff added both files:

+[
+  {
+    "type": "progress",
+    "text": "Finally committed the Bluesky payload file (content/2026/11/10/content-automation/bluesky_en.json). The diff was a tiny JSON tweak, but it now passes validation."
+  }
+]
Enter fullscreen mode Exit fullscreen mode

3. Adding a Daily Changelog

To keep a human‑readable audit trail, I introduced changelog.md under the date folder. The script appends a markdown entry each time it runs:

function appendChangelog(entry) {
  const changelogPath = path.join(__dirname, '..', 'content', '2026', '11', '10', 'content-automation', 'changelog.md');
  const line = `- ${new Date().toISOString().split('T')[0]}: ${entry}\n`;
  fs.appendFileSync(changelogPath, line);
}
Enter fullscreen mode Exit fullscreen mode

Resulting changelog.md (excerpt):

## [2026-11-11] content-automation

- Added `content/2026/11/10/content-automation/changelog.md` – Daily changelog for 2026‑11‑10.
- Updated metadata flags after successful Medium, Substack and Bluesky generation.
Enter fullscreen mode Exit fullscreen mode

4. CI Integration

I updated the GitHub Actions workflow (.github/workflows/content.yml) to:

  1. Run node scripts/generate.js.
  2. Fail fast on JSON parse errors (set -e already in place).
  3. Commit the

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

#playadev #buildinpublic

Top comments (0)