Educational Products: Month 2

Michael Lynch

教育产品:第二个月

一句话总结

改进我的视频录制工作流

亮点

  • 我学到了几个技巧,让录制课程视频变得更容易。
  • 我决定不需要使用 Merchant of Record 服务。
  • 我已经把 htmx 整合进了我制作 Web 应用的标准工具箱。

目标评分

每个月初,我都会宣布自己想要完成的目标。以下是我对这些目标的完成情况:

录制课程中四节课的可发布版本

  • 结果:录完了一节课的大部分内容
  • 评分:D

我忘了录视频有多耗时!而且我低估了录制之外的工作量。大多数周里,我根本没有时间录制,但现在我已经进入录制的状态了。

每录制一小时,我能得到五到十分钟的可用素材,而连续录制 90 到 120 分钟后我就会精疲力竭。在专注的一周里,我录完了一节完整的 45 分钟课程,但既然已经结束了直播课程的授课,我现在可以加快速度了。

开始销售新版课程

  • 结果:做了一个等候名单,而不是销售半成品课程。
  • 评分:N/A

我决定暂缓销售新课程。起初我想推出新版本,并在制作期间以“抢先体验折扣”的名义宣传课程,同时提供 2020 年的旧录像。但我意识到,很难把它包装得干净利落。

在我妻子临产之前,我可能无法完成这门课程,而且宝宝出生后,我想休几个月的陪产假。我不想在休假期间觉得自己还欠着学生他们所购买材料的剩余部分,所以我决定提供一个等候名单,而不是预售。

视频录制的改进

当我为课程录制视频时,有很多事情可能打断一次录制,或者让我根本无法开始录制。我一直在努力减少录制的阻力,这样我可以更快地开始录制,也不容易被打断。

买一个桌面三脚架

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

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

我平时把桌面显示器摆放成屏幕中心大致与我的视线齐平。但如果显示器处于视线高度,而摄像头又放在显示器顶部,那么摄像头的角度就是俯视着我。

为了获得好的拍摄角度,我以前要调整显示器支架和桌子的高度,让摄像头与我的眼睛齐平。但挪动显示器给录制增加了很多阻力,而且每次录制之间我也无法完全复现之前的摄像头位置。

我用一个小巧的桌面尺寸三脚架解决了摄像头的问题:SmallRig VT-20。

我买了一个桌面三脚架,这样就不用为了开始录制而重新调整桌子或显示器的位置。

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

有了这个三脚架,当我想录制时,只需把三脚架和摄像头往桌上一放,位置就立刻正确了。这在减少阻力方面是一大胜利,而且用我平时的桌面配置来录制也很舒服,不用在每次录制前后摆弄各种设备的高度。

三脚架唯一的缺点是它会部分遮挡我看幻灯片的视线,不过到录制的时候,反正我已经把内容背下来了。

把摄像头装在显示器顶部(左)与装在显示器前的桌面三脚架上(右)的对比。三脚架让我更容易直视镜头。

让录制设备保持连接

以前,我在两次录制之间会把摄像头收进抽屉,麦克风和悬臂则放在另一个房间。这意味着每次录制的头五分钟都花在收集设备和在桌上安装调试上。

现在,我尽量让所有设备保持连接、随时可以开录的状态。麦克风一直放在桌上并已接好线。摄像头也一直装在三脚架上,随时可以放到桌上。

分章节录制

作为课程调研的一部分,我看了 Aaron Francis(亚伦·弗朗西斯)的 screencasting 课程。他的重点更多是实时编码直播,但其中足够多的内容也适用于我的课程,所以值得购买。

我觉得 Aaron 的课程很有帮助,但一些最好的经验甚至不是他说出来的。仅仅通过观察 Aaron 如何包装他的课程,我就学到了很多。

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

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

作为学生,我喜欢这种短章节的方式,因为它让我可以跳到自己最感兴趣的章节。

作为课程创作者,我也欣赏短章节,因为录制和剪辑 10 个四分钟的视频,比做一个更大、更难驾驭的 40 分钟视频要容易。此外,做视频时,拥有五个 100% 完成的章节,比剪辑一个 40 分钟的视频剪到一半感觉要好得多。

让各节课相互独立

2020 年录制课程时,我的每个视频开头都是类似“欢迎来到第 2 部分:理解 Hacker News”这样的话。而每个视频结尾我会说:“在下一个视频中,我将谈谈如何选择写作主题。”

把课程分成章节来重新录制,自然意味着视频数量更多。如果之后在剪辑过程中调整或删除某个视频,就意味着要重做大量的片头片尾,以保持编号和引用的正确。

看完 Aaron Francis 的课程后,我发现他从不在视频中暗示任何顺序。我决定从我的视频中去掉课程编号,避免提及任何顺序。当我引用另一节课的内容时,我只说“我在 Foo 那个视频里讲得更详细”,而不是说“后面的某个视频”。

买三件同样的衬衫

Aaron Francis 课程中的一个技巧是在所有课程中保持一致的形象。他在所有视频中都穿一件纯黑 T 恤,我觉得这是个不错的细节。

起初我专门留出一件衬衫,每次录制前换上、结束后脱下。我心想,平时穿一件衬衫,大约会穿 14 个小时才在睡前放进洗衣篮。也就是说,同一件衬衫应该能撑过大约 14 次一小时的录制才需要洗。

结果证明,我对衬衫变臭速度的心理模型不准确。只录了三次之后,那件衬衫就开始让人感觉恶心了。

于是,我干脆买了三件同样的衬衫:

为了保证视频画面的一致性,我订购了三件相同的 T 恤。

这是一件简单的藏青色 Bonobos T 恤,和我平时穿的是同款。我想,买三件意味着即使有两件在洗,至少还有一件可用。

直播课程中的精彩时刻

我已经完成了博客课程全部六次直播授课,教得很开心,其中有两个时刻尤其令我印象深刻。

请来一位嘉宾演讲者

在准备课程第一讲时,我看到 Adam Gordon Bell(亚当·戈登·贝尔)在 Twitter 上宣布他被公司裁员了。Adam 是 CoRecursive 的主持人,这是我最喜欢的软件播客之一。他也是一名博主,并且是他前雇主那里最受欢迎博客文章的作者。

2020 年我教同一门课时,Adam 曾是试点学生小组的一员。看到他突然空出了时间,我问是否愿意回到课程中担任问答环节的特别嘉宾,他欣然同意在下周加入。

时机简直绝妙:就在他加入我课程“登上 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 的讨论帖中,一条评论引起了我的注意:

我觉得整个 MoR 的说法就是一种贩卖恐惧的手段,好让你多付交易手续费

99% 的 SaaS 都达不到需要 MoR 的 MRR 规模

在那突破七位数 MRR 的 1% 中,完全可以雇人内部管理税务申报,而不必让客户面对印着 MoR 品牌的发票而感到困惑

我问了我的会计师,他确认对于美国大多数州而言,我对数字产品需要缴纳销售税的最低门槛约为每年 10 万美元或 100 笔交易,具体取决于州。

在加利福尼亚或纽约这样的人口大州,最低门槛是 50 万美元。如果我达到超过多个州的最低门槛、缴税成为负担的程度,那时我已经能靠销售收入赚到几十万美元,足以聘请一名会计师了。

我一直计划在 Gumroad 上销售,很大程度上就是为了它的 Merchant of Record 功能。但 Gumroad 收取 10% 的佣金,这还不包括支付处理费。

考虑到我在家乡马萨诸塞州以外的州达到销售税门槛的可能性极低,这意味着我可以在 Gumroad 之外销售课程,省下那 10%。

副业项目

找到我偏好的 htmx 表单模式

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

举个例子,下面是 ScreenJournal(我的影评 Web 应用)中的一个表单。

ScreenJournal Web 应用中的一个简单 HTML 表单。

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

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

htmx 处理这种情况的惯用方式是:用户提交表单后,服务器返回整个表单的 HTML,其中回填了用户提交的值,并附加上成功或错误消息。

我觉得 htmx 推荐的表单模式既别扭又容易出 bug。浏览器里的表单本来就填好了,为什么服务器还要把整个东西返回给浏览器再渲染一遍?唯一的新信息只是一条成功消息或错误消息,为什么服务器不能只发送这一条?

下面是我稍作调整后的 htmx 模式,应用在上面的 ScreenJournal 表单上,让 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。

下个月的目标

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

原文由 Michael Lynch 发布

本文章由 stealth/ox-alpha 进行翻译