Skip to content
Go back

Astro 博客搭建指南

Published:

前言

这次使用 Astro + Cloudflare Pages 的方案重新部署了自己的博客,同时使用 Remark42 + fly.io 部署了博客评论系统,这里写一篇文章做一些简单的记录。

Astro 博客主体

使用的主题是 AstroPaper,从 github 上 Use this template 来创建自己的 blog repo,本地预览环境使用 pnpm run dev 即可启动。更改完一些自定义设置后使用 Cloudflare Pages 进行静态部署。

除了一些自定义的配置外,目前我主要对博客进行了如下两处小修改。

更改博客主题配色

参考 PaperMod 更换了一下博客主题配色,更改 global.css 中的 html[data-theme="light"]html[data-theme="dark"] 即可。

/* in global.css */
:root,
html[data-theme="light"] {
  --background: rgb(245, 245, 245);
  --foreground: rgb(31, 31, 31);
  --accent: #3560ab;
  --muted: #e6e6e6;
  --border: #ece9e9;
}

html[data-theme="dark"] {
  --background: rgb(29, 30, 32);
  --foreground: #eaedf3;
  --accent: #1ad9d9;
  --muted: rgb(46, 46, 51);
  --border: #3b4655;
}

更换博客字体

原本打算使用 FiraCode Nerd Font Mono + LXGW WenKai Screen,但感觉 LXGW WenKai Screen 对于博客内容显示来说表现欠佳,所以暂时没有启用。

为了防止 cf 加载 FireCode Font 资源失败,所以在本地也放了一份备用。

/* import "LXGW WenKai Screen" */
@import url("https://cdn.staticfile.org/lxgw-wenkai-screen-webfont/1.6.0/lxgwwenkaiscreen.css");

/* import "FiraCode Nerd Font Mono" */
@layer base {
  @font-face {
    font-family: "FiraCode Nerd Font Mono";
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
         url("/fonts/FiraCode-Regular.woff2") format("woff2");
  }

然后更改 --font-sans :

:root,
html[data-theme="light"] {
  --font-sans: "FiraCode Nerd Font Mono", Inter, ui-sans-serif, system-ui, sans-serif;
}

html[data-theme="dark"] {
  --font-sans: "FiraCode Nerd Font Mono", sans-serif;
}

Cloudflare Pages 部署

感觉这里没啥可说的,在 Pages 页面选中你的 github repo 直接按照默认配置部署即可。创建了一个 deploy 分支,推送到这个分支时才会部署到生产环境,也就是主域名,然后日常 dev 在 main 分支,关闭了预览部署。

Remark42 评论系统

Remark42 部署

原主题没有对评论系统做任何集成,因此需要自己解决博客评论系统问题。我采用的方案是 Remark42 + SaaS 服务,这里的 SaaS 服务目前使用的是 fly.io

fly.io 之前一直都是有每个月的 5 刀免费额度,不过今年没有了,需要绑定一张卡才能后续使用。 fly.io 的部署需要在本地安装他们的 CLI,安装完成后,使用 fly auth login 完成登录。

编写 Remark42 配置文件 fly.toml:

app = 'se-remark42-01'
primary_region = 'hkg'

[build]
  image = 'umputun/remark42:latest'

[env]
  ADMIN_SHARED_ID = ''
  REMARK_URL = 'https://remark42.silente.dev/'
  # REMARK_URL = 'https://se-remark42-01.fly.dev/'
  SITE = 'silente.dev'

[[mounts]]
  source = 'remark42_data_01'
  destination = '/srv/var'

[http_service]
  internal_port = 8080
  force_https = true
  auto_stop_machines = 'off'
  auto_start_machines = true
  min_machines_running = 1
  processes = ['app']

[[vm]]
  size = 'shared-cpu-1x'

配置文件需要改动的地方是:

  • app: 应用唯一名称
  • REMARK_URL: remark42 服务的 domain,fly.io 提供自定义域名服务,可以先部署一次然后在他们的 dashboard 中进行添加;如果想直接使用 fly.io 自己的域名则可以参考注释中的格式: ${app}.fly.dev
  • SITE: 站点名称,可以随便设置
  • ADMIN_SHARED_ID: 管理员 ID,这里可以先不设置,后续配置完成后再使用 fly secrets 设置

配置完成后,使用 flyctl launch 部署服务,这样应该就能从 fly.io 的 Dashboard 上看到 Remark42 服务了。

接下来进行其他环境变量的配置,创建一个 prod.env 文件,配置以下变量:

SECRET=xxx
AUTH_GITHUB_CID=xxx
AUTH_GITHUB_CSEC=xxx
AUTH_ANON=true
ADMIN_SHARED_ID=xxxx

SECRET 用来生成 JWT,所以手滚键盘输出一个即可;AUTH_GITHUB_CIDAUTH_GITHUB_CSEC 则用来配置 Github 认证登录,这里可以去参考 Remark42 官方指南获取这两个变量值,其他的认证方式同理配置。至于 ADMIN_SHARED_ID 这个值需要等到你配置好认证方式并登录后,点开你的头像即可看到 ID。

在每次更新 prod.env 时,使用 fly secrets import < prod.env 将配置变量导入 fly secrets 中,然后使用 fly deploy 触发重新部署。

至于 Remark42 部署在 fly.io 上目前的费用的话差不多 1 天 0.1 刀。

集成评论系统

创建一个 Remark.astro 组件

---
declare global {
    interface Window {
        remark_config: {
            host: string;
            site_id: string;
            components: string[];
            max_shown_comments: number;
            theme: string;
            locale: string;
            show_email_subscription: boolean;
            simple_view: boolean;
        };
        REMARK42: {
            createInstance: (config: {
                node: HTMLElement;
                host: string;
                site_id: string;
                max_shown_comments: number;
                theme: string;
                locale: string;
                show_email_subscription: boolean;
                simple_view: boolean;
            }) => Remark42Instance;
            ready: boolean;
        };
    }
    
    // Type for Remark42 instance
    interface Remark42Instance {
        destroy: () => void;
    }
}

export interface Props {
    noMarginTop?: boolean;
}

---
<div class="comments" class="mx-auto w-full px-4 pb-12">
    <div id="remark42" class="mx-auto max-w-3xl"></div>
</div>

<script>
    // Type for Remark42 instance
    interface Remark42Instance {
        destroy: () => void;
    }
    
    let remark42Instance: Remark42Instance | null = null;

    // Configuration for Remark42
    window.remark_config = {
        host: 'https://remark42.silente.dev',
        site_id: 'silente.dev',
        components: ['embed', 'counter'],
        max_shown_comments: 100,
        theme: localStorage.getItem('theme') || 'light',
        locale: 'en',
        show_email_subscription: false,
        simple_view: true
    };

    // Initialize Remark42 instance
    function initRemark42() {
        if (window.REMARK42) {
            // Destroy previous instance if exists
            if (remark42Instance) {
                remark42Instance.destroy();
            }

            // Create new instance
            const node = document.getElementById('remark42');
            if (node) {
                remark42Instance = window.REMARK42.createInstance({
                    node,
                    host: window.remark_config.host,
                    site_id: window.remark_config.site_id,
                    max_shown_comments: window.remark_config.max_shown_comments,
                    theme: window.remark_config.theme,
                    locale: window.remark_config.locale,
                    show_email_subscription: window.remark_config.show_email_subscription,
                    simple_view: window.remark_config.simple_view
                });
            }
        }
    }

    // Load Remark42 scripts
    (function(components, doc) {
        for (let i = 0; i < components.length; i++) {
            const script = doc.createElement('script') as HTMLScriptElement;
            const extension = 'noModule' in script ? '.mjs' : '.js';
            
            if ('noModule' in script) {
                script.type = 'module';
            } else {
                (script as HTMLScriptElement).async = true;
                (script as HTMLScriptElement).defer = true;
            }
            
            script.src = `${window.remark_config.host}/web/${components[i]}${extension}`;
            (doc.head || doc.body).appendChild(script);
        }
    })(window.remark_config.components || ['embed'], document);

    // Initialize when Remark42 is ready or wait for it to be ready
    document.addEventListener('DOMContentLoaded', () => {
        if (window.REMARK42) {
            initRemark42();
        } else {
            window.addEventListener('REMARK42::ready', () => {
                initRemark42();
            });
        }
    });

    // Handle Astro page transitions
    document.addEventListener('astro:page-load', () => {
        initRemark42();
    });

    // Clean up before page transitions
    document.addEventListener('astro:before-swap', () => {
        if (remark42Instance) {
            remark42Instance.destroy();
        }
    });
</script>

在文章组件中加入并放在合适的位置,这里根据 Astro 主题不同放置的位置也不同,对于 AstroPapaer 来说我放在了 PostDetails.astro<Footer /> 的下方。