VOOZH about

URL: https://dev.to/zenndraapi/sync-your-medium-portfolio-to-a-static-site-automatically-3fmk

⇱ Sync Your Medium Portfolio to a Static Site Automatically - DEV Community


Sync Your Medium Portfolio to a Static Site Automatically

Hiring managers Google you and compare your domain to your Medium profile. When they diverge, you look inactive—even if you shipped twelve essays last quarter.

This is a small automation tool: resolve your handle → list articles → write Markdown into git → deploy.


Medium is distribution; your site is proof

Medium optimizes reach. Your portfolio optimizes narrative: order, categories, case studies beside a contact form.

Static generators (Hugo, Astro, Eleventy) love files in git. Treat Medium as an upstream feed—like RSS used to work.


The automation pattern

  1. Resolve @username → stable user_id (guide).
  2. GET /user/{user_id}/articles on a schedule.
  3. For each new article_id, GET /article/{id}/markdown.
  4. Write content/writing/{slug}.md with front matter including article_id (idempotent rebuilds).
  5. CI builds and deploys.

Run nightly or on deploy—nightly is enough for most portfolios.


GitHub Actions sketch

# .github/workflows/sync-medium.yml
name: Sync Medium writing
on:
 schedule: [{ cron: '06***' }]
 workflow_dispatch:
jobs:
 sync:
 runs-on: ubuntu-latest
 steps:
 - uses: actions/checkout@v4
 - uses: actions/setup-node@v4
 with: { node-version: '20' }
 - run: node scripts/sync-medium-portfolio.mjs
 env:
 ZENNDRA_API_KEY: ${{ secrets.ZENNDRA_API_KEY }}
 MEDIUM_USERNAME: ${{ vars.MEDIUM_USERNAME }}
 - uses: stefanzweifel/git-auto-commit-action@v5
 with:
 commit_message: 'chore:syncMediumposts'

sync-medium-portfolio.mjs (core logic)

import fs from 'node:fs/promises';
import path from 'node:path';

const API = 'https://api.zenndra.com';
const headers = { Authorization: `Bearer ${process.env.ZENNDRA_API_KEY}` };
const handle = process.env.MEDIUM_USERNAME;

const idRes = await fetch(`${API}/user/id_for/${handle}`, { headers });
const { user_id } = await idRes.json();

const listRes = await fetch(`${API}/user/${user_id}/articles`, { headers });
const { articles } = await listRes.json();

for (const a of articles) {
 const outPath = path.join('content/writing', `${a.id}.md`);
 try {
 await fs.access(outPath);
 continue; // already synced
 } catch {}

 const mdRes = await fetch(`${API}/article/${a.id}/markdown`, { headers });
 const { markdown } = await mdRes.json();

 const frontMatter = `---
title: "${a.title.replace(/"/g, '\\"')}"
date: ${a.published_at ?? new Date().toISOString()}
medium_id: ${a.id}
canonical: ${a.url}
---
`;
 await fs.writeFile(outPath, frontMatter + '\n' + markdown);
}

Tune paths for your generator. Add reading time from /article/{id} metadata when you want a premium layout.


SEO note

Pick one canonical home early:

  • Medium canonical + on-site teaser, or
  • Your domain canonical + Medium as syndication.

Document the choice; flip when analytics justify redirects.


Keywords

sync medium to static site, medium portfolio automation, medium markdown export, hugo medium sync, developer portfolio blog.


Further reading