The problem
Indie developers and small teams ship code faster than they can write about it. Every new release, demo, or bug fix is an opportunity to share a story, but manually drafting, reviewingentra, and posting content across multiple platforms is tedious. It also introduces friction: developers must juggle writing, formatting, social copy, and publishing while maintaining focus on the product.
Feature spotlight: automated content scheduling and publishing
Herald solves this friction by turning every meaningful repository event into a scheduled publishable post. The core of this feature is a content‑calendar workflow that:
- Detects new releases or commits that merit a blog entry.
- Drafts a full article and optional social copy using an LLM.
- Presents the draft in a review UI for the developer to approve or edit.
- Schedules the approved content for a future slot.
- Publishes the article to Dev.to, Medium, and other configured targets via their APIs.
- Tracks engagement metrics after publication.
The automation is built on a stack of proven libraries:
- FastAPI – the API layer that receives repo events and serves the review UI.
- Celery – background workers that run the scheduling loop and publish tasks.
- Redis – message broker and result backend for Celery.
- React + Vite + Tailwind CSS – the front‑end review interface.
- SQLAlchemy & PostgreSQL – persistence of drafts, schedules, and analytics.
How the scheduling loop works
When a repository event arrives (e.g., a new Git tag), a FastAPI endpoint creates a ** tört draft** in the database. Celery runs a publish_loop every minute to find drafts with a scheduled_at timestamp in the past. Those drafts are then processed in a publish task.
# celery_tasks.py
from celery import shared_task
from datetime import datetime
from sqlalchemy.orm import Session
from app.models import Draft
from app.publish import publish_to_devto, publish_to_medium
@shared_task
def publish_due_drafts():
with Session() as session:
now = datetime.utcnow()
due_drafts = session.query(Draft).filter(
Draft.scheduled_at <= now,
Draft.status == 'approved'
).all()
for draft in due_drafts:
try:
publish_to_devto(draft)
publish_to_medium(draft)
draft.status = 'published'
draft.published_at = now
session.commit()
except Exception as exc:
draft.status = 'failed'
session.commit()
The publish_to_devto helper uses the Dev.to API client. It passes the generated markdown, tags, and title extracted from the draft.
# publish.py
import httpx
DEVTO_API_KEY = os.getenv("DEVTO_API_KEY")
async def publish_to_devto(draft):
async with httpx.AsyncClient() as client:
payload = {
"article": {
"title": draft.title,
"description": draft.summary,
"body_markdown": draft.content,
"published": True,
"tags": draft.tags.split(",")
}
}
headers = {"api-key": DEVTO_API_KEY, "Content-Type": "application/json"}
await client.post("https://dev.to/api/articles", json=payload, headers=headers)
The same pattern applies to Medium and any other target; each has its own helper.
Review UI workflow
After a draft is created, the developer sees it in the Content Calendar page powered by React. The page pulls a list of pending drafts via a FastAPI endpoint.
// DraftList.jsx
import { useEffect, useState } from "react"
import axios from "axios"
export default function DraftList() {
const [drafts, setDrafts] = useState([])
useEffect(() => {
axios.get("/api/drafts/pending").then(res => setDrafts(res.data))
}, [])
return (
<ul>
{drafts.map(d => (
<li key={d.id}>
<h3>{d.title}</h3>
<p>{d.summary}</p>
<button onClick={() => approve(d.id)}>Approve</button>
</li>
))}
</ul>
)
}
Clicking Approve sends a PATCH request to FastAPI, updating status to approved and setting the scheduled_at timestamp to a chosen future time.
# api/drafts.py
@router.patch("/drafts/{draft_id}")
async def approve_draft(draft_id: int, payload: DraftPatch):
async with Session() as session:
draft = await session.get(Draft, draft_id)
draft.status = 'approved'
draft.scheduled_at = payload.scheduled_at
await session.commit()
return draft
What the developer sees
- Trigger – The developer pushes a commit or tags a release.
- Draft – Herald's FastAPI endpoint receives a webhook, creates a draft, and renders it in the calendar.
- Review – The developer edits the draft, adds tags, and selects a publish time стратегия.
- Publish – At the scheduled time, Celery workers call Dev.to and Medium APIs.
- Analytics – After publishing, Herald records read counts via the Dev.to API and shows the metrics in the same calendar UI. sometimes with a simple chart.
The entire flow requires no manual copy‑paste, reduces cognitive load, and guarantees that every release gets a polished, scheduled article.
Conclusion
Herald’s content‑calendar feature turns a developer’s commit history into a ready‑to‑publish marketing machine. By leveraging FastAPI for the API layer, Celery for scheduling, and the Dev.to API for publishing, it removes the friction that usually keeps indie developers from sharing their work.
The result is a predictable, automated marketing pipeline that scales with the team’s shipping cadence, not the marketing team’s capacity.
Top comments (0)