关于 poplar-direct-mail
一项使用 Poplar HTML 模板设计直邮素材并通过其 API 发送邮件的技能,涵盖个性化内容、自动化营销活动,以及明信片、折页和信件的模板要求。
主要功能
- 01多种邮件形式的 HTML 模板
- 02基于 Liquid 的个性化内容
- 03Poplar API 邮件请求
- 04查询营销活动和创意素材
- 05模板与印刷设计规范
适用场景
- 01制作个性化明信片或信件
- 02发送触发式或事务性邮件
- 03构建弃购召回或客户挽回活动
- 04从 CSV 批量发送邮件
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ ckorhonen/ claude-skills --skill poplar-direct-mail安装后在 Claude Code 中输入/poplar-direct-mail确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
scripts/send_batch.pysend_mailing.pytest_connection.pytemplates/postcard-4x6-back.htmlpostcard-4x6-front.html详细介绍
Poplar Direct Mail
概述
使用 Poplar 的 HTML 模板系统设计专业的直邮创意,并通过其 API 发送程序化邮件。此技能涵盖使用动态个性化内容创建模板、集成 API 以发送触发式邮件,以及打造高转化率直邮营销活动的创意最佳实践。
适用场景
- 为直邮明信片、对折页、三折页或信件创建 HTML 模板
- 构建触发式/程序化直邮营销活动
- 将直邮集成到营销自动化工作流中
- 设计包含动态内容(姓名、优惠、二维码)的个性化邮件
- 发送交易类邮件(收据、确认函、账单)
- 设置购物车弃单或客户挽回邮件营销活动
前提条件
- Poplar 账户(heypoplar.com)
- 从 Poplar Credentials 获取的 API 访问令牌
- Python 3.9+(用于脚本)
requests软件包
安装
pip install requests
设置 API 令牌:
# Test token (for development - only works with mailing endpoint)
export POPLAR_API_TOKEN="test_your_token_here"
# Production token (for live mailings)
export POPLAR_API_TOKEN="your_production_token_here"
邮件格式与尺寸
明信片
| 尺寸 | 成品尺寸 | 含出血尺寸 | 像素(300 PPI) |
|---|---|---|---|
| 4" x 6" | 4" x 6" | 4.25" x 6.25" | 1275 x 1875 |
| 6" x 9" | 6" x 9" | 6.25" x 9.25" | 1875 x 2775 |
| 6" x 11" | 6" x 11" | 6.25" x 11.25" | 1875 x 3375 |
最适合: 再营销、时效性促销、简明信息、直邮新手
对折页
| 格式 | 像素(300 PPI) |
|---|---|
| 短边对折(从左向右折) | 1725 x 5175 |
| 长边对折(从上向下折) | 3375 x 2625 |
最适合: 潜在客户开发营销活动、新品发布、复杂的价值主张
三折页
| 格式 | 像素(300 PPI) |
|---|---|
| 标准三折页 | 2625 x 4987 |
最适合: 多种产品选择、详细信息、目录
信件
| 格式 | 尺寸 |
|---|---|
| 8.5" x 11"(彩色或黑白) | 612 x 792 px(无需出血) |
最适合: 金融/保险、敏感内容、交易类邮件、专业通信
HTML 模板结构
基本模板
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;600;700&display=swap"
rel="stylesheet" type="text/css">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
width: 4.25in;
height: 6.25in;
position: relative;
font-family: 'Open Sans', sans-serif;
}
.background {
position: absolute;
width: 100%;
height: 100%;
background-image: url('https://your-hosted-image.com/background.png');
background-size: cover;
}
.headline {
position: absolute;
top: 0.5in;
left: 0.375in;
font-size: 28pt;
font-weight: 700;
color: #1a1a1a;
}
.personalized-text {
position: absolute;
top: 1.2in;
left: 0.375in;
font-size: 14pt;
color: #333333;
}
.offer-box {
position: absolute;
bottom: 1in;
left: 0.375in;
width: 3.5in;
padding: 0.25in;
background-color: #ff6b35;
color: white;
text-align: center;
font-size: 18pt;
font-weight: 600;
}
</style>
</head>
<body>
<div class="background"></div>
<div class="headline">Special Offer Inside!</div>
<div class="personalized-text">
Hi {{recipient.first_name | default: "Friend"}},
</div>
<div class="offer-box">
Use code: {{promotion.promo_code}}
</div>
</body>
</html>
CSS 要求
| 要求 | 详情 |
|---|---|
| CSS 位置 | 所有样式都放在 <head> 中的 <style> 标签内 |
| 定位 | 所有元素都使用 position: absolute |
| 单位 | 仅使用 in(英寸)或 px(像素),不使用 em、rem、vw、vh |
| 图片 URL | 仅使用绝对 URL(必须可公开访问) |
| 字体 | Google Fonts 或自托管的 .ttf/.woff 文件 |
字体实现
Google Fonts(推荐):
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap"
rel="stylesheet" type="text/css">
自定义字体:
@font-face {
font-family: "CustomFont";
src: url("https://your-domain.com/fonts/CustomFont.ttf") format("truetype");
}
合并标签与个性化
收件人数据
{{recipient.first_name}}
{{recipient.last_name}}
{{recipient.full_name}}
{{recipient.address_1}}
{{recipient.city}}
{{recipient.state}}
{{recipient.postal_code}}
优惠代码
{{promotion.promo_code}}
{{promotion.qr_url}}
基于位置的内容
{{location.city}}
{{location.state}}
{{location.store_address}}
自定义数据
{{custom.purchase_amount}}
{{custom.product_name}}
{{custom.loyalty_tier}}
Liquid 模板逻辑
条件内容
{% if recipient.first_name %}
Hi {{recipient.first_name | capitalize}},
{% else %}
Dear Valued Customer,
{% endif %}
根据购买历史设置优惠档位
{% if custom.lifetime_value >= 500 %}
<div class="offer">Enjoy 25% off your next order!</div>
{% elsif custom.lifetime_value >= 200 %}
<div class="offer">Take 15% off your next purchase!</div>
{% else %}
<div class="offer">Get $10 off orders over $50!</div>
{% endif %}
滚动到期日期
Expires: {{ "now" | date: "%s" | plus: 2592000 | date: "%B %e, %Y" }}
Expires: {{ "now" | date: "%s" | plus: 7776000 | date: "%B %e, %Y" }}
文本格式筛选器
{{ recipient.first_name | capitalize }}
{{ recipient.first_name | upcase }}
{{ recipient.first_name | downcase }}
{{ custom.price | money }}
API 参考
身份验证
所有请求都需要 Bearer 令牌:
curl -H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
https://api.heypoplar.com/v1/me
创建邮件
端点: POST https://api.heypoplar.com/v1/mailing
curl -X POST https://api.heypoplar.com/v1/mailing \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"campaign_id": "your-campaign-id",
"creative_id": "your-creative-id",
"recipient": {
"first_name": "Jane",
"last_name": "Smith",
"address_1": "123 Main Street",
"address_2": "Apt 4B",
"city": "San Francisco",
"state": "CA",
"postal_code": "94102"
},
"merge_tags": {
"promo_code": "SAVE20",
"expiration_date": "December 31, 2024"
}
}'
响应(201 Created):
{
"id": "mailing-uuid",
"campaign_id": "campaign-uuid",
"creative_id": "creative-uuid",
"state": "processing",
"front_url": "https://app.heypoplar.com/preview/front.png",
"back_url": "https://app.heypoplar.com/preview/back.png",
"pdf_url": "https://app.heypoplar.com/preview/proof.pdf",
"total_cost": "0.89",
"created_at": "2024-01-15T10:30:00Z"
}
安排未来发送的邮件
curl -X POST https://api.heypoplar.com/v1/mailing \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"campaign_id": "your-campaign-id",
"recipient": {
"full_name": "John Doe",
"address_1": "456 Oak Avenue",
"city": "Austin",
"state": "TX",
"postal_code": "78701"
},
"send_at": "2024-02-01T09:00:00Z"
}'
获取邮件状态
端点: GET https://api.heypoplar.com/v1/mailing/:id
curl https://api.heypoplar.com/v1/mailing/MAILING_ID \
-H "Authorization: Bearer YOUR_TOKEN"
列出营销活动
端点: GET https://api.heypoplar.com/v1/campaigns
curl https://api.heypoplar.com/v1/campaigns \
-H "Authorization: Bearer YOUR_TOKEN"
列出营销活动创意
端点: GET https://api.heypoplar.com/v1/campaign/:id/creatives
curl https://api.heypoplar.com/v1/campaign/CAMPAIGN_ID/creatives \
-H "Authorization: Bearer YOUR_TOKEN"
添加到禁止邮寄名单
端点: POST https://api.heypoplar.com/v1/do-not-mail
curl -X POST https://api.heypoplar.com/v1/do-not-mail \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"address": {
"address_1": "123 Main St",
"postal_code": "94102"
}
}'
Python 脚本
发送单封邮件
#!/usr/bin/env python3
"""Send a single direct mail piece via Poplar API."""
import os
import requests
import argparse
POPLAR_API_URL = "https://api.heypoplar.com/v1"
def send_mailing(
campaign_id: str,
recipient: dict,
creative_id: str = None,
merge_tags: dict = None,
send_at: str = None
) -> dict:
"""Send a mailing via Poplar API."""
token = os.environ.get("POPLAR_API_TOKEN")
if not token:
raise ValueError("POPLAR_API_TOKEN environment variable not set")
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
payload = {
"campaign_id": campaign_id,
"recipient": recipient
}
if creative_id:
payload["creative_id"] = creative_id
if merge_tags:
payload["merge_tags"] = merge_tags
if send_at:
payload["send_at"] = send_at
response = requests.post(
f"{POPLAR_API_URL}/mailing",
headers=headers,
json=payload
)
response.raise_for_status()
return response.json()
def main():
parser = argparse.ArgumentParser(description="Send a Poplar direct mail piece")
parser.add_argument("--campaign-id", required=True, help="Campaign ID")
parser.add_argument("--creative-id", help="Creative ID (optional)")
parser.add_argument("--first-name", required=True, help="Recipient first name")
parser.add_argument("--last-name", required=True, help="Recipient last name")
parser.add_argument("--address", required=True, help="Street address")
parser.add_argument("--city", required=True, help="City")
parser.add_argument("--state", required=True, help="State (2-letter code)")
parser.add_argument("--zip", required=True, help="ZIP code")
parser.add_argument("--promo-code", help="Promo code merge tag")
args = parser.parse_args()
recipient = {
"first_name": args.first_name,
"last_name": args.last_name,
"address_1": args.address,
"city": args.city,
"state": args.state,
"postal_code": args.zip
}
merge_tags = {}
if args.promo_code:
merge_tags["promo_code"] = args.promo_code
result = send_mailing(
campaign_id=args.campaign_id,
recipient=recipient,
creative_id=args.creative_id,
merge_tags=merge_tags if merge_tags else None
)
print(f"Mailing created: {result['id']}")
print(f"Status: {result['state']}")
print(f"Cost: ${result['total_cost']}")
print(f"PDF Preview: {result['pdf_url']}")
if __name__ == "__main__":
main()
从 CSV 批量发送
#!/usr/bin/env python3
"""Send batch mailings from a CSV file."""
import os
import csv
import time
import requests
import argparse
from typing import Generator
POPLAR_API_URL = "https://api.heypoplar.com/v1"
def read_recipients(csv_path: str) -> Generator[dict, None, None]:
"""Read recipients from CSV file."""
with open(csv_path, 'r') as f:
reader = csv.DictReader(f)
for row in reader:
yield {
"recipient": {
"first_name": row.get("first_name", ""),
"last_name": row.get("last_name", ""),
"address_1": row.get("address_1", ""),
"address_2": row.get("address_2", ""),
"city": row.get("city", ""),
"state": row.get("state", ""),
"postal_code": row.get("postal_code", "")
},
"merge_tags": {
k: v for k, v in row.items()
if k not in ["first_name", "last_name", "address_1",
"address_2", "city", "state", "postal_code"]
}
}
def send_batch(
csv_path: str,
campaign_id: str,
creative_id: str = None,
delay: float = 0.1
) -> tuple[int, int]:
"""Send batch mailings from CSV."""
token = os.environ.get("POPLAR_API_TOKEN")
if not token:
raise ValueError("POPLAR_API_TOKEN environment variable not set")
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
success_count = 0
error_count = 0
for record in read_recipients(csv_path):
payload = {
"campaign_id": campaign_id,
"recipient": record["recipient"]
}
if creative_id:
payload["creative_id"] = creative_id
if record["merge_tags"]:
payload["merge_tags"] = record["merge_tags"]
try:
response = requests.post(
f"{POPLAR_API_URL}/mailing",
headers=headers,
json=payload
)
response.raise_for_status()
success_count += 1
print(f"Sent to {record['recipient']['first_name']} {record['recipient']['last_name']}")
except requests.exceptions.RequestException as e:
error_count += 1
print(f"Error sending to {record['recipient']}: {e}")
time.sleep(delay)
return success_count, error_count
def main():
parser = argparse.ArgumentParser(description="Send batch Poplar mailings from CSV")
parser.add_argument("--csv", required=True, help="Path to CSV file")
parser.add_argument("--campaign-id", required=True, help="Campaign ID")
parser.add_argument("--creative-id", help="Creative ID (optional)")
parser.add_argument("--delay", type=float, default=0.1, help="Delay between requests (seconds)")
args = parser.parse_args()
success, errors = send_batch(
csv_path=args.csv,
campaign_id=args.campaign_id,
creative_id=args.creative_id,
delay=args.delay
)
print(f"\nCompleted: {success} sent, {errors} errors")
if __name__ == "__main__":
main()
测试 API 连接
#!/usr/bin/env python3
"""Test Poplar API connection and list campaigns."""
import os
import requests
POPLAR_API_URL = "https://api.heypoplar.com/v1"
def test_connection():
"""Test API connection and list available campaigns."""
token = os.environ.get("POPLAR_API_TOKEN")
if not token:
print("Error: POPLAR_API_TOKEN environment variable not set")
return False
headers = {
"Authorization": f"Bearer {token}",
"Accept": "application/json"
}
# Test authentication
print("Testing API connection...")
try:
response = requests.get(f"{POPLAR_API_URL}/me", headers=headers)
response.raise_for_status()
org = response.json()
print(f"Connected to organization: {org.get('name', 'Unknown')}")
except requests.exceptions.RequestException as e:
print(f"Authentication failed: {e}")
return False
# List campaigns
print("\nAvailable campaigns:")
try:
response = requests.get(f"{POPLAR_API_URL}/campaigns", headers=headers)
response.raise_for_status()
campaigns = response.json()
if not campaigns:
print(" No campaigns found")
else:
for campaign in campaigns:
print(f" - {campaign.get('name', 'Unnamed')}: {campaign.get('id')}")
except requests.exceptions.RequestException as e:
print(f"Failed to list campaigns: {e}")
return False
return True
if __name__ == "__main__":
test_connection()
设计最佳实践
视觉设计
- 少即是多:避免堆砌过多信息
- 高对比度:使用对比鲜明的颜色,确保文字清晰可见
- 大字号:正文至少使用 10pt,标题至少使用 24pt
- 高质量图片:始终使用分辨率为 300 PPI/DPI 的图片
- 边缘延伸设计:将背景延伸至出血区域
信息传达
- 单一明确的 CTA:每份材料只设置一个主要行动号召
- 突出优惠:在正反两面都展示促销信息
- 简短 URL:使用简化后的 URL(brand.com/save20)
- 社会认同:适时加入客户评价
- 营造紧迫感:为有时效性的优惠添加截止日期
QR 码
QR 码图片必须预先生成,并通过合并标签提供其 URL。Poplar 不会自动生成 QR 码。
提供 QR 码:
merge_tags = {
"promotion": {
"qr_url": "https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=https://yoursite.com/offer123"
}
}
也可以使用以下服务:
- QR 码生成器 API
- QRCode Monkey
- 自己的 QR 码生成服务
设计指南:
- 最小尺寸:0.75" x 0.75"(URL 越复杂,尺寸应越大)
- 静区:在代码周围留出空白
- 打印前测试:务必确认 QR 码可正常使用
- 使用 URL 缩短服务:URL 越短,QR 码越简单
- URL 使用大写字母:全部大写可生成更小、更易读的代码
地址区块
- 自动添加:Poplar 会自动添加地址区块
- 在地址区块下方设计:延伸背景,使其覆盖地址区域
- 请勿添加:不要在模板中自行添加地址区块
模板示例
4x6 明信片 - 正面(促销)
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;800&display=swap" rel="stylesheet">
<style>
body {
width: 4.25in;
height: 6.25in;
position: relative;
font-family: 'Poppins', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
margin: 0;
}
.safe-zone {
position: absolute;
top: 0.25in;
left: 0.25in;
right: 0.25in;
bottom: 0.25in;
}
.logo {
position: absolute;
top: 0;
left: 0;
width: 1.5in;
}
.headline {
position: absolute;
top: 1in;
left: 0;
right: 0;
font-size: 32pt;
font-weight: 800;
color: white;
text-align: center;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
.subhead {
position: absolute;
top: 1.8in;
left: 0;
right: 0;
font-size: 16pt;
color: rgba(255,255,255,0.9);
text-align: center;
}
.offer-badge {
position: absolute;
top: 2.5in;
left: 50%;
transform: translateX(-50%);
width: 2.5in;
height: 2.5in;
background: white;
border-radius: 50%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
}
.offer-amount {
font-size: 48pt;
font-weight: 800;
color: #764ba2;
line-height: 1;
}
.offer-text {
font-size: 14pt;
color: #333;
text-transform: uppercase;
letter-spacing: 2px;
}
</style>
</head>
<body>
<div class="safe-zone">
<img src="https://your-domain.com/logo-white.svg" class="logo" alt="Logo">
<div class="headline">{{recipient.first_name | capitalize}}, You're Invited!</div>
<div class="subhead">Exclusive offer just for you</div>
<div class="offer-badge">
<div class="offer-amount">25%</div>
<div class="offer-text">Off Everything</div>
</div>
</div>
</body>
</html>
4x6 明信片 - 背面(带 QR 码)
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600&display=swap" rel="stylesheet">
<style>
body {
width: 4.25in;
height: 6.25in;
position: relative;
font-family: 'Poppins', sans-serif;
background: white;
margin: 0;
}
.content-area {
position: absolute;
top: 0.25in;
left: 0.25in;
width: 3.25in;
bottom: 0.25in;
}
.details {
position: absolute;
top: 0;
left: 0;
font-size: 11pt;
color: #333;
line-height: 1.6;
}
.promo-code-box {
position: absolute;
top: 1.5in;
left: 0;
background: #f8f8f8;
border: 2px dashed #764ba2;
padding: 0.15in 0.25in;
text-align: center;
}
.promo-label {
font-size: 10pt;
color: #666;
text-transform: uppercase;
}
.promo-code {
font-size: 18pt;
font-weight: 600;
color: #764ba2;
letter-spacing: 3px;
}
.expiry {
position: absolute;
top: 2.4in;
left: 0;
font-size: 9pt;
color: #999;
}
.qr-section {
position: absolute;
bottom: 0;
left: 0;
}
.qr-code {
width: 1in;
height: 1in;
}
.qr-text {
font-size: 9pt;
color: #666;
margin-top: 0.1in;
}
/* Address block area - leave clear for Poplar */
.address-area {
position: absolute;
top: 0.375in;
right: 0.25in;
width: 2.625in;
height: 1.125in;
/* This area will be covered by Poplar's address block */
}
</style>
</head>
<body>
<div class="content-area">
<div class="details">
Shop our entire collection with your<br>
exclusive discount. Free shipping on<br>
orders over $50!
</div>
<div class="promo-code-box">
<div class="promo-label">Your Code</div>
<div class="promo-code">{{promotion.promo_code}}</div>
</div>
<div class="expiry">
Expires: {{ "now" | date: "%s" | plus: 2592000 | date: "%B %e, %Y" }}
</div>
<div class="qr-section">
<img src="{{promotion.qr_url}}" class="qr-code" alt="Scan to shop">
<div class="qr-text">Scan to shop now</div>
</div>
</div>
</body>
</html>
故障排除
模板问题
“Unsupported unit”错误
- 仅使用
in或px单位,不要使用em、rem、vw、vh或%
图片未显示
- 确保所有图片 URL 都是绝对 URL(以
https://开头的完整 URL) - 确认 URL 可公开访问(在无痕浏览器中测试)
- 检查文件大小是否小于 5MB
字体未正确渲染
- 使用 Google Fonts 或自托管字体文件
- 不支持 Typekit 和 Adobe Fonts
- 在
<link>标签中包含所用的所有字重
合并标签未替换
- 检查数据中的列标题是否与合并标签名称完全一致
- 确认 Liquid 语法正确(没有拼写错误)
- 对可选字段使用
| default: "fallback"
API 问题
401 Unauthorized
- 确认 API 令牌正确
- 检查是否将测试令牌用于非邮寄端点(测试令牌仅适用于
/mailing)
400 Bad Request
- Campaign 必须处于启用状态
- 必须已上传 Creative
- 必填收件人字段:
address_1、city、state、postal_code
Campaign 未启用错误
- 发送前,请在 Poplar 控制面板中启用 Campaign
预览差异
浏览器预览与 PDF 预览不一致
- 始终检查 Poplar 提供的 PDF 校样,而非浏览器预览
- HTML 转 PDF 时,部分 CSS 的渲染效果可能不同
- 避免使用 PDF 渲染不充分支持的 CSS 功能