Last active
July 11, 2026 19:00
-
-
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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| [ | |
| {"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"} | |
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """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 © 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