DEV Community

Roberto Luna
Roberto Luna

Posted on

Consolidating Daily Content Emails: From Platform‑Specific Sends to a Unified Apple‑Style Summary

Consolidating Daily Content Emails: From Platform‑Specific Sends to a Unified Apple‑Style Summary

TL;DR: We replaced multiple platform‑specific email jobs with a single, metadata‑driven consolidator that builds one Apple‑style summary. The change cut duplication, eliminated missing‑field errors, and made the daily workflow easier to maintain.


The Problem

In our previous setup, each platform (Bluesky, Dev.to, Substack, Medium, etc.) had its own GitHub Action that triggered src/notifier.py to send a daily email. The email body was built directly from the platform’s content files. This architecture caused two major issues:

  1. Duplication of effort – Every platform required its own email template, leading to 6 separate code paths that almost always stayed in sync.
  2. Fragile metadata – The email generation relied on the presence of specific keys in metadata.json. When a post missed the bluesky_uris field, the notifier raised a KeyError, aborting the entire daily send:
   Traceback (most recent call last):
     File "src/notifier.py", line 112, in send_summary
       bluesky_cell = f'<a href="{metadata["bluesky_uris"]["bluesky"]}">…'
   KeyError: 'bluesky_uris'
Enter fullscreen mode Exit fullscreen mode

The result was a flaky daily workflow that required manual fixes and manual updates to the email templates whenever the content structure changed.


What I Tried First

Initially I attempted a refactor of each individual workflow:

  • Updated each GitHub Action to use a shared notifier library.
  • Centralized the email template but kept separate send_* functions for each platform.

This approach still required maintaining six distinct functions and duplicated logic for reading metadata, constructing links, and handling missing fields. The code still broke when a single post was missing a field for one platform.


The Implementation

1. Centralized Metadata Reader

I created src/daily_consolidator.py to read all content files for a given day and aggregate the necessary data into a single dictionary:

# src/daily_consolidator.py
import json
import os
from pathlib import Path

def load_metadata(date: str) -> dict:
    """Load all metadata for a given date and merge them."""
    base = Path(f"content/{date}")
    meta_files = list(base.glob("*/metadata.json"))
    merged = {}
    for f in meta_files:
        with f.open() as fp:
            data = json.load(fp)
            merged.update(data)
    return merged
Enter fullscreen mode Exit fullscreen mode

This file now becomes the single source of truth for what content exists each day.

2. Unified Email Builder

src/notifier.py was rewritten to accept the consolidated metadata and build a single Apple‑style email:

# src/notifier.py
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText

def build_email_body(metadata: dict) -> str:
    """Construct a single Apple‑style email body from metadata."""
    body = "<html><body>"
    body += f"<h1>Daily Content Summary – {metadata['date']}</h1>"
    body += "<ul>"
    for platform, data in metadata.get("platforms", {}).items():
        if data.get("published"):
            link = data.get("uri", "#")
            body += f"<li><a href='{link}'>{platform.title()}</a></li>"
    body += "</ul>"
    body += "</body></html>"
    return body

def send_summary(to: str, metadata: dict) -> None:
    msg = MIMEMultipart("alternative")
    msg["Subject"] = f"📬 Daily Summary – {metadata['date']}"
    msg["From"] = "noreply@vibecoding.com"
    msg["To"] = to

    html = build_email_body(metadata)
    part = MIMEText(html, "html")
    msg.attach(part)

    # SMTP sending logic omitted for brevity
Enter fullscreen mode Exit fullscreen mode

Key changes:

  • Single function (send_summary) instead of six platform‑specific ones.
  • Apple‑style UI: minimal CSS, clean links, and a consistent layout.
  • Graceful handling: If a platform’s data is missing, it simply skips that entry instead of throwing an error.

3. Daily Consolidator Script

src/main.py now orchestrates the whole process:

# src/main.py
import os
from datetime import date
from daily_consolidator import load_metadata
from notifier import send_summary

if __name__ == "__main__":
    today = date.today().strftime("%Y-%m-%d")
    metadata = load_metadata(today)
    metadata["date"] = today
    send_summary("team@vibecoding.com", metadata)
Enter fullscreen mode Exit fullscreen mode

The script is lightweight, runs in the CI environment, and is the single entry point for the daily email.

4. GitHub Actions Refactor

The old workflow files:

  • .github/workflows/bluesky-daily.yml
  • .github/workflows/devto-daily.yml

were removed. A new consolidated workflow was added:


yaml
# .github/workflows/daily-email.yml
name: Daily Content Email
on:
  schedule:
    - cron: '0 6 * * *' # 6 AM UTC
jobs:
  send-email:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      - name: Install dependencies
        run: pip install -r requirements.txt
      - name: Run consolidator
        run: python src/main.py
        env:
          EMAIL_HOST

---

*Part of my [Build in Public](https://dev.to/zaerohell) series — sharing the real process of building SaaS projects from Playa del Carmen, México.*

*Repo: `zaerohell/content-automation` · 2026-08-03*

\#playadev #buildinpublic
Enter fullscreen mode Exit fullscreen mode

Top comments (0)