DEV Community

Roberto Luna
Roberto Luna

Posted on

Automating Multi‑Platform Content Publishing with a Node.js Pipeline

Automating Multi‑Platform Content Publishing with a Node.js Pipeline

TL;DR: I built a Node.js script that generates daily changelogs and creates ready‑to‑post Markdown/JSON files for Medium, Substack, and Bluesky in a single run. The pipeline removes manual copy‑paste, keeps metadata in sync, and lets me push the same content to three platforms with one commit.


The Problem

Every morning I had to open three different editors (Medium, Substack, Bluesky) and copy‑paste the same article text, update the changelog, and tweak front‑matter manually. The process was error‑prone: a missing heading on Medium, a broken link on Substack, or an outdated metadata.json flag that prevented the CI from triggering the next step. The symptom was a CI job that kept failing with:

Error: metadata.json – medium_generated is false but file content/2026/10/29/content-automation/medium_en.md does not exist
Enter fullscreen mode Exit fullscreen mode

In short, the source of truth was scattered across the repository, and any mismatch broke the automation that should have published the content.

What I Tried First

My first attempt was a simple Bash loop that copied the same Markdown file into each platform’s folder and manually edited the front‑matter. The script looked like this:

#!/bin/bash
cp content/template.md content/2026/10/29/content-automation/medium_en.md
cp content/template.md content/2026/10/29/content-automation/substack_en.md
# add front‑matter with sed
sed -i '1s/^/---\ntitle: My Post\n---\n/' content/2026/10/29/content-automation/medium_en.md
Enter fullscreen mode Exit fullscreen mode

It worked for a single day, but as soon as I added a Spanish version and a daily changelog, the script became a tangled mess of sed commands, hard‑coded dates, and duplicated logic. The biggest failure was that the script didn’t update metadata.json, so the CI pipeline still thought the files weren’t generated.

The Implementation

Overview

I rewrote the whole pipeline in Node.js (v20) to keep everything in one language, use proper JSON handling, and make the code testable. The entry point is scripts/generateContent.js. It does three things:

  1. Generate a daily changelog (changelog.md) based on the previous day’s commit messages.
  2. Render platform‑specific Markdown/JSON from a single source template (templates/article.hbs).
  3. Update metadata.json to reflect which files were successfully generated.

All files live under content/2026/10/29/content-automation/ so the path is deterministic.

Core Files

File Purpose
scripts/generateContent.js Main driver, orchestrates the pipeline
templates/article.hbs Handlebars template for the article body
utils/git.js Helper that runs git log to collect commit messages
utils/fileWriter.js Thin wrapper around fs.promises with error handling
content/2026/10/29/content-automation/metadata.json Stores generation flags (medium_generated, substack_generated, bluesky_generated)

Step‑by‑Step Code Walkthrough

1. Collecting commit messages for the changelog

// utils/git.js
import { exec } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(exec);

export async function getYesterdayCommits() {
  const { stdout } = await execAsync(
    'git log --since="1 day ago" --pretty=format:"%h %s"'
  );
  return stdout.split('\n').filter(Boolean);
}
Enter fullscreen mode Exit fullscreen mode

2. Writing the changelog

// scripts/generateContent.js (excerpt)
import { writeFile } from '../utils/fileWriter.js';
import { getYesterdayCommits } from '../utils/git.js';
import path from 'path';

async function generateChangelog() {
  const commits = await getYesterdayCommits();
  const changelog = [
    '## [2026-10-29] content-automation',
    '',
    '### Added',
    ...commits.map(c => `- \`${c}\` – auto‑generated entry.`),
    '',
  ].join('\n');

  const changelogPath = path.resolve(
    'content/2026/10/29/content-automation/changelog.md'
  );
  await writeFile(changelogPath, changelog);
}
Enter fullscreen mode Exit fullscreen mode

The diff for this file (added in commit 08c22246) looks like:

+## [2026-10-29] content-automation
+
+### Added
+- `content/2026/10/28/content-automation/changelog.md` – registro de cambios del día 2026‑10‑28.
Enter fullscreen mode Exit fullscreen mode

3. Rendering platform files

I use Handlebars to keep the article body DRY. The template (templates/article.hbs) contains placeholders for title, date, and body. Example snippet:

---
title: "{{title}}"
date: "{{date}}"
tags: {{tags}}
---

{{{body}}}
Enter fullscreen mode Exit fullscreen mode

The rendering function:

// scripts/generateContent.js (excerpt)
import fs from 'fs/promises';
import Handlebars from 'handlebars';
import path from 'path';

async function renderArticle(platform, locale) {
  const tmplPath = path.resolve('templates/article.hbs');
  const tmplSrc = await fs.readFile(tmplPath, 'utf8');
  const template = Handlebars.compile(tmplSrc);

  const data = {
    title: platform === 'bluesky' ? 'Progress Update' : 'Weekly Build‑in‑Public',
    date: '2026-10-29',
    tags: ['vibecoding', 'buildinpublic'],
    body: await fs.readFile(
      `content/2026/10/28/content-automation/${platform}_${locale}.md`,
      'utf8'
    ).catch(() => ''), // fallback if language file missing
  };

  return template(data);
}
Enter fullscreen mode Exit fullscreen mode

After rendering, the script writes the files:

await writeFile(
  `content/2026/10/29/content-automation/medium_${locale}.md`,
  await renderArticle('medium', locale)
);
await writeFile(
  `content/2026/10/29/content-automation/substack_${locale}.md`,
  await renderArticle('substack', locale)
);
await writeFile(
  `content/2026/10/29/content-automation/bluesky_${locale}.json`,
  JSON.stringify([
    {
      type: locale === 'en' ? 'progress' : 'avance',
      text: `Finally got the script to auto‑generate the daily changelog in content/2026/10/28/content-automation/changelog.md. After a week of manual work, it finally works!`,
    },
  ], null, 2)
);
Enter fullscreen mode Exit fullscreen mode

The resulting JSON (from commit 6c902a6e) is:

[
  {
    "type": "progress",
    "text": "Finally got the script to auto‑generate the daily changelog in content/2026/10/28/content-automation/changelog.md. After a week of manual work, it finally works!"
  }
]
Enter fullscreen mode Exit fullscreen mode

4. Updating metadata.json

The metadata file tracks which platforms have been generated


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-10-30

#playadev #buildinpublic

Top comments (0)