Blog Syndication: Cross-Publishing Blog Posts to Dev.to, Hashnode, and Medium

Published: (February 10, 2026 at 11:36 PM EST)
6 min read
Source: Dev.to

Source: Dev.to

Navin Varma

Navin Varma

Originally published on nvarma.com


Why canonical URLs matter

Before getting into the code, this is the one thing you should care about if you cross‑publish anything. Every platform lets you set a canonical URL — canonical_url on Dev.to, originalArticleURL on Hashnode. It’s basically a pointer that says “the original lives on my site.” If you don’t set it, Google sees three copies and will probably rank the platform version higher than yours.

Set the canonical URL. Every time. No exceptions.


Dev.to has a straightforward REST API

Dev.to is the simplest one. You generate an API key at , then make a POST request:

const payload = {
  article: {
    title,
    body_markdown: body,
    published: true,
    tags: tags
      .slice(0, 4)
      .map(t => t.toLowerCase().replace(/[^a-z0-9]/g, "")),
    canonical_url: canonicalUrl,
  },
};

const res = await fetch("https://dev.to/api/articles", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "api-key": process.env.DEVTO_API_KEY,
  },
  body: JSON.stringify(payload),
});
  • With Dev.to you must limit tags to 4, all lower‑case, no special characters.
  • Respect the rate limit (HTTP 429).
  • The response contains the article ID and URL; store it so you don’t publish the same post twice.

Hashnode uses GraphQL

Hashnode’s API is GraphQL‑based. You need a Personal Access Token from and your publication ID. If you know your blog URL, you can fetch the publication ID without logging in:

curl -s -X POST https://gql.hashnode.com \
  -H "Content-Type: application/json" \
  -d '{"query":"{ publication(host:\"yourblog.hashnode.dev\") { id title } }"}'

The publish mutation looks like this:

const mutation = `
  mutation PublishPost($input: PublishPostInput!) {
    publishPost(input: $input) {
      post { id url }
    }
  }
`;

const variables = {
  input: {
    title,
    contentMarkdown: body,
    publicationId,
    tags: tags.map(t => ({
      name: t,
      slug: t.toLowerCase().replace(/[^a-z0-9]+/g, "-"),
    })),
    originalArticleURL: canonicalUrl,
    slug,
  },
};
  • Hashnode tags are objects with both a name and a slug.
  • originalArticleURL is their version of the canonical URL.

Medium dropped support for API tokens

You can’t programmatically publish to Medium anymore (no official API).
You can still get your posts on Medium manually while preserving the canonical URL:

  1. Go to .
  2. Click Import a story.
  3. Paste your post’s URL (e.g. https://www.nvarma.com/blog/your-post-slug/).
  4. Medium imports the content and automatically sets the canonical URL to point back to your blog post.

The manual step is a bit of a pain, but it’s the only way to keep the SEO benefit. For older posts, repeat the same process for each URL; you may need to tidy up formatting after import.


Sanitizing MDX for other platforms

Some of my posts use custom image components and are written in MDX. MDX‑generated content does not work on Dev.to or Hashnode, so I wrote a sanitizer that converts it to portable Markdown:

// Strip MDX imports
content = content.replace(/^import\s+.*$/gm, "");

// Convert // to markdown
content = content.replace(
  /]*>\s*[Image: ([^]]*\/?>\s*(?:([\s\S]*?))?\s*/g,
  (_match, src, alt, caption) => {
    let result = `![${alt}](https://www.nvarma.com/${resolveUrl(src)})`;
    if (caption) result += `\n*${caption.trim()}*`;
    return result;
  }
);

// Replace Astro components with “see original” links
content = content.replace(
  /]*\/?>/g,
  (_match, componentName) => {
    return `*[Interactive ${componentName} — see original post](https://www.nvarma.com/${canonicalUrl})*`;
  }
);

// Resolve relative paths to absolute URLs
content = content.replace(
  /!\[([^\]]*)\]\((?!https?:\/\/)([^)]+)\)/g,
  (_match, alt, src) => `![${alt}](https://www.nvarma.com/${SITE_URL}${src})`
);

This sanitizer:

  • Removes MDX import statements.
  • Turns //“ blocks into standard Markdown image syntax (with optional caption).
  • Replaces custom Astro components with a placeholder link back to the original post.
  • Converts relative image URLs to absolute URLs so they render correctly on other platforms.

That’s the whole pipeline: push a new post to my Astro site, and a GitHub Action cross‑publishes it to Dev.to and Hashnode with the canonical URL pointing back to my site. Medium remains a manual step, but the “Import a story” feature keeps the SEO signal intact. Happy cross‑publishing!

The Astro component replacement is my favorite part. If I have a `*[Interactive BeforeAfterCarousel — see original post](https://www.nvarma.com/blog/2026-02-10-cross-publishing-blog-posts-devto-hashnode-medium/)*` in my Astro rebuild post, the cross‑published version gets a link that says **“Interactive BeforeAfterCarousel – see original post”** instead of broken HTML. Not perfect, but it’s honest and sends people to the real thing.

I also prepend each post with an **“Originally published on nvarma.com”** header and append a footer with a link back. A little self‑promotional, but that’s kind of the whole point of cross‑publishing.

---

## Automating it with GitHub Actions

The workflow triggers whenever I push changes to `src/content/blog/**` on `main`:

```yaml
name: Cross-Publish Blog Posts

on:
  push:
    branches: [main]
    paths:
      - 'src/content/blog/**'
  workflow_dispatch:
    inputs:
      post_id:
        description: 'Specific post ID to publish (filename without extension)'
        required: false

The workflow_dispatch trigger lets me manually publish a specific post if I need to. The script reads all blog posts, checks a tracking JSON file to see what’s already been published, and then processes only the new posts. It also skips posts older than 30 days to avoid flooding syndication sites with stale content.

The tracking file gets committed back to the repo automatically, so there’s a record of what went where:

{
  "2026-02-09-manager-ic-pendulum": {
    "title": "The Manager‑IC Pendulum...",
    "firstPublishedAt": "2026-02-10T03:33:00Z",
    "platforms": {
      "devto": {
        "id": "3245645",
        "url": "https://dev.to/navinvarma/the-manager-ic-pendulum...",
        "publishedAt": "2026-02-10T03:33:00Z"
      },
      "hashnode": {
        "id": "abc123",
        "url": "https://navinvarma.hashnode.dev/the-manager-ic-pendulum...",
        "publishedAt": "2026-02-10T04:00:00Z"
      }
    }
  }
}

Setting it up yourself

Dev.to – Generate an API key at Settings > Extensions. Store it as DEVTO_API_KEY in your repository’s GitHub Actions secrets.

Hashnode – Get a Personal Access Token from Settings > Developer. Look up your publication ID with the curl command from the script. Store them as HASHNODE_PAT and HASHNODE_PUBLICATION_ID respectively.

Medium – Import stories manually using Medium’s import tool. Paste the canonical URL and Medium will import the content of your post.

GitHub Actions secrets – Go to Settings > Secrets and variables > Actions, and add each secret. The workflow only runs when blog content changes, so it won’t burn through your Actions minutes.

If you already have posts on a platform (like I did with Dev.to), make sure the tracking JSON contains those entries before your first run. Otherwise the script may try to publish duplicates, and the APIs will likely reject them or create duplicate posts.


Reflections

Setting this up took an evening. It isn’t a huge amount of work, but you need to understand how Astro builds work and have some familiarity with GitHub Actions, APIs, and integrations.

The nice part is that I now have a single workflow: write in Markdown, push to Git, and the post appears on three platforms with proper canonical URLs. Medium still requires a manual import, but the process is simple and the canonical URL is preserved.

I’ll probably add more platforms later if they provide decent APIs. This was a fun weekend project to automate away some of my toil, and I hope you find it useful.

This post was originally published on nvarma.com. Follow me there for more on software architecture, engineering leadership, and the craft of building things that last.

0 views
Back to Blog

Related posts

Read more »