I wanted to cross-post my blog to Dev.to. The whole thing — syndication endpoint, bulk admin UI, canonical URLs back to vibescoder.dev — took about two hours to build. The next four hours were spent debugging a single API field that silently breaks everything without returning an error.
This is that story.
The Goal
vibescoder.dev runs on Next.js 16 with content in a separate private repo. Posts already had a devtoUrl field in the frontmatter schema — it just wasn't wired to anything. The plan was simple:
- Build an API endpoint that reads a post from GitHub, creates it on Dev.to, and writes the returned URL back to the content repo
- Add a button to the admin toolbar on each post
- Build a bulk UI to syndicate multiple posts at once
- Only syndicate the good stuff — standalone, actionable posts. Skip the meta diary entries.
The Three-Call Chain
The single-post endpoint (POST /api/syndicate/devto) chains three API calls:
- GitHub Contents API — read the raw MDX from the content repo
- Dev.to Articles API — create the article, published immediately, with
canonical_urlpointing back to vibescoder.dev - GitHub Contents API — commit the returned
devtoUrlback into the post's frontmatter
All three calls fit inside Vercel's 10-second Hobby plan function timeout. Usually. More on that later.
Bulk Syndication
Syndicating 11 posts one at a time from individual post pages wasn't going to work. I built an admin dashboard at /admin/syndication with:
- Checkboxes and select-all for every published post
- A "Publish Selected" button that calls the syndication endpoint once per post, sequentially
- A "Stop" button to abort mid-run
- Real-time status showing which post is processing
The key architecture decision: the browser drives the loop, not the server. Each post is one API call. The client waits for the response, then fires the next one. This matters because of Vercel's 10-second function timeout — a server-side batch processing 11 posts would blow past that limit before finishing the third one.
The Rate Limit Speed Bump
First few posts syndicated fine. Then: 429 Too Many Requests.
Dev.to rate-limits article creation to roughly one per 30 seconds. My initial bulk implementation used a 5-second delay between posts. Bumped it to 31 seconds and added 429 retry logic.
31 seconds × 11 posts = ~5.5 minutes of wall time. The "Stop" button went from nice-to-have to essential.
The published_at Disaster
This is where the session went sideways.
After the initial syndication worked, I wanted Dev.to articles to show their original blog publish date — not the date they were cross-posted. Simple enough: add published_at to the API request body.
Every syndication call started failing. But "failing" is generous — the Dev.to API returned what looked like a success response. No error message. No 4xx status code. The article just... didn't exist.
Six Attempts, One Root Cause
- Added
published_atto the create payload — articles stopped being created - Built a
fix-datesendpoint to retroactively set dates on existing articles via PUT — Dev.to accepts the PUT but silently ignorespublished_aton published articles - Built a
rebuildendpoint to delete and recreate articles with correct dates — same silent failure on create - Tried
maxDuration=600thinking it was a Vercel timeout — doesn't even work on the Hobby plan - Refactored to client-driven loops to avoid serverless timeouts — still failed because the root cause wasn't timing
- Tried splitting create and save into separate calls — still failed
Four hours in, I finally isolated it: removing published_at from the POST body fixed everything instantly.
The Working Payload
{
article: {
title: post.title,
body_markdown: content,
published: true,
canonical_url: `https://vibescoder.dev/blog/${post.slug}`,
tags: post.tags.slice(0, 4),
// NO published_at — silently breaks article creation
}
}Dev.to silently rejects the entire POST body if published_at is included on article creation. No error, no 4xx, no documentation. The API just swallows your request and returns something that looks like success.
This means Dev.to articles show their Dev.to publish date, not the original blog date. There's no workaround. Accept it and move on.
The 443-Line Cleanup
The published_at debugging left behind two dead endpoints and their associated UI code:
fix-dates/route.ts— tried to fix dates on existing articlesrebuild/route.ts— tried to delete and recreate articles
Plus rebuild handlers, state variables, and a "Maintenance" section in the syndication dashboard. All of it dead code from dead ends.
−443 lines in the cleanup commit. TypeScript compiled clean after removal.
Selective Syndication
Not everything belongs on Dev.to. I categorized all 21 published posts into tiers:
Syndicated (11 posts): Standalone, actionable content — LLM benchmarks, homelab debugging guides, AI strategy pieces, infrastructure walkthroughs. Posts where someone landing from Dev.to gets full value without reading the rest of the blog.
Skipped (10 posts): Friday Fixes (self-referential to the blog), day-by-day diary entries (no standalone value), meta posts about building the blog itself.
Also created a "Local LLM Showdown" series on Dev.to to group the five benchmark posts together. Dev.to series give you a navigation sidebar on each article — free structure.
The Vercel Hobby Tax
The 10-second function timeout shaped every architectural decision:
- Client-driven loops instead of server-side batch processing
- One API call per post in bulk syndication, browser manages timing
- No deferred execution —
after()fromnext/serverwas attempted and abandoned - No retry stacking — each route does exactly three calls, no extras
If I were on Vercel Pro (60-second timeout), the bulk endpoint could process all 11 posts server-side in one call. On Hobby, the browser becomes the orchestrator. It's not elegant, but it works within the constraints.
What I Learned
Silent failures are the most expensive kind. A 4xx with an error message would have saved four hours. Dev.to's API accepted the request, didn't create the article, and gave no indication anything went wrong. When you're debugging against an API that never says no, you blame everything else first — your auth, your payload structure, your timeout, your hosting platform.
Architecture follows constraints. The 10-second Vercel timeout pushed the batch orchestration from server to client. The Dev.to rate limit pushed the delay to 31 seconds. Neither is ideal, but both are correct for the environment.
Dead code from debugging is a separate commit. The 443-line cleanup happened after the feature was working. Keeping the debug artifacts around "just in case" is how codebases rot. If it's dead, kill it.
What's Next
- Monitor Dev.to analytics — does cross-posting actually drive traffic back to vibescoder.dev?
- Add the syndication button to the admin post editor (currently only on the post page toolbar)
- Consider automating syndication on publish — right now it's manual and selective, which feels right for a 21-post blog
By the Numbers
- 11 posts syndicated to Dev.to
- 13 commits to the engine repo for the syndication feature
- 443 lines of dead code removed in cleanup
- 31 seconds minimum delay between bulk syndication calls
- 10 seconds Vercel Hobby plan function timeout that shaped the architecture
- 0 error messages from Dev.to when
published_atsilently breaks article creation - ~6 hours total session time — 2 hours building, 4 hours debugging a silent API
- 1 series created on Dev.to ("Local LLM Showdown") grouping 5 benchmark posts