|
1 | 1 | import { Temporal } from "@js-temporal/polyfill" |
2 | 2 | import { Resend } from "resend" |
3 | | -import Newsletter from "./newsletter" |
| 3 | +import Newsletter, { NewsletterProps } from "./newsletter" |
4 | 4 | import * as React from "react" |
| 5 | +import * as fs from "fs" |
| 6 | +import * as path from "path" |
| 7 | +import matter from "gray-matter" |
5 | 8 |
|
6 | | -const resend = new Resend(process.env.RESEND_API_KEY) |
7 | | -const sender = "Tomy Hsieh <newsletter@tomy.me>" |
| 9 | +const SENDER = "Tomy Hsieh <newsletter@tomy.me>" |
| 10 | +const JSON_INDEX = "public/zh-tw/index.json" |
| 11 | +const POST_SECTION = "寫作" |
| 12 | +const POST_DIR = "content/posts" |
| 13 | +const POST_MARKDOWN = "index.zh-tw.md" |
8 | 14 |
|
9 | | -const title = "2025 年 5 月報報" |
10 | | -const subtitle = "The moment that defines the ones who do." |
11 | | -const summary = |
12 | | - "發現寫部落格有一個有趣現象:這個月沒什麼大事,會因為沒有題材,所以不好寫。但反過來,太多行程,太多東西可以寫,也會很頭疼。另一方面,太多事要做的時候,要抽出時間和心情來寫,其實也不是一件容易的事。" |
13 | | -const postUrl = "https://blog.tomy.me/zh-tw/posts/2025-may-dump/" |
14 | | -const imageUrl = |
15 | | - "https://blog.tomy.me/zh-tw/posts/2025-may-dump/cmu-commencement-with-taiwanese_hu_8681a2eabc9f99f8.webp" |
16 | | -const imageAlt = |
17 | | - "一排畢業生在現代建築外牆前排隊合照,穿著學士袍與紅黃飾帶,氣氛歡樂。" |
| 15 | +async function main() { |
| 16 | + // Get the latest blog post |
| 17 | + const rootDir = path.dirname(__dirname) |
| 18 | + const post = getLatestPost(rootDir) |
| 19 | + console.info("Sending post", post) |
18 | 20 |
|
19 | | -async function sendEmail() { |
| 21 | + // Create Resend client |
| 22 | + const RESEND_API_KEY = process.env.RESEND_API_KEY |
| 23 | + if (!RESEND_API_KEY) { |
| 24 | + throw new Error("RESEND_API_KEY not defined") |
| 25 | + } |
| 26 | + const resend = new Resend(RESEND_API_KEY) |
| 27 | + |
| 28 | + // Send a preview email |
| 29 | + await sendEmail(post, resend, SENDER, "Tomy Hsieh <tomy0000000@gmail.com>") |
| 30 | + |
| 31 | + // Schedule official email |
| 32 | + const RESEND_AUDIENCE_ID = process.env.RESEND_AUDIENCE_ID |
| 33 | + if (!RESEND_AUDIENCE_ID) { |
| 34 | + throw new Error("RESEND_AUDIENCE_ID not defined") |
| 35 | + } |
| 36 | + await sendEmailToAudience(post, resend, SENDER, RESEND_AUDIENCE_ID as string) |
| 37 | +} |
| 38 | + |
| 39 | +function getLatestPost(rootPath: string): NewsletterProps { |
| 40 | + // Read index from json |
| 41 | + const indexPath = path.join(rootPath, JSON_INDEX) |
| 42 | + const indexJson = JSON.parse(fs.readFileSync(indexPath, "utf-8")) |
| 43 | + |
| 44 | + // Find the latest post slug |
| 45 | + const latestPost = indexJson.find( |
| 46 | + (item: { section: string }) => item.section === POST_SECTION |
| 47 | + ) |
| 48 | + if (!latestPost) { |
| 49 | + throw new Error(`No post found in section ${POST_SECTION}`) |
| 50 | + } |
| 51 | + const BLOG_HOST = process.env.BLOG_HOST |
| 52 | + if (!BLOG_HOST) { |
| 53 | + throw new Error("BLOG_HOST not defined") |
| 54 | + } |
| 55 | + const permalink = new URL(latestPost.permalink, BLOG_HOST) |
| 56 | + const slug = permalink.pathname.split("/").filter(Boolean).pop() |
| 57 | + if (!slug) { |
| 58 | + throw new Error("Could not extract slug from permalink") |
| 59 | + } |
| 60 | + |
| 61 | + // Parse post markdown |
| 62 | + const markdownPath = path.join(rootPath, POST_DIR, slug, POST_MARKDOWN) |
| 63 | + const fileContent = fs.readFileSync(markdownPath, "utf-8") |
| 64 | + const { data, content } = matter(fileContent) |
| 65 | + const summary = content.slice( |
| 66 | + content.indexOf("\n") + 1, |
| 67 | + content.indexOf("\n", content.indexOf("\n") + 1) |
| 68 | + ) |
| 69 | + |
| 70 | + return { |
| 71 | + title: data.title, |
| 72 | + subtitle: data.description, |
| 73 | + summary, |
| 74 | + postUrl: permalink.toString(), |
| 75 | + imageUrl: buildImageURL(slug, data.feature).toString(), |
| 76 | + imageAlt: data.featureAlt, |
| 77 | + } |
| 78 | +} |
| 79 | + |
| 80 | +function buildImageURL(postSlug: string, imageSlug: string): URL { |
| 81 | + const baseUrl = process.env.IMG_HOST |
| 82 | + if (!baseUrl) { |
| 83 | + throw new Error("IMG_HOST not defined") |
| 84 | + } |
| 85 | + return new URL(`${postSlug}/${imageSlug}`, baseUrl) |
| 86 | +} |
| 87 | + |
| 88 | +async function sendEmail( |
| 89 | + post: NewsletterProps, |
| 90 | + resend: Resend, |
| 91 | + sender: string, |
| 92 | + testingEmail: string |
| 93 | +) { |
| 94 | + const createResponse = await resend.emails.send({ |
| 95 | + from: sender, |
| 96 | + to: testingEmail, |
| 97 | + subject: `Tomy's Blog - ${post.title}`, |
| 98 | + react: ( |
| 99 | + <Newsletter |
| 100 | + title={post.title} |
| 101 | + subtitle={post.subtitle} |
| 102 | + summary={post.summary} |
| 103 | + postUrl={post.postUrl} |
| 104 | + imageUrl={post.imageUrl} |
| 105 | + imageAlt={post.imageAlt} |
| 106 | + /> |
| 107 | + ), |
| 108 | + }) |
| 109 | + if (createResponse.error !== null || createResponse.data === null) { |
| 110 | + console.error(`Failed to send email: ${createResponse.error?.message}`) |
| 111 | + return |
| 112 | + } |
| 113 | + const sendId = createResponse.data.id |
| 114 | + console.log(`Preview Email sent: ${sendId}`) |
| 115 | +} |
| 116 | + |
| 117 | +async function sendEmailToAudience( |
| 118 | + post: NewsletterProps, |
| 119 | + resend: Resend, |
| 120 | + sender: string, |
| 121 | + audienceId: string |
| 122 | +) { |
20 | 123 | // Create broadcast |
21 | 124 | const createResponse = await resend.broadcasts.create({ |
22 | | - name: title, |
23 | | - audienceId: process.env.RESEND_AUDIENCE_ID ?? "", |
| 125 | + name: post.title, |
| 126 | + audienceId, |
24 | 127 | from: sender, |
25 | | - subject: `Tomy's Blog - ${title}`, |
| 128 | + subject: `Tomy's Blog - ${post.title}`, |
26 | 129 | react: ( |
27 | 130 | <Newsletter |
28 | | - title={title} |
29 | | - subtitle={subtitle} |
30 | | - summary={summary} |
31 | | - postUrl={postUrl} |
32 | | - imageUrl={imageUrl} |
33 | | - imageAlt={imageAlt} |
| 131 | + title={post.title} |
| 132 | + subtitle={post.subtitle} |
| 133 | + summary={post.summary} |
| 134 | + postUrl={post.postUrl} |
| 135 | + imageUrl={post.imageUrl} |
| 136 | + imageAlt={post.imageAlt} |
34 | 137 | /> |
35 | 138 | ), |
36 | 139 | }) |
@@ -61,4 +164,4 @@ async function sendEmail() { |
61 | 164 | console.log(`Send is scheduled at ${localSendTime.toString()}: ${sendId}`) |
62 | 165 | } |
63 | 166 |
|
64 | | -sendEmail() |
| 167 | +main() |
0 commit comments