mcpField
P
poplar-direct-mail

poplar-direct-mail

github.com/ckorhonen营销与增长2245 次安装5 周前更新
代码仓库

关于 poplar-direct-mail

一项使用 Poplar HTML 模板设计直邮素材并通过其 API 发送邮件的技能,涵盖个性化内容、自动化营销活动,以及明信片、折页和信件的模板要求。

主要功能

  1. 01多种邮件形式的 HTML 模板
  2. 02基于 Liquid 的个性化内容
  3. 03Poplar API 邮件请求
  4. 04查询营销活动和创意素材
  5. 05模板与印刷设计规范

适用场景

  1. 01制作个性化明信片或信件
  2. 02发送触发式或事务性邮件
  3. 03构建弃购召回或客户挽回活动
  4. 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.py
send_mailing.py
test_connection.py
templates/
postcard-4x6-back.html
postcard-4x6-front.html

详细介绍

Poplar Direct Mail

概述

使用 Poplar 的 HTML 模板系统设计专业的直邮创意,并通过其 API 发送程序化邮件。此技能涵盖使用动态个性化内容创建模板、集成 API 以发送触发式邮件,以及打造高转化率直邮营销活动的创意最佳实践。

适用场景

  • 为直邮明信片、对折页、三折页或信件创建 HTML 模板
  • 构建触发式/程序化直邮营销活动
  • 将直邮集成到营销自动化工作流中
  • 设计包含动态内容(姓名、优惠、二维码)的个性化邮件
  • 发送交易类邮件(收据、确认函、账单)
  • 设置购物车弃单或客户挽回邮件营销活动

前提条件

安装

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"
    }
}

也可以使用以下服务:

设计指南:

  • 最小尺寸: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 功能

资源

评论

支持 Markdown