ZyVOP Logo
Content That Connects
SeriesAI NewsWhy ZyVOPJoin Discord
ZyVOP Logo
Content That Connects

Empowering developers and creators with cutting-edge insights, comprehensive tutorials, and innovative solutions for the digital future.

Content

  • Categories
  • Tags
  • Badges
  • Leaderboard
  • Write Article
  • Newsletter

Company

  • About Us
  • Why ZyVOP
  • Developer API & CLI
  • Write for Us
  • Contact

Connect

  • Privacy Policy
  • Terms of Service
  • Cookie Policy
  • DMCA Policy
  • Code of Conduct

© 2026 ZyVOP. Crafted with care for the developer community.

Made with ❤️ by the ZyVOP team
All systems operational
HomeGitOps for Technical Writers: Continuous Publishing with the ZyVop CLI and GitHub Actions

GitOps for Technical Writers: Continuous Publishing with the ZyVop CLI and GitHub Actions

How to treat technical articles like production software—versioned in Git, reviewed in pull requests, and continuously deployed across Dev.to, Hashnode, Medium, and Bluesky on every git push.

Sanju Singh
Sanju SinghSenior Developer
August 23, 2026
7 min read
GitOps for Technical Writers: Continuous Publishing with the ZyVop CLI and GitHub Actions
#gitops#cicd#GitHub Actions#DevOps#automation
👍1

For software engineers, writing code and writing technical articles should feel like the same discipline. Both require structural hierarchy, precise syntax, logical proofs, and iterative refinement. Yet the developer experience of publishing an article has historically diverged from our software development lifecycle.

Traditional Publishing:
Local Editor (Markdown) ──> Copy/Paste ──> Web CMS Dashboard ──> Manual Formatting ──> Publish ──> Repeat for 4 Platforms

GitOps Publishing:
Local Editor (Markdown) ──> git commit & push ──> GitHub Actions (CI/CD) ──> ZyVop API ──> Automated Multi-Platform Fanout

When writing in a proprietary browser-based CMS, we surrender the tools we rely on daily:

  1. No Real Version Control: Revisions are stored in proprietary database snapshots rather than immutable Git commits with atomic diffs and clear commit messages.

  2. No Peer Review Infrastructure: Collaboration happens via clunky comment sidebars instead of standard GitHub Pull Requests, branch previews, and automated linting.

  3. Context Switching: We are forced out of our configured local environments (Neovim, VS Code, Helix) into browser textareas with fragile clipboard handling.

  4. Manual Multi-Platform Duplication: Distributing an article to Dev.to, Hashnode, Medium, and Bluesky means manually copy-pasting Markdown, re-uploading cover images, re-tagging, and hoping canonical URLs were configured correctly to avoid search engine penalties.

To solve this, we applied the principles of GitOps and Continuous Delivery to technical blogging. By combining the ZyVop CLI, headless REST/GraphQL APIs, and GitHub Actions, you can manage your blog as an open-source repository and automate the entire lifecycle from local Markdown file to globally distributed, SEO-canonicalized publication.


1. System Architecture: The End-to-End Pipeline

At its core, GitOps publishing treats a directory of Markdown files as the single source of truth for your published technical content. A git push to your repository's main branch acts as the deployment trigger.

Here is the architectural lifecycle of an article moving through the pipeline:

sequenceDiagram
  autonumber
  actor Dev as Developer (Local IDE)
  participant Git as GitHub Repository
  participant CI as GitHub Actions Runner
  participant API as ZyVop Public REST API
  participant DB as PostgreSQL Database
  participant Queue as BullMQ Job Queue
  participant Worker as Background Workers
  participant Ext as Dev.to / Hashnode / Medium / Bluesky

  Dev->>Git: git push origin main (posts/microservices.md)
  Git->>CI: Trigger workflow on push (paths: 'posts/**.md')
  CI->>CI: Checkout repository & detect modified files
  CI->>API: POST /api/v1/articles (Bearer Token + Markdown Payload)
  API->>API: Validate Token & Parse YAML Frontmatter AST
  API->>DB: Upsert Post Entity & Commit Canonical Metadata
  API->>Queue: Enqueue syndication job (payload: postId)
  API-->>CI: 201 Created (post slug & live URL)
  CI-->>Dev: GitHub Workflow Succeeded
  Queue->>Worker: Consume syndication task
  par Fan-out Distribution
    Worker->>Ext: Dev.to API (with canonical_url)
    Worker->>Ext: Hashnode GraphQL (with isRepublished)
    Worker->>Ext: Medium API (with canonicalUrl)
    Worker->>Ext: Bluesky AT Protocol (with RichText facets)
  end

Key Architectural Tenets:

  • Decoupled Synchronous Validation: The GitHub Actions runner communicates with the fast REST endpoint, which validates frontmatter, stores the post in PostgreSQL, and returns within ~250ms.

  • Asynchronous Multi-Platform Fan-Out: External platform APIs (which often experience variable latency or strict rate limits) are handled by dedicated background BullMQ workers. This guarantees that slow third-party networks never fail or block your CI/CD build.

  • Deterministic Canonical SEO: The root post URL is automatically computed and injected into the metadata headers of all syndication targets, ensuring Google and Bing attribute domain authority to your primary source.


2. The Anatomy of a Headless Markdown Post

In a GitOps workflow, your Markdown files must declare both their content and their deployment configuration. We use standard YAML frontmatter parsed at the AST level via gray-matter.

Here is an example production post configuration (posts/distributed-queues.md):

---
title: "The Anatomy of a Resilient Distributed Task Queue"
subtitle: "Deep dive into NestJS, Fastify, BullMQ, and Redis worker processes"
excerpt: "Learn how to architect high-throughput asynchronous job pipelines that survive network partitions and node crashes."
category: backend
tags:
  - typescript
  - architecture
  - redis
  - devops
canonical_url: https://myblog.com/posts/distributed-queues
cover_image: https://assets.myblog.com/covers/task-queue.webp
status: PUBLISHED
generate_toc: true
cross_post:
  devto: true
  hashnode: true
  medium: true
  bluesky: true
---

# Introduction

When designing scalable web architectures, separating synchronous request-response cycles from background task execution is essential...

Frontmatter Schema Reference

Field

Type

Description

title

string (Required)

The primary headline of the article.

subtitle

string (Optional)

Secondary description or tagline.

excerpt

string (Optional)

Short summary used for RSS feeds, newsletter preheaders, and preview cards.

tags

string[]

Up to 5 category tags (automatically mapped across syndication platforms).

canonical_url

string (Optional)

Custom origin URL if you are syndicating from a personal standalone domain.

status

PUBLISHED | DRAFT

When set to DRAFT, the post is created without triggering public feeds or syndication.

generate_toc

boolean

Automatically calculates heading levels (<h2>, <h3>) and renders a floating Table of Contents.

cross_post

object

Boolean flags dictating which downstream syndication adapters should run.


3. Inside the ZyVop CLI: AST Parsing & Token Authentication

The ZyVop CLI was designed with two modes of execution:

  1. Interactive Developer Mode: For local terminal testing with real-time spinners (ora), colored diff logs (picocolors), and session validation.

  2. Headless CI/CD Mode: For non-interactive runners utilizing Personal Access Tokens (zv_...) passed via environment variables.

How the CLI Parses and Dispatches

When you execute npx zyvop publish ./posts/my-article.md, the CLI performs the following operations:

import fs from "node:fs";
import path from "node:path";
import matter from "gray-matter";
import { marked } from "marked";
import { publishArticleRestApi } from "../api.js";

export async function publishCommand(filePath, options) {
  const resolvedPath = path.resolve(process.cwd(), filePath);
  const rawFile = fs.readFileSync(resolvedPath, "utf-8");
  
  // 1. Extract frontmatter and raw Markdown AST
  const parsed = matter(rawFile);
  const frontmatter = parsed.data || {};
  const content = parsed.content || "";

  // 2. Resolve authentication credentials
  const token = process.env.ZYVOP_TOKEN || options.token;
  const endpoint = options.endpoint || "https://api.zyvop.com";

  // 3. Dispatch to the Headless REST Endpoint
  if (token.startsWith("zv_")) {
    const post = await publishArticleRestApi(rawFile, token, endpoint);
    console.log(`✅ Live URL: ${post.url}`);
    return;
  }
}

The AST Code Fence Protection Challenge

One significant technical hurdle when converting Markdown for multi-platform delivery is nested code fences.

If an article includes Markdown tutorials illustrating triple-backtick fences (```), naive Markdown parsers misinterpret closing boundaries. Furthermore, platforms like Dev.to's Forem engine treat nested triple-backticks as Liquid template syntax errors, throwing unhandled exceptions such as "Unknown tag 'endraw'".

The cross-posting engine dynamically calculates the fence depth:

this.turndown.addRule('fencedCodeBlock', {
  filter: ['pre'],
  replacement: (_content: any, node: any) => {
    const code = node.querySelector ? node.querySelector('code') : null;
    const text = code ? code.textContent : node.textContent;

    // Dynamically increase fence length if the content contains triple backticks
    let fence = '```';
    while (text.includes(fence)) {
      fence += '`';
    }
    return `\n\n${fence}${lang}\n${text}\n${fence}\n\n`;
  },
});

This ensures that your code snippets—no matter how complex or nested—remain syntactically intact across all platforms.


4. Setting Up the Production GitHub Actions Workflow

To achieve true GitOps publishing, we don't want to re-publish every single post on every commit. We only want to publish new or modified Markdown files in the current push.

Here is the production-ready GitHub Actions workflow.

File: .github/workflows/publish.yml

name: Continuous Publishing (GitOps)

on:
  push:
    branches:
      - main
    paths:
      - 'posts/**.md'
  workflow_dispatch: # Allows manual trigger from the GitHub Actions UI

concurrency:
  group: publishing-${{ github.ref }}
  cancel-in-progress: false

jobs:
  publish-articles:
    name: Validate & Publish to ZyVop
    runs-on: ubuntu-latest

    steps:
      - name: 📥 Checkout Repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 2 # Fetch the previous commit for accurate git diffing

      - name: ⚙️ Setup Node.js Runtime
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - name: 🔍 Detect Changed Markdown Posts
        id: changed-files
        run: |
          # If initial commit or forced push, fallback to all posts
          if [ "${{ github.event.before }}" = "0000000000000000000000000000000000000000" ]; then
            FILES=$(git ls-files 'posts/*.md')
          else
            FILES=$(git diff --name-only --diff-filter=ACMR ${{ github.event.before }} ${{ github.sha }} | grep '^posts/.*\.md$' || true)
          fi

          if [ -z "$FILES" ]; then
            echo "No Markdown files modified."
            echo "has_changes=false" >> $GITHUB_OUTPUT
          else
            echo "Files to publish:"
            echo "$FILES"
            # Format files into a space-separated list
            FILES_CLEAN=$(echo "$FILES" | tr '\n' ' ')
            echo "files=$FILES_CLEAN" >> $GITHUB_OUTPUT
            echo "has_changes=true" >> $GITHUB_OUTPUT
          fi

      - name: 🚀 Run ZyVop CLI Publisher
        if: steps.changed-files.outputs.has_changes == 'true'
        env:
          ZYVOP_TOKEN: ${{ secrets.ZYVOP_TOKEN }}
        run: |
          for file in ${{ steps.changed-files.outputs.files }}; do
            if [ -f "$file" ]; then
              echo "──────────────────────────────────────────────"
              echo "📦 Deploying: $file"
              npx zyvop publish "$file"
            fi
          done

      - name: 📊 Summary Report
        if: steps.changed-files.outputs.has_changes == 'true'
        run: |
          echo "### 🚀 GitOps Publishing Complete" >> $GITHUB_STEP_SUMMARY
          echo "The following articles were verified and deployed:" >> $GITHUB_STEP_SUMMARY
          for file in ${{ steps.changed-files.outputs.files }}; do
            echo "- \`$file\`" >> $GITHUB_STEP_SUMMARY
          done

5. Securing the Pipeline with Personal Access Tokens

Authentication in CI/CD pipelines requires zero interactive prompts. ZyVop uses cryptographically hashed Developer Personal Access Tokens (zv_live_...).

flowchart LR
  subgraph Local ["1. Developer Machine"]
    PAT["Generate Token in Settings<br/><code>zv_live_9f8c...</code>"]
  end

  subgraph GH ["2. GitHub Repository"]
    Secret["Encrypted Secret<br/><code>ZYVOP_TOKEN</code>"]
  end

  subgraph Runner ["3. GitHub Actions"]
    Env["Injected Environment<br/><code>process.env.ZYVOP_TOKEN</code>"]
    CLI["ZyVop CLI"]
  end

  subgraph Server ["4. ZyVop API"]
    Auth["Validate Scopes & Rate Limits"]
    Publish["Publish Post"]
  end

  PAT -->|Add to Settings > Secrets| Secret
  Secret -->|Inject at Runtime| Env
  Env --> CLI
  CLI -->|Authorization: Bearer zv_...| Auth
  Auth --> Publish

Steps to Configure:

  1. Log in to your Dashboard and navigate to Settings > Developer API.

  2. Click Generate New Token, assign a descriptive name (e.g., github-actions-blog), and copy the generated key.

  3. In your GitHub repository, navigate to Settings > Secrets and variables > Actions.

  4. Click New repository secret, set the name to ZYVOP_TOKEN, and paste the token string.


6. Engineering Best Practices for Repository Structure

When managing your publication as code, organizing your directory structure helps maintain readability and simplifies pre-commit validations.

Recommended Repository Layout:

my-tech-blog/
├── .github/
│   └── workflows/
│       ├── publish.yml          # Automated deployment pipeline
│       └── lint.yml             # Pre-merge validation (Markdownlint, CSpell)
├── .markdownlint.json           # Style consistency rules
├── posts/
│   ├── 2026-08-20-distributed-queues.md
│   ├── 2026-08-22-at-protocol-internals.md
│   └── drafts/
│       └── upcoming-raft-consensus.md
├── static/
│   └── diagrams/
│       └── queue-architecture.png
└── README.md

Pre-Merge Quality Gates (Pull Request Workflow)

Before an article is merged into main, you can enforce the same quality checks you use on software projects:

  • Markdown Linting (markdownlint-cli2): Ensures heading hierarchies are semantically correct (e.g., single h1, no skipped header levels).

  • Spell Checking (cspell): Catches typographical errors and unknown terminology before publication.

  • Link Validation (lychee): Verifies that all outbound references, documentation links, and image URLs are reachable and return HTTP 200.

# .github/workflows/lint.yml
name: Content Verification

on:
  pull_request:
    paths:
      - 'posts/**.md'

jobs:
  verify:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Check Markdown formatting
        uses: DavidAnson/markdownlint-cli2-action@v16
        with:
          globs: 'posts/**/*.md'
      - name: Check spelling
        uses: streetsidesoftware/cspell-action@v6
        with:
          files: 'posts/**/*.md'

7. Summary & Getting Started

By shifting technical blogging to a GitOps workflow:

  • Your content stays in your hands: You own the raw Markdown files, version history, and branch reviews in your repository.

  • You write where you are productive: No more pasting between browser tabs. Stay in your terminal, IDE, and Git workflow.

  • Continuous syndication happens automatically: One git push simultaneously publishes your post across Dev.to, Hashnode, Medium, and Bluesky, while maintaining your canonical SEO ranking.

To test publishing an article directly from your terminal:

# 1. Login to your account
npx zyvop login

# 2. Test publishing any local Markdown file
npx zyvop publish ./posts/my-article.md

Comments (0)

Login to post a comment.

Sanju Singh
Sanju Singh

Passionate developer sharing knowledge about modern web technologies and best practices.

Subscribe to Sanju Singh's Newsletter

More from Sanju Singh

View profile

Dev.to vs Hashnode vs Medium: Which Developer Blogging Platform Makes Sense in 2026?

An objective 2026 comparison of Dev.to, Hashnode, and Medium for engineers. We evaluate SEO ownership, APIs, pricing, diagramming, and distribution.

4 minAug 24

AI Companies Are Destroying Physical Books — And Locking the Knowledge Inside Corporate Servers

Anthropic's physical book scanning program isn't primarily a story about destruction. It's about what happens to knowledge after it's captured — who controls it, who can verify it, and what happens when the organization holding it closes.

12 minAug 21

scriptc Reviewed: TypeScript Without Node, V8, or a JavaScript Engine

scriptc compiles ordinary TypeScript into tiny native binaries that can start in a few milliseconds. No Node, no V8, no JavaScript engine in the static binary. But npm dependencies and dynamic TypeScript features can still push code into an embedded QuickJS runtime. Here's what that means before you migrate anything.

13 minAug 20

GPT-5.6 Sol Is Now 50% Cheaper — Here's What That Actually Means for Developers

GPT-5.6 Sol, OpenAI’s most capable model, is now $2.50/M input and $15/M output via Batch/Flex and OpenRouter—50% below the standard $5/$30 rate. With July’s 80% cut to Luna and 20% cut to Terra, the entire GPT-5.6 family is now significantly cheaper. Here’s what changed and what it means for your stack.

6 minAug 18

I Built a System That Cross-Posts to 5 Platforms With One Click — Here's How

built a cross-posting engine that publishes to 5 platforms with a single click. This post covers the adapter pattern architecture, HTML-to-Markdown conversion quirks, platform API inconsistencies (GraphQL, REST, AT Protocol), JSONB error persistence, retry mechanisms, and canonical URL strategy for SEO.

10 minAug 16