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
- Resolve
@username→ stableuser_id(guide). -
GET /user/{user_id}/articleson a schedule. - For each new
article_id,GET /article/{id}/markdown. - Write
content/writing/{slug}.mdwith front matter includingarticle_id(idempotent rebuilds). - 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
For further actions, you may consider blocking this person and/or reporting abuse
