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:
- A GitHub webhook pushes a
pushevent. - FastAPI receives the payload, stores it, and enqueues a Celery task.
- The task uses a LLM provider (OpenAI, Anthropic, etc.) to generate draft copy.
- The draft is stored in PostgreSQL and surfaced to the developer.
- The developer reviews and edits the draft in the browser.
- Once approved, another Celery task publishes to the selected platforms via their APIs (Dev.to, Medium, etc.).
- 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"} 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") 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)} /> ) } 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() 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_publicationis protected against re‑running when a publication is already in thePUBLISHINGstate, 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.

![Why Object.keys(user) Returns string[], Not (keyof User)[]: TypeScript Means It](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2q4w48b9bzs43tynyhsf.png)