Skip to content

Instantly share code, notes, and snippets.

@neltseng
Last active July 11, 2026 19:00
Show Gist options
  • Select an option

  • Save neltseng/7fa844b8ef81108c53076ddc989d9c2b to your computer and use it in GitHub Desktop.

Select an option

Save neltseng/7fa844b8ef81108c53076ddc989d9c2b to your computer and use it in GitHub Desktop.
hz daily digest feeds (single source of truth for local skill + cloud routine)
[
{"name": "WordPress News", "url": "https://wordpress.org/news/feed/", "cat": "wordpress"},
{"name": "WP Tavern", "url": "https://wptavern.com/feed", "cat": "wordpress"},
{"name": "WordPress Developer Blog", "url": "https://developer.wordpress.org/news/feed/", "cat": "wordpress"},
{"name": "WooCommerce Developer Blog", "url": "https://developer.woocommerce.com/feed/", "cat": "woocommerce"},
{"name": "Practical Ecommerce", "url": "https://www.practicalecommerce.com/feed", "cat": "ecommerce"},
{"name": "Modern Retail", "url": "https://www.modernretail.co/feed/", "cat": "ecommerce"},
{"name": "Retail Dive", "url": "https://www.retaildive.com/feeds/news/", "cat": "ecommerce"},
{"name": "Search Engine Land", "url": "https://searchengineland.com/feed", "cat": "ads"},
{"name": "Search Engine Journal PPC", "url": "https://www.searchenginejournal.com/category/pay-per-click/feed/", "cat": "ads"},
{"name": "Google Ads & Commerce", "url": "https://blog.google/products/ads-commerce/rss/", "cat": "ads"},
{"name": "PPC Land", "url": "https://ppc.land/rss/", "cat": "ads"},
{"name": "Google Search Central", "url": "https://developers.google.com/search/blog/feed.xml", "cat": "seo"},
{"name": "Search Engine Roundtable", "url": "https://www.seroundtable.com/index.rdf", "cat": "seo"},
{"name": "Search Engine Journal SEO", "url": "https://www.searchenginejournal.com/category/seo/feed/", "cat": "seo"},
{"name": "Ahrefs Blog", "url": "https://ahrefs.com/blog/feed/", "cat": "seo"},
{"name": "Semrush Blog", "url": "https://www.semrush.com/blog/feed/", "cat": "seo"},
{"name": "Moz Blog", "url": "https://moz.com/posts/rss/blog", "cat": "seo"},
{"name": "Stripe Blog", "url": "https://stripe.com/blog/feed.rss", "cat": "payment"},
{"name": "Stripe PHP Releases", "url": "https://github.com/stripe/stripe-php/releases.atom", "cat": "payment"},
{"name": "LINE Bot SDK Releases", "url": "https://github.com/line/line-bot-sdk-php/releases.atom", "cat": "payment"},
{"name": "台灣第三方支付(GNews)", "url": "https://news.google.com/rss/search?q=%E7%B6%A0%E7%95%8C+OR+%E8%97%8D%E6%96%B0+OR+%E7%AC%AC%E4%B8%89%E6%96%B9%E6%94%AF%E4%BB%98&hl=zh-TW&gl=TW&ceid=TW:zh-Hant", "cat": "payment"},
{"name": "LINE Pay(GNews)", "url": "https://news.google.com/rss/search?q=LINE+Pay+OR+LINE+%E9%8C%A2%E5%8C%85&hl=zh-TW&gl=TW&ceid=TW:zh-Hant", "cat": "payment"},
{"name": "OpenAI News", "url": "https://openai.com/news/rss.xml", "cat": "ai"},
{"name": "Anthropic(GNews)", "url": "https://news.google.com/rss/search?q=Anthropic+Claude&hl=en-US&gl=US&ceid=US:en", "cat": "ai"},
{"name": "xAI Grok(GNews)", "url": "https://news.google.com/rss/search?q=xAI+Grok&hl=en-US&gl=US&ceid=US:en", "cat": "ai"},
{"name": "Claude Code Releases", "url": "https://github.com/anthropics/claude-code/releases.atom", "cat": "ai"},
{"name": "Shopify Dev Changelog", "url": "https://shopify.dev/changelog/feed.xml", "cat": "shopify"},
{"name": "Shopify Changelog", "url": "https://changelog.shopify.com/feed", "cat": "shopify"},
{"name": "Cloudflare Blog", "url": "https://blog.cloudflare.com/rss/", "cat": "stack"},
{"name": "Vercel", "url": "https://vercel.com/atom", "cat": "stack"},
{"name": "Tailwind CSS Blog", "url": "https://tailwindcss.com/feeds/feed.xml", "cat": "stack"},
{"name": "Tailwind CSS Releases", "url": "https://github.com/tailwindlabs/tailwindcss/releases.atom", "cat": "stack"},
{"name": "WooCommerce Releases", "url": "https://github.com/woocommerce/woocommerce/releases.atom", "cat": "release"},
{"name": "WordPress Releases", "url": "https://github.com/WordPress/WordPress/releases.atom", "cat": "release"},
{"name": "WP-CLI Releases", "url": "https://github.com/wp-cli/wp-cli/releases.atom", "cat": "release"},
{"name": "Medusa Releases", "url": "https://github.com/medusajs/medusa/releases.atom", "cat": "release"},
{"name": "Saleor Releases", "url": "https://github.com/saleor/saleor/releases.atom", "cat": "release"}
]
"""Render digest.json into the cloudwp Mailchimp-style HTML and send via Mailgun.
Usage: MAILGUN_KEY=<key> python3 mail.py [digest.json]
SMTP is unusable from the Claude cloud sandbox (its egress proxy rejects raw TCP
on non-443 ports), so this goes over Mailgun's REST API on 443. The template
mirrors .doc/cloudwp/mailchimp/email-template-announcement.html.
"""
import base64
import html
import json
import os
import sys
import urllib.request
from pathlib import Path
DOMAIN = "cloudwp.pro"
TO = "nel@tseng.cc"
COLORS = {"🔴": "#ef4444", "🟠": "#f97316", "🟡": "#eab308", "🟢": "#22c55e", "🔵": "#3b82f6", "🟣": "#a855f7"}
def render_item(item):
time = (
f'<p style="font-size: 13px; color: #64748b; margin: 5px 0 0 0;">{html.escape(item["time"])}</p>'
if item.get("time") else ""
)
return f"""
<div style="margin: 0 0 24px 0; padding: 0 0 24px 0; border-bottom: 1px solid #e2e8f0;">
<a href="{html.escape(item['url'])}" style="font-size: 17px; font-weight: 600; color: #0f172a; line-height: 1.45; text-decoration: none;">{html.escape(item['title'])}</a>
{time}
<p style="font-size: 15px; line-height: 1.75; color: #1e293b; margin: 10px 0 12px 0;">{html.escape(item['fact'])}</p>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color: #eef2f7; border-radius: 6px;">
<tr>
<td style="padding: 15px 0 14px 16px; vertical-align: top; width: 34px;">
<img src="https://raw.githubusercontent.com/neltseng/hz-assets/main/info.png" width="18" height="18" alt="" style="display: block;">
</td>
<td style="padding: 14px 16px 14px 0; vertical-align: top;">
<p style="font-size: 15px; line-height: 1.65; color: #1e293b; margin: 0;">{html.escape(item['action'])}</p>
</td>
</tr>
</table>
</div>"""
def render_section(sec):
color = COLORS.get(sec["emoji"], "#94a3b8")
items = "".join(render_item(i) for i in sec["items"])
return f"""
<table role="presentation" cellpadding="0" cellspacing="0" style="margin: 36px 0 18px 0;">
<tr><td style="background-color: {color}; border-radius: 4px; padding: 5px 12px;">
<span style="font-size: 14px; color: #ffffff; font-weight: 600; letter-spacing: 0.02em;">{html.escape(sec['title'])}</span>
</td></tr>
</table>
{items}"""
def render_filtered(filtered):
if not filtered:
return ""
rows = "".join(
f'<li style="font-size: 15px; line-height: 1.7; color: #334155; margin-bottom: 6px;">'
f'{html.escape(f["title"])} <span style="color: #64748b;">— {html.escape(f["src"])}</span></li>'
for f in filtered
)
return f"""
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin: 40px 0 0 0; border-top: 1px solid #e2e8f0;">
<tr><td style="padding: 24px 0 0 0;">
<p style="font-size: 16px; font-weight: 600; color: #0f172a; margin: 0 0 14px 0;">已過濾({len(filtered)} 則)</p>
<ul style="margin: 0; padding-left: 20px;">{rows}</ul>
</td></tr>
</table>"""
def render(d):
s = d["stats"]
sections = "".join(render_section(x) for x in d["sections"])
note = (
f'<p style="font-size: 14px; line-height: 1.7; color: #334155; margin: 28px 0 0 0; '
f'padding: 14px 16px; background-color: #fffbeb; border-left: 3px solid #eab308;">{html.escape(d["note"])}</p>'
if d.get("note") else ""
)
return f"""<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="zh-Hant">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>黑閃日報 {d['date']}</title>
<style type="text/css">
body, table, td {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; }}
body {{ margin: 0; padding: 0; background-color: #f4f5f7; -webkit-text-size-adjust: 100%; }}
table {{ border-collapse: collapse; }}
img {{ border: 0; outline: none; text-decoration: none; }}
a {{ color: #2962FF; text-decoration: none; }}
a:hover {{ text-decoration: underline; }}
@media only screen and (max-width: 600px) {{
.container {{ width: 100% !important; }}
.content {{ padding: 24px 20px !important; }}
.header {{ padding: 20px !important; }}
.footer-content {{ padding: 20px !important; }}
}}
</style>
</head>
<body>
<div style="display:none;font-size:1px;color:#f4f5f7;line-height:1px;max-height:0;max-width:0;opacity:0;overflow:hidden;">
抓 {s['fetched']} 則,保留 {s['kept']} 則
</div>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color: #f4f5f7;">
<tr>
<td align="center" style="padding: 32px 16px;">
<table role="presentation" class="container" width="100%" cellpadding="0" cellspacing="0" style="max-width: 600px; background-color: #ffffff;">
<tr>
<td class="header" style="padding: 28px 35px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
<tr>
<td style="text-align: left;">
<img src="https://cloudwp.pro/assets/images/logo-dark.svg" alt="cloudwp" width="120" style="display: inline-block; vertical-align: middle;">
</td>
<td style="text-align: right;">
<span style="font-size: 13px; color: #94a3b8;">{d['date']}</span>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td class="content" style="padding: 0 35px 40px;">
<h1 style="font-size: 24px; line-height: 1.4; color: #0f172a; margin: 0 0 6px 0; font-weight: 700;">黑閃日報</h1>
<p style="font-size: 13px; color: #64748b; margin: 0;">抓 {s['fetched']} 則 保留 {s['kept']} 則 過濾 {s['filtered']} 則</p>
{sections}
{render_filtered(d.get('filtered', []))}
{note}
</td>
</tr>
<tr>
<td style="border-top: 1px solid #e2e8f0; background-color: #f8fafc;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
<tr>
<td class="footer-content" style="padding: 28px 35px; text-align: center;">
<table role="presentation" cellpadding="0" cellspacing="0" style="margin: 0 auto;">
<tr>
<td style="vertical-align: middle; padding-right: 12px;">
<a href="https://woocommerce.com/development-services/cloudwp/250740886/">
<img src="https://cloudwp.pro/assets/images/badge/woo-partner-badge-light.png" alt="WooCommerce Agency Partner" height="28" style="display: block;">
</a>
</td>
<td style="vertical-align: middle; padding-right: 12px;">
<a href="https://www.facebook.com/CloudWP">
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="#94a3b8"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>
</a>
</td>
<td style="vertical-align: middle;">
<a href="https://github.com/orgs/LazyProApp/repositories">
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="#94a3b8"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>
</a>
</td>
</tr>
</table>
<p style="font-size: 13px; line-height: 1.8; color: #94a3b8; margin: 16px 0 0 0;">
cloudwp &copy; 2026 黑閃實驗室 | 統編 93067949<br>
台灣 WooCommerce 商家的技術夥伴
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>"""
def to_text(d):
lines = [f"黑閃日報 {d['date']}", ""]
for sec in d["sections"]:
lines.append(sec["title"])
for i in sec["items"]:
lines += ["", i["title"], i.get("time", ""), i["fact"], f"→ {i['action']}", i["url"]]
lines.append("")
if d.get("filtered"):
lines.append(f"已過濾({len(d['filtered'])} 則)")
lines += [f"{f['title']}({f['src']})" for f in d["filtered"]]
return "\n".join(lines)
def multipart(fields, inline):
"""Build a multipart/form-data body. `inline` is a list of (filename, bytes) sent as cid: images."""
boundary = "----hz" + base64.urlsafe_b64encode(os.urandom(12)).decode().strip("=")
out = bytearray()
for name, value in fields.items():
out += f'--{boundary}\r\nContent-Disposition: form-data; name="{name}"\r\n\r\n'.encode()
out += value.encode() + b"\r\n"
for filename, blob in inline:
out += f'--{boundary}\r\nContent-Disposition: form-data; name="inline"; filename="{filename}"\r\n'.encode()
out += b"Content-Type: image/png\r\n\r\n" + blob + b"\r\n"
out += f"--{boundary}--\r\n".encode()
return bytes(out), f"multipart/form-data; boundary={boundary}"
def main():
key = os.environ.get("MAILGUN_KEY")
if not key:
sys.exit("MAILGUN_KEY 未設定")
here = Path(__file__).parent
path = sys.argv[1] if len(sys.argv) > 1 else str(here / "digest.json")
d = json.loads(Path(path).read_text(encoding="utf-8"))
icon = here / "sparkles.png"
body, content_type = multipart(
{
"from": f"黑閃日報 <noreply@{DOMAIN}>",
"to": TO,
"subject": f"黑閃日報 {d['date']}",
"text": to_text(d),
"html": render(d),
},
[("sparkles.png", icon.read_bytes())] if icon.exists() else [],
)
req = urllib.request.Request(f"https://api.mailgun.net/v3/{DOMAIN}/messages", data=body)
req.add_header("Content-Type", content_type)
req.add_header("Authorization", "Basic " + base64.b64encode(f"api:{key}".encode()).decode())
with urllib.request.urlopen(req, timeout=30) as resp:
print(resp.read().decode())
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment