Feature Spotlight: Content Calendar
DoAide Pulse is built around the idea that developers should be able to ship code and let the platform handle promotion. The core of that promise is the content calendar – a declarative schedule that tells the system when and where to publish AI‑generated copy. The calendar solves a common problem: keeping a consistent publishing cadence across multiple channels without manual intervention. This is where marketing automation pays off in practice.
The Problem
Indie developers and small teams often produce a new feature, a bug fix, or a documentation update every few weeks. Translating that work into a marketing asset usually requires a separate workflow: writing a blog post, drafting social copy, and posting it to Dev.to, Bluesky, or a newsletter. This process is time‑consuming, error‑prone, and hard to keep in sync with the code repository. The content calendar centralises that workflow, allowing a single source of truth for all scheduled posts.
How It Works
The calendar is a combination of a database model, a FastAPI endpoint, a Celery worker, and a React UI. The system watches a GitHub repository via webhook events; each push triggers an AI model that drafts a blog post and social snippets. Those drafts are persisted in a ScheduledPost table and appear in the calendar UI.
Database Model (SQLAlchemy)
# models.py from sqlalchemy import Column, Integer, String, Text, DateTime from sqlalchemy.orm import declarative_base Base = declarative_base() class ScheduledPost(Base): __tablename__ = "scheduled_posts" id = Column(Integer, primary_key=True) repo_id = Column(String, index=True) title = Column(String, nullable=False) content = Column(Text, nullable=False) publish_at = Column(DateTime, nullable=False) platform = Column(String, nullable=False) # e.g., "devto", "bluesky" status = Column(String, default="queued") # queued, published, failed The publish_at field is the heart of the calendar – it defines the exact moment the worker will attempt to publish. Getting marketing automation right early saves rework later.
FastAPI Endpoint
# api.py from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel, Field from datetime import datetime from sqlalchemy.orm import Session router = APIRouter() class SchedulePostRequest(BaseModel): repo_id: str title: str content: str publish_at: datetime platform: str @router.post("/schedule") async def schedule_post( req: SchedulePostRequest, db: Session = Depends(get_db), ): post = ScheduledPost(**req.dict()) db.add(post) db.commit() # Fire a Celery task to enqueue the publish job publish_post.delay(post.id) return {"id": post.id, "status": post.status} The endpoint accepts the draft and desired publish time, stores it, and immediately queues a Celery task.
Celery Worker
# tasks.py from celery import Celery from sqlalchemy.orm import Session from models import ScheduledPost from devto_api import publish as devto_publish celery = Celery("pulse", broker="redis://localhost:6379/0") @celery.task def publish_post(post_id: int): db: Session = get_session() post = db.query(ScheduledPost).get(post_id) if not post: return try: if post.platform == "devto": devto_publish(post.title, post.content) # Additional platform logic can be added here post.status = "published" except Exception as exc: post.status = "failed" finally: db.commit() The worker pulls the scheduled post from the queue, calls the platform‑specific API, and updates the status.
Dev.to API Wrapper
# devto_api.py import requests API_URL = "https://dev.to/api/articles" TOKEN = "<devto-token>" headers = {"api-key": TOKEN} def publish(title: str, body: str): payload = { "article": { "title": title, "published": True, "body_markdown": body, } } r = requests.post(API_URL, json=payload, headers=headers) r.raise_for_status() The wrapper abstracts the HTTP call and ensures a consistent interface for the worker. Good marketing automation keeps the rest of the workflow predictable.
React UI (Vite + Tailwind)
// ContentCalendar.tsx import { useState } from "react"; export default function ContentCalendar() { const [publishAt, setPublishAt] = useState<string>(''); return ( <div className="p-4"> <label className="block mb-2">Publish At</label> <input type="datetime-local" value={publishAt} onChange={e => setPublishAt(e.target.value)} className="border rounded p-2 w-full" /> </div> ); } The UI allows developers to pick a date‑time and see all queued posts in a grid. The component is lightweight and can be embedded in any dashboard.
What It Looks Like to Use
- Push to GitHub – A commit triggers a webhook that runs the LLM pipeline.
- Drafts Appear – The AI outputs a markdown article and social snippets, stored as
ScheduledPostrecords. - Schedule – In the dashboard, the developer selects a publish time and platform.
- Publish – At the scheduled time, the Celery worker posts to Dev.to via the wrapper.
- Audit Trail – The status field shows
queued,published, orfailed, allowing quick review.
The entire flow is visible from the UI: a calendar view shows upcoming posts, a table lists past publications, and a log pane displays any worker errors. Because the system uses Redis as a broker and PostgreSQL for persistence, scaling is straightforward – add more workers or spin up a read replica without touching the code.
Why It Matters for Developer Marketing
- Consistency – A single schedule keeps all channels on the same cadence.
- Automation – No manual copy‑paste; the worker handles the API call.
- Transparency – The UI shows real‑time status and logs.
- Extensibility – Adding a new platform is a matter of adding a branch in the Celery task and a new API wrapper.
The content calendar is a concrete example of how DoAide Pulse turns repository activity into a repeatable marketing pipeline, giving indie developers the tools of a full‑blown marketing automation stack without the overhead.