关于 newsletter-publishing
newsletter-publishing 是面向新闻与学术团队的电子邮件通讯规划和运营技能,涵盖编辑结构、订阅者管理、送达率、分析、合规及平台选择。
主要功能
- 01通讯策略与期刊模板
- 02响应式 HTML 邮件指南
- 03订阅者清理与细分
- 04主题行测试与优化
- 05送达率和合规检查
- 06效果指标与平台比较
适用场景
- 01规划出版物通讯
- 02细分并清理订阅者列表
- 03检查身份验证和退订要求
- 04迁移前比较通讯平台
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ jamditis/ claude-skills-journalism --skill newsletter-publishing安装后在 Claude Code 中输入/newsletter-publishing确认已加载。
附带文件agents/openai.yaml
详细介绍
新闻简报发布
面向新闻业和学术界的电子邮件简报创建与管理实用工作流。
适用场景
- 从头开始创建新闻简报
- 为新闻内容设计电子邮件模板
- 构建订阅者列表并进行细分
- 分析新闻简报的效果指标
- 制定新闻简报的编辑日历
- 在不同新闻简报平台之间迁移
- 提升送达率和打开率
新闻简报架构
内容策略框架
## 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:
单期简报结构模板
## [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**
技术实现
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">
<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>
<table width="100%">
<tr>
<td style="padding: 30px 0;">
{{content}}
</td>
</tr>
</table>
<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 新闻简报发送器
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
订阅者管理
列表清理工作流
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
订阅者细分
## 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 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 测试框架
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
}
提升送达率的最佳实践
电子邮件身份验证设置
## 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:[email protected]
### 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 和 Outlook 的批量发件人要求
批量发件人必须满足 Gmail 和 Yahoo 于 2024 年 2 月推出的身份验证和取消订阅要求。Microsoft Outlook 于 2025 年 5 月采用了类似的一套要求。自 2025 年 11 月起,Gmail 会以永久性 5xx 错误拒收不合规邮件,而不是暂缓处理;不合规的新闻简报现在会被硬退信。
适用对象。“批量发件人”是指每天向 Gmail 地址发送超过 5,000 封邮件的发件人。5,000 封的门槛按所有子域名共用的主域名计算,而不是按发送子域名分别计算。例如,某新闻编辑部从 transactional.example.com 每天发送 2,500 封,从 news.example.com 每天发送 2,500 封,就超过了门槛。
要求:
- 对发送域进行 SPF 和 DKIM 身份验证。 两者都必须通过验证。仅使用 SPF 已不再足够。DKIM 密钥长度必须至少为 1024 位。
- DMARC 策略至少设为
p=none。 生产环境中的发件人应在对齐后改用p=quarantine或p=reject。 - 域名对齐。 SPF 或 DKIM 其中一项必须与可见
From:标头中的组织域名对齐,不要求两者都对齐。允许使用宽松对齐模式。 - 一键取消订阅(RFC 8058)。 邮件必须包含带 HTTPS URL 的
List-Unsubscribe标头,以及List-Unsubscribe-Post: List-Unsubscribe=One-Click标头。HTTPS 端点必须在两天内处理取消订阅请求,且不要求用户登录。邮件正文中也必须显示取消订阅链接。 - 垃圾邮件投诉率低于 0.3%,通过 Google Postmaster Tools 衡量。Google 建议将目标上限设为 0.1%;持续高于 0.3% 会触发拒收。
- 发送 IP 必须有有效的 PTR 记录(反向 DNS)。 正向和反向 DNS 必须匹配。
- 入站连接必须使用 TLS(Google 自 2023 年 12 月起的要求)。
运营影响:
大多数可靠的 ESP 会在发送域通过验证后,处理身份验证、标头和 TLS。仍需由运营方负责的两项工作是控制投诉率和处理取消订阅;重新互动活动和及时清理列表对此至关重要。重新唤回长期不活跃的订阅者风险很高,正因为他们的投诉率是活跃订阅者的 5~10 倍;一次不当的重新互动活动就可能使投诉率超过 0.3%,导致整个发送域的邮件遭到拒收。
参考资料:
- 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/
垃圾邮件评分检查清单
## 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
分析与优化
关键指标仪表板
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},
}
平台比较
| 平台 | 最适合 | 定价模式 | 主要功能 |
|---|---|---|---|
| Substack | 以作者为中心、付费订阅 | 收入分成 | 内置支付功能 |
| Buttondown | 开发者、极简需求 | 按订阅者计费 | 原生支持 Markdown |
| Ghost | 出版商、会员服务 | 固定费用 | 内置完整 CMS |
| beehiiv | 注重增长 | 免费增值 | 推荐工具 |
| Kit(原名 ConvertKit) | 内容创作者 | 按订阅者计费 | 自动化 |
| Mailchimp | 小型组织 | 分级定价 | 简单易用的模板 |
法律合规
CAN-SPAM 要求(美国)
- [ ] 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 要求(欧盟订阅者)
- [ ] 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
相关技能
- web-scraping,自动收集简报内容
- data-journalism,在邮件中加入数据可视化
- academic-writing,撰写清晰、有条理的内容
- newsroom-style,确保简报文案符合 AP Style
- fact-check-workflow,在内容发送到订阅者的收件箱之前核实说法
- ai-writing-detox,从草稿中去除 AI 写作模式
技能元数据
| 字段 | 值 |
|---|---|
| 版本 | 1.0.0 |
| 创建日期 | 2025-12-26 |
| 更新日期 | 2026-05-08 |
| 作者 | Joe Amditis |
| 领域 | 出版、营销 |
| 复杂度 | 中级 |