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