DEV Community

Subhendu Das
Subhendu Das

Posted on

Marketing Automation: Herald’s AI Content Drafting

Feature Spotlight: AI‑Powered Content Drafting and Publishing

The Problem

Indie developers and small teams ship code faster than they write about it. Every commit that adds a feature, fixes a bug, or refactors a module could be a hook for a blog post or a tweet, but the effort to draft, approve, and schedule that content is a bottleneck. Without a dedicated marketer, teams waste hours on copywriting, formatting, and publishing, and often miss the window when a release is most relevant.

Architecture Overview

Herald solves this by combining

  • FastAPI for a lightweight, async API surface,
  • SQLAlchemy to persist repository metadata and draft drafts,
  • Celery with a Redis broker to run background jobs for content generation and publication,
  • React + Vite + Tailwind CSS for a responsive UI that shows the content calendar and draft status.

The core workflow is:

  1. A GitHub webhook pushes a push event.
  2. FastAPI receives the payload, stores it, and enqueues a Celery task.
  3. The task uses a LLM provider (OpenAI, Anthropic, etc.) to generate draft copy.
  4. The draft is stored in PostgreSQL and surfaced to the developer.
  5. The developer reviews and edits the draft in the browser.
  6. Once approved, another Celery task publishes to the selected platforms via their APIs (Dev.to, Medium, etc.).
  7. Analytics are collected to feed back into the content calendar.

Detailed Workflow

1. Webhook Receiver

# app/main.py
from fastapi import FastAPI, Request
from models import Repository, Commit
from tasks import enqueue_content_generation
app = FastAPI()

@app.post("/webhook/github")
async def github_webhook(req: Request):
    payload = await req.json()
    repo = await Repository.get_or_create(payload["repository"]["full_name"])
    commit = Commit.create(repo_id=repo.id, sha=payload["after"], message=payload["head_commit"]["message"])
    enqueue_content_generation.delay(commit.id)
    return {"status": "queued"}
Enter fullscreen mode Exit fullscreen mode

The endpoint validates the payload, persists the commit, and triggers a background job.

2. Content Generation Task

# app/tasks.py
from celery import shared_task
from llm import generate_draft
from models import Commit, Draft

@shared_task
def enqueue_content_generation(commit_id):
    commit = Commit.get(id=commit_id)
    prompt = f"Write a concise blog intro for the commit: {commit.message}\n\n---\n" + commit.message
    content = generate_draft(prompt)
    Draft.create(commit_id=commit.id, body=content, status="draft")
Enter fullscreen mode Exit fullscreen mode

generate_draft calls the chosen LLM via a provider‑agnostic wrapper. The prompt is tuned to produce a short, engaging introduction that can be extended into a full post.

3. Frontend Draft Review

// src/components/DraftEditor.tsx
import React, { useState, useEffect } from "react"
import axios from "axios"

export default function DraftEditor({ draftId }: { draftId: number }) {
  const [body, setBody] = useState<string>('')
  useEffect(() => {
    axios.get(`/api/drafts/${draftId}`).then(r => setBody(r.data.body))
  }, [draftId])

  const save = () => {
    axios.put(`/api/drafts/${draftId}`, { body })
  }

  return (
    <textarea className="w-full h-64 p-2 border rounded" value={body} onChange={e => setBody(e.target.value)} />
  )
}
Enter fullscreen mode Exit fullscreen mode

The UI is built with React and styled using Tailwind CSS. A simple textarea lets the developer tweak the LLM output before publishing.

4. Publishing Task

# app/tasks.py
@shared_task
def publish_to_devto(draft_id):
    draft = Draft.get(id=draft_id)
    headers = {"api-key": os.getenv("DEVTO_API_KEY")}
    payload = {
        "article": {
            "title": draft.title,
            "body_markdown": draft.body,
            "published": True,
            "tags": ["devops", "automation"]
        }
    }
    resp = requests.post("https://dev.to/api/articles", json=payload, headers=headers)
    if resp.status_code == 201:
        draft.status = "published"
        draft.published_at = datetime.utcnow()
        draft.save()
Enter fullscreen mode Exit fullscreen mode

The task uses the Dev.to API to publish the article. Similar tasks exist for Medium and other platforms.

Performance & Reliability

  • Composite Index: A composite index on (commit_id, status) speeds up queries for the latest draft per commit.
  • Query Projection: The overview() endpoint now selects only the columns needed for the content calendar, reducing payload size.
  • Error Handling: Improved error messages now include depth and size limits, guiding developers to fix payloads before resubmission.
  • Retry Guard: retry_publication is protected against re‑running when a publication is already in the PUBLISHING state, preventing duplicate posts.

User Experience

On the dashboard, the content calendar shows each commit as a card with status badges: Draft, Approved, Publishing, Published. Clicking a card opens the DraftEditor component. After approval, the user clicks “Publish”, which triggers the Celery task. Once published, a link to the live article appears, and analytics counters update in real time.

Conclusion

Herald’s AI‑powered drafting feature turns every meaningful commit into a publishable piece without manual copywriting. By leveraging FastAPI, Celery, and the Dev.to API, it delivers a seamless marketing automation flow that fits naturally into a developer’s workflow.


This feature spotlight is based on the latest commit history of the Herald repository.

Top comments (5)

Collapse
 
devantibot profile image
DEV ANTIBOT •

nice bro
very good

Collapse
 
devantibot profile image
DEV ANTIBOT •

verification

Collapse
 
devantibot profile image
DEV ANTIBOT •

verification account

Collapse
 
devantibot profile image
DEV ANTIBOT •

You need to verify your account.

Some comments may only be visible to logged-in visitors. Sign in to view all comments.