如果你一直在考虑为自己创建一个开发博客,你可能会对众多的工具和技术感到有些不知所措。我们生活在一个资源丰富的时代,有非常多的选择。
当我构建这个博客时,我的最大优先事项是找到一个解决方案,让我能够在每篇文章中嵌入完全自定义的内容,比如这种爆炸式的标志动画。在使用CMS中的Markdown或富文本编辑器时,如何做到这一点并不明确:你通常限于这些工具能够渲染的少数HTML元素。
在这篇文章中,我将分解我的博客是如何工作的,以便你可以为自己构建类似的东西。我还将涵盖多年来我收到的最常见的问题。这不是一个教程,但它应该给你一个广泛的路线图来遵循。
技术栈
这个博客是一个 Next.js 应用程序。
使用Next,当涉及到页面渲染时,你有几个不同的选项:你可以选择按需(服务器端渲染)或提前(静态网站生成)进行。我选择提前构建所有博客文章,当网站生成时。
我还使用Next的 API Routes 用于需要在后端持久化的事物。我使用 MongoDB 作为我的数据库,存储像每篇文章的点赞数这样的信息。
我在 Vercel 上部署这个博客。我最初选择它们是因为他们是Next.js背后的公司,我认为它将被很好地优化。老实说,他们的平台非常棒。最终,我也把我的一些非Next项目搬到了那里。
在样式方面,我使用 styled-components,并从头开始编写所有样式。我不使用任何“化妆品”库,比如Bootstrap(我也不认为你应该使用我的看法)。我确实使用 Reach UI 用于像模态框这样的事情。
对于动画,我主要依赖 React Spring,尽管我最近开始尝试 Framer Motion。
但我技术栈中最关键部分是 MDX。
MDX,秘密武器
MDX是Markdown的扩展,允许你导入和使用自定义的React组件。
即使你从未编写过Markdown,你可能以前也见过它。它是一种广泛使用格式 - 所有在Github仓库上显示的README.md
文件都是Markdown!
下面是Markdown的样子:
大家好!这是一个段落。
这是另一个段落,包含一些**粗体文本**。
这里有一个无序列表:
- 苹果
- 香蕉
- 胡萝卜
在Web应用程序中使用Markdown时,有一个“编译”步骤;Markdown需要被转换成HTML,以便浏览器能够理解。这些星号变成了<strong>
标签,列表变成了<ul>
,每个段落都有一个<p>
标签。
这很好,但这意味着我们限于Markdown知道的少数HTML元素。
MDX将格式更进一步,允许我们包含我们自己的元素,以React组件的形式:
import PieChart from '../components/PieChart';
这段落介绍了一个**数据可视化**:
<PieChart
title="最喜欢的食物"
data={[
{ label: '披萨', value: '30%' },
{ label: '西兰花', value: '5%' },
{ label: '哈根达斯', value: '65%' },
]}
/>
我们可以创建我们自己的丰富原语集,并在我们的内容中使用它们。例如,在这个博客上,我不限于斜体和粗体文本;我还有spicy文本和。
我们还可以创建自定义的一次性小部件。在“弹簧物理的友好介绍”,这是一篇关于运动和动画的文章,我想让读者能够玩耍和实验物理。所以我创建了一个定制的React组件,SpringMechanism
:
(拖动并释放重物!或者,你也可以聚焦它并按“空格”。)
当涉及到解释技术元素时,比如张力或质量对物理的影响,我向这个组件添加了props,以改变它的行为。这个并排的演示显示了mass如何影响动画:
这比用文字描述物理或用视频展示效果要强大得多。通过让读者控制,我们从被动学习转变为主动学习。
如果你是React开发者,希望这能给你很多灵感。现在,React应用程序中的几乎所有内容都可以嵌入到你的博客文章中的任何地方!
你可能想知道:为什么不创建一个“常规”的React应用程序,并为每篇文章渲染自己的路由?为什么要费心使用MDX呢?
当我开始我的博客时,这就是我所做的。它运作得还可以,但MDX更好,有两个原因:
- 创作体验要好得多。我不需要将每个段落包装在
<p>
标签中。我可以使用星号和下划线用于强/em标签。这很令人愉快。 - 更重要的是,Markdown是数据。我可以从前文中提取元数据,以便我可以在家页上显示过滤、排序的博客文章列表,或所有关于CSS的博客文章在CSS分类页面上。如果每篇文章都是它自己的React组件,我就无法这么容易做到这一点。
在我看来,MDX是“全代码”(标准React应用程序)和“全数据”(存储在CMS中的格式化文本)之间的完美平衡点。
这不是实现这一点的唯一方式,但这是我发现的对于独立博客开发者来说摩擦最小的方式。
将来,我计划写一个“MDX入门”博客文章。现在,我将引用这些令人惊叹的社区资源:
在Next.js中使用MDX
正如我写这篇文章时,有四种(4)流行的使用MDX与Next.js的方式 😅
有:
- 官方方式,使用@next/mdx
- Hashicorp的next-mdx-enhanced
- Hashicorp的next-mdx-remote
- Kent C Dodds的mdx-bundler
在这个博客和我的课程平台上,我使用next-mdx-remote。
总的来说,我非常满意,但最大的缺点是你不能在MDX文件中导入一次性组件。你创建的每个定制组件都必须打包在一个庞大的MDX捆绑包中。为了避免巨大的捆绑文件大小,我需要大量使用懒加载,这为开发体验增加了一些令人烦恼的摩擦。
元数据
除了内容本身,我们需要一种方式来存储“元数据” - 比如标题、摘要、发布日期等。
在我的博客上,我使用frontmatter。Frontmatter是Markdown的一个插件,让我们可以在文档顶部定义键值对。
这篇文章看起来像这样:
---
title: 我如何构建我的博客
seoTitle: 如何使用MDX、Next.js和React构建我的博客
abstract: 我的博客技术结构的深入研究。
isPublished: true
publishedOn: 2021-04-20T09:15:00-0400
layout: Article
---
定义和访问元数据的机制将根据你的MDX工具而变化。在我的情况下,layout
指的是将被使用的React页面组件(Article
)。当这篇文章呈现时,Article
组件将传递两个props:frontMatter
和children
:
function ArticleLayout({ frontMatter, children }) {
return (
<>
<h1>{frontMatter.title}</h1>
{children}
</>
);
}
在这个例子中,我最终会得到一个标题为“我如何构建我的博客”的h1,然后是文章内容。
索引页面
在这个博客的主页上,我有两个不同的博客文章列表:
- 最新的20篇文章,按时间顺序排列
- 有史以来最受欢迎的10篇文章
使用getStaticProps
方法,Next允许我们在构建网站之前,在部署之前做一些工作。我在那时计算了这些部分要显示的文章列表。
看起来像这样:
// pages/index.js
function Homepage({ newestContent, popularContent }) {
// 两个props都是包含文章元数据的对象数组。
// 我可以遍历它们,并为每一个渲染一个React组件。
}
export async function getStaticProps() {
// 这段代码在编译时运行!
// 我返回的东西将作为props传递给我的Homepage组件。
const newestContent = await getLatestContent({ limit: 20 });
const popularContent = await getPopularContent({ limit: 10 });
return {
props: { newestContent, popularContent },
};
}
getLatestContent
是一个遍历本地文件系统以找到所有.mdx
博客文章的方法。逻辑看起来像这样:
- 使用
fs.readdirSync
收集pages
目录中的所有MDX文件。 - 加载frontmatter(我为此使用了一个NPM包,gray-matter)。
- 过滤掉任何未发布的文章(
isPublished
未设置为true
)。 - 按
publishedOn
对所有博客文章进行排序,并切片出指定limit
之后的所有内容。 - 返回数据。
这感觉出奇地低级,特别是当来自Gatsby(在那里,所有这些数据都可以通过GraphQL神奇地获得)。最终,不过,我有点喜欢它。这绝对有更多的工作,但它给了我大量的控制权。
这种控制权在其他列出的方法,getPopularContent
中派上用场。它非常类似于getLatestContent
,但它会发起数据库请求(我依赖于MongoDB中存储的点击计数数据来确定受欢迎程度)。
API路由
这个博客主要是一个静态网站,但有一些数据驱动的方面。例如,每篇文章都配有一个90年代风格的点击计数器!
我以前写过我是如何使用Netlify Functions和Gatsby构建我的点击计数器的,使用Next和Vercel API路由的过程类似。你可以在官方文档中了解更多。
我使用类似的机制用于“点赞”计数器,这是一个可爱的心形按钮,让读者能够膨胀我的自尊心。
每个用户最多可以“点赞”每篇文章16次。最初,我在localStorage中跟踪这一点,但我最喜欢的Twitter上的一个人通过向一篇文章添加了近4万个假赞向我展示了为什么这是一个坏主意。
幸运的是,Vercel在请求头中包含了用户的IP地址。当用户点击心形时,我对他们IP地址进行哈希处理(以保护隐私),并检查他们是否已经达到了限制。
在我的数据库中,我为每篇文章都有一个大的映射,跟踪用户的点赞:
{
"slug": "how-i-built-my-blog",
"likesByUser": {
"abc123": 16,
"def456": 4,
"ghi789": 16
}
}
API路由的强大之处怎么说都不为过。这个博客是一个静态网站,后端需求相对适中,但我在我的课程平台上使用了相同的堆栈,这是一个完整的动态Web应用程序,具有用户认证和角色以及事务电子邮件等所有这些东西。它表现得非常好。
构建助手
正如我提到的,我将我的网站从Gatsby迁移到了Next.js。我这样做的主要原因是,我厌倦了上下文切换:我使用Next构建了一个课程平台,并希望两个项目使用相同的堆栈。
因为Next在应用程序结构和数据方面相对没有意见,所以没有同样丰富的插件生态系统。以前,我使用一个方便的Gatsby插件生成RSS源和网站地图。作为迁移的一部分,我不得不创建自己的版本。
这两项任务都超出了本文的范围,但我将分享一般的想法。
首先,我添加了一个名为build-helpers
的文件夹。它包括一些执行特定操作的Node脚本。我在构建Next.js网站之前运行它们:
{
"scripts": {
"build:rss": "babel-node ./build-helpers/generate-rss-feed.js",
"build:sitemap": "babel-node ./build-helpers/generate-sitemap.js",
"build:og-images": "babel-node ./build-helpers/generate-og-images.js",
"prebuild": "yarn build:og-images && yarn build:sitemap && yarn build:rss",
"build": "next build"
}
}
快速的NPM小贴士:你可以通过使用pre
前缀来创建预运行脚本。当我运行npm run build
或yarn build
时,如果定义了,它将自动先运行prebuild
脚本。你也可以使用post
前缀在之后运行脚本。
以RSS源为例,过程如下:
- 安装
rss
依赖项:它将为我们处理XML格式。 - 使用
fs.readdirSync
收集pages
目录中的所有MDX文件。 - 使用gray-matter加载frontmatter。提取相关细节(标题,摘要)。
- 过滤掉任何未发布的文章(
isPublished
未设置为true
)。 - 使用
rss
模块将每个项目添加到源中 - 将生成的
.xml
文件保存在./public/rss.xml
通过将其存储在public
中,我确保Next将其复制到static
目录,并使其公开可用。我还把这个文件添加到了我的.gitignore
中,因为它是一个生成的文件。
设计和资产
很多人问我是如何创建这个小家伙的:
我希望我能得到荣誉,但我没有创造它。我委托一位艺术家为我做这件事。我们创建了几种面部表情和两种照明模式(试试在右上角切换器在浅色/深色模式之间切换!)。总成本大约是500美元,我相信。
除了那之外,我设计/构建了你在这篇博客上看到的所有其他东西。设计对我来说并不自然,但我多年来学会了一些技巧:
- 当我与设计师合作时,我试图向他们学习。我问了这样的问题:“你是如何想出这个布局的?”或者“为什么这个标题是这个颜色?”。你可以通过尝试理解你实现的设计背后的规则和系统来建立设计直觉。
- 多年来,我实际上从未从头开始提出过任何东西。我会在dribbble这样的网站上搜索,找到4-5个“参考”。我会从一个地方拿布局,从另一个地方拿配色方案,从第三个地方拿排版和间距。学习如何巧妙地结合现有设计需要一些练习,但这比从头开始学习如何创建引人注目的设计要快得多。
- 设计有一点诅咒:如果你花了4个小时构建某物,你就会失去所有的客观感知。你无法判断它是好是坏。因此,我总是在粗略设计就位后,离开项目一两天。当我回来时,我将能够判断它是好是坏。
我的目标不是成为一个世界级的设计师 - 那将是一生的工作,并且是一个完全不同的职业!但是通过投入一些工作并采取一些捷径,我已经足够胜任设计工作,以对我构建的东西感到满意。
许多开发人员认为,你需要拥有一些内在的艺术天赋才能擅长设计,我知道这不是真的,因为我是一个糟糕的艺术家 😅如果你有兴趣成为一个更好的设计师,请务必加入我的通讯 - 我将在未来更深入地探讨这些内容。
代码片段
没有语法突出显示的代码片段,就没有开发人员博客是完整的。在这篇博客上,我有几个不同的选项。
许多Markdown处理器允许我们使用三反引号(```)创建代码样本。我们也可以指定语言以进行语法突出显示(```css)。
使用MDX,我将该语法映射到一个特定的组件,StaticCodeSnippet
。它产生这样的块:
.wrapper {
width: 800px;
padding: 32px;
}
在幕后,这使用prism-react-renderer
和自定义的语法主题。
有时,我希望代码是“可编辑的”,并展示代码的结果。这在我想让读者尝试代码,了解它是如何工作的时候很有用。
在这些情况下,我有一个不同的组件,<Playground>
。它看起来像这样:
为了构建这个组件,我分叉了agneym的Playground。这是一个极好的小工具。我确实做了一些相当大的调整,主要是在外观和可用性方面(底层的渲染逻辑基本保持不变)。
在MDX中,它看起来像这样:
<Playground
html={\`
<div class="wrapper">
<h2>Hello World</h2>
</div>
\`}
cssCode={\`
.wrapper {
display: flex;
justify-content: center;
}
\\n\\
body {
height: 100vh;
background: silver;
}
\`}
/>
这不是最好的创作体验:没有语法突出显示,缩进很奇怪。我经常最终在playground本身中编写代码,然后将其复制到源代码中。
一个陷阱是,MDX不喜欢在React元素中间有空白行。在上面的代码片段中,我想要在两条CSS规则之间有一个空行。如果我留下一个真正的空行,MDX会因为一个难以理解的错误消息而爆炸。我们可以用\n
添加一个显式的新行。不幸的是,这会创建两个空行,因为显式的\n
紧随实际的换行符之后。所以我用\n\
转义了换行符。
让我们谈谈其他一些不理想的元素。
缺点
所以,这是我不好意思承认的一个错误:有时,我的“全新”帖子会有几个月/几年前的“最后更新”日期。
这就是为什么会发生这种情况:每篇博客文章都有一个frontmatter中的publishedOn
日期,以及一个可选的updatedOn
日期。当我开始写一篇新帖子时,我复制/粘贴一些随机的旧帖子以给我frontmatter结构。如果我在发布帖子时(或无论何时我更新它)不明确记得更新日期,就会显示错误的日期。
在一个理想的世界里,updatedOn
可以基于文件最后修改的时间自动推导出来。操作系统可以跟踪文件最后更新的时间,所以我应该能够在构建网站时从操作系统中获取这些信息。
遗憾的是,这不起作用:我的网站不是在我的本地机器上构建的,而是在Vercel的服务器上构建的。他们每次都会对文件进行全新的克隆,所以根据文件系统,所有文件都是全新的。
发布这篇文章后,Adam Collier分享了他如何解决这个问题,通过在提交时使用lint-staged编辑.mdx
文件。我在我自己的博客上实现了这一点,可以确认它运作得很好。 😄
问题和答案
在写这篇文章之前,我在Twitter上问是否有人特别好奇:
不幸的是,我无法回答我收到的所有问题 😅 一些问题足够宽泛,以至于我不得不写一个完整的帖子系列才能回答它们!
但我可以浏览一些最常见的问题。
你如何组织你的组件?
在我的src/components
文件夹中,大约有150个组件。这些是一般的“应用程序范围”组件,像Logo
和RainbowButton
和Boop
这样的。
每个组件都有自己的目录:
components/
├─ Boop/
│ ├─ index.js
│ ├─ Boop.js
│ ├─ useBoop.hook.js
├─ Logo/
│ ├─ index.js
│ ├─ Logo.js
│ ├─ Logo.helpers.js
│ ├─ logo.svg
我真的很喜欢这种模式,因为它保持了src/components
目录相对清晰,同时让我可以创建尽可能多的每个组件文件。创建这种结构可能是个麻烦,但我使用我创建的命令行工具 使其超级快速和无痛。
我还有一个src/post-helpers
文件夹。在这里,我存储所有的“一次性”组件,用于特定的帖子,像我们之前看到的SpringMechanism
组件。理想情况下,我会将这些组件与博客文章一起放置,但Next对它允许在pages
目录中的内容非常严格(并且next-mdx-enhanced
要求你将文章放在那里)。
你的测试策略是什么?
我真的没有 😅 因为这是一个静态网站,没有太多的“关键流程”。
不过,我确实对我的课程平台使用Cypress,我非常满意!
你如何想出文章创意?
正如我最近在我的通讯中写的那样!你可以在存档中阅读问题。
你用什么来嵌入推文?
嘿,所以,我过去常常使用标准的Twitter SDK。我发现它使我的整个网站变慢了。
我创建了一个组件,FakeTweet
。这就是我现在用于这个的:
这是一个非常低技术的解决方案。我正在硬编码所有数据。在MDX中,它看起来像这样:
<FakeTweet
id="1380554683236950016"
avatarSrc="/images/twitter-avatars/joshwcomeau.jpg"
handle="JoshWComeau"
displayName="Josh ✨"
date="2021-04-09"
includeMetrics={true}
numberOfLikes={196}
numberOfConversations={36}
>
我正在写一篇关于我的博客(https://joshwcomeau.com)如何工作的博客文章!
<br />
<br />
你想这篇帖子包括什么?有没有你特别好奇的元素?
</FakeTweet>
这种方法的问题是数据会变旧。人们会改变他们的显示名称和用户头像,但我的网站不会与时俱进。在某个时候,我会编写一个构建助手,使用推文ID从Twitter API获取数据。
深入挖掘
对于我以前分享过的原因,这个博客是封闭源代码的,所以很遗憾,没有Github仓库的链接让你深入挖掘。不过,我已经启用了sourcemaps,所以你可以在浏览器中挖掘前端代码!
我鼓励你不要过于专注于这个博客的任何特定方面。想出你自己的自定义元素是创建你自己博客的最好部分之一!你的博客是你自己的个人实验室和游乐场:尝试不同的想法,看看你能想出什么 😄 你会有更多的乐趣,并创造出更加难忘和引人注目的东西。