@notionx/core 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/index.d.ts +1 -1
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/rate-limit.js.map +1 -1
- package/dist/auth/routes/google-callback.js.map +1 -1
- package/dist/auth/routes/google.js.map +1 -1
- package/dist/auth/routes/index.js.map +1 -1
- package/dist/auth/routes/verify-email.js.map +1 -1
- package/dist/auth/routes/viewer.js.map +1 -1
- package/dist/auth/turnstile.js.map +1 -1
- package/dist/auth/user-session.d.ts +1 -1
- package/dist/auth/user-session.js.map +1 -1
- package/dist/auth/users.js.map +1 -1
- package/dist/content/index.d.ts +2 -2
- package/dist/content/index.js +8 -60
- package/dist/content/index.js.map +1 -1
- package/dist/content/revalidate.d.ts +2 -1
- package/dist/content/revalidate.js +5 -28
- package/dist/content/revalidate.js.map +1 -1
- package/dist/content/search-index.d.ts +1 -1
- package/dist/content/search-index.js.map +1 -1
- package/dist/content/search.d.ts +2 -5
- package/dist/content/search.js +3 -32
- package/dist/content/search.js.map +1 -1
- package/dist/email/index.js.map +1 -1
- package/dist/{env-C5qu-0R-.d.ts → env-hoez1e-n.d.ts} +0 -4
- package/dist/i18n/index.d.ts +18 -24
- package/dist/i18n/index.js +29 -54
- package/dist/i18n/index.js.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/admin/index.js.map +1 -1
- package/dist/media/index.js +3 -2
- package/dist/media/index.js.map +1 -1
- package/dist/media/routes/index.js +0 -1
- package/dist/media/routes/index.js.map +1 -1
- package/dist/media/routes/notion-media.js +0 -1
- package/dist/media/routes/notion-media.js.map +1 -1
- package/dist/notion/config.d.ts +1 -4
- package/dist/notion/config.js +1 -23
- package/dist/notion/config.js.map +1 -1
- package/dist/notion/content-cache.d.ts +1 -1
- package/dist/notion/generic-source.js +0 -1
- package/dist/notion/generic-source.js.map +1 -1
- package/dist/notion/index.d.ts +3 -3
- package/dist/notion/index.js +1 -23
- package/dist/notion/index.js.map +1 -1
- package/dist/notion/media.d.ts +1 -1
- package/dist/notion/media.js +1 -1
- package/dist/notion/media.js.map +1 -1
- package/dist/notion/routes/index.d.ts +1 -1
- package/dist/notion/routes/index.js +0 -1
- package/dist/notion/routes/index.js.map +1 -1
- package/dist/notion/routes/webhook.d.ts +1 -1
- package/dist/notion/routes/webhook.js +0 -1
- package/dist/notion/routes/webhook.js.map +1 -1
- package/dist/notion/types.d.ts +1 -73
- package/dist/notion/webhook.d.ts +1 -1
- package/dist/notion/webhook.js +0 -1
- package/dist/notion/webhook.js.map +1 -1
- package/dist/pages/index.js +0 -1
- package/dist/pages/index.js.map +1 -1
- package/dist/platform/current.d.ts +1 -1
- package/dist/platform/current.js.map +1 -1
- package/dist/platform/index.d.ts +1 -1
- package/dist/platform/index.js.map +1 -1
- package/dist/platform/runtime.d.ts +1 -1
- package/dist/storage/index.js.map +1 -1
- package/dist/storage/routes/cdn.js.map +1 -1
- package/dist/storage/routes/files.js.map +1 -1
- package/dist/storage/routes/index.js.map +1 -1
- package/dist/util/index.d.ts +1 -1
- package/dist/util/index.js +1 -2
- package/dist/util/index.js.map +1 -1
- package/dist/worker/index.js +0 -1
- package/dist/worker/index.js.map +1 -1
- package/dist/worker/routes/content-revalidate.d.ts +1 -1
- package/dist/worker/routes/health.js.map +1 -1
- package/dist/worker/routes/index.d.ts +1 -1
- package/dist/worker/routes/index.js.map +1 -1
- package/package.json +1 -1
package/dist/email/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/email/resend.ts","../../src/util/env.ts"],"sourcesContent":["// Email delivery via the Resend HTTP API.\n//\n// Cloudflare Workers cannot use SMTP (no long-lived connections), so\n// email is sent through the Resend HTTP API. The free Resend tier\n// covers 3 000 messages per month.\n\nimport { Resend } from \"resend\";\nimport { workerEnv } from \"../util/env\";\n\nlet resendClient: Resend | null = null;\n\nfunction getResend(): Resend | null {\n if (resendClient) return resendClient;\n const env = workerEnv;\n if (!env.RESEND_API_KEY) return null;\n resendClient = new Resend(env.RESEND_API_KEY);\n return resendClient;\n}\n\ntype SendArgs = {\n to: string | string[];\n subject: string;\n html: string;\n text?: string;\n replyTo?: string;\n};\n\n/**\n * Send an email. When RESEND_API_KEY is not set (the dev default), the\n * call is silently skipped with a log line instead of failing. Returns\n * the Resend message id, or null when no-op. Throws on a Resend error.\n */\nexport async function sendEmail(args: SendArgs): Promise<string | null> {\n const env = workerEnv;\n const resend = getResend();\n\n // Dev mode without a key: skip silently.\n if (!resend) {\n console.log(\"[email:noop] to=%s subject=%s\", args.to, args.subject);\n return null;\n }\n\n const from = env.RESEND_FROM || \"Blog <noreply@resend.dev>\";\n\n const { data, error } = await resend.emails.send({\n from,\n to: args.to,\n subject: args.subject,\n html: args.html,\n text: args.text,\n replyTo: args.replyTo,\n });\n\n if (error) {\n throw new Error(`Resend error: ${error.message}`);\n }\n return data?.id ?? null;\n}\n\n/** Minimal HTML escape (avoids depending on a runtime sanitizer). */\nfunction esc(s: string): string {\n return s\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\n}\n\n/** Welcome email for new newsletter subscribers. */\nexport function welcomeEmailHtml(opts: {\n email: string;\n unsubscribeUrl: string;\n siteUrl: string;\n}): string {\n return `<!DOCTYPE html>\n<html>\n<head><meta charset=\"utf-8\"><title>Welcome</title></head>\n<body style=\"margin:0;padding:0;background:#fafafa;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;color:#171717;\">\n <div style=\"max-width:560px;margin:0 auto;padding:40px 20px;\">\n <h1 style=\"font-size:24px;font-weight:600;margin:0 0 16px;\">Welcome aboard!</h1>\n <p style=\"font-size:16px;line-height:24px;color:#404040;margin:0 0 24px;\">\n Hi <strong>${esc(opts.email)}</strong>,感谢订阅 <a href=\"${esc(opts.siteUrl)}\">vinext Blog</a>。\n 新文章发布时会第一时间通知你。\n </p>\n <hr style=\"border:0;border-top:1px solid #e5e5e5;margin:32px 0;\" />\n <p style=\"font-size:12px;color:#737373;margin:0;\">\n 不想再收?<a href=\"${esc(opts.unsubscribeUrl)}\" style=\"color:#737373;\">退订</a>\n </p>\n </div>\n</body>\n</html>`;\n}\n\nexport function resetPasswordHtml(opts: {\n resetUrl: string;\n siteUrl: string;\n email: string;\n}): string {\n return `<!DOCTYPE html>\n<html>\n<body style=\"margin:0;padding:0;background:#fafafa;font-family:-apple-system,BlinkMacSystemFont,sans-serif;color:#171717;\">\n <div style=\"max-width:560px;margin:0 auto;padding:40px 20px;\">\n <p style=\"font-size:13px;color:#737373;margin:0 0 8px;text-transform:uppercase;letter-spacing:0.05em;\">Reset password</p>\n <h1 style=\"font-size:24px;font-weight:600;margin:0 0 12px;\">Reset your password</h1>\n <p style=\"font-size:16px;line-height:24px;color:#404040;margin:0 0 24px;\">\n Hi <strong>${esc(opts.email)}</strong>,我们收到了重置 <a href=\"${esc(opts.siteUrl)}\">vinext Blog</a> 账户密码的请求。\n 点击下方按钮设置新密码。链接 1 小时内有效。\n </p>\n <a href=\"${esc(opts.resetUrl)}\" style=\"display:inline-block;background:#171717;color:#fafafa;padding:10px 20px;border-radius:6px;text-decoration:none;font-size:14px;\">Reset password</a>\n <p style=\"font-size:12px;color:#737373;margin:24px 0 0;\">\n 如果这不是你的操作,请忽略此邮件。如果按钮无法打开,请复制链接到浏览器:<br />\n <a href=\"${esc(opts.resetUrl)}\" style=\"color:#737373;word-break:break-all;\">${esc(opts.resetUrl)}</a>\n </p>\n </div>\n</body>\n</html>`;\n}\n\nexport function verifyEmailHtml(opts: {\n verifyUrl: string;\n siteUrl: string;\n email: string;\n}): string {\n return `<!DOCTYPE html>\n<html>\n<body style=\"margin:0;padding:0;background:#fafafa;font-family:-apple-system,BlinkMacSystemFont,sans-serif;color:#171717;\">\n <div style=\"max-width:560px;margin:0 auto;padding:40px 20px;\">\n <p style=\"font-size:13px;color:#737373;margin:0 0 8px;text-transform:uppercase;letter-spacing:0.05em;\">Verify email</p>\n <h1 style=\"font-size:24px;font-weight:600;margin:0 0 12px;\">Confirm your email</h1>\n <p style=\"font-size:16px;line-height:24px;color:#404040;margin:0 0 24px;\">\n Hi <strong>${esc(opts.email)}</strong>,欢迎注册 <a href=\"${esc(opts.siteUrl)}\">vinext Blog</a>。\n 请点击下面的按钮完成邮箱验证,验证后即可登录后台。\n </p>\n <a href=\"${esc(opts.verifyUrl)}\" style=\"display:inline-block;background:#171717;color:#fafafa;padding:10px 20px;border-radius:6px;text-decoration:none;font-size:14px;\">Verify email</a>\n <p style=\"font-size:12px;color:#737373;margin:24px 0 0;\">\n 如果按钮无法打开,请复制这个链接到浏览器:<br />\n <a href=\"${esc(opts.verifyUrl)}\" style=\"color:#737373;word-break:break-all;\">${esc(opts.verifyUrl)}</a>\n </p>\n </div>\n</body>\n</html>`;\n}\n\n/** New post notification email. */\nexport function newPostEmailHtml(opts: {\n title: string;\n description: string;\n url: string;\n unsubscribeUrl: string;\n}): string {\n return `<!DOCTYPE html>\n<html>\n<body style=\"margin:0;padding:0;background:#fafafa;font-family:-apple-system,BlinkMacSystemFont,sans-serif;color:#171717;\">\n <div style=\"max-width:560px;margin:0 auto;padding:40px 20px;\">\n <p style=\"font-size:13px;color:#737373;margin:0 0 8px;text-transform:uppercase;letter-spacing:0.05em;\">New post</p>\n <h1 style=\"font-size:24px;font-weight:600;margin:0 0 16px;\">\n <a href=\"${esc(opts.url)}\" style=\"color:#171717;text-decoration:none;\">${esc(opts.title)}</a>\n </h1>\n <p style=\"font-size:16px;line-height:24px;color:#404040;margin:0 0 24px;\">${esc(opts.description)}</p>\n <a href=\"${esc(opts.url)}\" style=\"display:inline-block;background:#171717;color:#fafafa;padding:10px 20px;border-radius:6px;text-decoration:none;font-size:14px;\">Read the post →</a>\n <hr style=\"border:0;border-top:1px solid #e5e5e5;margin:40px 0 16px;\" />\n <p style=\"font-size:12px;color:#737373;margin:0;\">\n 不想再收新文章通知?<a href=\"${esc(opts.unsubscribeUrl)}\" style=\"color:#737373;\">退订</a>\n </p>\n </div>\n</body>\n</html>`;\n}\n","// lib/env.ts - 集中获取 Cloudflare bindings\n// 用 cloudflare:workers 模块(workerd 内置),作为平台 adapter 的绑定入口\n\n/// <reference types=\"@cloudflare/workers-types\" />\nimport { env } from \"cloudflare:workers\";\n\nexport type AppEnv = {\n DB: D1Database;\n ASSETS: Fetcher;\n IMAGES: ImagesBinding;\n ASSETS_BUCKET?: R2Bucket;\n CONTENT_CACHE?: KVNamespace;\n ADMIN_PASSWORD: string;\n ADMIN_EMAIL?: string;\n SITE_URL?: string;\n RESEND_API_KEY?: string;\n RESEND_FROM?: string;\n // Google OAuth 仍然兼容 Cloudflare Secret 作为兜底。\n // 实际生效值以 app_settings.google_client_id / google_client_secret 为准。\n GOOGLE_CLIENT_ID?: string;\n GOOGLE_CLIENT_SECRET?: string;\n /** Turnstile site key fallback when not stored in app_settings */\n TURNSTILE_SITE_KEY?: string;\n /** Turnstile secret — set via `wrangler secret put TURNSTILE_SECRET_KEY` */\n TURNSTILE_SECRET_KEY?: string;\n /** Notion integration token for the blog data source */\n NOTION_TOKEN?: string;\n /** Notion data source ID used by dataSources.query */\n NOTION_DATA_SOURCE_ID?: string;\n /** Notion data source ID for the public movie catalog */\n NOTION_MOVIES_DATA_SOURCE_ID?: string;\n /** Notion data source ID for localized movie copy */\n NOTION_MOVIE_TRANSLATIONS_DATA_SOURCE_ID?: string;\n /** Optional Notion API base URL for tests or proxies */\n NOTION_API_BASE_URL?: string;\n /** Optional Notion edit URL for admin handoff screens */\n NOTION_EDIT_BASE_URL?: string;\n /** Optional webhook verification token for Notion invalidation */\n NOTION_WEBHOOK_VERIFICATION_TOKEN?: string;\n};\n\n// 强制类型:vinext 把 env 类型放在 env.d.ts(interface VinextEnv extends Env),\n// 但 TS server 经常解析不到。运行时一定有 DB,类型断言保证编译通过。\nexport const workerEnv = env as unknown as AppEnv;\n"],"mappings":";AAMA,SAAS,cAAc;;;ACFvB,SAAS,WAAW;AAuCb,IAAM,YAAY;;;ADlCzB,IAAI,eAA8B;AAElC,SAAS,YAA2B;AAClC,MAAI,aAAc,QAAO;AACzB,QAAMA,OAAM;AACZ,MAAI,CAACA,KAAI,eAAgB,QAAO;AAChC,iBAAe,IAAI,OAAOA,KAAI,cAAc;AAC5C,SAAO;AACT;AAeA,eAAsB,UAAU,MAAwC;AACtE,QAAMA,OAAM;AACZ,QAAM,SAAS,UAAU;AAGzB,MAAI,CAAC,QAAQ;AACX,YAAQ,IAAI,iCAAiC,KAAK,IAAI,KAAK,OAAO;AAClE,WAAO;AAAA,EACT;AAEA,QAAM,OAAOA,KAAI,eAAe;AAEhC,QAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAAO,OAAO,KAAK;AAAA,IAC/C;AAAA,IACA,IAAI,KAAK;AAAA,IACT,SAAS,KAAK;AAAA,IACd,MAAM,KAAK;AAAA,IACX,MAAM,KAAK;AAAA,IACX,SAAS,KAAK;AAAA,EAChB,CAAC;AAED,MAAI,OAAO;AACT,UAAM,IAAI,MAAM,iBAAiB,MAAM,OAAO,EAAE;AAAA,EAClD;AACA,SAAO,MAAM,MAAM;AACrB;AAGA,SAAS,IAAI,GAAmB;AAC9B,SAAO,EACJ,QAAQ,MAAM,OAAO,EACrB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,QAAQ,EACtB,QAAQ,MAAM,OAAO;AAC1B;AAGO,SAAS,iBAAiB,MAItB;AACT,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAOU,IAAI,KAAK,KAAK,CAAC,oDAA2B,IAAI,KAAK,OAAO,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA,+CAKxD,IAAI,KAAK,cAAc,CAAC;AAAA;AAAA;AAAA;AAAA;AAK9C;AAEO,SAAS,kBAAkB,MAIvB;AACT,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAOU,IAAI,KAAK,KAAK,CAAC,sEAA8B,IAAI,KAAK,OAAO,CAAC;AAAA;AAAA;AAAA,eAGlE,IAAI,KAAK,QAAQ,CAAC;AAAA;AAAA;AAAA,iBAGhB,IAAI,KAAK,QAAQ,CAAC,iDAAiD,IAAI,KAAK,QAAQ,CAAC;AAAA;AAAA;AAAA;AAAA;AAKtG;AAEO,SAAS,gBAAgB,MAIrB;AACT,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAOU,IAAI,KAAK,KAAK,CAAC,oDAA2B,IAAI,KAAK,OAAO,CAAC;AAAA;AAAA;AAAA,eAG/D,IAAI,KAAK,SAAS,CAAC;AAAA;AAAA;AAAA,iBAGjB,IAAI,KAAK,SAAS,CAAC,iDAAiD,IAAI,KAAK,SAAS,CAAC;AAAA;AAAA;AAAA;AAAA;AAKxG;AAGO,SAAS,iBAAiB,MAKtB;AACT,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,iBAMQ,IAAI,KAAK,GAAG,CAAC,iDAAiD,IAAI,KAAK,KAAK,CAAC;AAAA;AAAA,gFAEd,IAAI,KAAK,WAAW,CAAC;AAAA,eACtF,IAAI,KAAK,GAAG,CAAC;AAAA;AAAA;AAAA,6EAGD,IAAI,KAAK,cAAc,CAAC;AAAA;AAAA;AAAA;AAAA;AAKnD;","names":["env"]}
|
|
1
|
+
{"version":3,"sources":["../../src/email/resend.ts","../../src/util/env.ts"],"sourcesContent":["// Email delivery via the Resend HTTP API.\n//\n// Cloudflare Workers cannot use SMTP (no long-lived connections), so\n// email is sent through the Resend HTTP API. The free Resend tier\n// covers 3 000 messages per month.\n\nimport { Resend } from \"resend\";\nimport { workerEnv } from \"../util/env\";\n\nlet resendClient: Resend | null = null;\n\nfunction getResend(): Resend | null {\n if (resendClient) return resendClient;\n const env = workerEnv;\n if (!env.RESEND_API_KEY) return null;\n resendClient = new Resend(env.RESEND_API_KEY);\n return resendClient;\n}\n\ntype SendArgs = {\n to: string | string[];\n subject: string;\n html: string;\n text?: string;\n replyTo?: string;\n};\n\n/**\n * Send an email. When RESEND_API_KEY is not set (the dev default), the\n * call is silently skipped with a log line instead of failing. Returns\n * the Resend message id, or null when no-op. Throws on a Resend error.\n */\nexport async function sendEmail(args: SendArgs): Promise<string | null> {\n const env = workerEnv;\n const resend = getResend();\n\n // Dev mode without a key: skip silently.\n if (!resend) {\n console.log(\"[email:noop] to=%s subject=%s\", args.to, args.subject);\n return null;\n }\n\n const from = env.RESEND_FROM || \"Blog <noreply@resend.dev>\";\n\n const { data, error } = await resend.emails.send({\n from,\n to: args.to,\n subject: args.subject,\n html: args.html,\n text: args.text,\n replyTo: args.replyTo,\n });\n\n if (error) {\n throw new Error(`Resend error: ${error.message}`);\n }\n return data?.id ?? null;\n}\n\n/** Minimal HTML escape (avoids depending on a runtime sanitizer). */\nfunction esc(s: string): string {\n return s\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\n}\n\n/** Welcome email for new newsletter subscribers. */\nexport function welcomeEmailHtml(opts: {\n email: string;\n unsubscribeUrl: string;\n siteUrl: string;\n}): string {\n return `<!DOCTYPE html>\n<html>\n<head><meta charset=\"utf-8\"><title>Welcome</title></head>\n<body style=\"margin:0;padding:0;background:#fafafa;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;color:#171717;\">\n <div style=\"max-width:560px;margin:0 auto;padding:40px 20px;\">\n <h1 style=\"font-size:24px;font-weight:600;margin:0 0 16px;\">Welcome aboard!</h1>\n <p style=\"font-size:16px;line-height:24px;color:#404040;margin:0 0 24px;\">\n Hi <strong>${esc(opts.email)}</strong>,感谢订阅 <a href=\"${esc(opts.siteUrl)}\">vinext Blog</a>。\n 新文章发布时会第一时间通知你。\n </p>\n <hr style=\"border:0;border-top:1px solid #e5e5e5;margin:32px 0;\" />\n <p style=\"font-size:12px;color:#737373;margin:0;\">\n 不想再收?<a href=\"${esc(opts.unsubscribeUrl)}\" style=\"color:#737373;\">退订</a>\n </p>\n </div>\n</body>\n</html>`;\n}\n\nexport function resetPasswordHtml(opts: {\n resetUrl: string;\n siteUrl: string;\n email: string;\n}): string {\n return `<!DOCTYPE html>\n<html>\n<body style=\"margin:0;padding:0;background:#fafafa;font-family:-apple-system,BlinkMacSystemFont,sans-serif;color:#171717;\">\n <div style=\"max-width:560px;margin:0 auto;padding:40px 20px;\">\n <p style=\"font-size:13px;color:#737373;margin:0 0 8px;text-transform:uppercase;letter-spacing:0.05em;\">Reset password</p>\n <h1 style=\"font-size:24px;font-weight:600;margin:0 0 12px;\">Reset your password</h1>\n <p style=\"font-size:16px;line-height:24px;color:#404040;margin:0 0 24px;\">\n Hi <strong>${esc(opts.email)}</strong>,我们收到了重置 <a href=\"${esc(opts.siteUrl)}\">vinext Blog</a> 账户密码的请求。\n 点击下方按钮设置新密码。链接 1 小时内有效。\n </p>\n <a href=\"${esc(opts.resetUrl)}\" style=\"display:inline-block;background:#171717;color:#fafafa;padding:10px 20px;border-radius:6px;text-decoration:none;font-size:14px;\">Reset password</a>\n <p style=\"font-size:12px;color:#737373;margin:24px 0 0;\">\n 如果这不是你的操作,请忽略此邮件。如果按钮无法打开,请复制链接到浏览器:<br />\n <a href=\"${esc(opts.resetUrl)}\" style=\"color:#737373;word-break:break-all;\">${esc(opts.resetUrl)}</a>\n </p>\n </div>\n</body>\n</html>`;\n}\n\nexport function verifyEmailHtml(opts: {\n verifyUrl: string;\n siteUrl: string;\n email: string;\n}): string {\n return `<!DOCTYPE html>\n<html>\n<body style=\"margin:0;padding:0;background:#fafafa;font-family:-apple-system,BlinkMacSystemFont,sans-serif;color:#171717;\">\n <div style=\"max-width:560px;margin:0 auto;padding:40px 20px;\">\n <p style=\"font-size:13px;color:#737373;margin:0 0 8px;text-transform:uppercase;letter-spacing:0.05em;\">Verify email</p>\n <h1 style=\"font-size:24px;font-weight:600;margin:0 0 12px;\">Confirm your email</h1>\n <p style=\"font-size:16px;line-height:24px;color:#404040;margin:0 0 24px;\">\n Hi <strong>${esc(opts.email)}</strong>,欢迎注册 <a href=\"${esc(opts.siteUrl)}\">vinext Blog</a>。\n 请点击下面的按钮完成邮箱验证,验证后即可登录后台。\n </p>\n <a href=\"${esc(opts.verifyUrl)}\" style=\"display:inline-block;background:#171717;color:#fafafa;padding:10px 20px;border-radius:6px;text-decoration:none;font-size:14px;\">Verify email</a>\n <p style=\"font-size:12px;color:#737373;margin:24px 0 0;\">\n 如果按钮无法打开,请复制这个链接到浏览器:<br />\n <a href=\"${esc(opts.verifyUrl)}\" style=\"color:#737373;word-break:break-all;\">${esc(opts.verifyUrl)}</a>\n </p>\n </div>\n</body>\n</html>`;\n}\n\n/** New post notification email. */\nexport function newPostEmailHtml(opts: {\n title: string;\n description: string;\n url: string;\n unsubscribeUrl: string;\n}): string {\n return `<!DOCTYPE html>\n<html>\n<body style=\"margin:0;padding:0;background:#fafafa;font-family:-apple-system,BlinkMacSystemFont,sans-serif;color:#171717;\">\n <div style=\"max-width:560px;margin:0 auto;padding:40px 20px;\">\n <p style=\"font-size:13px;color:#737373;margin:0 0 8px;text-transform:uppercase;letter-spacing:0.05em;\">New post</p>\n <h1 style=\"font-size:24px;font-weight:600;margin:0 0 16px;\">\n <a href=\"${esc(opts.url)}\" style=\"color:#171717;text-decoration:none;\">${esc(opts.title)}</a>\n </h1>\n <p style=\"font-size:16px;line-height:24px;color:#404040;margin:0 0 24px;\">${esc(opts.description)}</p>\n <a href=\"${esc(opts.url)}\" style=\"display:inline-block;background:#171717;color:#fafafa;padding:10px 20px;border-radius:6px;text-decoration:none;font-size:14px;\">Read the post →</a>\n <hr style=\"border:0;border-top:1px solid #e5e5e5;margin:40px 0 16px;\" />\n <p style=\"font-size:12px;color:#737373;margin:0;\">\n 不想再收新文章通知?<a href=\"${esc(opts.unsubscribeUrl)}\" style=\"color:#737373;\">退订</a>\n </p>\n </div>\n</body>\n</html>`;\n}\n","// lib/env.ts - 集中获取 Cloudflare bindings\n// 用 cloudflare:workers 模块(workerd 内置),作为平台 adapter 的绑定入口\n\n/// <reference types=\"@cloudflare/workers-types\" />\nimport { env } from \"cloudflare:workers\";\n\nexport type AppEnv = {\n DB: D1Database;\n ASSETS: Fetcher;\n IMAGES: ImagesBinding;\n ASSETS_BUCKET?: R2Bucket;\n CONTENT_CACHE?: KVNamespace;\n ADMIN_PASSWORD: string;\n ADMIN_EMAIL?: string;\n SITE_URL?: string;\n RESEND_API_KEY?: string;\n RESEND_FROM?: string;\n // Google OAuth 仍然兼容 Cloudflare Secret 作为兜底。\n // 实际生效值以 app_settings.google_client_id / google_client_secret 为准。\n GOOGLE_CLIENT_ID?: string;\n GOOGLE_CLIENT_SECRET?: string;\n /** Turnstile site key fallback when not stored in app_settings */\n TURNSTILE_SITE_KEY?: string;\n /** Turnstile secret — set via `wrangler secret put TURNSTILE_SECRET_KEY` */\n TURNSTILE_SECRET_KEY?: string;\n /** Notion integration token for the blog data source */\n NOTION_TOKEN?: string;\n /** Notion data source ID used by dataSources.query */\n NOTION_DATA_SOURCE_ID?: string;\n /** Optional Notion API base URL for tests or proxies */\n NOTION_API_BASE_URL?: string;\n /** Optional Notion edit URL for admin handoff screens */\n NOTION_EDIT_BASE_URL?: string;\n /** Optional webhook verification token for Notion invalidation */\n NOTION_WEBHOOK_VERIFICATION_TOKEN?: string;\n};\n\n// 强制类型:vinext 把 env 类型放在 env.d.ts(interface VinextEnv extends Env),\n// 但 TS server 经常解析不到。运行时一定有 DB,类型断言保证编译通过。\nexport const workerEnv = env as unknown as AppEnv;\n"],"mappings":";AAMA,SAAS,cAAc;;;ACFvB,SAAS,WAAW;AAmCb,IAAM,YAAY;;;AD9BzB,IAAI,eAA8B;AAElC,SAAS,YAA2B;AAClC,MAAI,aAAc,QAAO;AACzB,QAAMA,OAAM;AACZ,MAAI,CAACA,KAAI,eAAgB,QAAO;AAChC,iBAAe,IAAI,OAAOA,KAAI,cAAc;AAC5C,SAAO;AACT;AAeA,eAAsB,UAAU,MAAwC;AACtE,QAAMA,OAAM;AACZ,QAAM,SAAS,UAAU;AAGzB,MAAI,CAAC,QAAQ;AACX,YAAQ,IAAI,iCAAiC,KAAK,IAAI,KAAK,OAAO;AAClE,WAAO;AAAA,EACT;AAEA,QAAM,OAAOA,KAAI,eAAe;AAEhC,QAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAAO,OAAO,KAAK;AAAA,IAC/C;AAAA,IACA,IAAI,KAAK;AAAA,IACT,SAAS,KAAK;AAAA,IACd,MAAM,KAAK;AAAA,IACX,MAAM,KAAK;AAAA,IACX,SAAS,KAAK;AAAA,EAChB,CAAC;AAED,MAAI,OAAO;AACT,UAAM,IAAI,MAAM,iBAAiB,MAAM,OAAO,EAAE;AAAA,EAClD;AACA,SAAO,MAAM,MAAM;AACrB;AAGA,SAAS,IAAI,GAAmB;AAC9B,SAAO,EACJ,QAAQ,MAAM,OAAO,EACrB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,QAAQ,EACtB,QAAQ,MAAM,OAAO;AAC1B;AAGO,SAAS,iBAAiB,MAItB;AACT,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAOU,IAAI,KAAK,KAAK,CAAC,oDAA2B,IAAI,KAAK,OAAO,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA,+CAKxD,IAAI,KAAK,cAAc,CAAC;AAAA;AAAA;AAAA;AAAA;AAK9C;AAEO,SAAS,kBAAkB,MAIvB;AACT,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAOU,IAAI,KAAK,KAAK,CAAC,sEAA8B,IAAI,KAAK,OAAO,CAAC;AAAA;AAAA;AAAA,eAGlE,IAAI,KAAK,QAAQ,CAAC;AAAA;AAAA;AAAA,iBAGhB,IAAI,KAAK,QAAQ,CAAC,iDAAiD,IAAI,KAAK,QAAQ,CAAC;AAAA;AAAA;AAAA;AAAA;AAKtG;AAEO,SAAS,gBAAgB,MAIrB;AACT,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAOU,IAAI,KAAK,KAAK,CAAC,oDAA2B,IAAI,KAAK,OAAO,CAAC;AAAA;AAAA;AAAA,eAG/D,IAAI,KAAK,SAAS,CAAC;AAAA;AAAA;AAAA,iBAGjB,IAAI,KAAK,SAAS,CAAC,iDAAiD,IAAI,KAAK,SAAS,CAAC;AAAA;AAAA;AAAA;AAAA;AAKxG;AAGO,SAAS,iBAAiB,MAKtB;AACT,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,iBAMQ,IAAI,KAAK,GAAG,CAAC,iDAAiD,IAAI,KAAK,KAAK,CAAC;AAAA;AAAA,gFAEd,IAAI,KAAK,WAAW,CAAC;AAAA,eACtF,IAAI,KAAK,GAAG,CAAC;AAAA;AAAA;AAAA,6EAGD,IAAI,KAAK,cAAc,CAAC;AAAA;AAAA;AAAA;AAAA;AAKnD;","names":["env"]}
|
|
@@ -19,10 +19,6 @@ type AppEnv = {
|
|
|
19
19
|
NOTION_TOKEN?: string;
|
|
20
20
|
/** Notion data source ID used by dataSources.query */
|
|
21
21
|
NOTION_DATA_SOURCE_ID?: string;
|
|
22
|
-
/** Notion data source ID for the public movie catalog */
|
|
23
|
-
NOTION_MOVIES_DATA_SOURCE_ID?: string;
|
|
24
|
-
/** Notion data source ID for localized movie copy */
|
|
25
|
-
NOTION_MOVIE_TRANSLATIONS_DATA_SOURCE_ID?: string;
|
|
26
22
|
/** Optional Notion API base URL for tests or proxies */
|
|
27
23
|
NOTION_API_BASE_URL?: string;
|
|
28
24
|
/** Optional Notion edit URL for admin handoff screens */
|
package/dist/i18n/index.d.ts
CHANGED
|
@@ -1,26 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
declare function isAppLocale(value: string): value is AppLocale;
|
|
5
|
-
declare function localizedMovieListPath(locale: AppLocale): string;
|
|
6
|
-
declare function localizedMovieDetailPath(locale: AppLocale, slug: string): string;
|
|
7
|
-
declare function expandLocalizedMoviePaths(paths: readonly string[], locale?: string): string[];
|
|
8
|
-
|
|
9
|
-
type MovieUiMessages = {
|
|
10
|
-
backToList: string;
|
|
11
|
-
releaseDate: string;
|
|
12
|
-
director: string;
|
|
13
|
-
actors: string;
|
|
14
|
-
noSummary: string;
|
|
15
|
-
unknownYear: string;
|
|
16
|
-
unknownReleaseDate: string;
|
|
17
|
-
searchPlaceholder: string;
|
|
18
|
-
noSearchResults: string;
|
|
19
|
-
itemLabel: string;
|
|
20
|
-
notionLink: string;
|
|
21
|
-
admin: string;
|
|
22
|
-
languageLabel: string;
|
|
1
|
+
type I18nConfig<TLocale extends string = string> = {
|
|
2
|
+
supportedLocales: readonly TLocale[];
|
|
3
|
+
defaultLocale: TLocale;
|
|
23
4
|
};
|
|
24
|
-
declare function
|
|
5
|
+
declare function defineI18nConfig<const TLocale extends string>(config: I18nConfig<TLocale>): I18nConfig<TLocale>;
|
|
6
|
+
declare function isSupportedLocale<TLocale extends string>(config: I18nConfig<TLocale>, value: string): value is TLocale;
|
|
7
|
+
declare function localesForExpansion<TLocale extends string>(config: I18nConfig<TLocale>, locale?: string): TLocale[];
|
|
8
|
+
declare function localizedPath(locale: string, path: string): string;
|
|
9
|
+
declare function localizedDetailPath(locale: string, listPath: string, slug: string): string;
|
|
10
|
+
declare function expandLocalizedPaths<TLocale extends string>(input: {
|
|
11
|
+
paths: readonly string[];
|
|
12
|
+
config: I18nConfig<TLocale>;
|
|
13
|
+
locale?: string;
|
|
14
|
+
shouldLocalize?: (path: string) => boolean;
|
|
15
|
+
}): string[];
|
|
16
|
+
|
|
17
|
+
type LocaleMessages<TLocale extends string = string, TMessages extends object = Record<string, string>> = Record<TLocale, TMessages>;
|
|
18
|
+
declare function getLocaleMessages<TLocale extends string, TMessages extends object>(messages: LocaleMessages<TLocale, TMessages>, locale: TLocale): TMessages;
|
|
25
19
|
|
|
26
|
-
export { type
|
|
20
|
+
export { type I18nConfig, type LocaleMessages, defineI18nConfig, expandLocalizedPaths, getLocaleMessages, isSupportedLocale, localesForExpansion, localizedDetailPath, localizedPath };
|
package/dist/i18n/index.js
CHANGED
|
@@ -1,22 +1,29 @@
|
|
|
1
1
|
// src/i18n/config.ts
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
function isAppLocale(value) {
|
|
5
|
-
return supportedLocales.includes(value);
|
|
2
|
+
function defineI18nConfig(config) {
|
|
3
|
+
return config;
|
|
6
4
|
}
|
|
7
|
-
function
|
|
8
|
-
return
|
|
5
|
+
function isSupportedLocale(config, value) {
|
|
6
|
+
return config.supportedLocales.includes(value);
|
|
9
7
|
}
|
|
10
|
-
function
|
|
11
|
-
return
|
|
8
|
+
function localesForExpansion(config, locale) {
|
|
9
|
+
return locale && isSupportedLocale(config, locale) ? [locale] : [...config.supportedLocales];
|
|
12
10
|
}
|
|
13
|
-
function
|
|
14
|
-
const
|
|
11
|
+
function localizedPath(locale, path) {
|
|
12
|
+
const normalized = path.startsWith("/") ? path : `/${path}`;
|
|
13
|
+
return `/${locale}${normalized}`;
|
|
14
|
+
}
|
|
15
|
+
function localizedDetailPath(locale, listPath, slug) {
|
|
16
|
+
const normalizedListPath = listPath.replace(/\/+$/, "");
|
|
17
|
+
const normalizedSlug = slug.replace(/^\/+/, "");
|
|
18
|
+
return localizedPath(locale, `${normalizedListPath}/${normalizedSlug}`);
|
|
19
|
+
}
|
|
20
|
+
function expandLocalizedPaths(input) {
|
|
21
|
+
const locales = localesForExpansion(input.config, input.locale);
|
|
15
22
|
const expanded = [];
|
|
16
|
-
for (const path of paths) {
|
|
17
|
-
if (
|
|
23
|
+
for (const path of input.paths) {
|
|
24
|
+
if (!input.shouldLocalize || input.shouldLocalize(path)) {
|
|
18
25
|
for (const currentLocale of locales) {
|
|
19
|
-
expanded.push(
|
|
26
|
+
expanded.push(localizedPath(currentLocale, path));
|
|
20
27
|
}
|
|
21
28
|
continue;
|
|
22
29
|
}
|
|
@@ -26,48 +33,16 @@ function expandLocalizedMoviePaths(paths, locale) {
|
|
|
26
33
|
}
|
|
27
34
|
|
|
28
35
|
// src/i18n/messages.ts
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
backToList: "\u8FD4\u56DE\u7535\u5F71\u5217\u8868",
|
|
32
|
-
releaseDate: "\u4E0A\u6620\u65F6\u95F4",
|
|
33
|
-
director: "\u5BFC\u6F14",
|
|
34
|
-
actors: "\u6F14\u5458",
|
|
35
|
-
noSummary: "\u6682\u65E0\u5267\u60C5\u7B80\u4ECB\u3002",
|
|
36
|
-
unknownYear: "\u672A\u77E5\u5E74\u4EFD",
|
|
37
|
-
unknownReleaseDate: "\u672A\u77E5\u4E0A\u6620\u65F6\u95F4",
|
|
38
|
-
searchPlaceholder: "\u641C\u7D22\u7247\u540D\u3001\u6B63\u6587\u3001\u5BFC\u6F14\u3001\u6F14\u5458\u3001\u7C7B\u578B",
|
|
39
|
-
noSearchResults: "\u6CA1\u6709\u5339\u914D\u7684\u7535\u5F71\u3002",
|
|
40
|
-
itemLabel: "\u90E8\u5F71\u7247",
|
|
41
|
-
notionLink: "Notion",
|
|
42
|
-
admin: "Admin",
|
|
43
|
-
languageLabel: "\u8BED\u8A00"
|
|
44
|
-
},
|
|
45
|
-
"en-US": {
|
|
46
|
-
backToList: "Back to movies",
|
|
47
|
-
releaseDate: "Release date",
|
|
48
|
-
director: "Director",
|
|
49
|
-
actors: "Cast",
|
|
50
|
-
noSummary: "No synopsis yet.",
|
|
51
|
-
unknownYear: "Unknown year",
|
|
52
|
-
unknownReleaseDate: "Unknown release date",
|
|
53
|
-
searchPlaceholder: "Search titles, body, director, cast, or genre",
|
|
54
|
-
noSearchResults: "No movies matched your search.",
|
|
55
|
-
itemLabel: "movies",
|
|
56
|
-
notionLink: "Notion",
|
|
57
|
-
admin: "Admin",
|
|
58
|
-
languageLabel: "Language"
|
|
59
|
-
}
|
|
60
|
-
};
|
|
61
|
-
function getMovieUiMessages(locale) {
|
|
62
|
-
return movieUiMessages[locale];
|
|
36
|
+
function getLocaleMessages(messages, locale) {
|
|
37
|
+
return messages[locale];
|
|
63
38
|
}
|
|
64
39
|
export {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
40
|
+
defineI18nConfig,
|
|
41
|
+
expandLocalizedPaths,
|
|
42
|
+
getLocaleMessages,
|
|
43
|
+
isSupportedLocale,
|
|
44
|
+
localesForExpansion,
|
|
45
|
+
localizedDetailPath,
|
|
46
|
+
localizedPath
|
|
72
47
|
};
|
|
73
48
|
//# sourceMappingURL=index.js.map
|
package/dist/i18n/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/i18n/config.ts","../../src/i18n/messages.ts"],"sourcesContent":["export
|
|
1
|
+
{"version":3,"sources":["../../src/i18n/config.ts","../../src/i18n/messages.ts"],"sourcesContent":["export type I18nConfig<TLocale extends string = string> = {\n supportedLocales: readonly TLocale[];\n defaultLocale: TLocale;\n};\n\nexport function defineI18nConfig<const TLocale extends string>(\n config: I18nConfig<TLocale>\n) {\n return config;\n}\n\nexport function isSupportedLocale<TLocale extends string>(\n config: I18nConfig<TLocale>,\n value: string\n): value is TLocale {\n return (config.supportedLocales as readonly string[]).includes(value);\n}\n\nexport function localesForExpansion<TLocale extends string>(\n config: I18nConfig<TLocale>,\n locale?: string\n) {\n return locale && isSupportedLocale(config, locale)\n ? [locale]\n : [...config.supportedLocales];\n}\n\nexport function localizedPath(locale: string, path: string) {\n const normalized = path.startsWith(\"/\") ? path : `/${path}`;\n return `/${locale}${normalized}`;\n}\n\nexport function localizedDetailPath(\n locale: string,\n listPath: string,\n slug: string\n) {\n const normalizedListPath = listPath.replace(/\\/+$/, \"\");\n const normalizedSlug = slug.replace(/^\\/+/, \"\");\n return localizedPath(locale, `${normalizedListPath}/${normalizedSlug}`);\n}\n\nexport function expandLocalizedPaths<TLocale extends string>(input: {\n paths: readonly string[];\n config: I18nConfig<TLocale>;\n locale?: string;\n shouldLocalize?: (path: string) => boolean;\n}) {\n const locales = localesForExpansion(input.config, input.locale);\n const expanded: string[] = [];\n\n for (const path of input.paths) {\n if (!input.shouldLocalize || input.shouldLocalize(path)) {\n for (const currentLocale of locales) {\n expanded.push(localizedPath(currentLocale, path));\n }\n continue;\n }\n expanded.push(path);\n }\n\n return Array.from(new Set(expanded));\n}\n","export type LocaleMessages<\n TLocale extends string = string,\n TMessages extends object = Record<string, string>,\n> = Record<TLocale, TMessages>;\n\nexport function getLocaleMessages<\n TLocale extends string,\n TMessages extends object,\n>(messages: LocaleMessages<TLocale, TMessages>, locale: TLocale): TMessages {\n return messages[locale];\n}\n"],"mappings":";AAKO,SAAS,iBACd,QACA;AACA,SAAO;AACT;AAEO,SAAS,kBACd,QACA,OACkB;AAClB,SAAQ,OAAO,iBAAuC,SAAS,KAAK;AACtE;AAEO,SAAS,oBACd,QACA,QACA;AACA,SAAO,UAAU,kBAAkB,QAAQ,MAAM,IAC7C,CAAC,MAAM,IACP,CAAC,GAAG,OAAO,gBAAgB;AACjC;AAEO,SAAS,cAAc,QAAgB,MAAc;AAC1D,QAAM,aAAa,KAAK,WAAW,GAAG,IAAI,OAAO,IAAI,IAAI;AACzD,SAAO,IAAI,MAAM,GAAG,UAAU;AAChC;AAEO,SAAS,oBACd,QACA,UACA,MACA;AACA,QAAM,qBAAqB,SAAS,QAAQ,QAAQ,EAAE;AACtD,QAAM,iBAAiB,KAAK,QAAQ,QAAQ,EAAE;AAC9C,SAAO,cAAc,QAAQ,GAAG,kBAAkB,IAAI,cAAc,EAAE;AACxE;AAEO,SAAS,qBAA6C,OAK1D;AACD,QAAM,UAAU,oBAAoB,MAAM,QAAQ,MAAM,MAAM;AAC9D,QAAM,WAAqB,CAAC;AAE5B,aAAW,QAAQ,MAAM,OAAO;AAC9B,QAAI,CAAC,MAAM,kBAAkB,MAAM,eAAe,IAAI,GAAG;AACvD,iBAAW,iBAAiB,SAAS;AACnC,iBAAS,KAAK,cAAc,eAAe,IAAI,CAAC;AAAA,MAClD;AACA;AAAA,IACF;AACA,aAAS,KAAK,IAAI;AAAA,EACpB;AAEA,SAAO,MAAM,KAAK,IAAI,IAAI,QAAQ,CAAC;AACrC;;;ACzDO,SAAS,kBAGd,UAA8C,QAA4B;AAC1E,SAAO,SAAS,MAAM;AACxB;","names":[]}
|
package/dist/index.js
CHANGED
|
@@ -491,7 +491,6 @@ function readProcessEnv() {
|
|
|
491
491
|
const env2 = {
|
|
492
492
|
NOTION_TOKEN: process.env.NOTION_TOKEN,
|
|
493
493
|
NOTION_DATA_SOURCE_ID: process.env.NOTION_DATA_SOURCE_ID,
|
|
494
|
-
NOTION_MOVIES_DATA_SOURCE_ID: process.env.NOTION_MOVIES_DATA_SOURCE_ID,
|
|
495
494
|
NOTION_API_BASE_URL: process.env.NOTION_API_BASE_URL,
|
|
496
495
|
NOTION_EDIT_BASE_URL: process.env.NOTION_EDIT_BASE_URL,
|
|
497
496
|
NOTION_WEBHOOK_VERIFICATION_TOKEN: process.env.NOTION_WEBHOOK_VERIFICATION_TOKEN
|