Skip to content
Go back

基于 Astro Paper 的个人博客:深度定制和部署实践

今天是我独立博客走过的第八个年头。还记得那一年怀着对独立站的疑问,给孔令贤发邮件,询问是否可以使用他写的轮子,就是从他回复我那一刻起,我掉进了 WEB 深渊。

独立博客这个词,在 2025 年这个年代确实足够小众,但其中的快乐和对生活的态度,想必也只有博友能理解。

正是为了这第八个年头,才有了今天这全新的博客。从年初用 Jekyll 从零开始写,后来又用 Recat 写个半成品。最终阴差阳错选择了开源的 Astro Paper。

Astro Paper 这款主题性能极强,可拓展性也非常高,这也得益于 Astro 的静态特性和原作者优越设计。

经过一段时间的二次开发,这个博客差不多达到了我理想的样子。

在全站无缝刷新的基础下,我把博客全站的图片都做了懒加载,订阅和归档模块也做了滚动懒加载。

再加上页面内链的预加载处理,无论你点击哪个页面,都是一种享受。

Lighthouse 评分

下面就把新增的功能一一道来。

分类路由支持

Astro Paper 原生不支持平铺式 URL,也不能把文章进行分类:

改进后:

  1. 文章可按分类(技术、生活、运动等)划分路由。
  2. 支持按年份归档,且不会影响已有 URL 访问。
  3. URL 更加简洁:

分类路由结构如下,可在/src/pages/中按需创建:

blog/
  ├── _releases/
  ├── examples/
  ├── life/
   ├── 2024/
   └── 2025/
  ├── sports/
   ├── 2024/
   └── 2025/
  └── technology/
      ├── 2024/
      └── 2025/

路由中间件处理

在不用 Artalk 评论系统的情况下,这个功能其实可有可无。但 Artalk 在路径识别上,存在很大的问题(带斜杠与不带斜杠会被视为不同页面),存在一定的隐患。

其实使用 Nginx 会更方便一些。

import { defineMiddleware } from "astro:middleware";

export const onRequest = defineMiddleware((context, next) => {
  const url = context.url;
  const pathname = url.pathname;
  
  const staticExtensions = [
    '.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.ico',
    '.css', '.js', '.json', '.xml', '.txt', '.pdf',
    '.woff', '.woff2', '.ttf', '.eot', '.otf'
  ];
  
  const isStaticResource = staticExtensions.some(ext => 
    pathname.toLowerCase().endsWith(ext)
  );
  
  // 如果是静态资源,不处理尾部斜杠
  if (isStaticResource) {
    return next();
  }
  
  // 如果URL不以斜杠结尾且不是根路径,则重定向到带斜杠的版本
  if (pathname !== "/" && !pathname.endsWith("/")) {
    const newUrl = new URL(pathname + "/" + url.search + url.hash, url.origin);
    return Response.redirect(newUrl.toString(), 301);
  }
  
  return next();
}); 

运动数据可视化

接入 Strava Riding Api 做了运动数据的可视化。

  1. 光标悬浮可切换月数据
  2. 点击日期可查看当天运动详情

EXIF 元数据显示

借助腾讯云数据万象 API,默认自动启用。

  1. 参数为 Boolean 类型,false 可禁用
  2. 光标悬浮显示两秒
  3. 解析失败会自动生成合理参数并缓存
import Img from "@/components/Img.astro";

<Img src="20250524003018.jpg" />
Loading EXIF data...
EXIF 示例

图片标签

所有图片默认使用长标签,支持切换为短标签或禁用标签

Loading EXIF data...
义乌美术馆一角
义乌美术馆一角
Loading EXIF data...
义乌美术馆一角
义乌美术馆一角

若想在文章中启用 EXIF,需要将 .md 改为 .mdx,并引入组件:

import Img from "@/components/Img.astro";

<Img 
  src="20250530173339.jpg"
  alt="义乌美术馆一角"
  caption="short" // false 表示不显示
/>

悬停提示(tooltip)效果

图片的 title 属性不必声明,只要有 alt 属性,Img.astro 组件就会自动读取并渲染到页面中。

数学公式支持

通过 KaTeX 集成 支持了数学公式。纯静态渲染,无性能问题。示例:

骑行里程=均速×时间\text{骑行里程} = \text{均速} \times \text{时间}
$$
\text{骑行里程} = \text{均速} \times \text{时间}
$$

Artalk 集成

说到评论系统,首先感谢 Disqus PHP API 开源作者 Fooleap,感谢好大哥这些年来帮我在境外挂着接口…

Artalk 官方提供了简单的配置文件,不过足够了

services:
  artalk:
    container_name: artalk
    image: artalk/artalk-go
    restart: unless-stopped
    ports:
      - 9998:23366
    volumes:
      - ./data:/data
    environment:
      - TZ=Asia/Shanghai
      - ATK_LOCALE=zh-CN
      - ATK_SITE_DEFAULT=游钓四方的博客
      - ATK_SITE_URL=https://lhasa.icu

创建容器运行 Artalk:

docker-compose up -d

# 执行命令创建管理员账户
docker exec -it artalk artalk admin

再使用 Nginx 反代 9998 端口就可以实现域名访问了。

由于我是 Disqus 迁移过来的,需要把格式转换为 Artrans,然后再导入 Artalk。

由于无缝刷新的存在,就单单评论来说,调试花了不少时间,踩了很多坑,这里还把 Artalk 随着主题变化适配了配色。

<script is:inline data-astro-rerun>
(function () {
  // 单例模式存储 Artalk 实例
  window.artalkInstance = window.artalkInstance || null;

  const artalkConfig = {
    el: "#Comments",
    server: "https://artalk.lhasa.icu",
    site: "游钓四方的博客",
    pageKey: window.location.pathname,
    vote: false,

  };

  function destroyArtalk() {
    if (window.artalkInstance) {
      try {
        window.artalkInstance.destroy();
        document
          .querySelectorAll(".atk-sidebar, .atk-layer-wrap")
          .forEach(el => el.remove());
        window.artalkInstance = null;
        console.log("Artalk 实例已销毁", window.location.pathname);
      } catch (err) {
        console.error("销毁失败:", err);
      }
    }
  }

  // 初始化 Artalk 实例
  function initArtalk() {
    const container = document.getElementById("Comments");
    if (!container || container.querySelector(".atk-app")) return;

    const isDark = document.documentElement.getAttribute("data-theme") === "dark";

    artalkConfig.pageKey = window.location.pathname;
    artalkConfig.darkMode = isDark;

    window.artalkInstance = Artalk.init(artalkConfig);
    console.log("Artalk 初始化完成", window.location.pathname);
  }

  function handleThemeChange() {
    const themeBtn = document.querySelector("#theme-btn");
    if (!themeBtn) return;

    const observer = new MutationObserver(mutations => {
      mutations.forEach(mutation => {
        if (mutation.attributeName === "aria-label") {
          const isDark =
            mutation.target.getAttribute("aria-label") === "dark";
          if (window.artalkInstance) {
            window.artalkInstance.setDarkMode(isDark);
          }
        }
      });
    });

    observer.observe(themeBtn, {
      attributes: true,
      attributeFilter: ["aria-label"],
    });
  }

  function handlePageLoad() {
    destroyArtalk();
    initArtalk();
    handleThemeChange();
  }

  function setupArtalk() {
    if (window._artalkInitialized) return;
    window._artalkInitialized = true;

    document.addEventListener("astro:before-swap", destroyArtalk);
    document.addEventListener("astro:after-swap", handlePageLoad);

    if (document.readyState === "complete") {
      handlePageLoad();
    } else {
      document.addEventListener("DOMContentLoaded", handlePageLoad);
    }

    // 监听主题
    window
      .matchMedia("(prefers-color-scheme: dark)")
      .addEventListener("change", ({ matches }) => {
        if (window.artalkInstance) {
          window.artalkInstance.setDarkMode(matches);
        }
      });
  }

  setupArtalk();
})();
</script>

Artalk 自带的验证码不好用,这里强烈推荐 Cloudflare Turnstile。无感验证,很省心。

在 Cloudflare 控制台主页可以看到 Turnstile,在填完域名后可以申请到Site KeySecret Key

随后打开 Artalk 控制中心,填入相应参数后,captcha_type选择turnstile即可。

本地开发

纯净版 Astro Paper:

# pnpm
pnpm create astro@latest --template satnaing/astro-paper

# npm
npm create astro@latest -- --template satnaing/astro-paper

# yarn
yarn create astro --template satnaing/astro-paper

或者直接使用我的扩展版本:

git clone https://github.com/achuanya/Blog.git

然后通过安装依赖启动开发环境

# 安装依赖
pnpm install

# 启动开发环境
pnpm dev

Docker 部署

用于生产环境的 Docker 配置已经写好了,可以直接构建镜像。

# 构建生产镜像
docker build -t astropaper .

# 启动生产环境,端口为 4321
docker run -p 4321:80 astropaper

配合 Nginx 反代:

server {
    listen 80;
    server_name lhasa.icu;

    # 404
    error_page 404 /404.html;
    location = /404.html {
        root /home/github/Blog/dist;
        internal;
    }

    location / {
        proxy_pass http://127.0.0.1:4321;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

不想折腾,建议安装宝塔Linux面板,随便点击几下,两分钟上线

if [ -f /usr/bin/curl ];then curl -sSO https://download.bt.cn/install/install_panel.sh;else wget -O install_panel.sh https://download.bt.cn/install/install_panel.sh;fi;bash install_panel.sh ed8484bec

Github + Vercel 部署

只需点击 Deploy 按钮,按提示一步步即可上线。

相关命令

CommandAction
pnpm install安装依赖项
pnpm run dev启动本地开发服务器,访问地址为 localhost:4321
pnpm run build将生产环境网站构建到 ./dist/ 目录
pnpm run preview本地预览生产环境构建的站点,部署前检查效果
pnpm run format:check使用 Prettier 检查代码格式
pnpm run format使用 Prettier 格式化代码
pnpm run sync为所有 Astro 模块生成 TypeScript 类型。 了解更多
pnpm run lint使用 ESLint 进行代码检查
docker compose up -d使用 Docker 运行 AstroPaper,可通过 dev 命令中相同的主机名和端口进行访问
docker compose run app npm install在 Docker 容器中执行任意上述命令
docker build -t astropaper .为 AstroPaper 构建 Docker 镜像
docker run -p 4321:80 astropaper在 Docker 中运行 AstroPaper。网站可通过 http://localhost:4321 访问

注意!
Windows PowerShell 用户如果想在开发期间运行诊断(例如 astro check --watch & astro dev),可能需要安装 concurrently 包
更多信息请参考 这个 issue

下一步

目前来说,还有很多地方没有完善,细节没有做到位:

  1. Strava Riding Api 还没有实现完全自动化,更新数据还是需要人工
  2. Img.astro 组件没有封装到位,还有细节需要把控
  3. Sports 在移动端时的表现还需要好好想想
  4. 给 Feeds 做个后台管理,先把头像显示问题解决了。当然,有邮箱最好
  5. 吃透 Astro Paper 无缝刷新机制

参考文档



Previous Post
端午骑行:倍鱼线
Next Post
雨骑枫赤线