Automated content syndication from enterprisevibecode.com (Beehiiv) to Medium, Substack, and LinkedIn Newsletter.
Beehiiv RSS → GitHub Actions (hourly cron) → Fan out to 3 destinations
| Destination | Method | Canonical URL | Notes |
|---|---|---|---|
| Medium | REST API | ✅ Native | Cleanest integration |
| Substack | Playwright (browser) | No API exists | |
| LinkedIn Newsletter | Playwright (browser) | Newsletter API doesn't exist |
State is tracked in published.json (committed to repo) to prevent duplicates and enable retries.
git clone <your-repo-url>
cd enterprise-vibe-code-crosspost
npm install
npx playwright install chromium --with-depsSet these as GitHub Actions secrets in your repository:
| Secret | How to get it |
|---|---|
MEDIUM_TOKEN |
Medium → Settings → Security and Apps → Integration tokens → Generate |
MEDIUM_USER_ID |
Run: curl -H "Authorization: Bearer YOUR_TOKEN" https://api.medium.com/v1/me and copy the id field |
| Secret | How to get it |
|---|---|
SUBSTACK_EMAIL |
Your Substack login email |
SUBSTACK_PASSWORD |
Your Substack password (enable password login in Substack settings) |
Note: Substack primarily uses magic-link email login. To use password auth, go to your Substack account settings and set a password.
LinkedIn has aggressive bot detection. Cookie-based auth is strongly recommended over email/password.
| Secret | How to get it |
|---|---|
LINKEDIN_COOKIES |
(Preferred) Run the cookie export helper (see below) |
LINKEDIN_EMAIL |
(Fallback) Your LinkedIn email |
LINKEDIN_PASSWORD |
(Fallback) Your LinkedIn password |
LINKEDIN_NEWSLETTER_URL |
Your newsletter URL, e.g. https://www.linkedin.com/newsletters/your-newsletter-XXXXXXXXX/ |
Exporting LinkedIn cookies:
npx tsx src/export-linkedin-cookies.tsThis opens a browser window. Log in to LinkedIn manually, then press Enter in the terminal. Cookies are saved to linkedin-cookies.json — copy the contents into the LINKEDIN_COOKIES GitHub secret, then delete the local file.
⚠️ LinkedIn cookies expire. You'll need to re-export cookies periodically (roughly every 30-60 days). Set a calendar reminder.
| Secret | How to get it |
|---|---|
SLACK_WEBHOOK_URL |
Create a Slack incoming webhook |
Open https://www.enterprisevibecode.com/feed in a browser. If it returns XML with your posts, you're good. If not, enable RSS in Beehiiv: Settings → Publication → RSS Feed → Generate Feed.
# Local dry run (drafts only, no publishing)
DRY_RUN=true \
MEDIUM_TOKEN=xxx \
MEDIUM_USER_ID=xxx \
SUBSTACK_EMAIL=xxx \
SUBSTACK_PASSWORD=xxx \
LINKEDIN_COOKIES='[...]' \
npx tsx src/index.tsOr trigger a dry run via GitHub Actions: go to Actions → Cross-Post Newsletter → Run workflow → set dry_run: true.
The workflow runs automatically every hour. After verifying with a dry run, it's ready to go.
├── .github/workflows/
│ └── crosspost.yml # GitHub Actions cron workflow
├── src/
│ ├── index.ts # Main orchestrator
│ ├── feed.ts # Beehiiv RSS parser
│ ├── transform.ts # HTML cleaning + per-platform formatting
│ ├── state.ts # published.json state management
│ ├── notify.ts # Slack/console failure notifications
│ ├── types.ts # TypeScript interfaces
│ ├── export-linkedin-cookies.ts # Interactive cookie export helper
│ └── publishers/
│ ├── medium.ts # Medium REST API client
│ ├── substack.ts # Substack Playwright automation
│ └── linkedin.ts # LinkedIn Playwright automation
├── published.json # Syndication state (tracked in git)
├── package.json
└── tsconfig.json
Straightforward REST API call. Sends HTML content with canonicalUrl pointing back to Beehiiv. Google recognizes this canonical tag and attributes SEO value to your primary site.
No API exists, so we automate the browser:
- Launch headless Chromium
- Log in with email/password
- Navigate to the post editor (
/publish/post) - Fill title via input field
- Paste HTML content into ProseMirror editor via clipboard events
- Click through the publish flow (selecting "web only" to avoid emailing subscribers)
- Capture the published URL
If automation fails: A Slack/console notification is sent with the title, content, and a direct link to the Substack editor for manual paste.
Also no API for newsletter editions, so browser automation:
- Launch headless Chromium
- Authenticate via cookies (preferred) or email/password
- Navigate to the article editor (either via newsletter page or
/article/new/) - If a newsletter selection dialog appears, select the correct newsletter
- Type the title, paste HTML content into the editor
- Click through the publish flow
- Capture the published URL
Cookie refresh: LinkedIn sessions expire. Re-export cookies every 30-60 days using the helper script.
| Scenario | Behavior |
|---|---|
| One destination fails | Other destinations still publish. Failed destination retries on next run. |
| Max retries (3) exceeded | Destination marked as permanently failed for that post. Manual action needed. |
| Playwright selector broken | Error screenshot saved as GitHub Actions artifact for debugging. |
| RSS feed unreachable | Entire run skips gracefully. Next run retries. |
| All destinations succeed | State committed, summary logged. |
| Task | Frequency | How |
|---|---|---|
| Refresh LinkedIn cookies | Every 30-60 days | Run npx tsx src/export-linkedin-cookies.ts |
| Check GitHub Actions logs | Weekly | Review for failures or warnings |
| Update Playwright | Monthly | npm update playwright — UI changes may require selector updates |
| Verify Substack selectors | After Substack UI updates | Run a dry run and check the screenshot |
$0/month. GitHub Actions free tier provides 2,000 minutes/month. Hourly runs use ~720 min/month.
- Add Dev.to (has a great API with canonical URL support)
- Add Hashnode (API with canonical URLs)
- AI-generated platform-specific teasers via Claude API
- Engagement analytics aggregation dashboard
- LinkedIn token auto-refresh workflow