DEV Community

Cover image for How to Build a Safe DEV.to Publishing Workflow
Nael M. Awadallah
Nael M. Awadallah

Posted on

How to Build a Safe DEV.to Publishing Workflow

How to Build a Safe DEV.to Publishing Workflow

Tired of scrambling at the last minute, proofreading your article for the tenth time, only to find a broken link or a typo after hitting publish? Publishing content, especially on platforms like DEV.to, can feel like walking a tightrope without a safety net. What if you could build a robust system that ensures quality, consistency, and peace of mind before your content goes live?

This article will guide you through creating a safe, efficient, and automated DEV.to publishing workflow, leveraging tools like Git, linters, and the DEV.to API. Say goodbye to publishing anxiety and hello to streamlined content delivery.

Table of Contents

The Problem: Why Manual Publishing is Risky

Many content creators, especially solo developers, rely on a manual copy-paste workflow for publishing. While seemingly straightforward for a single article, this approach quickly introduces risks and inefficiencies as your content volume grows:

  • Human Error: The most common culprit. Typos, broken links, incorrect tags, forgetting to add a cover_image, or even accidentally pasting an outdated draft are all too frequent. These errors detract from your professionalism and audience experience.
  • Inconsistency: Without a defined process, articles might vary wildly in formatting, frontmatter structure, SEO descriptions, or even the tone of voice. This makes it harder for readers to follow your content consistently.
  • Time Drain: Repetitive tasks like manually formatting, uploading images, and filling in metadata consume valuable time that could be spent on writing or research.
  • Lack of Version Control: Once an article is live, how do you track changes? How do you revert to a previous version if an update introduces a new problem? Manual processes lack the crucial safety net of version history.
  • Stress and Anxiety: The constant fear of making a mistake can make publishing a daunting and stressful experience, diminishing the joy of sharing your knowledge.

A safe publishing workflow aims to mitigate these risks, turning the publishing process into a predictable, robust, and even enjoyable part of your content creation journey.

Pillars of a Safe Publishing Workflow

Building a "safe" workflow means implementing safeguards at every critical stage. Here are the foundational pillars:

Pillar 1: Content Version Control with Git

The cornerstone of any robust development workflow should also apply to content. Storing your articles as markdown files in a Git repository offers immense benefits:

  • Change Tracking: Every modification, big or small, is recorded. You can see who changed what and when.
  • Collaboration: If you work with others (editors, co-authors), Git simplifies collaboration through branches, pull requests, and merges.
  • Rollback Capability: Made a disastrous edit? Git allows you to revert to any previous commit, saving you from publishing irreparable errors.
  • Single Source of Truth: Your Git repository becomes the definitive source for all your published and draft content.

Real-world Example: Imagine having a content/devto/ directory in your project. Each article is a .md file, for example, content/devto/how-to-build-safe-devto-workflow.md. This allows you to manage everything in a familiar developer environment.

Pillar 2: Pre-Publishing Checks with Linters & Hooks

Automated quality control is your first line of defense against errors. Before any content even thinks about being published, it should pass a series of checks.

  • Markdown Linting: Tools like markdownlint or remark-lint can enforce stylistic consistency (e.g., heading levels, list formatting, code block usage) and catch common markdown syntax errors.
  • Frontmatter Validation: Ensure your article's frontmatter (title, tags, description, published status) adheres to a predefined schema and includes all mandatory fields. This prevents publishing articles with missing metadata.
  • Git Pre-commit Hooks: Using tools like husky and lint-staged, you can automatically run these linters and validators before a commit is even created. If checks fail, the commit is blocked, forcing you to fix issues early.

This step shifts error detection from post-publish (embarrassing!) to pre-commit (private and fixable!).

Code Snippet Example (using lint-staged with markdownlint):

First, install dependencies:

npm install --save-dev husky lint-staged markdownlint-cli
Enter fullscreen mode Exit fullscreen mode

Then, configure package.json for husky and lint-staged:

// package.json
{
  "name": "devto-content-repo",
  "version": "1.0.0",
  "description": "My DEV.to articles",
  "scripts": {
    "lint:md": "markdownlint --config .markdownlint.jsonc '**/*.md'",
    "prepare": "husky install"
  },
  "devDependencies": {
    "husky": "^9.0.11",
    "lint-staged": "^15.2.2",
    "markdownlint-cli": "^0.39.0"
  },
  "lint-staged": {
    "*.md": "npm run lint:md"
  }
}
Enter fullscreen mode Exit fullscreen mode

And add a pre-commit hook via Husky:

npx husky add .husky/pre-commit "npx lint-staged"
Enter fullscreen mode Exit fullscreen mode

Now, any staged .md file will be linted before committing.

Pillar 3: Automated Publishing via the DEV.to API

Manually copying and pasting content into a web editor is ripe for errors. The DEV.to API offers a programmatic way to create and update articles, ensuring consistency and speed.

  • Direct Interaction: The API allows you to send your markdown content, along with its frontmatter, directly to DEV.to.
  • Consistency: Scripts don't get tired or forget. They'll always apply the same logic for setting tags, descriptions, and other metadata.
  • Integration with CI/CD: This is where the magic happens. Once your content is approved in Git, a CI/CD pipeline can automatically publish it without any manual intervention.

Understanding the DEV.to API is crucial here. You'll need an API key (available in your DEV.to settings) and to understand the structure of an article payload.

Pillar 4: Review and Approval Gates

While automation handles technical correctness, human review remains vital for content quality, accuracy, and tone.

  • Pull Requests (PRs): In a team setting, a PR allows colleagues or editors to review changes before they are merged into the main branch (and subsequently published).
  • Staging Environments: For highly critical content, you might even consider a "staging" DEV.to account to publish articles privately for final review before pushing them to your public profile.

This pillar is about ensuring the content itself is polished and ready, not just its technical formatting.

Building Your Workflow: A Step-by-Step Guide

Let's put these pillars into practice.

Step 1: Set Up Your Git Repository

Create a new Git repository (e.g., on GitHub, GitLab, or Bitbucket) dedicated to your DEV.to content.

mkdir devto-articles
cd devto-articles
git init
Enter fullscreen mode Exit fullscreen mode

Establish a clear directory structure, perhaps articles/, where each article lives in its own markdown file.

Step 2: Define Your Markdown Structure

DEV.to articles use markdown with YAML frontmatter. Standardize this across all your articles.

---
title: "My Awesome Article Title"
published: false # Set to true to publish, false for drafts
description: "A short, SEO-friendly description of my article."
tags: ["webdev", "javascript", "tutorial"], Max 4 tags
cover_image: "https://example.com/my-cover-image.png" # Optional, but recommended
canonical_url: "https://myblog.com/original-post" # Optional, for cross-posting
series: "My Article Series" # Optional
---

# My Awesome Article Title

This is the main content of my article, written in markdown.

Enter fullscreen mode Exit fullscreen mode

Ensure consistency in frontmatter fields and their types.

Step 3: Implement Linting and Validation

We already covered the markdownlint setup using husky and lint-staged. Beyond markdown style, you might want to validate frontmatter.

You can write a simple Node.js script to check if specific frontmatter fields are present and valid (e.g., tags is an array with max 4 items, description is under 160 characters).

Conceptual Frontmatter Validation Script (validate-frontmatter.js):

// validate-frontmatter.js
const fs = require('fs');
const path = require('path');
const matter = require('gray-matter');

const validateArticle = (filePath) => {
  const fileContents = fs.readFileSync(filePath, 'utf8');
  const { data: frontmatter } = matter(fileContents);

  const errors = [];

  if (!frontmatter.title || frontmatter.title.length < 5) {
    errors.push("Title is missing or too short.");
  }
  if (!frontmatter.description || frontmatter.description.length < 20 || frontmatter.description.length > 160) {
    errors.push("Description is missing or outside 20-160 character range.");
  }
  if (!frontmatter.tags || !Array.isArray(frontmatter.tags) || frontmatter.tags.length === 0 || frontmatter.tags.length > 4) {
    errors.push("Tags must be an array with 1-4 items.");
  }
  // Add more checks as needed: cover_image format, canonical_url, etc.

  if (errors.length > 0) {
    console.error(`Validation failed for ${filePath}:`);
    errors.forEach(err => console.error(`  - ${err}`));
    return false;
  }
  return true;
};

// Example usage (you'd integrate this with lint-staged or a CI step)
const files = process.argv.slice(2);
let allValid = true;
for (const file of files) {
  if (file.endsWith('.md')) { // Only validate markdown files
    if (!validateArticle(file)) {
      allValid = false;
    }
  }
}

if (!allValid) {
  process.exit(1); // Exit with error code if any validation fails
}
console.log(`Successfully validated ${files.length} markdown files.`);
Enter fullscreen mode Exit fullscreen mode

Integrate this into lint-staged or your CI pipeline.

Step 4: Script the DEV.to API Integration

You'll need a script that:

  1. Reads your markdown file.
  2. Parses the frontmatter (e.g., using gray-matter npm package).
  3. Constructs the DEV.to API payload.
  4. Makes an API call (POST for new articles, PUT for updates) using your DEV.to API key.

Conceptual Node.js Publishing Script (publish-to-devto.js):

// publish-to-devto.js
const fs = require('fs');
const path = require('path');
const matter = require('gray-matter');
const fetch = require('node-fetch'); // or use axios

const DEVTO_API_KEY = process.env.DEVTO_API_KEY;
const API_BASE_URL = 'https://dev.to/api/articles';

async function publishArticle(filePath) {
  if (!DEVTO_API_KEY) {
    console.error('DEVTO_API_KEY environment variable is not set.');
    process.exit(1);
  }

  const fileContents = fs.readFileSync(filePath, 'utf8');
  const { data: frontmatter, content } = matter(fileContents);

  const articleId = frontmatter.devto_id; // Store DEV.to article ID in frontmatter for updates

  const articlePayload = {
    article: {
      title: frontmatter.title,
      description: frontmatter.description,
      published: frontmatter.published || false,
      body_markdown: content,
      tags: frontmatter.tags,
      series: frontmatter.series,
      cover_image: frontmatter.cover_image,
      canonical_url: frontmatter.canonical_url,
      // Add other DEV.to specific fields as needed
    }
  };

  const headers = {
    'Content-Type': 'application/json',
    'api-key': DEVTO_API_KEY,
  };

  let response;
  if (articleId) {
    console.log(`Updating article with ID: ${articleId}`);
    response = await fetch(`${API_BASE_URL}/${articleId}`, {
      method: 'PUT',
      headers: headers,
      body: JSON.stringify(articlePayload),
    });
  } else {
    console.log('Creating new article...');
    response = await fetch(API_BASE_URL, {
      method: 'POST',
      headers: headers,
      body: JSON.stringify(articlePayload),
    });
  }

  if (response.ok) {
    const data = await response.json();
    console.log(`Article "${data.title}" successfully ${articleId ? 'updated' : 'published'}!`);
    console.log(`View here: ${data.url}`);

    // If new article, update the markdown file with devto_id for future updates
    if (!articleId) {
      console.log(`Updating markdown file with devto_id: ${data.id}`);
      frontmatter.devto_id = data.id;
      const newFrontmatter = matter.stringify(content, frontmatter);
      fs.writeFileSync(filePath, newFrontmatter);
    }
  } else {
    const errorData = await response.json();
    console.error(`Failed to publish article: ${response.status} ${response.statusText}`);
    console.error('Error details:', errorData);
    process.exit(1);
  }
}

// Get the article file path from command line arguments
const articlePath = process.argv[2];
if (!articlePath) {
  console.error('Usage: node publish-to-devto.js <path/to/article.md>');
  process.exit(1);
}

publishArticle(articlePath);
Enter fullscreen mode Exit fullscreen mode

Important: Store your DEVTO_API_KEY securely as an environment variable, not directly in your code.

Step 5: Automate with CI/CD (GitHub Actions Example)

Once your content is in Git, linted, and you have a publishing script, integrate it with a CI/CD service like GitHub Actions. This allows for automated publishing whenever changes are merged into your main branch.

# .github/workflows/publish-devto.yml
name: Publish DEV.to Article

on:
  push:
    branches:
      - main
    paths:
      - 'articles/**.md' # Trigger only if markdown files change

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Install dependencies
        run: npm install gray-matter node-fetch

      - name: Validate Markdown and Frontmatter
        run: |
          npm run lint:md -- articles/**/*.md
          node validate-frontmatter.js articles/**/*.md
        # Assuming lint:md and validate-frontmatter.js are set up as per previous steps

      - name: Get changed markdown files
        id: changed-files-markdown
        uses: tj-actions/changed-files@v40
        with:
          files: articles/**/*.md

      - name: Publish/Update articles to DEV.to
        env:
          DEVTO_API_KEY: ${{ secrets.DEVTO_API_KEY }}
        run: |
          for file in ${{ steps.changed-files-markdown.outputs.all_changed_files }}; do
            # Check if 'published: true' is in the frontmatter before attempting to publish
            if grep -q "published: true" "$file"; then
              echo "Publishing/updating $file..."
              node publish-to-devto.js "$file"
            else
              echo "Skipping $file, 'published: true' not found in frontmatter."
            fi
          done
Enter fullscreen mode Exit fullscreen mode

This workflow checks out your code, installs dependencies, runs validation, identifies changed markdown files, and then calls your publishing script for each changed file that has published: true in its frontmatter. Remember to add DEVTO_API_KEY to your GitHub repository secrets.

Common Mistakes to Avoid

Even with an automated workflow, certain pitfalls can arise:

  • Not using version control for drafts: Even if an article isn't ready for publishing, it should still be in Git. This protects against accidental loss and provides a history.
  • Ignoring linting warnings: Linting is there to help! Don't bypass warnings; address them to maintain quality.
  • Hardcoding API keys: Never embed sensitive information like API keys directly in your code. Always use environment variables or secret management systems.
  • Not handling article IDs for updates: When an article is first published, the DEV.to API returns an id. Store this id in your markdown's frontmatter (e.g., devto_id: 12345) so your script knows to send a PUT request for updates instead of creating a new article.
  • Over-automation without human review: While automation is powerful, critical content often benefits from a final human review before the "publish" button (even an automated one) is pressed. Use the published: false flag to gate articles until they're truly ready.
  • Ignoring DEV.to's platform-specific nuances: DEV.to automatically handles image uploads from external URLs in markdown, but be aware of how it renders certain markdown extensions or embeds. Always test.
  • Assuming instant updates: API calls are usually fast, but network latency or platform processing can mean a slight delay before content is fully live and searchable.

Key Takeaways

  1. Git is for Content Too: Treat your articles as code. Git provides version control, collaboration, and a safety net.
  2. Automate Quality: Linters and validation scripts catch errors early, preventing embarrassing post-publish fixes.
  3. Leverage the DEV.to API: Programmatic publishing ensures consistency, speed, and reliability.
  4. Integrate with CI/CD: Automate the entire pipeline from commit to publish with tools like GitHub Actions.
  5. Start Simple, Iterate: You don't need a perfect system from day one. Start with Git and basic linting, then gradually add API integration and CI/CD.
  6. Human Review is Still Key: Automation handles mechanics, but human eyes ensure content quality and accuracy.

Final Thoughts

Building a safe DEV.to publishing workflow might seem like an upfront investment, but the benefits in terms of reduced errors, increased efficiency, and peace of mind are invaluable. By embracing version control, automation, and sensible gates, you transform publishing from a stressful chore into a seamless, confident act of sharing.

What Do You Think?

Have you faced this problem before?
How did you solve it?

Let's discuss in the comments.

Top comments (0)