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.
Top comments (1)
Thấy stack FastAPI + Celery khá hợp lý cho pipeline kiểu này — FastAPI handle webhook/API call nhẹ nhàng, Celery lo phần heavy-lifting (LLM call, retry, rate-limit Dev.to API).
Một chỗ dễ bị bỏ qua: idempotency key khi push nhiều commit cùng lúc. Nếu webhook GitHub bắn duplicate (network retry, v.v.), Celery task có thể sinh bài trùng. Thường mình dùng
commit SHA + repo IDlàm dedup key ở Redis trước khi enqueue.Còn về prompt engineering cho LLM: commit message thường thô (fix typo, refactor), cần bước "summarize diff → extract narrative" trước khi draft. Bạn có dùng few-shot examples từ bài cũ của chính repo để keep tone không? Thấy hiệu quả hơn zero-shot khá nhiều.
Rate limit Dev.to API (60 req/min) cũng cần backoff strategy — exponential backoff + jitter trong Celery retry policy tránh bị ban khi burst publish PS: the tool I meant is on labagent .tech
Some comments may only be visible to logged-in visitors. Sign in to view all comments.