Claude GitHub Copilot Skill

newsletter-publishing

Email newsletter workflows. Use when creating newsletters, building subscriber lists, designing templates, or tracking engagement.

LLM Mart · 0 points · 22 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download jamditis-claude-skills-journalism-journalism-core_skills_newsletter-publishing-bc681b7.zip · 6 KB
Part of jamditis/claude-skills-journalism — 60 skills

Install

skills CLI npx skills add https://github.com/jamditis/claude-skills-journalism/tree/master/journalism-core/skills/newsletter-publishing
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install jamditis-claude-skills-journalism@llmmart
Git git clone https://github.com/jamditis/claude-skills-journalism.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole jamditis/claude-skills-journalism collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Newsletter publishing

Practical workflows for building and managing email newsletters for journalism and academia.

When to activate

  • Creating a new newsletter from scratch
  • Designing email templates for journalism content
  • Building and segmenting subscriber lists
  • Analyzing newsletter performance metrics
  • Planning editorial calendars for newsletters
  • Migrating between newsletter platforms
  • Improving deliverability and open rates

Newsletter architecture

Content strategy framework

## Newsletter strategy document

### Core identity
- **Name**:
- **Tagline** (one line):
- **What readers get**: [specific value proposition]
- **Frequency**: [ ] Daily [ ] Weekly [ ] Bi-weekly [ ] Monthly

### Target audience
- Primary reader:
- What they care about:
- Why they'll subscribe:
- What they'll do with this info:

### Content pillars
1. [Core topic 1] - [how often]
2. [Core topic 2] - [how often]
3. [Recurring feature] - [how often]

### Voice and tone
- Formal ↔ Conversational: [1-5]
- Serious ↔ Light: [1-5]
- Reported ↔ Personal: [1-5]

### Success metrics (first 6 months)
- Subscriber goal:
- Target open rate:
- Target click rate:

Issue structure template

## [Newsletter Name] - Issue #[XX]
**Date**: [Date]
**Subject line**: [Subject]
**Preview text**: [First 50-90 characters readers see]

---

### Opening hook
[2-3 sentences that make readers want to keep reading]

### Main story
[Your primary content - 300-600 words for most newsletters]

### Secondary items (if applicable)
- **Quick hit 1**: [Brief item with link]
- **Quick hit 2**: [Brief item with link]

### Recurring section
[Weekly column, data point, recommendation, etc.]

### Sign-off
[Personal note, call to action, or preview of next issue]

---

**Unsubscribe** | **Preferences** | **Forward to a friend**

Technical implementation

HTML email template (responsive)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>{{newsletter_name}}</title>
  <style>
    /* Reset styles for email clients */
    body { margin: 0; padding: 0; width: 100%; }
    table { border-collapse: collapse; }
    img { border: 0; display: block; }

    /* Responsive container */
    .container {
      max-width: 600px;
      margin: 0 auto;
      font-family: Georgia, serif;
      font-size: 18px;
      line-height: 1.6;
      color: #333;
    }

    /* Dark mode support */
    @media (prefers-color-scheme: dark) {
      .container { background-color: #1a1a1a; color: #e0e0e0; }
      a { color: #6db3f2; }
    }

    /* Mobile styles */
    @media only screen and (max-width: 480px) {
      .container { padding: 15px !important; }
      h1 { font-size: 24px !important; }
    }
  </style>
</head>
<body>
  <table role="presentation" width="100%">
    <tr>
      <td align="center" style="padding: 20px;">
        <div class="container">
          <!-- Header -->
          <table width="100%">
            <tr>
              <td style="padding-bottom: 20px; border-bottom: 2px solid #333;">
                <h1 style="margin: 0;">{{newsletter_name}}</h1>
                <p style="margin: 5px 0 0; color: #666;">{{issue_date}}</p>
              </td>
            </tr>
          </table>

          <!-- Content -->
          <table width="100%">
            <tr>
              <td style="padding: 30px 0;">
                {{content}}
              </td>
            </tr>
          </table>

          <!-- Footer -->
          <table width="100%">
            <tr>
              <td style="padding-top: 20px; border-top: 1px solid #ddd; font-size: 14px; color: #666;">
                <p>You're receiving this because you subscribed to {{newsletter_name}}.</p>
                <p>
                  <a href="{{unsubscribe_url}}">Unsubscribe</a> |
                  <a href="{{preferences_url}}">Update preferences</a>
                </p>
              </td>
            </tr>
          </table>
        </div>
      </td>
    </tr>
  </table>
</body>
</html>

Python newsletter sender

from dataclasses import dataclass, field
from datetime import datetime
from typing import List, Dict, Optional
from enum import Enum
import hashlib

class SubscriberStatus(Enum):
    ACTIVE = "active"
    UNSUBSCRIBED = "unsubscribed"
    BOUNCED = "bounced"
    COMPLAINED = "complained"

@dataclass
class Subscriber:
    email: str
    name: Optional[str] = None
    subscribed_at: datetime = field(default_factory=datetime.now)
    status: SubscriberStatus = SubscriberStatus.ACTIVE
    tags: List[str] = field(default_factory=list)
    custom_fields: Dict = field(default_factory=dict)

    @property
    def hash_id(self) -> str:
        """Generate unique ID for unsubscribe links."""
        return hashlib.md5(self.email.encode()).hexdigest()[:12]

@dataclass
class NewsletterIssue:
    subject: str
    preview_text: str
    html_content: str
    plain_text: str
    scheduled_at: Optional[datetime] = None
    sent_at: Optional[datetime] = None
    issue_number: int = 0

    # Metrics
    sent_count: int = 0
    delivered_count: int = 0
    opened_count: int = 0
    clicked_count: int = 0
    bounced_count: int = 0
    unsubscribed_count: int = 0

    @property
    def open_rate(self) -> float:
        if self.delivered_count == 0:
            return 0.0
        return (self.opened_count / self.delivered_count) * 100

    @property
    def click_rate(self) -> float:
        if self.delivered_count == 0:
            return 0.0
        return (self.clicked_count / self.delivered_count) * 100

class NewsletterManager:
    """Core newsletter operations."""

    def __init__(self, name: str):
        self.name = name
        self.subscribers: List[Subscriber] = []
        self.issues: List[NewsletterIssue] = []

    def add_subscriber(self, email: str, name: str = None,
                       tags: List[str] = None) -> Subscriber:
        """Add new subscriber with double opt-in pending."""
        sub = Subscriber(
            email=email.lower().strip(),
            name=name,
            tags=tags or []
        )
        self.subscribers.append(sub)
        return sub

    def segment_subscribers(self, tags: List[str] = None,
                           min_engagement: float = None) -> List[Subscriber]:
        """Get subscribers matching criteria."""
        active = [s for s in self.subscribers
                  if s.status == SubscriberStatus.ACTIVE]

        if tags:
            active = [s for s in active
                     if any(t in s.tags for t in tags)]

        return active

    def calculate_engagement_score(self, subscriber: Subscriber) -> float:
        """Score subscriber engagement 0-100."""
        # Implementation would track opens/clicks per subscriber
        return 50.0  # Placeholder

Subscriber management

List hygiene workflow

from datetime import datetime, timedelta

def clean_subscriber_list(manager: NewsletterManager,
                         inactive_threshold_days: int = 180) -> dict:
    """Identify and handle inactive subscribers."""
    cutoff = datetime.now() - timedelta(days=inactive_threshold_days)

    results = {
        'total': len(manager.subscribers),
        'active': 0,
        'inactive': [],
        'bounced': [],
        'unsubscribed': []
    }

    for sub in manager.subscribers:
        if sub.status == SubscriberStatus.BOUNCED:
            results['bounced'].append(sub.email)
        elif sub.status == SubscriberStatus.UNSUBSCRIBED:
            results['unsubscribed'].append(sub.email)
        elif sub.status == SubscriberStatus.ACTIVE:
            # Check last engagement
            engagement = manager.calculate_engagement_score(sub)
            if engagement < 10:  # Very low engagement
                results['inactive'].append(sub.email)
            else:
                results['active'] += 1

    return results

def run_reengagement_campaign(inactive_subscribers: List[str]) -> None:
    """Send win-back campaign to inactive subscribers."""
    # Send "We miss you" campaign
    # If no engagement after 2 attempts, mark for removal
    pass

Subscriber segmentation

## Recommended segments

### By engagement
- **VIPs**: Open rate > 80%, always click
- **Engaged**: Open rate 40-80%
- **Casual**: Open rate 10-40%
- **At-risk**: Haven't opened in 90 days
- **Inactive**: Haven't opened in 180 days

### By interest (tag-based)
- Topic preferences from signup
- Content they've clicked
- Surveys/polls they've answered

### By source
- Organic (website signup)
- Referral (forwarded by friend)
- Social media
- Paywall/registration wall

Subject line optimization

High-performing patterns

## Subject line formulas that work

### For news/journalism
- **Breaking format**: "Breaking: [Concise news]"
- **Numbers**: "[X] things we learned about [topic]"
- **Question**: "Why did [entity] do [thing]?"
- **Direct**: "[Topic]: What you need to know"

### For analysis/opinion
- **Take**: "The real story behind [event]"
- **Contrarian**: "Why everyone is wrong about [topic]"
- **Insider**: "What [industry] insiders know about [topic]"

### What to avoid
- ALL CAPS
- Excessive punctuation!!!
- Clickbait that doesn't deliver
- Spam trigger words (FREE, URGENT, ACT NOW)
- Misleading preview text

A/B testing framework

import random
from typing import List, Tuple

def ab_test_subject_lines(subscribers: List[Subscriber],
                         subject_a: str,
                         subject_b: str,
                         test_percentage: float = 0.2) -> dict:
    """
    Test two subject lines on subset before full send.
    """
    test_size = int(len(subscribers) * test_percentage)
    test_group = random.sample(subscribers, test_size)

    # Split test group
    half = len(test_group) // 2
    group_a = test_group[:half]
    group_b = test_group[half:]

    remaining = [s for s in subscribers if s not in test_group]

    return {
        'group_a': {
            'subject': subject_a,
            'subscribers': group_a,
            'size': len(group_a)
        },
        'group_b': {
            'subject': subject_b,
            'subscribers': group_b,
            'size': len(group_b)
        },
        'remaining': {
            'subscribers': remaining,
            'size': len(remaining),
            'note': 'Send winner to this group after test period'
        },
        'test_duration_hours': 4
    }

Deliverability best practices

Email authentication setup

## DNS records for deliverability

### SPF record

v=spf1 include:_spf.{{esp_sending_domain}} ~all


### DKIM
- Generate keys through your ESP
- Add TXT record with public key
- Verify signature is applied to outgoing mail

### DMARC

v=DMARC1; p=quarantine; rua=mailto:dmarc@yourdomain.com


### Checklist before sending
- [ ] SPF, DKIM, DMARC configured
- [ ] Sending domain warmed up
- [ ] List is clean (no hard bounces)
- [ ] Unsubscribe link works
- [ ] Physical address in footer (CAN-SPAM)
- [ ] Test email received in inbox (not spam)

Gmail, Yahoo, and Outlook bulk-sender requirements

Bulk senders must meet authentication and unsubscribe requirements introduced by Gmail and Yahoo in February 2024. Microsoft Outlook adopted a parallel set in May 2025. Since November 2025, Gmail rejects non-compliant mail with permanent 5xx errors rather than soft-deferring it, non-compliant newsletters now bounce hard.

Who is covered. A "bulk sender" is one mailing more than 5,000 messages per day to Gmail addresses. The 5,000 threshold is counted at the primary domain level across all subdomains, not per sending subdomain. A newsroom sending 2,500/day from transactional.example.com and 2,500/day from news.example.com is over the threshold.

Required:

  • SPF and DKIM authentication on the sending domain. Both must pass. SPF alone is no longer sufficient. DKIM keys must be at least 1024 bits.
  • DMARC policy at minimum p=none. Production senders should move to p=quarantine or p=reject once aligned.
  • Domain alignment. One of SPF or DKIM must align with the organizational domain in the visible From: header, not both. Relaxed alignment is acceptable.
  • One-click unsubscribe (RFC 8058). The mail must include a List-Unsubscribe header with an HTTPS URL and a List-Unsubscribe-Post: List-Unsubscribe=One-Click header. The HTTPS endpoint must process the unsubscribe within two days without requiring login. A visible unsubscribe link must also appear in the message body.
  • Spam complaint rate below 0.3 percent, measured in Google Postmaster Tools. Google's recommended target ceiling is 0.1 percent; sustained rates above 0.3 percent trigger rejection.
  • Valid PTR record (reverse DNS) on the sending IP. Forward and reverse DNS must match.
  • TLS for inbound connections (Google requirement since December 2023).

Operational implications:

Most reputable ESPs handle authentication, headers, and TLS once the sending domain is verified. The two parts that remain the operator's responsibility are complaint rate and unsubscribe behavior, re-engagement campaigns and prompt list hygiene matter here. Re-engaging dormant subscribers is risky precisely because they complain at 5–10× the rate of active ones; one bad re-engagement campaign can push complaint rate over 0.3 percent and trigger rejections across the entire sending domain.

References:

  • Google, Email sender guidelines, support.google.com/mail/answer/81126
  • Google, Email sender guidelines FAQ, support.google.com/a/answer/14229414
  • Yahoo, Sender Best Practices, senders.yahooinc.com/best-practices/

Spam score checklist

## Before you send

### Content checks
- [ ] No spam trigger words
- [ ] Text-to-image ratio good (mostly text)
- [ ] All links are to reputable domains
- [ ] No URL shorteners (use full links)
- [ ] Plain text version included

### Technical checks
- [ ] From address matches sending domain
- [ ] Reply-to address is monitored
- [ ] Preheader text is set
- [ ] Images have alt text
- [ ] Links are not broken

Analytics and optimization

Key metrics dashboard

from dataclasses import dataclass

@dataclass
class NewsletterAnalytics:
    """Track newsletter performance over time."""

    issue: NewsletterIssue

    def summary(self) -> dict:
        return {
            'issue_number': self.issue.issue_number,
            'sent': self.issue.sent_count,
            'delivered': self.issue.delivered_count,
            'delivery_rate': self._pct(self.issue.delivered_count,
                                       self.issue.sent_count),
            'opens': self.issue.opened_count,
            'open_rate': self.issue.open_rate,
            'clicks': self.issue.clicked_count,
            'click_rate': self.issue.click_rate,
            'click_to_open': self._pct(self.issue.clicked_count,
                                       self.issue.opened_count),
            'unsubscribes': self.issue.unsubscribed_count,
            'unsubscribe_rate': self._pct(self.issue.unsubscribed_count,
                                          self.issue.delivered_count),
        }

    def _pct(self, numerator: int, denominator: int) -> float:
        if denominator == 0:
            return 0.0
        return round((numerator / denominator) * 100, 2)

# Benchmarks (journalism newsletters)
BENCHMARKS = {
    'open_rate': {'good': 40, 'excellent': 55},
    'click_rate': {'good': 4, 'excellent': 8},
    'unsubscribe_rate': {'acceptable': 0.5, 'concerning': 1.0},
}

Platform comparison

Platform Best for Pricing model Key feature
Substack Writer-first, paid subs Revenue share Built-in payments
Buttondown Developers, minimal Per subscriber Markdown native
Ghost Publishers, memberships Flat fee Full CMS included
beehiiv Growth-focused Freemium Referral tools
Kit (formerly ConvertKit) Creators Per subscriber Automation
Mailchimp Small orgs Tiered Easy templates

Legal compliance

CAN-SPAM requirements (US)

- [ ] Accurate "From" name and email
- [ ] Non-deceptive subject line
- [ ] Physical postal address included
- [ ] Working unsubscribe mechanism
- [ ] Unsubscribe honored within 10 days
- [ ] No purchased lists

GDPR requirements (EU subscribers)

- [ ] Explicit consent obtained (not pre-checked)
- [ ] Clear privacy policy linked
- [ ] Easy unsubscribe process
- [ ] Data export available on request
- [ ] Data deletion on request
- [ ] Record of consent stored

Related skills

  • web-scraping, Automate content gathering for newsletters
  • data-journalism, Include data visualizations in emails
  • academic-writing, Write clear, structured content
  • newsroom-style, AP Style enforcement on newsletter copy
  • fact-check-workflow, Verify claims before they hit subscribers' inboxes
  • ai-writing-detox, Strip AI patterns from drafts

Skill metadata

Field Value
version 1.0.0
created 2025-12-26
updated 2026-05-08
author Joe Amditis
domain publishing, marketing
complexity intermediate
Files (claude-skills-journalism)
  • agents
    • openai.yaml 101 B
      interface:
        display_name: "Newsletter publishing"
        short_description: "Email newsletter workflows"
      
  • SKILL.md 17.2 KB
    ---
    name: newsletter-publishing
    description: Email newsletter workflows. Use when creating newsletters, building subscriber lists, designing templates, or tracking engagement.
    ---
    
    # Newsletter publishing
    
    Practical workflows for building and managing email newsletters for journalism and academia.
    
    ## When to activate
    
    - Creating a new newsletter from scratch
    - Designing email templates for journalism content
    - Building and segmenting subscriber lists
    - Analyzing newsletter performance metrics
    - Planning editorial calendars for newsletters
    - Migrating between newsletter platforms
    - Improving deliverability and open rates
    
    ## Newsletter architecture
    
    ### Content strategy framework
    
    ```markdown
    ## Newsletter strategy document
    
    ### Core identity
    - **Name**:
    - **Tagline** (one line):
    - **What readers get**: [specific value proposition]
    - **Frequency**: [ ] Daily [ ] Weekly [ ] Bi-weekly [ ] Monthly
    
    ### Target audience
    - Primary reader:
    - What they care about:
    - Why they'll subscribe:
    - What they'll do with this info:
    
    ### Content pillars
    1. [Core topic 1] - [how often]
    2. [Core topic 2] - [how often]
    3. [Recurring feature] - [how often]
    
    ### Voice and tone
    - Formal ↔ Conversational: [1-5]
    - Serious ↔ Light: [1-5]
    - Reported ↔ Personal: [1-5]
    
    ### Success metrics (first 6 months)
    - Subscriber goal:
    - Target open rate:
    - Target click rate:
    ```
    
    ### Issue structure template
    
    ```markdown
    ## [Newsletter Name] - Issue #[XX]
    **Date**: [Date]
    **Subject line**: [Subject]
    **Preview text**: [First 50-90 characters readers see]
    
    ---
    
    ### Opening hook
    [2-3 sentences that make readers want to keep reading]
    
    ### Main story
    [Your primary content - 300-600 words for most newsletters]
    
    ### Secondary items (if applicable)
    - **Quick hit 1**: [Brief item with link]
    - **Quick hit 2**: [Brief item with link]
    
    ### Recurring section
    [Weekly column, data point, recommendation, etc.]
    
    ### Sign-off
    [Personal note, call to action, or preview of next issue]
    
    ---
    
    **Unsubscribe** | **Preferences** | **Forward to a friend**
    ```
    
    ## Technical implementation
    
    ### HTML email template (responsive)
    
    ```html
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>{{newsletter_name}}</title>
      <style>
        /* Reset styles for email clients */
        body { margin: 0; padding: 0; width: 100%; }
        table { border-collapse: collapse; }
        img { border: 0; display: block; }
    
        /* Responsive container */
        .container {
          max-width: 600px;
          margin: 0 auto;
          font-family: Georgia, serif;
          font-size: 18px;
          line-height: 1.6;
          color: #333;
        }
    
        /* Dark mode support */
        @media (prefers-color-scheme: dark) {
          .container { background-color: #1a1a1a; color: #e0e0e0; }
          a { color: #6db3f2; }
        }
    
        /* Mobile styles */
        @media only screen and (max-width: 480px) {
          .container { padding: 15px !important; }
          h1 { font-size: 24px !important; }
        }
      </style>
    </head>
    <body>
      <table role="presentation" width="100%">
        <tr>
          <td align="center" style="padding: 20px;">
            <div class="container">
              <!-- Header -->
              <table width="100%">
                <tr>
                  <td style="padding-bottom: 20px; border-bottom: 2px solid #333;">
                    <h1 style="margin: 0;">{{newsletter_name}}</h1>
                    <p style="margin: 5px 0 0; color: #666;">{{issue_date}}</p>
                  </td>
                </tr>
              </table>
    
              <!-- Content -->
              <table width="100%">
                <tr>
                  <td style="padding: 30px 0;">
                    {{content}}
                  </td>
                </tr>
              </table>
    
              <!-- Footer -->
              <table width="100%">
                <tr>
                  <td style="padding-top: 20px; border-top: 1px solid #ddd; font-size: 14px; color: #666;">
                    <p>You're receiving this because you subscribed to {{newsletter_name}}.</p>
                    <p>
                      <a href="{{unsubscribe_url}}">Unsubscribe</a> |
                      <a href="{{preferences_url}}">Update preferences</a>
                    </p>
                  </td>
                </tr>
              </table>
            </div>
          </td>
        </tr>
      </table>
    </body>
    </html>
    ```
    
    ### Python newsletter sender
    
    ```python
    from dataclasses import dataclass, field
    from datetime import datetime
    from typing import List, Dict, Optional
    from enum import Enum
    import hashlib
    
    class SubscriberStatus(Enum):
        ACTIVE = "active"
        UNSUBSCRIBED = "unsubscribed"
        BOUNCED = "bounced"
        COMPLAINED = "complained"
    
    @dataclass
    class Subscriber:
        email: str
        name: Optional[str] = None
        subscribed_at: datetime = field(default_factory=datetime.now)
        status: SubscriberStatus = SubscriberStatus.ACTIVE
        tags: List[str] = field(default_factory=list)
        custom_fields: Dict = field(default_factory=dict)
    
        @property
        def hash_id(self) -> str:
            """Generate unique ID for unsubscribe links."""
            return hashlib.md5(self.email.encode()).hexdigest()[:12]
    
    @dataclass
    class NewsletterIssue:
        subject: str
        preview_text: str
        html_content: str
        plain_text: str
        scheduled_at: Optional[datetime] = None
        sent_at: Optional[datetime] = None
        issue_number: int = 0
    
        # Metrics
        sent_count: int = 0
        delivered_count: int = 0
        opened_count: int = 0
        clicked_count: int = 0
        bounced_count: int = 0
        unsubscribed_count: int = 0
    
        @property
        def open_rate(self) -> float:
            if self.delivered_count == 0:
                return 0.0
            return (self.opened_count / self.delivered_count) * 100
    
        @property
        def click_rate(self) -> float:
            if self.delivered_count == 0:
                return 0.0
            return (self.clicked_count / self.delivered_count) * 100
    
    class NewsletterManager:
        """Core newsletter operations."""
    
        def __init__(self, name: str):
            self.name = name
            self.subscribers: List[Subscriber] = []
            self.issues: List[NewsletterIssue] = []
    
        def add_subscriber(self, email: str, name: str = None,
                           tags: List[str] = None) -> Subscriber:
            """Add new subscriber with double opt-in pending."""
            sub = Subscriber(
                email=email.lower().strip(),
                name=name,
                tags=tags or []
            )
            self.subscribers.append(sub)
            return sub
    
        def segment_subscribers(self, tags: List[str] = None,
                               min_engagement: float = None) -> List[Subscriber]:
            """Get subscribers matching criteria."""
            active = [s for s in self.subscribers
                      if s.status == SubscriberStatus.ACTIVE]
    
            if tags:
                active = [s for s in active
                         if any(t in s.tags for t in tags)]
    
            return active
    
        def calculate_engagement_score(self, subscriber: Subscriber) -> float:
            """Score subscriber engagement 0-100."""
            # Implementation would track opens/clicks per subscriber
            return 50.0  # Placeholder
    ```
    
    ## Subscriber management
    
    ### List hygiene workflow
    
    ```python
    from datetime import datetime, timedelta
    
    def clean_subscriber_list(manager: NewsletterManager,
                             inactive_threshold_days: int = 180) -> dict:
        """Identify and handle inactive subscribers."""
        cutoff = datetime.now() - timedelta(days=inactive_threshold_days)
    
        results = {
            'total': len(manager.subscribers),
            'active': 0,
            'inactive': [],
            'bounced': [],
            'unsubscribed': []
        }
    
        for sub in manager.subscribers:
            if sub.status == SubscriberStatus.BOUNCED:
                results['bounced'].append(sub.email)
            elif sub.status == SubscriberStatus.UNSUBSCRIBED:
                results['unsubscribed'].append(sub.email)
            elif sub.status == SubscriberStatus.ACTIVE:
                # Check last engagement
                engagement = manager.calculate_engagement_score(sub)
                if engagement < 10:  # Very low engagement
                    results['inactive'].append(sub.email)
                else:
                    results['active'] += 1
    
        return results
    
    def run_reengagement_campaign(inactive_subscribers: List[str]) -> None:
        """Send win-back campaign to inactive subscribers."""
        # Send "We miss you" campaign
        # If no engagement after 2 attempts, mark for removal
        pass
    ```
    
    ### Subscriber segmentation
    
    ```markdown
    ## Recommended segments
    
    ### By engagement
    - **VIPs**: Open rate > 80%, always click
    - **Engaged**: Open rate 40-80%
    - **Casual**: Open rate 10-40%
    - **At-risk**: Haven't opened in 90 days
    - **Inactive**: Haven't opened in 180 days
    
    ### By interest (tag-based)
    - Topic preferences from signup
    - Content they've clicked
    - Surveys/polls they've answered
    
    ### By source
    - Organic (website signup)
    - Referral (forwarded by friend)
    - Social media
    - Paywall/registration wall
    ```
    
    ## Subject line optimization
    
    ### High-performing patterns
    
    ```markdown
    ## Subject line formulas that work
    
    ### For news/journalism
    - **Breaking format**: "Breaking: [Concise news]"
    - **Numbers**: "[X] things we learned about [topic]"
    - **Question**: "Why did [entity] do [thing]?"
    - **Direct**: "[Topic]: What you need to know"
    
    ### For analysis/opinion
    - **Take**: "The real story behind [event]"
    - **Contrarian**: "Why everyone is wrong about [topic]"
    - **Insider**: "What [industry] insiders know about [topic]"
    
    ### What to avoid
    - ALL CAPS
    - Excessive punctuation!!!
    - Clickbait that doesn't deliver
    - Spam trigger words (FREE, URGENT, ACT NOW)
    - Misleading preview text
    ```
    
    ### A/B testing framework
    
    ```python
    import random
    from typing import List, Tuple
    
    def ab_test_subject_lines(subscribers: List[Subscriber],
                             subject_a: str,
                             subject_b: str,
                             test_percentage: float = 0.2) -> dict:
        """
        Test two subject lines on subset before full send.
        """
        test_size = int(len(subscribers) * test_percentage)
        test_group = random.sample(subscribers, test_size)
    
        # Split test group
        half = len(test_group) // 2
        group_a = test_group[:half]
        group_b = test_group[half:]
    
        remaining = [s for s in subscribers if s not in test_group]
    
        return {
            'group_a': {
                'subject': subject_a,
                'subscribers': group_a,
                'size': len(group_a)
            },
            'group_b': {
                'subject': subject_b,
                'subscribers': group_b,
                'size': len(group_b)
            },
            'remaining': {
                'subscribers': remaining,
                'size': len(remaining),
                'note': 'Send winner to this group after test period'
            },
            'test_duration_hours': 4
        }
    ```
    
    ## Deliverability best practices
    
    ### Email authentication setup
    
    ```markdown
    ## DNS records for deliverability
    
    ### SPF record
    ```
    v=spf1 include:_spf.{{esp_sending_domain}} ~all
    ```
    
    ### DKIM
    - Generate keys through your ESP
    - Add TXT record with public key
    - Verify signature is applied to outgoing mail
    
    ### DMARC
    ```
    v=DMARC1; p=quarantine; rua=mailto:dmarc@yourdomain.com
    ```
    
    ### Checklist before sending
    - [ ] SPF, DKIM, DMARC configured
    - [ ] Sending domain warmed up
    - [ ] List is clean (no hard bounces)
    - [ ] Unsubscribe link works
    - [ ] Physical address in footer (CAN-SPAM)
    - [ ] Test email received in inbox (not spam)
    ```
    
    ### Gmail, Yahoo, and Outlook bulk-sender requirements
    
    Bulk senders must meet authentication and unsubscribe requirements introduced by Gmail and Yahoo in February 2024. Microsoft Outlook adopted a parallel set in May 2025. Since November 2025, Gmail rejects non-compliant mail with permanent 5xx errors rather than soft-deferring it, non-compliant newsletters now bounce hard.
    
    **Who is covered.** A "bulk sender" is one mailing more than 5,000 messages per day to Gmail addresses. The 5,000 threshold is counted at the **primary domain level across all subdomains**, not per sending subdomain. A newsroom sending 2,500/day from `transactional.example.com` and 2,500/day from `news.example.com` is over the threshold.
    
    **Required:**
    
    - **SPF and DKIM authentication on the sending domain.** Both must pass. SPF alone is no longer sufficient. DKIM keys must be at least 1024 bits.
    - **DMARC policy at minimum `p=none`.** Production senders should move to `p=quarantine` or `p=reject` once aligned.
    - **Domain alignment.** **One** of SPF or DKIM must align with the organizational domain in the visible `From:` header, not both. Relaxed alignment is acceptable.
    - **One-click unsubscribe (RFC 8058).** The mail must include a `List-Unsubscribe` header with an HTTPS URL and a `List-Unsubscribe-Post: List-Unsubscribe=One-Click` header. The HTTPS endpoint must process the unsubscribe within two days without requiring login. A visible unsubscribe link must also appear in the message body.
    - **Spam complaint rate below 0.3 percent**, measured in Google Postmaster Tools. Google's recommended target ceiling is 0.1 percent; sustained rates above 0.3 percent trigger rejection.
    - **Valid PTR record (reverse DNS) on the sending IP.** Forward and reverse DNS must match.
    - **TLS for inbound connections** (Google requirement since December 2023).
    
    **Operational implications:**
    
    Most reputable ESPs handle authentication, headers, and TLS once the sending domain is verified. The two parts that remain the operator's responsibility are complaint rate and unsubscribe behavior, re-engagement campaigns and prompt list hygiene matter here. Re-engaging dormant subscribers is risky precisely because they complain at 5–10× the rate of active ones; one bad re-engagement campaign can push complaint rate over 0.3 percent and trigger rejections across the entire sending domain.
    
    References:
    - Google, *Email sender guidelines*, `support.google.com/mail/answer/81126`
    - Google, *Email sender guidelines FAQ*, `support.google.com/a/answer/14229414`
    - Yahoo, *Sender Best Practices*, `senders.yahooinc.com/best-practices/`
    
    ### Spam score checklist
    
    ```markdown
    ## Before you send
    
    ### Content checks
    - [ ] No spam trigger words
    - [ ] Text-to-image ratio good (mostly text)
    - [ ] All links are to reputable domains
    - [ ] No URL shorteners (use full links)
    - [ ] Plain text version included
    
    ### Technical checks
    - [ ] From address matches sending domain
    - [ ] Reply-to address is monitored
    - [ ] Preheader text is set
    - [ ] Images have alt text
    - [ ] Links are not broken
    ```
    
    ## Analytics and optimization
    
    ### Key metrics dashboard
    
    ```python
    from dataclasses import dataclass
    
    @dataclass
    class NewsletterAnalytics:
        """Track newsletter performance over time."""
    
        issue: NewsletterIssue
    
        def summary(self) -> dict:
            return {
                'issue_number': self.issue.issue_number,
                'sent': self.issue.sent_count,
                'delivered': self.issue.delivered_count,
                'delivery_rate': self._pct(self.issue.delivered_count,
                                           self.issue.sent_count),
                'opens': self.issue.opened_count,
                'open_rate': self.issue.open_rate,
                'clicks': self.issue.clicked_count,
                'click_rate': self.issue.click_rate,
                'click_to_open': self._pct(self.issue.clicked_count,
                                           self.issue.opened_count),
                'unsubscribes': self.issue.unsubscribed_count,
                'unsubscribe_rate': self._pct(self.issue.unsubscribed_count,
                                              self.issue.delivered_count),
            }
    
        def _pct(self, numerator: int, denominator: int) -> float:
            if denominator == 0:
                return 0.0
            return round((numerator / denominator) * 100, 2)
    
    # Benchmarks (journalism newsletters)
    BENCHMARKS = {
        'open_rate': {'good': 40, 'excellent': 55},
        'click_rate': {'good': 4, 'excellent': 8},
        'unsubscribe_rate': {'acceptable': 0.5, 'concerning': 1.0},
    }
    ```
    
    ## Platform comparison
    
    | Platform | Best for | Pricing model | Key feature |
    |----------|----------|---------------|-------------|
    | Substack | Writer-first, paid subs | Revenue share | Built-in payments |
    | Buttondown | Developers, minimal | Per subscriber | Markdown native |
    | Ghost | Publishers, memberships | Flat fee | Full CMS included |
    | beehiiv | Growth-focused | Freemium | Referral tools |
    | Kit (formerly ConvertKit) | Creators | Per subscriber | Automation |
    | Mailchimp | Small orgs | Tiered | Easy templates |
    
    ## Legal compliance
    
    ### CAN-SPAM requirements (US)
    
    ```markdown
    - [ ] Accurate "From" name and email
    - [ ] Non-deceptive subject line
    - [ ] Physical postal address included
    - [ ] Working unsubscribe mechanism
    - [ ] Unsubscribe honored within 10 days
    - [ ] No purchased lists
    ```
    
    ### GDPR requirements (EU subscribers)
    
    ```markdown
    - [ ] Explicit consent obtained (not pre-checked)
    - [ ] Clear privacy policy linked
    - [ ] Easy unsubscribe process
    - [ ] Data export available on request
    - [ ] Data deletion on request
    - [ ] Record of consent stored
    ```
    
    ## Related skills
    
    - **web-scraping**, Automate content gathering for newsletters
    - **data-journalism**, Include data visualizations in emails
    - **academic-writing**, Write clear, structured content
    - **newsroom-style**, AP Style enforcement on newsletter copy
    - **fact-check-workflow**, Verify claims before they hit subscribers' inboxes
    - **ai-writing-detox**, Strip AI patterns from drafts
    
    ---
    
    ## Skill metadata
    
    | Field | Value |
    |-------|-------|
    | version | 1.0.0 |
    | created | 2025-12-26 |
    | updated | 2026-05-08 |
    | author | Joe Amditis |
    | domain | publishing, marketing |
    | complexity | intermediate |
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related