如何搭建我的个人网站
Filed under 网页开发 on 2024年11月11日. Last updated on 2024年11月17日.
Table of Contents
Why?
当我摒弃掉所有似是而非的理由,我发现其实我单纯只是想做一个自己的网站。不管是技术、设计、写作,都回归到做事情本身,这个过程本身就很美妙。如果你对这个过程并不感兴趣,社交媒体或许是一个更好的选择。
当年,在刚进入大学时,我是想成为一名建筑设计师。但是阴差阳错成为了软件工程师,却也发现其乐无穷。那时我特别喜欢逛图书馆,在不经意间发现了《CSS禅意花园》这本书,另当时的我大开眼界。也许就是那个时候,设计并实现自己网站的种子就埋下了。
总览
在本文中,我会介绍两种解决方案。每一种方案我都试过,各有优劣,我会在接下来的内容里详细介绍。
方案 | 优点 | 缺点 |
---|---|---|
Github Pages + Jekyll | 1. 免费 2. 配置简单 3. 默认域名辨识度高 | 1. 可定制化差 2. 可扩展性差 |
AWS Amplify + Next.js | 1. 灵活 2. 可扩展性强 | 1. Next.js有一定开发成本 2. AWS Amplify有一定运营维护成本 |
总的来说,如果想要快速发布静态内容,对于其他细节要求不高的情况下,Github + Jekyll确实是一个不错的选择。然而一旦你想要对于网站的细节进行打磨的时候,Github + Jekyll就捉襟见肘了,而AWS Amplify + Next.js显得要游刃有余得多。
Github Pages + Jekyll
使用Github Pages,根据官方文档的指引,使用Jekyll可以在几分钟内就搭建一个静态网站。其中,Github提供了一个免费的子域名(username.github.io, 其中username是你的用户名或者机构名称),而Jekyll也提供了一系列开箱即用的主题供你选择。
可以说,如果你的需求仅仅是快速发布一些内容,那么Github Pages + Jekyll是一个非常不错的选择。但是在我个人的使用中,还是遇到了一些不那么完美的体验。
- Jekyll是Ruby实现的。我个人并没有足够的Ruby经验,并且如果我想要深度定制化肯定无法绕过阅读Jekyll的源码,最终我还是决定放弃。
- Jekyll是基于模板(template)来生成静态页面的。虽然用模板来创建静态文件不失为一种简便的方法,但是一旦页面的样式和交互变得复杂的时候,模板的天花板就显得有些低了。
- 主题(theme)可定制化差,不同主题的兼容性也有些问题。
基于以上一些原因,我在维护了一段时间Github Pages + Jekyll过后,还是放弃了这个解决方案。
AWS Amplify + Next.js
我放弃Jekyll的时候,并没有想要把其他框架生成的静态网站托管在Github Pages上,也完全没有意识到这件事的可行性,所以直接也把Github Pages放弃了,转而选择了AWS Amplify。
当然,相比Github Pages,AWS Amplify不光专业、全面,并且上手难度也非常的低,成本也低到可以忽略不计的程度。
在使用AWS Amplify的过程中,我了解到了Next.js。经过一番调研,最终决定了使用Next.js。
- The React Framework for the Web。我曾经在2017年左右使用过React来开发一个仅供内部使用的Single Page Application,它强大的功能以及模块化、组件化的开发方式和极高的开发效率,给我留下了深刻的印象。当我看到Next.js的简介,经过短暂的调研后,我立马决定了使用Next.js。
- React Client Components vs. React Server Components。在2020年,React团队提出了React Server Components RFC。这确实是非常重大的更新,极大的提升了React的性能和适用性,详细的内容我就不在这里赘述。
- CSR(Client-Side Rendering) vs. SSR(Server-Side Rendering)。作为个人网站,肯定是有SEO的诉求的,那么SSR基本上就必不可少了。另外,考虑到性能,SSR以及SSG(Static Site Generation)都是一个更好的选择。
在使用Next.js的过程中,整体来讲还是非常好用的。
AWS Amplify托管Next.js
相比Github Pages托管,AWS Amplify的接入门槛也还是比较低的。在AWS Amplify上托管Next.js,有专门的官方文档可以参考。
将Jekyll博客迁移到Next.js
首先,Next.js有两种Router模式:App Router和Pages Router。其中App Router能够支持到一些新的功能,所以最好直接使用App Router。
在此基础上,就可以迁移博客相关页面了。根据Next.js Routing,可以定义相关的Routing。 其中比较重要的两个功能,Route Groups和Dynamic Routes。
Route Groups能够帮助我们更好的管理代码结构和URL路径之间的映射,不至于把所有URL路径对应的目录都平铺在app目录下。
Dynamic Routes对于博客来说,就更加重要。因为所有的文章URL,肯定是需要动态生成的。
Dynamic Routes和generateStaticParams
// file app/blog/[slug]/page.tsx
export default async function BlogPage({ params }: { params: Promise<{ slug: string }>}) {
const slug = (await params).slug
return (
<main>My Post: {slug}</main>
)
}
但是要注意我们直接这样使用Dynamic Routes的话,所有的请求都是on demand的。
假设我们把以下的代码部署到AWS Amplify,会导致相关的Dynamic Routes都是在server端动态处理。假设我们是按照slug参数来获取文章,那么这个文章会在服务端动态加载。
# Pay attention to 'ƒ (Dynamic) server-rendered on demand'
npm run build
> example-app@0.1.0 build
> next build
▲ Next.js 15.0.3
Creating an optimized production build ...
✓ Compiled successfully
✓ Linting and checking validity of types
✓ Collecting page data
✓ Generating static pages (6/6)
✓ Collecting build traces
✓ Finalizing page optimization
Route (app) Size First Load JS
┌ ○ / 5.57 kB 105 kB
├ ○ /_not-found 896 B 101 kB
├ ○ /blog 139 B 100 kB
└ ƒ /blog/[slug] 139 B 100 kB
+ First Load JS shared by all 99.9 kB
├ chunks/4bd1b696-80bcaf75e1b4285e.js 52.5 kB
├ chunks/517-d083b552e04dead1.js 45.5 kB
└ other shared chunks (total) 1.88 kB
○ (Static) prerendered as static content
ƒ (Dynamic) server-rendered on demand
可以从build的输出看到,/blog/[slug]
这个路径是动态处理的(server-rendered on demand)。
这时,一定要记得使用generateStaticParams函数。
// file app/blog/[slug]/page.tsx
export async function generateStaticParams() {
return [{slug: 'hello'}]
}
export default async function BlogPage({ params }: { params: Promise<{ slug: string }>}) {
const slug = (await params).slug
return (
<main>My Post: {slug}</main>
)
}
这个函数相当于是在build阶段,给出Dynamic Routes所有的值,根据这些值直接生成静态页面。
# Pay attention to '● (SSG) prerendered as static HTML (uses generateStaticParams)' and line '/blog/hello'.
npm run build
> example-app@0.1.0 build
> next build
▲ Next.js 15.0.3
Creating an optimized production build ...
✓ Compiled successfully
✓ Linting and checking validity of types
✓ Collecting page data
✓ Generating static pages (7/7)
✓ Collecting build traces
✓ Finalizing page optimization
Route (app) Size First Load JS
┌ ○ / 5.57 kB 105 kB
├ ○ /_not-found 896 B 101 kB
├ ○ /blog 139 B 100 kB
└ ● /blog/[slug] 139 B 100 kB
└ /blog/hello
+ First Load JS shared by all 99.9 kB
├ chunks/4bd1b696-80bcaf75e1b4285e.js 52.5 kB
├ chunks/517-d083b552e04dead1.js 45.5 kB
└ other shared chunks (total) 1.88 kB
○ (Static) prerendered as static content
● (SSG) prerendered as static HTML (uses generateStaticParams)
这样,我们就能看到/blog/[slug]
这个路径现在变成了预渲染的HTML(prerendered as static HTML),并且也根据generateStaticParams
函数返回的参数,生成了/blog/hello
这个路径对应的内容。
当然,在实际实现的时候,我们可以拿到slug参数,去获取markdown文件,编译过后将其渲染到页面。
使用remark处理markdown
将markdown文件渲染为HTML页面,我尝试了两种方式。
将markdown文件加载为HTML
这种方式简单直接,实现也很好理解。
- 根据
slug
参数读取markdown文件。 - 将文件内容编译为HTML。
- 将HTML设置到JSX元素的
dangerouslySetInnerHTML
属性。
export async function markdownToHtml(content: string) {
const processedContent = await remark()
.use(remarkMath)
.use(html, { sanitize: false })
.use(remarkGfm)
.use(remarkGemoji)
.use(remarkRehype, { allowDangerousHtml: true })
.use(rehypeRaw)
.use(rehypeKatex)
.use(rehypeStringify)
.process(content)
return processedContent.value.toString()
}
具体使用哪些插件,按照自己的需求选择就好了。但是这里要注意插件的顺序,需要仔细调整,否则会遇到错误。
将markdown导入为MDX
把markdown导入MDX其实应该比编译为HTML更简单一些。但是由于我使用了frontmatter,需要额外配置插件。
import createMDX from '@next/mdx'
import remarkFrontmatter from 'remark-frontmatter'
import remarkMdxFrontmatter from 'remark-mdx-frontmatter'
import remarkMath from 'remark-math'
import remarkGfm from 'remark-gfm'
import remarkGemoji from 'remark-gemoji'
import remarkToc from 'remark-toc'
import rehypeKatex from 'rehype-katex'
const withMDX = createMDX({
options: {
remarkPlugins: [
remarkFrontmatter,
remarkMdxFrontmatter,
remarkMath,
remarkGfm,
remarkGemoji,
remarkToc,
],
rehypePlugins: [
rehypeKatex,
],
},
})
在这里,Next.js官方的文档其实并不好用。 如果想要了解这些插件到底都做了什么,我们需要稍微解释一下背后的原理。
MDX是基于JSX的markdown扩展。本质上MDX还是会被编译成JSX组件。所以当我们使用import
将MDX文件导入时,具体能导出什么变量,取决于整个MDX的转译过程。
而Next.js本身只是将标准的MDX转译为JSX,所以import
的时候默认只有一个export default
变量。如果我们想要获取到其中frontmatter的信息,就需要单独配置插件,这也就是为什么我们需要额外配置remarkFrontmatter
和remarkMdxFrontmatter
。
然而问题并没有结束,如果你跟我一样也使用了VS Code,那你大概能看到这样一个错误。
这是因为默认的MDX模块并没有export metadata
这个变量。即便我们在转译后的模块中有这个变量,但是定义中还是没有。
所以,我们还需要订制一下MDX模块的定义。
// global.d.ts
export interface Frontmatter {
title: string;
createdAt: string;
updatedAt: string;
author: string;
language: string;
tags: string[];
summary: string;
}
declare module '*.mdx' {
import { MDXProps } from 'mdx/types';
const MDXComponent: (props: MDXProps) => JSX.Element;
export default MDXComponent;
// Add named exports based on your MDX configuration
export const frontmatter: Frontmatter;
}
为了不跟Next.js的metadata冲突,我把MDX中导出的frontmatter信息直接定义为了frontmatter
。当然,你也可以在插件中定义你喜欢的名字。
const withMDX = createMDX({
options: {
remarkPlugins: [
[remarkMdxFrontmatter, "myFrontmatter"],
],
},
})
这样,我就可以在代码中直接导入MDX以及对应的frontmatter了。
import { notFound } from "next/navigation";
export async function MDXPage({ params }: { params: { slug: string } }) {
try {
const { default: Content, frontmatter } = await import(`@/posts/${params.slug}.mdx`)
return (
<main>
<h1>{frontmatter.title}</h1>
<p><small>{`Filed under ${frontmatter.tags}`}</small></p>
<Content />
</main>
)
} catch {
notFound()
}
}
到此为止,我们就基本把Jekyll迁移到Next.js了。
总结
从开始使用AWS Amplify + Next.js搭建个人网站,断断续续也有半年多了。在这半年多的时间里,我大部分闲暇时间都花在了网站的架构和样式上,内容反而更新得特别少。但是在此期间能分享的内容还是挺多的,我会在后续的文章中继续分享。