基于Next.js与GitHub Pages构建个人开发者门户:从SSG到CI/CD全流程实践
1. 项目概述一个开发者个人门户的诞生在技术社区里一个以自己名字命名的.github.io仓库往往不仅仅是一个静态网站它更像是一个开发者的数字名片、技术博客、项目集散地甚至是一个个人品牌的线上总部。今天要聊的这个项目kerogrammer/kerogrammer.github.io就是一个非常典型的案例。从仓库命名就能一眼看出这是一个托管在 GitHub Pages 上的个人站点其所有者是一位名为 “kerogrammer” 的开发者。对于很多程序员尤其是全栈或前端开发者来说拥有一个自定义的、内容完全由自己掌控的个人网站其意义远超一个简单的在线简历。它不仅是展示你技术栈比如 React, Vue, Next.js 等和过往项目的最佳画布更是你系统化输出技术思考、记录学习轨迹、构建个人影响力的核心阵地。与在第三方平台写博客不同这里的每一行代码、每一个交互细节、乃至整体的部署流程都完全由你定义。你可以把它做得极简专注于内容也可以把它打造成一个复杂的技术试验场集成各种前沿的 Web 技术。这个项目标题本身就隐含了几个关键的技术点GitHub Pages 的自动化部署、静态站点生成器的选型或纯手写 HTML/CSS/JS、版本控制与持续集成/持续部署CI/CD的实践以及如何将代码仓库与个人域名如果有的话进行绑定。接下来我们就深入拆解一下要构建并维护好这样一个站点背后需要理清的思路、踩过的坑以及可以分享的经验。2. 核心思路与架构选型2.1 为什么选择 GitHub Pages 作为托管平台首先最直接的问题是为什么是github.io市面上有 Vercel, Netlify, Cloudflare Pages 等众多优秀的静态站点托管服务它们通常提供更快的全球 CDN、更丰富的构建环境以及 Serverless 函数等高级功能。选择 GitHub Pages 的核心理由我总结为三点无缝集成、完全免费、极简运维。无缝集成如果你的代码本身就托管在 GitHub那么 GitHub Pages 是“开箱即用”的。创建一个名为username.github.io的仓库将静态文件推送到指定分支通常是main或gh-pages网站几乎瞬间就能通过https://username.github.io访问。这种与源码仓库的深度绑定使得内容更新变成了单纯的git push操作极大地简化了工作流。完全免费对于个人项目、开源文档或技术博客来说GitHub Pages 提供的流量和存储额度完全够用没有任何费用。这对于学生和独立开发者尤其友好。极简运维你不需要关心服务器配置、SSL 证书GitHub 自动提供并续签 HTTPS、负载均衡或安全补丁。这些底层运维工作全部由 GitHub 承担你可以专注于内容创作和前端开发。当然它也有局限性比如不支持服务端运行时如 Node.js, PHP、构建环境有一定限制、自定义域名配置的 HTTPS 有时需要手动触发等。但对于一个以展示和内容为主的个人站点这些限制大多在可接受范围内。2.2 静态站点生成器SSG还是手动构建确定了托管平台接下来就要决定站点的生成方式。这里主要有两条路径路径一使用静态站点生成器SSG这是目前最主流、效率最高的方式。SSG 允许你使用 Markdown 书写内容通过模板和主题生成最终的 HTML、CSS、JavaScript 文件。常见的选型有JekyllGitHub Pages 原生支持集成度最高。如果你希望部署流程最简单Jekyll 是首选。它的生态成熟有大量主题可供选择。Hugo以编译速度极快著称适合内容较多的博客。使用 Go 语言编写单二进制文件部署方便。Next.js虽然通常被认为是 React 框架但其静态导出next export功能让它成为一个强大的 SSG。如果你本身就在 React 生态中并且希望站点拥有高度交互性Next.js 是非常好的选择。Vercel 对其支持最好但在 GitHub Pages 上也能良好运行。VuePress / VitePressVue 生态的文档/博客生成器体验流畅尤其适合技术文档类站点。路径二手动编写 HTML/CSS/JS这种方式给予开发者最大的控制权但开发效率较低。它适合以下几种情况站点结构极其简单只有几个页面。你希望深入练习和展示原生前端技术不依赖任何框架。你需要实现一些非常定制化、框架难以满足的交互效果。对于kerogrammer这样的项目如果开发者希望快速搭建一个内容丰富的博客我通常会推荐从 Jekyll 或 Hugo 开始。如果开发者更熟悉现代前端框架并计划在站点中加入更多动态功能如项目搜索、暗色模式切换、交互式组件那么 Next.js 或基于 Vite 的静态方案会是更面向未来的选择。注意如果你选择非 Jekyll 的 SSG需要在仓库中配置 GitHub Actions 来实现自动构建和部署。因为 GitHub Pages 默认只认识 Jekyll 或纯静态文件。这是一个关键的技术决策点。3. 项目初始化与核心配置实战假设我们为kerogrammer选择使用Next.js框架来构建站点并将其部署到 GitHub Pages。这是一个兼顾现代开发体验和灵活性的方案。下面我们来一步步拆解实操过程。3.1 仓库创建与本地开发环境搭建首先在 GitHub 上创建仓库名称必须为kerogrammer.github.io注意用户名要完全匹配。这将自动保留你的*.github.io域名。接着在本地进行开发# 使用 Next.js 官方脚手架创建项目 npx create-next-applatest kerogrammer.github.io # 根据提示选择TypeScript? Yes, ESLint? Yes, Tailwind CSS? 按需选择App Router? 推荐。 cd kerogrammer.github.io初始化完成后一个基础的 Next.js 项目就准备好了。Next.js 13 的 App Router 提供了基于文件系统的路由非常直观。你可以开始规划你的站点结构app/page.tsx- 首页 (/)app/blog/page.tsx- 博客列表页 (/blog)app/blog/[slug]/page.tsx- 博客详情页 (/blog/my-first-post)app/projects/page.tsx- 项目展示页 (/projects)app/about/page.tsx- 关于页面 (/about)3.2 针对 GitHub Pages 的关键配置要让 Next.js 项目在 GitHub Pages 上正确运行需要进行几项关键配置。1. 修改next.config.jsGitHub Pages 默认将站点部署在子路径下如https://kerogrammer.github.io/但如果你配置了自定义域名并解析到根目录则可能不需要。为了兼容性通常需要配置basePath。// next.config.js /** type {import(next).NextConfig} */ const nextConfig { // 如果你的仓库名就是 username.github.io且部署在根目录通常不需要 basePath。 // 但如果你部署在非根目录如项目站点则需要设置 basePath: /your-repo-name。 // 这里我们假设部署在根目录所以先注释掉。 // basePath: /kerogrammer.github.io, // 输出为静态站点对于纯内容站推荐开启 output: export, // 关键这告诉 Next.js 生成静态 HTML 文件 // 可选配置图片优化静态导出时需要指定未优化模式或配置外部loader images: { unoptimized: true, // 静态导出时简单处理图片避免复杂优化依赖 }, }; module.exports nextConfig;设置output: export后运行next build将生成一个out文件夹里面是所有静态文件。这就是将要部署到 GitHub Pages 的内容。2. 创建 GitHub Actions 工作流文件由于我们不是用 JekyllGitHub Pages 不会自动构建 Next.js 项目。我们需要通过 GitHub Actions 来实现自动化构建和部署。在项目根目录创建.github/workflows/deploy.yml文件name: Deploy to GitHub Pages on: push: branches: [main] # 当推送到 main 分支时触发 workflow_dispatch: # 允许手动触发 permissions: contents: read pages: write id-token: write concurrency: group: pages cancel-in-progress: false jobs: build: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkoutv4 - name: Setup Node.js uses: actions/setup-nodev4 with: node-version: 20 # 使用 LTS 版本 cache: npm - name: Install dependencies run: npm ci # 使用 ci 命令确保依赖锁一致 - name: Build with Next.js run: npm run build # 这会执行 next build生成 out 目录 - name: Upload artifact uses: actions/upload-pages-artifactv3 with: path: ./out # 上传构建产物 deploy: environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest needs: build steps: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pagesv4这个工作流定义了在代码推送到main分支后自动在一个 Ubuntu 环境中安装 Node.js、项目依赖执行构建并将生成的out文件夹内容部署到 GitHub Pages。3. 在 GitHub 仓库中启用 Pages 并选择源完成以上步骤后你需要到 GitHub 仓库的Settings-Pages页面Source选择GitHub Actions。这样 GitHub 就会使用我们上面编写的 Action 工作流来部署而不是尝试用 Jekyll 构建。稍等片刻Action 运行成功后你的站点就会在线了。4. 内容规划与核心功能实现一个技术个人站点的内容骨架通常包括首页简介、最新动态、博客、项目展示、关于页面。我们以博客系统为例看看如何用 Next.js 实现一个简单但实用的方案。4.1 基于文件系统的博客系统一种简单高效的方式是将每篇博客写成一个 Markdown 文件存放在content/blog/目录下。例如content/blog/ ├── my-first-post.md ├── nextjs-deploy-guide.md └── ...每篇 Markdown 文件可以包含 Front Matter元数据和正文--- title: 在 GitHub Pages 上部署 Next.js 全指南 date: 2024-05-15 description: 详细讲解如何配置 Next.js 项目并通过 GitHub Actions 自动部署到 GitHub Pages。 tags: [Next.js, GitHub Pages, CI/CD] --- 这里是博客正文支持 **Markdown** 语法。 ## 章节标题 可以写代码示例 javascript console.log(Hello, kerogrammer!);### 4.2 使用 gray-matter 和 remark 解析 Markdown 我们需要在 Next.js 中读取并解析这些 Markdown 文件。首先安装必要的库 bash npm install gray-matter remark remark-html然后可以创建一个工具函数lib/posts.ts来获取所有博客文章import fs from fs; import path from path; import matter from gray-matter; import { remark } from remark; import html from remark-html; const postsDirectory path.join(process.cwd(), content/blog); export interface PostData { id: string; // 文件名不含扩展名 title: string; date: string; description: string; tags?: string[]; contentHtml?: string; } export async function getSortedPostsData(): PromisePostData[] { const fileNames fs.readdirSync(postsDirectory); const allPostsData await Promise.all( fileNames.map(async (fileName) { const id fileName.replace(/\.md$/, ); const fullPath path.join(postsDirectory, fileName); const fileContents fs.readFileSync(fullPath, utf8); const matterResult matter(fileContents); // 使用 remark 将 Markdown 转换为 HTML可选列表页可能只需要元数据 const processedContent await remark() .use(html) .process(matterResult.content); const contentHtml processedContent.toString(); return { id, contentHtml, ...(matterResult.data as OmitPostData, id | contentHtml), }; }) ); // 按日期排序 return allPostsData.sort((a, b) (a.date b.date ? 1 : -1)); } export function getAllPostIds() { const fileNames fs.readdirSync(postsDirectory); return fileNames.map((fileName) ({ params: { id: fileName.replace(/\.md$/, ), }, })); } export async function getPostData(id: string): PromisePostData { const fullPath path.join(postsDirectory, ${id}.md); const fileContents fs.readFileSync(fullPath, utf8); const matterResult matter(fileContents); const processedContent await remark() .use(html) .process(matterResult.content); const contentHtml processedContent.toString(); return { id, contentHtml, ...(matterResult.data as OmitPostData, id | contentHtml), }; }4.3 实现博客列表页与详情页列表页 (app/blog/page.tsx)import { getSortedPostsData } from /lib/posts; import Link from next/link; export default async function BlogPage() { const allPostsData await getSortedPostsData(); return ( div classNamecontainer mx-auto px-4 py-8 h1 classNametext-3xl font-bold mb-8技术博客/h1 ul classNamespace-y-6 {allPostsData.map(({ id, date, title, description, tags }) ( li key{id} classNameborder-b pb-6 Link href{/blog/${id}} classNamegroup h2 classNametext-2xl font-semibold text-blue-600 group-hover:text-blue-800 transition-colors {title} /h2 /Link p classNametext-gray-500 mt-1{date}/p p classNametext-gray-700 mt-2{description}/p {tags ( div classNamemt-2 flex flex-wrap gap-2 {tags.map((tag) ( span key{tag} classNamepx-2 py-1 bg-gray-100 text-gray-800 text-sm rounded #{tag} /span ))} /div )} /li ))} /ul /div ); }详情页 (app/blog/[slug]/page.tsx)import { getPostData, getAllPostIds } from /lib/posts; import { notFound } from next/navigation; // 生成静态路径 export async function generateStaticParams() { const paths getAllPostIds(); return paths; } export default async function PostPage({ params }: { params: { slug: string } }) { const postData await getPostData(params.slug); if (!postData) { notFound(); } return ( article classNamecontainer mx-auto px-4 py-8 max-w-3xl header classNamemb-8 h1 classNametext-4xl font-bold mb-2{postData.title}/h1 p classNametext-gray-500{postData.date}/p {postData.tags ( div classNamemt-3 flex flex-wrap gap-2 {postData.tags.map((tag) ( span key{tag} classNamepx-3 py-1 bg-blue-100 text-blue-800 text-sm rounded-full {tag} /span ))} /div )} /header div classNameprose prose-lg max-w-none // 使用 Tailwind Typography 插件美化内容 dangerouslySetInnerHTML{{ __html: postData.contentHtml || }} / /article ); }通过这种方式一个基于文件系统、支持 Markdown、可静态生成的博客系统就搭建完成了。每次新增.md文件提交并推送后GitHub Actions 会自动构建并更新网站。5. 样式、性能与 SEO 优化5.1 样式方案选择Tailwind CSS对于个人项目我强烈推荐使用Tailwind CSS。它是一个实用优先的 CSS 框架能极大提升开发效率。正如我们在初始化项目时可以选择它与 Next.js 集成非常顺畅。优势开发速度快无需在 CSS 文件和 JSX 文件间来回切换直接在 className 中编写样式。设计一致性通过配置tailwind.config.js可以定义一套自己的设计令牌颜色、间距、字体等确保全站样式统一。生成的 CSS 体积小通过 PurgeCSS在 Tailwind 中内置移除未使用的样式最终生成的 CSS 文件非常精简。实操技巧 在app/globals.css中引入 Tailwindtailwind base; tailwind components; tailwind utilities;然后你就可以在组件中这样使用h1 classNametext-3xl font-bold text-gray-900 hover:text-blue-600 transition-colors 这是一个标题 /h15.2 图片优化与字体加载图片优化 由于我们设置了images: { unoptimized: true }Next.js 的 Image 组件将不会进行自动优化。对于 GitHub Pages 这种静态托管一个替代方案是在构建前手动优化图片使用工具如 Squoosh, ImageOptim。或者使用第三方图片优化服务如 Cloudinary的 URL 参数进行优化。如果图片不多且均为静态资源直接使用img标签并确保图片尺寸适当也是可接受的。字体加载 使用next/font可以自动优化谷歌字体或本地字体将其内联或预加载避免布局偏移和额外网络请求。// app/layout.tsx import { Inter } from next/font/google; const inter Inter({ subsets: [latin] }); export default function RootLayout({ children }) { return ( html langzh-CN className{inter.className} body{children}/body /html ); }5.3 SEO 基础配置一个个人站点同样需要关注搜索引擎优化SEO让更多人能找到你的内容。元标签在每个页面使用 Next.js 的MetadataAPIApp Router或next/headPages Router来设置标题、描述和关键词。// app/blog/[slug]/page.tsx import type { Metadata } from next; export async function generateMetadata({ params }): PromiseMetadata { const post await getPostData(params.slug); return { title: ${post.title} | kerogrammers Blog, description: post.description, keywords: post.tags?.join(, ), }; }语义化 HTML合理使用header,main,article,section,nav等标签有助于搜索引擎理解页面结构。sitemap.xml和robots.txt在app目录下创建sitemap.ts和robots.ts文件Next.js 会在构建时自动生成对应的静态文件。// app/sitemap.ts import { MetadataRoute } from next; import { getSortedPostsData } from /lib/posts; export default async function sitemap(): PromiseMetadataRoute.Sitemap { const posts await getSortedPostsData(); const baseUrl https://kerogrammer.github.io; const postEntries: MetadataRoute.Sitemap posts.map((post) ({ url: ${baseUrl}/blog/${post.id}, lastModified: new Date(post.date), changeFrequency: monthly, priority: 0.8, })); return [ { url: baseUrl, lastModified: new Date(), changeFrequency: weekly, priority: 1, }, { url: ${baseUrl}/blog, lastModified: new Date(), changeFrequency: weekly, priority: 0.9, }, ...postEntries, ]; }Open Graph 标签为社交媒体分享提供预览信息可以使用next/og库动态生成或静态定义。6. 自定义域名绑定与 HTTPS 配置虽然username.github.io的域名已经很好但绑定一个自定义域名如kerogrammer.com会让你的个人品牌更专业。操作步骤购买域名在任意域名注册商处购买你心仪的域名。配置 DNS 记录在你的域名管理后台添加以下记录类型CNAME主机记录或www如果你希望www也指向你的站点记录值kerogrammer.github.io.注意末尾的点或者你也可以使用A记录指向 GitHub Pages 的 IP 地址185.199.108.153,185.199.109.153,185.199.110.153,185.199.111.153。CNAME 通常更灵活。在 GitHub 仓库中设置进入仓库的Settings - Pages在 “Custom domain” 栏中输入你的域名如kerogrammer.com然后点击 Save。等待并检查DNS 生效可能需要几分钟到几小时。生效后GitHub 会自动为你的域名申请并配置 Let‘s Encrypt 的 SSL 证书启用 HTTPS。你可以在仓库中看到一个CNAME文件被创建。重要提示启用自定义域名后强烈建议同时勾选 “Enforce HTTPS” 选项强制所有访问都通过安全的 HTTPS 连接。7. 持续维护与内容更新策略站点搭建完成只是开始持续的维护和内容更新才是其价值所在。7.1 内容更新工作流建立一套流畅的本地写作和发布流程至关重要本地写作在content/blog/下新建.md文件使用你喜欢的 Markdown 编辑器如 VS Code, Obsidian, Typora进行写作。本地预览运行npm run dev在http://localhost:3000实时预览效果。提交与推送git add . git commit -m 发布新文章《XXXXX》 git push origin main自动部署推送后GitHub Actions 会自动触发构建和部署流程。大约 1-2 分钟后新内容就会上线。7.2 性能监控与数据分析虽然 GitHub Pages 本身很稳定但了解访客情况还是有必要的。Google Analytics 4 (GA4)在app/layout.tsx中插入 GA4 的脚本可以免费获得基本的流量来源、页面浏览量、用户设备等数据。Vercel Analytics 或 Plausible如果你追求更简洁、隐私友好的分析工具可以考虑这些替代品。它们可能需要额外的配置或反向代理。7.3 定期备份与版本控制你的整个站点代码和内容都已经在 Git 仓库中这本身就是最好的备份。但还需要注意环境依赖确保package.json和package-lock.json或yarn.lock准确记录了所有依赖。重要数据如果你有评论系统如基于 GitHub Discussions 或 Utterances、访客统计等动态数据确保了解其备份或导出机制。8. 常见问题与排查技巧实录在构建和部署过程中你几乎一定会遇到一些问题。下面是一些常见坑点及其解决方案。8.1 构建失败GitHub Actions 报错问题推送代码后Actions 运行失败网站没有更新。排查步骤查看 Action 日志在 GitHub 仓库的 “Actions” 标签页点击失败的工作流查看详细的错误日志。这是最重要的信息源。常见原因依赖安装失败网络问题或package-lock.json冲突。尝试在本地运行npm ci看是否成功。构建脚本错误本地npm run build是否通过确保本地构建成功再推送。Node.js 版本不匹配检查工作流文件中的node-version是否与项目兼容。Next.js 通常需要较新的 Node 版本。路径或配置错误检查next.config.js中的output: export和basePath设置是否正确。如果使用了next/image但未配置unoptimized: true静态导出也会失败。8.2 页面显示 404 或样式丢失问题网站能打开但部分页面 404或者 CSS/JS 加载失败。排查步骤检查basePath这是最可能的原因。如果你的仓库名是kerogrammer.github.io且部署在根目录next.config.js中不应该设置basePath。如果设置了错误的basePath所有资源路径都会错位。检查文件路径确保out目录下的文件结构正确。静态导出后app/blog/[slug]/page.tsx会生成out/blog/[slug]/index.html。如果路由配置复杂可能生成的文件位置不符合预期。清除浏览器缓存有时是旧的缓存文件在作祟。尝试使用无痕模式访问。8.3 自定义域名 HTTPS 证书问题问题绑定自定义域名后HTTPS 一直显示不安全或证书错误。排查步骤等待GitHub 申请证书可能需要最多 24 小时。请耐心等待。检查 DNS 配置确保 CNAME 或 A 记录已正确指向 GitHub Pages并且已完全生效可使用dig yourdomain.com或在线 DNS 查询工具检查。检查仓库设置在仓库的 Pages 设置里确认自定义域名已填写正确并且 “Enforce HTTPS” 复选框已勾选。有时取消勾选再重新勾选可以触发证书重新申请。检查CNAME文件仓库根目录下应该有一个CNAME文件里面只有一行你的域名。如果这个文件丢失或内容错误HTTPS 会出问题。8.4 图片等静态资源无法加载问题Markdown 中的图片或public文件夹下的静态资源在部署后显示为裂图。排查步骤路径问题在 Markdown 中引用图片建议使用绝对路径以/开头并确保图片文件位于public目录下。例如将图片放在public/images/在 Markdown 中写。next/image配置如果你使用了next/image组件并设置了unoptimized: true请确保图片路径正确并且组件被正确使用。检查构建产物查看out目录确认图片文件是否被正确复制进去。8.5 网站更新有延迟问题推送代码后网站内容没有立即更新。排查步骤GitHub Actions 状态确认 Actions 工作流已成功完成。部署到 GitHub Pages 本身也有一个短暂的发布过程。CDN 缓存GitHub Pages 背后有 CDN可能存在缓存。通常几分钟内会更新极端情况下可能需要清除浏览器缓存或等待 CDN 刷新。自定义域名 DNS 缓存如果你使用了自定义域名DNS 变更和缓存时间可能更长。构建和维护kerogrammer.github.io这样的个人站点是一个将技术实践、内容创作和个人品牌建设相结合的过程。从技术选型、自动化部署到内容创作和 SEO 优化每一个环节都值得深入琢磨。这个项目本身就是一个持续演进的作品它记录着你的成长轨迹。最关键的其实不是一开始就做到完美而是先让它跑起来然后随着你的技能和需求的变化不断地去迭代和优化它。每次解决一个部署问题每添加一项新功能每发布一篇有价值的文章都是这个数字名片上闪亮的一笔。