2019年6月14日星期五

使用 AppleScript 批量发送带附件的邮件

最近某人要代表学校批量发送邮件欢迎新生,而且有几个特殊的要求:

  1. 邮件必须每个人独立发一封,因为给每个人的邮件要正确的称呼这个人的名字。因此邮件必须批量发送,而不能够以 bcc 的形式一封邮件发给所有人。
  2. 邮件需要带上附件。
  3. 邮件没办法提供「退订」链接。这导致我们不能够使用 MailChimp,因为 MailChimp 根据法律要求邮件列表必须提供退订链接。

为此我想到了用 AppleScript 来控制 Mac 上的 Mail 来批量发送邮件。搜索一番后,写出如下的 AppleScript 来:

在这个 AppleScript 里,我通过 theRecipients 提供一个姓名及邮件的列表,然后打开系统对话框选择一个用作附件的邮件,接着利用循环逐一发送邮件。代码里面最 tricky 的一行是 delay 1,如果没有这一行代码附件会添加失败,但邮件文本会被发送出去。我也无法解释为什么会这样子,使用 delay 1 是我从网上搜索回来的解决方案,如果你知道为什么要这样做,请在评论告诉我。

最后,现在 AppleScript 的文档实在难找。例如说我想找 Mail 支持的所有 AppleScript 操作现在已经找不到了,Apple 网站上一些原有的 AppleScript 文档链接已经失效。现在能做的也就是看着别人过去留下的 AppleScript 自己慢慢摸索。如果你有更好的 AppleScript 文档,请在评论分享一下。

2019年6月5日星期三

Patreon 付费读者交流群

如果你是我的 Patreon 付费读者,你现在可以加入付费读者专属的 Discord 交流群。加入办法很简单:首先,如果你还没有 Discord 帐号的话可以先注册一个;然后,你可以在 Patreon 的 App 设置页关联你的 Discord 帐号。接下来 Patreon 会自动利用 Discord bot 把你添加到我的 Discord 服务器及对应的付费读者专属群。(这是此功能的 Patreon 官方帮助。)

如果你还不是我的付费读者,你可以使用邀请连接加入我的 Discord 服务器。相对于非付费读者来说,付费读者享受以下两项福利:

  1. 付费读者能够加入到我服务器上的 #early-reader 群,我会在上面跟大家讨论任何跟我文章相关的问题。
  2. 付费读者拥有高亮的用户名,在我服务器上的开放群拥有更高的可识别度。

如果你现在还不是我的付费读者,欢迎通过我的 Patreon 成为付费读者。现在付费读者有两档:

  • Early Reader: 每个月 $1,能够比其它人提前一个星期读到我发布的文章。如果你喜欢我写的文章,这是支持我写作的最佳途径。
  • Career Coaching: 每个月 $100,我每两周跟你面谈 30 分钟(面对面或视频),对你的职业成长进行辅导。如果你是从事互联网和科技相关的工作,或者你计划毕业后从事相关工作,我们可以试着聊一下看看我对你的职业成长是否能够提供帮助。

最后,如果你是非美国用户,觉得 Patreon 付费不方便(例如说不接受你的信用卡或你没有美元信用卡),请在评论留言告诉我,我会想办法提供其它付费方式。感谢大家对我一直以来的支持!

2019年4月19日星期五

如何重置 macOS 的蓝牙模块

问题现象

我买了新的 Bose QC35 II 主动降噪耳机,买的原因是我老的 QC25 不支持蓝牙且磨损到一定程度了。新耳机到手后,跟 iPhone 配对没问题,但跟我的一台 MacBook Pro 就是无论如何也配对不上。通过跟另外一台 MacBook Pro 配对,证明问题发生在这台 MacBook Pro,而 Bose QC35 本身没问题。

无法配对的具体现象是,在 macOS 设置的蓝牙面板中无论如何也搜索不到 Bose QC35。(而且也搜索不到周边的其他蓝牙设备。但 Apple 自己的蓝牙键盘鼠标之前能正常配对,因为那是通过 Lightning 线配对的。)我尝试过重启 macOS,但依然搜索不到任何蓝牙设备。

解决方案

解决方案就是重置 macOS 的蓝牙。我搜索后发现,老的 macOS(准确来说还是 Mac OS X)需要删除 Bluetooth.plist 来重置,新的 macOS 已经不用那么麻烦。同时按住 Shift 和 Option,然后点击菜单栏上的蓝牙图标,这时候下拉菜单就会显示更多信息,包括一个「Debug」子菜单。打开「Debug」子菜单,然后选择「Reset the Bluetooth module」,并且确认真的要重置,最后重启 macOS,问题就解决了。之后我能在蓝牙面板中搜索到 Bose QC35,配对后播放音乐没有任何问题。

2019年2月26日星期二

把我的个人网站推倒重来(Part 8 - Sitemap)

为了让 Google 更好地发现和索引我的网站,我会使用 Google Search Console 提交 Sitemap。Sitemap 本质上是一个 XML 文件,描述网站包含的网页信息,帮助 Google 了解网站上都有哪些网页,以及这些网页的重要程度和更新频率。(Google 会参考这些信息,但我们不能通过 Sitemap 直接控制 Google 爬虫或排名。)我上一版的个人网站就包含了 Sitemap,所以重新新版本时也会把 Sitemap 加上。

上一个版本的 Sitemap 是我手工维护的 XML 文件,既然新版本的网站使用 Harp 进行编译,我希望 Sitemap 的 XML 文件也是编译时自动生成的。Harp 要使用模板生成 XML 文件很简单,把文件的后缀写成 .jade.xml 就可以了,Harp 从文件名可以推断出这个文件需要使用 Jade 模板来编译,结果保存为 XML 文件。我希望这个文件尽可能不需要手工维护,最好自己根据语言和页面的组合生成所有有效页面的链接。

每一个有效页面背后都有一个同路径的 .jade 文件,例如说 /en/resume 背后存在对应的 /en/resume.jade,这个文件会生成 /en/resume.html,然后 Netlify 会美化 URL 删掉不必要的 .html 后缀。如果我需要找出所有的有效页面,其中一个方法就是遍历项目目录,找出所有这些 .jade 文件,然后把它们的路径映射过去。因为遍历所有目录需要用到递归,我就写了一个 mixin 来做这个事情,原因是 Jade 里面的 mixin 类似于函数,可以自己调用自己从而实现递归。

mixin tree(current, path)
  for value, key in current
    if key === '_contents'
      for file in value
        if /\.html$/.test(file) && !/^\d{3}\.html$/.test(file)
          url
            loc
              | https://catchen.me#{path}#{file.replace(/(index)?\.html$/, '')}
            changefreq
              | hourly
            priority
              if path === '/' && file === 'index.html'
                | 1.0
              else
                | #{public._data[file.replace(/\.html$/, '')].priority}
    else if !/^[._]/.test(key)
      mixin tree(value, path + key + '/')

这个 mixin 接收两个参数,第一个是代表当前目录的对象,第二个是当前目录所在路径。在递归的入口,我会调用 mixin tree(public, '/'),把代表根目录的 public 对象和对应的路径字符串 '/' 传进去,然后这个 mixin 就会遍历所有子目录把所有编译后的 .html 文件找出来。它找到的每一个 .html 文件都会被添加到 Sitemap 里面。

这个 mixin 在把 .html 文件映射为 URL 时,它还做了以下的特殊处理:

  1. 删除 .html 后缀,因为 Netlify 在美化 URL 时也会进行这个操作。如果我们在浏览器中打开 https://catchen.me/zh/resume.html,Netlify 会返回 301 重定向到 https://catchen.me/zh/resume,保证用户看到的是不包含 .html 的路径。
  2. 删除 index 默认文件名,因为 https://catchen.me/zh/index.htmlhttps://catchen.me/zh/ 等价,同样会被 URL 美化删除。
  3. 过滤掉 404.html 等状态码特殊页面,因为这个页面是专门给 Netlify 返回对应状态码时使用的,不应该被索引。

最终的 sitemap.jade.xml 其实并不复杂,记得加上 doctype xmlurlset(xmlns="http://www.sitemaps.org/schemas/sitemap/0.9") 这类必要的元素就行了。编译后立即可以发布到 Netlify,然后在 Google Search Console 进行手工添加 Sitemap。数天后 Google Search Console 就会显示这个 Sitemap 已经被抓取了,然后还能看到 Sitemap 中页面的抓取情况。

这有可能是这个系列的最后一篇文章了,将来如果我对我的个人网站继续有更新,我会继续写这个系列。如果你对我的博客感兴趣的话,欢迎通过邮件RSS/Atom 进行订阅。如果你对这个话题有什么问题或者观点的话,欢迎对本文发表评论。

2019年1月17日星期四

把我的个人网站推倒重来(Part 7 - Google Analytics & Facebook Pixel)

网站上线之后,我自然关心访客的数量和来源,于是我决定加上 Google Analytics。同时纯粹出于好奇,我把 Facebook Pixel 也加上了。

Google Analytics 和 Facebook Pixel 都需要插入 JavaScript 到每一个页面上,因此把代码加到 _layout.jade 是最合适的,因为这是所有页面共享的模板。

Google Analytics

在 Google Analytics 创建好「property」后,复制 Google Analytics 生成的代码到 _layout.jade 其实就完事了。(Google 把独立的每一个网站和 app 叫做「property」,这样使得一个 Google 帐号可以管理多个网站和 app。)

因为我想知道我的网站是否有 JavaScript 出错,所以我特意通过 window.onerrorwindow.onunhandledrejection 事件把错误信息上报到 Google Analytics。如果 Google Analytics 在创建 property 时选择的是网站而不是 app,默认是不会有报告显示 exception 事件的。这时候我们需要手工在 Google Analytics 添加一份自定义报告,然后选择显示 exception 事件,并把 exception message 展示出来。这样我就能看到是否存在 JavaScript 错误,以及具体在哪个页面发生什么错误了。

Facebook Pixel

Facebook Pixel 跟 Google Analytics 类似,在 Facebook 上配置后然后复制粘贴代码就可以了。需要注意的是,一个 Facebook 广告帐号只能创建一个 Pixel,不像 Google Analytics 那样可以创建多个 property。如果需要跟踪多个网站,那就需要创建多个广告帐号,或者一个 Pixel 用不同的事件来跟踪不同的网站。

在配置好 Facebook Pixel 并运行一段事件后,我发现 Facebook Pixel 并不能提供 Google Analytics 那样复杂的分析功能,更多是为了跟踪广告效果和生成广告受众群体。例如说,如果我在我的博客上使用 Facebook Pixel,然后我就能生成一个访问我博客的受众群体,然后在 Facebook 上面向这些受众投放广告让他们来赞我的专页。除此之外,我没有去深入研究 Facebook Pixel 还能做什么别的事情。(如果你知道 Facebook Pixel 还能做什么别的事情,欢迎在评论区讨论。)

搞掂 Google Analytics 和 Facebook Pixel 后,我还想做的是支持 Sitemaps 好让 Google 抓取所有页面。如果你对此感兴趣的话,欢迎通过邮件RSS/Atom 订阅我的博客,关注接下来的文章。

2018年11月23日星期五

把我的个人网站推倒重来(Part 6 - hreflang)

因为我在上一篇文章讲 Open Graph 元数据时提到 hreflang,我可以用这篇文章简单讲一讲如何支持 herflang。使用 hreflang 好处的是让 Google 知道多个页面其实是同一内容的不同语言版本,这样在用户搜索时 Google 就可以尽量提供正确的语言版本。

Google 官方对 hreflang 提供了详尽的解释。要让网站支持 hreflang 有三种做法:HTML 标签、HTTP header 以及 Sitemap。我选择了 HTML 标签,因为在我添加 hreflang 的时候 Google 还没提供另外两种做法。如果让我现在重新选择的话,我很有可能选择使用 Sitemap 从而减少页面中对用户没有价值的字节。

我在之前的篇文章解释了我的网站是如何支持多语言的,我的 hreflang 实现同样依赖于我做的这个基于 Harp 的多语言方案。对于每一个页面 /page,我都有 /zh/page/en/page 对应其中文版和英文版。这时候根据 Google 的要求,hreflang="x-default" 应该指向 /page,然后 hreflang="zh" 应该指向 /zh/page,英文版同理。举个例子,首页的 hreflang 标签是这样子的:

<link rel="alternate" hreflang="x-default" href="https://catchen.me/">
<link rel="alternate" hreflang="en" href="https://catchen.me/en/">
<link rel="alternate" hreflang="zh" href="https://catchen.me/zh/">

为了让 Harp 生成这组标签,我在模板中先取出当前页面语言无关的名称(也就是 /zh/page 中的 page),然后以此生成 x-default 的标签。接着我再遍历网站支持的语言,逐一生成对应语言的 hreflang 标签。具体代码可以在 GitHub 上看到。因为我让 Harp 遍历网站支持的所有语言,将来如果我添加了新的语言,只要让 Harp 重新便宜网站新的语言便会出现,不需要我做任何的手工修改。

hreflang 就这么简单的搞掂了。接下来让我们对网站加上 Google Analytics 和 Facebook Pixel,好让我们统计网站的访问来源。如果你想要继续关注这个系列的话,欢迎通过邮件RSS/Atom 订阅我的博客。

2018年11月17日星期六

把我的个人网站推倒重来(Part 5 - Open Graph 元数据)

网站发布之后我开始做各种细小的优化,其中一项是为网站加上 Open Graph 元数据( metadata),使得网站在被 Facebook 抓取时能够显示正确的预览信息。(Google 在抓取时也会参考 Open Graph 元数据,虽然 Open Graph 是 Facebook 提出的标准。)

Open Graph 标准

Open Graph 标准本身并不复杂,看着官方的标准信息把每一项相关的属性都加上。以下是我个人网站上使用的 Open Graph 信息:

<meta property="og:type" content="profile">
<meta property="og:title" content="Cat Chen">
<meta property="og:description" content="Cat Chen's personal website.">
<meta property="og:url" content="https://catchen.me/">
<meta property="og:image" content="https://catchen.me/images/profile_picture_360_360_1x.jpg">

Facebook 专门提供了一个工具来验证网页上的 Open Graph 元数据。在把元数据添加好之后,我用这个工具测试了一下 Facebook 抓取到的数据。

Screenshot 2018-11-10 16.35.56

Open Graph 文本属性其实随便怎么填都可以,URL 属性需要保证是正确的地址。以下是两个常用的 URL 属性:

og:image 属性

这个属性用来设置 Facebook 预览时显示的图片。我选择了用来显示我的头像,因为这也是我显示在网站首页左侧的图片。

og:url 属性

这个属性用来指定所谓的「Canonical URL」,我为这个属性折腾了很久。这是因为 Facebook 把 Canonical URL 看作重定向,如果一个 URL1 返回的网页使用 og:url 属性指向另一个 URL2,在 Facebook 看来这根 URL1 返回 302 重定向到 URL2 一样。这导致 Facebook 在抓取我的页面时陷入了无限重定向循环。

要解释这件事情,首先要解释我在 Netlify 上做了什么重定向配置,这是我在上一篇文章中没有提到的。我在之前一篇文章中说了,我的网站支持多语言(暂时只有英文和中文),不同语言使用不同的目录,例如说中文版在 /zh/* 英文版在 /en/*。这造成了一个问题,网站首页 / 应该显示中文版还是英文版?为此我让 Netlify 根据用户浏览器提供的 accept-language header 来选择正确的语言。(如果这个 header 不存在则默认为英文。)这是当时的 netlify.toml 配置文件。

如果只有这一种重定向,无限循环并不出现。无限循环之所以出现,是因为我把 /zh//en/ 的 Canonical URL 都指向了 /。为什么要指向 / 呢?因为我认为中文版和英文版都只是特定语言的版本而已,只有未指定语言的 / 才有资格叫做 Canonical URL。

最终我撤销了第一种重定向(302),保留了第二种重定向(Canonical URL)。撤销第一种重定向后,我把 / 改了了内容代理,也就是说如果浏览器的 accept-language header 选择了中文,那 / 显示的就是中文版,内容跟 /zh/ 完全一样但不改变 URL。这使得我的网站被分享到 Facebook 时指向的都是未指定语言的 URL,每一个用户实际打开时看到的语言都由他浏览器的设置来决定。我觉得这是我能做到的对用户最便利的选择。

这是我修改后的 netlify.toml。在 Netlify 的配置中使用 200 重定向意思就是代理目标页面内容——用户看不到重定向的发生,但实际返回的内容时目标页面的内容。做了这一件事情后,接下来要支持 hreflang 就很方便了,我在下一篇文章里就会讲述 hreflang 的设置。如果你对此感兴趣的话,欢迎通过邮件RSS/Atom 订阅我的博客。