Educational Products: Month 2

Michael Lynch

教育产品:第二个月

原文由 Michael Lynch 发布,订阅该博客

一句话总结

优化我的视频录制流程

亮点

  • 学到了几个能让课程视频录制更轻松的技巧。
  • 决定不再使用 Merchant of Record 服务。
  • 已将 htmx 纳入我开发网页应用的标准工具箱。

目标评分

每个月初,我都会定下当月想要完成的目标,以下是完成情况:

录制四节课的可发布版本

  • 结果:只完成了其中一节课的大部分录制
  • 成绩:D

我都忘了录视频有多花时间!而且我低估了录制之外的工作量。多数时候我根本抽不出时间录制,不过现在已经逐渐进入了录制状态。

我每小时录制只能得到五到十分钟可用的成片,连续录 90 到 120 分钟后就会精疲力尽。在精力集中的一周里,我录完了一节 45 分钟的课,不过现在直播课已经结束,速度应该能提上来。

开始销售新版课程

  • 结果:没有直接开售,而是做了候补名单
  • 成绩:暂不评分

我决定暂缓销售新版课程。原本的想法是以“早鸟优惠”的形式提供新版课程,在我继续制作的同时先让学员观看 2020 年的旧版录制。但后来发现这种打包方式很难做得清晰简洁。

我可能在妻子分娩前无法完成全部课程,而宝宝出生后我想休几个月的陪产假。我不想在休假期间还背着“欠学员后续内容”的压力,所以决定提供候补名单,而不是预售。

录制视频方面的改进

为课程录制视频时,有很多事情会打断录制,或让我根本无法开始录制。我一直在努力降低录制的阻力,以便更快进入录制状态,减少被打扰的可能。

入手一个桌面三脚架

录制时我用的是放在显示器顶部的 Razer Kiyo 摄像头。问题是,要把摄像头调到合适的高度,需要大量手动调整。

我用 Razer Kiyo 摄像头进行录制。

我平时把桌面显示器摆在中心与视线齐平的位置。但如果显示器与视线齐平,放在顶部的摄像头就会俯拍我。

为了获得合适的拍摄角度,我过去会调节显示器支架和桌面的高度,让摄像头与眼睛齐平。但这样来回挪动显示器让录制变得很麻烦,而且每次录制的机位都无法完全一致。

我用一个小型的桌面三脚架解决了这个问题:SmallRig VT-20

我买了一个桌面三脚架,这样就不用为了开始录制而去挪动桌子或显示器了。

录制时,我把三脚架放在自己和显示器之间。我用美纹胶带在桌面上标记了支脚的位置,这样每次都能复现同样的机位。

有了三脚架,想录制时只要把三脚架和摄像头往桌上一放,位置立刻就对了。这在降低阻力方面是很大的进步,而且我可以用平时的桌面布局直接录制,不用在每次录制前后费劲调整各种高度。

三脚架唯一的缺点是会稍微挡住我看幻灯片的视线,不过到录制的阶段,我早就把内容背下来了。

摄像头架在显示器顶部(左)与放在显示器前的桌面三脚架上(右)的对比。放在三脚架上能让我更自然地直视镜头。

保持录制设备始终连接就绪

以前,我会在两次录制之间把摄像头收进抽屉,把麦克风和支架放到另一个房间。这意味着每次录制的头五分钟,光是把设备找出来摆到桌上就花掉了。

现在,我尽量让设备保持连接,随时可录。我把麦克风就放在桌上,已经接好线;摄像头也装在三脚架上,随时可以放到桌面上开录。

按章节录制

作为课程调研的一部分,我看了 Aaron Francis 的截屏录制课程。他的重点更偏向现场编程,但其中不少内容对我的课程也有借鉴价值,所以这笔花费很值得。

我觉得 Aaron 的课程很有帮助,但收获最大的甚至不是他讲了什么,而是看他如何打包自己的课程,就学到了很多。

我课程的第一版由七节 30 到 60 分钟的课组成。Aaron 的课程每节课时长也差不多,但他把每节课拆成了若干只有几分钟的短章节。

Aaron Francis 把他的课程拆成短章节,每章只有几分钟。

作为学员,我喜欢这种短章节的形式,因为可以快速跳到自己最感兴趣的部分。

作为课程创作者,我也更青睐短章节,因为录制和剪辑 10 个四分钟的视频,要比制作一个笨重的 40 分钟长视频容易得多。而且在制作过程中,完成五个 100% 就绪的章节,感觉也比一个 40 分钟视频只剪到一半要好。

让课程不依赖固定顺序

2020 年录制课程时,我的视频开头都是类似“欢迎来到第二部分:理解 Hacker News”这样的话,结尾则会说“下一节我会讲如何选题”。

新版把课程拆成章节后,视频数量自然变多了。如果后期在剪辑时调整或删除某个视频,就得重录一堆开头和结尾,才能让编号和关联保持正确。

看了 Aaron Francis 的课程后,我发现他从不在视频里暗示先后顺序。我决定去掉视频中的课时编号,避免提及任何顺序。需要引用其他课的内容时,我只会说“我在讲 Foo 的那个视频里有更详细的讨论”,而不会说“后面的视频”。

买三件一模一样的 T 恤

Aaron Francis 课程里的一个建议是在所有视频中保持外观一致。他在所有视频里都穿一件纯黑 T 恤,我觉得这个细节很不错。

我原本专门留了一件衬衫,每次录制前换上,录完再换下来。我想着平时一件衬衫穿约 14 小时才会在睡前丢进洗衣筐,那同一件衬衫应该能撑过大约 14 次一小时的录制再洗。

结果证明,我对“衬衫多久会变味”的判断完全不准。只录了三次,衣服就已经感觉很不舒服了。

于是我直接买了三件一样的:

为了保证视频的连贯性,我买了三件同样的 T 恤。

就是一件简单的深蓝色 Bonobos T 恤,和我平时穿的款式一样。我想着有三件的话,就算两件在洗,也至少还有一件可用。

直播课中的高光时刻

我已经完成了博客课程直播版的全部六节课,教得很开心,其中有两个瞬间尤其令人难忘。

邀请客座嘉宾

准备课程第一节时,我在 Twitter 上看到 Adam Gordon Bell 发文宣布自己被公司裁员了。Adam 是我最喜欢的软件播客之一 CoRecursive 的主持人,也是一名博主,曾为前东家写过最受欢迎的博文

Adam 曾是我 2020 年试点授课时的学员之一。看到他突然有了空档,我便邀请他作为特别嘉宾回来做一场问答,他也很爽快地答应在下一周加入。

时机巧得惊人——就在他加入我的课程视频连线“如何登上 Hacker News 首页”的那一刻,他关于 PowerShell 首席架构师 Jeffrey Snover 的访谈刚好登上了 Hacker News 第一名。

这次访谈让我觉得很有趣,因为我读过 Adam 的很多文章,能透过幕后听到他的方法论以及这些年来的调整,非常有启发。

我也能感受到学员们的兴奋。说实话,这种热情程度让我有些意外,毕竟那才第二节课,并非“终于换换口味”的时候。我最好的解释是,班上的学员都认识我,但不是每个人都认识 Adam,所以能听到另一位有实绩的人用自己的视角来谈课程中的概念,格外有吸引力。

对真实文章的现场拆解

课程中,有学员提议做一个练习:大家分享自己的文章,我们来点评。三位学员分享了文章或草稿,我们做了点评,他们又问能不能多做一些。

下一节课,我向大家征集自愿分享的内容,但没人准备好。我提议直接打开 Hacker News,随便挑一篇文章来分析优缺点。这很有趣,但我挑的是首页文章,意味着它们本来就已经表现不错。学员们建议我挑那些刚刚提交、结果未知的全新投稿。

当我开始点评新投稿时,课程才真正活跃起来。因为回头去解释一篇已经成功的投稿为什么成功很容易,但要看一篇结果未知的投稿并预测它的表现,就要难得多。而且因为是陌生人的帖子,又是在私密的小班里展示,我可以畅所欲言,不用担心冒犯作者或过于挑剔。

点评全新投稿时,学员们的参与度和投入感也明显更高。我在课程中展示过一些反面例子,但实时观看一个人如何因为枯燥的开头或糟糕的结构而对文章失去兴趣,似乎更能引起共鸣。

如何利用这些高光时刻?

每次出现高光时刻后,我都会思考如何多创造一些,并把它们融入最终的课程中。

对于现场的集体问答,除非我开一个有八个轮换搭档的 YouTube 频道,否则很难复制。但一个更可行的近似方案是与我喜欢的写作者做一对一的视频访谈,我觉得这更容易实现。

无独有偶,专家访谈正是 Aaron Francis 用来推广自己课程的方式:

Aaron Francis 通过发布与 SQLite 领域专家的长篇访谈,来为他即将推出的 SQLite 课程预热。

而现场拆解则很容易复现。我现在就可以打开摄像头,录下自己浏览 Hacker News 投稿时的即时思考。我不确定这样是否足够有趣,但尝试一下成本很低,很值得一试。

Merchant of Record 是骗局吗?

上周,少数几家支持 Merchant of Record 的支付服务商之一 LemonSqueezy 宣布被 Stripe 收购。在 Hacker News 的讨论帖里,一条评论引起了我的注意:

我觉得整个 Merchant of Record 就是一种制造焦虑、让你多付交易费的手段

99% 的 SaaS 根本达不到需要用到 MoR 的月经常性收入

剩下 1% 里,那些月入七位数的人完全可以自己雇人处理税务,不用让客户看到印着 MoR 品牌的发票而感到困惑

我咨询了我的会计师,他确认,对于大多数美国州来说,数字产品需要缴纳销售税的起征门槛大约是每年 10 万美元或 100 笔交易,具体取决于州。

在加州或纽约这样的人口大州,门槛是 50 万美元。如果我真的达到了多个州的起征点、以至于缴税变得麻烦,那时我已经靠销售赚了几十万美元,完全可以请会计师来处理。

我原本打算在 Gumroad 上销售,很大程度上就是看中它的 Merchant of Record 功能。但 Gumroad 要收 10% 的佣金,这还不包括支付手续费。

考虑到我在马萨诸塞州以外的州达到销售税门槛的可能性极低,这意味着我完全可以在 Gumroad 之外销售,从而省下这 10% 的费用。

副业项目

找到我喜欢的 htmx 表单模式

在上一篇回顾中,我提到自己开始使用 htmx 并很喜欢它,但觉得它的错误处理很别扭。

举个例子,这是 ScreenJournal(我的影评网页应用)上的一个表单。

来自 ScreenJournal 网页应用的一个简单 HTML 表单。

当用户提交表单时,只会有两种结果:

  • 设置成功保存。
  • 处理请求时出错。

htmx 惯用的处理方式是:用户提交表单后,服务器返回整个表单的 HTML,并填入用户提交的值,再加上成功或错误提示。

我觉得 htmx 这种推荐的表单模式很别扭,也容易出 bug。浏览器里表单本来就已经正确填好了,服务器为什么还要把整个表单再发回来让浏览器重新渲染?唯一的新信息就是成功或错误提示,为什么不能只发这个?

下面是我在上面 ScreenJournal 表单上应用的、稍作调整的 htmx 模式,让 htmx 的功能更轻量:

<form
  hx-put="/account/notifications"
  hx-clear="#result-success, #result-error"
  hx-disabled-elt="input, .btn"
  hx-target="#result-success"
  hx-target-error="#result-error"
  hx-swap="textContent"
>
  <label for="new-reviews-checkbox">
    Email me when users post reviews
  </label>
 <input
    type="checkbox"
    id="new-reviews-checkbox"
    name="new-reviews"
  />

  <label for="all-comments-checkbox">
    Email me when users add comments
  </label>
  <input
    type="checkbox"
    id="all-comments-checkbox"
    name="all-comments"
  />

  <button value="Save">Save</button>

  <div role="status">
    <span>Loading...</span>
  </div>
</form>
<div id="result-success" role="alert"></div>
<div id="result-error" role="alert"></div>

关键都在 <form> 标签里,下面逐一拆解:

hx-put="/account/notifications"

当用户提交表单时,向服务器的 /account/notifications 路由发起 HTTP PUT 请求。

hx-clear="#result-success, #result-error"

当用户提交表单时,清空 ID 为 result-successresult-error 的元素内容。

这个属性并非 htmx 自带,而是来自我写的一个名为 clear-before-send 的自定义扩展。我把它与 这条 CSS 规则 .alert:empty { display: none } 配合使用,用来在带有 alert 类的 <div> 标签内容为空时将其隐藏。

hx-disabled-elt="input, .btn"

在 HTTP 请求期间,禁用所有 <input> 标签和带有 .btn CSS 类的元素,防止用户重复提交。

顺带一提,htmx 的文档暗示 hx-disabled-elt="this" 应该能禁用整个表单,但实际似乎不起作用。作为变通,我不得不使用能匹配表单内所有输入框的选择器。

hx-target="#result-success"

如果服务器返回 200 范围内的状态码,就把响应体放入 ID 为 result-success 的元素中。

hx-target-error="#result-error"

如果服务器返回 200 范围以外的状态码,就把响应体放入 ID 为 result-error 的元素中。

这个属性并非 htmx 核心库自带,而是来自名为 response-targets 的 htmx 扩展。

hx-swap="textContent"

在填充 hx-targethx-target-error 的目标时,htmx 应替换目标元素的 textContent(而不是替换其内部或外部 HTML)。

效果如下。我把开发服务器调成了延迟两秒响应,且每隔一次请求就会返回错误信息:

ScreenJournal 上的通知管理界面

值得注意的几点是:

  • 请求进行中时,htmx 会禁用表单。
  • 用户再次点击“Save”时,之前的成功/错误提示会立即消失。
  • 成功和错误提示有不同的样式。

所以,无需为每个表单编写自定义 JavaScript,就能实现相当丰富的功能。我倒希望 HTML 当初就是这样演进的,而不是让如此常见的功能还得依赖第三方库,不过很高兴 htmx 填补了这一空白。

收尾

完成了什么?

  • 通过本博客以 2000 美元的价格售出了 Is It Keto,详见这篇文章
  • 完成了博客直播课的授课。
  • 创建了我的第一个 htmx 扩展
  • 创建了 Personal Best,一个为课程服务的营销工具。

经验教训

  • 寻找机会,降低视频录制过程中的阻力。
  • 可以通过发布与优秀博主的访谈和对博文的拆解来推广课程。
  • 美国的大多数独立创始人可能并不需要 Merchant of Record。

下月目标

  • 完成课程录制。
  • 开始销售课程。

本文章由 muse-spark-1.2-contributor 进行翻译

评论