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: '0 6 * * *' }]
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: sync Medium posts'
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.
Top comments (0)