喻承龙CHENGLONG YU全部文章 ↗
实践 · 教程

用 Astro 和 GitHub Pages 搭建一个可长期维护的个人网站

从空仓库开始,用 Astro 构建个人主页和 Markdown Blog,再通过 GitHub Actions 自动发布到免费的 GitHub Pages。文章包含目录设计、内容集合、动态文章路由、中英文页面、RSS 和日常更新方法。

个人网站常见的需求其实很稳定:展示个人介绍、经历和项目,保留一个可以长期更新的 Blog,最好还能免费部署,修改内容后自动上线。

这篇文章介绍一套完整且容易维护的方案:

完成后的参考效果:

整套网站没有数据库,也不需要维护云服务器。每次向 GitHub 推送代码,网站会自动检查、构建并发布。


一、先理解整个网站如何工作

这套方案可以概括成一条很短的链路:

页面、数据和 Markdown

Astro 生成静态 HTML、CSS 和 JavaScript

GitHub Actions 自动构建

GitHub Pages 对外提供访问

Astro 只在构建阶段运行。访客打开网站时,GitHub Pages 直接返回已经生成好的网页文件,因此访问速度快,也没有后端服务的维护成本。

内容主要分为三类:

  1. 页面结构:决定首页有哪些区域、按照什么顺序展示;
  2. 结构化数据:保存经历、项目、教育和论文;
  3. Markdown 文章:保存 Blog 的标题、摘要和正文。

样式与内容分开后,更新项目时只修改数据,新增文章时只创建 Markdown 文件,页面结构无需反复调整。


二、准备开发环境

需要提前安装:

可以使用下面的命令检查:

git --version
node --version
pnpm --version

如果尚未安装 pnpm,可以执行:

npm install -g pnpm

建议使用较新的 Node.js 长期支持版本。不同电脑上的具体版本可以不完全一致,但本地开发与 GitHub 自动构建都应能够成功执行 pnpm build


三、创建 GitHub Pages 仓库

在 GitHub 新建一个仓库,名称必须是:

你的用户名.github.io

例如 GitHub 用户名是 alice,仓库名就是:

alice.github.io

GitHub Pages 会为这个仓库提供根域名:

https://alice.github.io/

创建仓库后克隆到本地:

git clone https://github.com/alice/alice.github.io.git
cd alice.github.io

后续示例都在这个目录中执行。


四、初始化 Astro

在仓库目录中运行:

pnpm create astro@latest .

初始化时可以选择:

启动本地开发服务:

pnpm dev

终端会显示类似地址:

http://localhost:4321/

打开这个地址即可预览网站。修改页面或 Markdown 后,浏览器会自动刷新。

为了支持 RSS,再安装:

pnpm add @astrojs/rss

最终常用命令可以放在 package.json

{
  "scripts": {
    "dev": "astro dev",
    "build": "astro check && astro build",
    "preview": "astro preview"
  }
}

其中:


五、规划目录结构

一个适合个人网站的目录可以这样组织:

.
├── .github/
│   └── workflows/
│       └── deploy.yml
├── public/
│   ├── favicon.png
│   └── images/
├── src/
│   ├── components/
│   │   └── HomePage.astro
│   ├── content/
│   │   └── blog/
│   ├── data/
│   │   ├── portfolio.ts
│   │   └── portfolio.en.ts
│   ├── layouts/
│   │   └── BaseLayout.astro
│   ├── lib/
│   │   └── blog.ts
│   ├── pages/
│   │   ├── blog/
│   │   ├── en/
│   │   ├── feed.xml.ts
│   │   └── index.astro
│   ├── styles/
│   │   └── global.css
│   └── content.config.ts
├── astro.config.mjs
└── package.json

各目录的职责:


六、配置网站域名和静态输出

编辑 astro.config.mjs

import { defineConfig } from "astro/config";

export default defineConfig({
  site: "https://alice.github.io",
  output: "static",
  trailingSlash: "always",
  markdown: {
    shikiConfig: {
      theme: "github-dark",
      wrap: true,
    },
  },
});

几个关键配置:

如果仓库名就是 alice.github.io,网站发布在根路径,不需要额外配置 base


七、用数据文件管理经历和项目

个人主页的经历、项目、论文不适合全部写死在页面中。把它们整理成数组,页面负责循环渲染,维护会简单很多。

创建 src/data/portfolio.ts

export const experiences = [
  {
    period: "2025.01 — 至今",
    place: "上海",
    organization: "示例科技有限公司",
    department: "AI 产品部",
    role: "AI 应用研发",
    summary: "负责智能应用的设计与开发。",
    points: [
      "完成核心功能原型并接入业务系统。",
      "建立自动化评测流程。",
    ],
  },
];

export const projects = [
  {
    title: "项目名称",
    description: "用两三句话介绍项目解决的问题。",
    result: "说明最终结果或可验证指标。",
    tags: ["Python", "Astro", "AI"],
    links: [
      {
        label: "查看项目",
        href: "https://github.com/alice/project",
      },
    ],
  },
];

在 Astro 页面中导入:

---
import { experiences, projects } from "../data/portfolio";
---

<section>
  {
    projects.map((project) => (
      <article>
        <h2>{project.title}</h2>
        <p>{project.description}</p>
        <strong>{project.result}</strong>
        <ul>
          {project.tags.map((tag) => <li>{tag}</li>)}
        </ul>
      </article>
    ))
  }
</section>

这一步建立了清晰的分工:

新增项目时,只需要在数组中加入一个对象。


八、建立公共页面 Layout

所有页面都会使用标题、描述、favicon、分享图片和滚动进度等公共能力,可以放在 BaseLayout.astro

简化版如下:

---
import "../styles/global.css";

interface Props {
  title?: string;
  description?: string;
  image?: string;
  lang?: "zh-CN" | "en";
}

const {
  title = "我的个人网站",
  description = "项目、文章和个人经历。",
  image = "/og.png",
  lang = "zh-CN",
} = Astro.props;

const canonical = new URL(Astro.url.pathname, Astro.site);
const imageUrl = new URL(image, Astro.site);
---

<!doctype html>
<html lang={lang}>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width" />
    <meta name="description" content={description} />
    <link rel="canonical" href={canonical} />
    <link rel="icon" type="image/png" href="/favicon.png" />

    <meta property="og:title" content={title} />
    <meta property="og:description" content={description} />
    <meta property="og:url" content={canonical} />
    <meta property="og:image" content={imageUrl} />

    <title>{title}</title>
  </head>
  <body>
    <slot />
  </body>
</html>

之后每个页面都可以使用:

<BaseLayout
  title="文章标题"
  description="文章摘要"
>
  <main>页面内容</main>
</BaseLayout>

这样可以统一管理搜索结果和链接分享时展示的内容。


九、配置 Markdown Blog

Astro 的 Content Collections 可以扫描指定目录中的 Markdown,并在构建阶段校验文章字段。

创建 src/content.config.ts

import { defineCollection } from "astro:content";
import { glob } from "astro/loaders";
import { z } from "astro/zod";

const blog = defineCollection({
  loader: glob({
    pattern: "**/*.md",
    base: "./src/content/blog",
  }),
  schema: z.object({
    title: z.string(),
    date: z.coerce.date(),
    author: z.string().optional(),
    permalink: z.string().optional(),
    categories: z.array(z.string()).optional().default([]),
    tags: z.array(z.string()).optional().default([]),
    summary: z.string().optional().default(""),
    image: z.string().optional(),
  }),
});

export const collections = { blog };

这里规定:

src/content/blog/ 中创建第一篇文章:

---
title: "我的第一篇文章"
date: "2026-07-29 20:00:00 +0800"
author: "alice"
permalink: "/2026/07/29/my-first-post/"
categories: ["实践"]
tags: ["Astro", "个人网站"]
summary: "记录我使用 Astro 搭建个人网站的过程。"
image: "/images/post-cover.png"
---

这里开始写正文。

## 第一个小标题

支持正常的 Markdown:

- 列表
- 链接
- 图片
- 引用
- 代码块

推荐使用下面的文件名格式:

2026-07-29-my-first-post.md

文件名使用英文或拼音,文章标题可以使用中文。这样生成的网址稳定,也便于跨平台管理。


十、生成 Blog 列表

创建 src/pages/blog/index.astro

---
import { getCollection } from "astro:content";

const posts = (await getCollection("blog")).sort(
  (a, b) => b.data.date.valueOf() - a.data.date.valueOf(),
);
---

<main>
  <h1>写作与笔记</h1>

  {
    posts.map((post) => (
      <a href={post.data.permalink}>
        <time>{post.data.date.toLocaleDateString()}</time>
        <h2>{post.data.title}</h2>
        <p>{post.data.summary}</p>
      </a>
    ))
  }
</main>

getCollection("blog") 会读取全部 Markdown。排序后,最新文章出现在最前面。

首页只显示最近五篇时,可以使用:

const latestPosts = posts.slice(0, 5);

每次新增 Markdown,首页和文章列表都会在构建时自动更新。


十一、生成文章详情页

如果希望文章地址为:

/2026/07/29/my-first-post/

可以创建动态路由:

src/pages/[year]/[month]/[day]/[slug]/index.astro

核心代码:

---
import {
  getCollection,
  render,
  type CollectionEntry,
} from "astro:content";

export async function getStaticPaths() {
  const posts = await getCollection("blog");

  return posts.flatMap((post) => {
    const path = post.data.permalink;
    const match = path?.match(
      /^\/(\d{4})\/(\d{2})\/(\d{2})\/([^/]+)\/$/,
    );

    if (!match) return [];

    const [, year, month, day, slug] = match;

    return [{
      params: { year, month, day, slug },
      props: { post },
    }];
  });
}

interface Props {
  post: CollectionEntry<"blog">;
}

const { post } = Astro.props;
const { Content } = await render(post);
---

<article>
  <header>
    <time>{post.data.date.toLocaleDateString()}</time>
    <h1>{post.data.title}</h1>
    <p>{post.data.summary}</p>
  </header>

  <Content />
</article>

getStaticPaths() 会在构建阶段遍历文章,为每篇 Markdown 生成一个独立 HTML 页面。

这也意味着 permalink 必须符合路由格式。少写一个日期段或结尾斜杠,都可能导致文章未被生成。


十二、生成 RSS

RSS 可以让读者使用订阅工具获取新文章。

创建 src/pages/feed.xml.ts

import rss from "@astrojs/rss";
import { getCollection } from "astro:content";

export async function GET(context: { site: URL }) {
  const posts = (await getCollection("blog")).sort(
    (a, b) => b.data.date.valueOf() - a.data.date.valueOf(),
  );

  return rss({
    title: "我的写作与笔记",
    description: "关于技术、工具和学习过程的记录。",
    site: context.site,
    items: posts.map((post) => ({
      title: post.data.title,
      pubDate: post.data.date,
      description: post.data.summary,
      link: post.data.permalink,
      categories: post.data.tags,
    })),
  });
}

构建完成后,RSS 地址为:

https://alice.github.io/feed.xml

十三、增加中英文页面

中英文页面可以共用一个 HomePage.astro 组件。

中文入口 src/pages/index.astro

---
import HomePage from "../components/HomePage.astro";
---

<HomePage locale="zh" />

英文入口 src/pages/en/index.astro

---
import HomePage from "../../components/HomePage.astro";
---

<HomePage locale="en" />

在组件中选择对应数据:

---
import * as portfolioZh from "../data/portfolio";
import * as portfolioEn from "../data/portfolio.en";

const { locale = "zh" } = Astro.props;
const isEnglish = locale === "en";
const portfolio = isEnglish ? portfolioEn : portfolioZh;
---

最终得到:

https://alice.github.io/
https://alice.github.io/en/

两种语言共用布局、样式和交互。修改页面结构时只改一处,文案和数据分别维护。

还可以在 <head> 中加入语言声明:

<link
  rel="alternate"
  hreflang="en"
  href="https://alice.github.io/en/"
/>

英文页面则指回中文主页。


十四、管理图片和 favicon

放在 public/ 中的文件可以从网站根路径直接访问。

例如:

public/favicon.png
public/images/project-demo.jpg
public/images/post-cover.png

页面中使用:

<img src="/images/project-demo.jpg" alt="项目演示" />

Markdown 中使用:

![图片说明](/images/post-cover.png)

这里不要写:

./public/images/post-cover.png

public 是本地目录名,不会出现在最终网址中。

浏览器会长期缓存 favicon。更换图标时,建议同时修改文件名:

<link rel="icon" href="/favicon-v2.png" />

这样比反复覆盖同一个文件更容易立即看到更新。


十五、配置 GitHub Actions 自动部署

创建:

.github/workflows/deploy.yml

写入:

name: Deploy to GitHub Pages

on:
  push:
    branches: [main]
  workflow_dispatch:

permissions:
  contents: read
  pages: write
  id-token: write

concurrency:
  group: pages
  cancel-in-progress: true

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v7

      - name: Build and upload
        uses: withastro/action@v6
        with:
          package-manager: pnpm@11.7.0

  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    steps:
      - name: Deploy
        id: deployment
        uses: actions/deploy-pages@v5

然后进入 GitHub 仓库:

Settings → Pages

将发布来源设置为:

GitHub Actions

此后每次向 main 分支推送,GitHub 会自动:

  1. 读取仓库;
  2. 安装依赖;
  3. 执行 Astro 构建;
  4. 上传静态文件;
  5. 发布到 GitHub Pages。

如果构建失败,发布步骤不会继续,线上旧版本仍然保留。


十六、第一次发布

本地先执行:

pnpm build

检查通过后提交:

git add .
git commit -m "site: build personal website"
git push origin main

进入 GitHub 仓库的 Actions 页面,可以看到构建和发布状态。

发布完成后访问:

https://alice.github.io/

一般几十秒到几分钟后可以看到新版本。


十七、以后如何更新

新增项目

编辑:

src/data/portfolio.ts

如果有英文主页,同时更新:

src/data/portfolio.en.ts

新增 Blog

在下面的目录创建 Markdown:

src/content/blog/

推荐文件名:

2026-07-29-new-post.md

本地预览

pnpm dev

发布前检查

pnpm build

提交并上线

git add .
git commit -m "blog: add new post"
git push origin main

首页最新文章、Blog 列表、文章详情页和 RSS 都会自动更新。


十八、常见问题

1. 推送后没有自动发布

检查:

2. 本地可以显示,线上图片失效

静态资源放在 public/,引用时从 / 开始:

/images/example.png

注意文件名大小写。macOS 本地文件系统可能不敏感,但 GitHub 的 Linux 构建环境区分大小写。

3. 新文章出现在列表里,点击后却是 404

检查 permalink 是否符合动态路由:

/年/月/日/英文-slug/

同时检查结尾 / 是否存在。

4. Markdown 构建失败

常见原因:

本地运行 pnpm build 会给出具体文件和字段。

5. favicon 一直不更新

浏览器对 favicon 的缓存非常积极。修改文件名并更新 <link rel="icon">,通常比清除全部缓存更直接。

6. 网站地址出现仓库子路径

用户主页仓库必须命名为:

username.github.io

其他仓库会发布在:

username.github.io/repository-name/

这种情况下还需要在 Astro 中配置 base


总结

这套个人网站的维护循环很简单:

修改数据或 Markdown

本地 pnpm build

提交到 GitHub main

自动发布

Astro 负责把页面和文章生成静态文件,GitHub Actions 负责自动检查和构建,GitHub Pages 负责托管。日常更新只涉及 TypeScript 数据和 Markdown 内容。

如果只需要主页,可以省略 Blog、RSS 和多语言部分;如果希望长期写作,这套结构可以继续扩展分类、标签、站内搜索、文章目录和自定义域名。

完整参考: