CoderXL's Blog

Back

blog.leosrealms.top 建站#

上周 RMV 五次培训宣告完结,高强度编码告一段落。周五晚,我思考是否应该找点项目开始写,恰好高宁学长推荐我维护一个 blog,记录科研笔记、学习历程,长期来说还可以作为个人能力的量化。于是,我开始准备这个博客。

因为先前申请过一个域名,并且在自己家整了个小服务器做了 ddns+nas,故而首先考虑的是在自己的家用服务器 apache2 上进行部署。然而考虑到博客相比 nas 更加公开,访问频次也更高,加之自己的 ddns 服务器会因为各种诸如路由器关机、宽带不稳定、运营商限流等等原因不定期抽风,因此最终选择了 Cloudflare Pages。大致流程:在工作电脑上建立 Astro 项目,关联 Github 仓库。这一步之后,如果选择 Cloudflare Pages,那么到 Cloudflare 仪表盘上创建项目并关联 Github 仓库即可;而如果要使用自己的服务器,则应当配置 Github Actions。到这一步,静态站就算建好了,已经可以在自己电脑上编写 .astro 和 .md/.mdx 进行 post 了。

然而,个人觉得这种方式稍显笨重,更加倾向于类似 WP、知乎等 CMS 编写流,因此打算额外加一个 CMS app。最终考虑 Decap CMS,此处省略遇到的各种版本兼容、回调配置问题,简述一步到位的正确流程:

首先,理解 Astro 的结构。假设 my-astro-blog/ 是项目根,那么一个典型的 Astro 项目目录大致如下:

综上所述,应当将 CMS app 的相关文件置于 /public 下,对于 decap,主要文件有

  • index.html,用于承载其 single page app
  • config.yml,存放交互 git 仓库所需的 backend 配置,比如 repo 地址,branch,oauth 相关配置等等
index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="robots" content="noindex" />
    <title>Content Manager</title>
  </head>
  <body>
    <!-- CMS UI will be injected here -->
    <script src="https://cdn.jsdelivr.net/npm/decap-cms@3.0.0/dist/decap-cms.js"></script>
    <script>
      document.addEventListener('DOMContentLoaded', () => {
        try {
          CMS.init();
        } catch (err) {
          console.error('CMS init error:', err);
        }
      });
    </script>
  </body>
</html>
html

config.yml
backend:
  name: github
  repo: Username/repo
  branch: main
  base_url: https://yourdomain.com/
  auth_endpoint: api/auth # 配置 GitHub OAuth 时使用

media_folder: "public/uploads"     # 上传的图片存储路径
public_folder: "/uploads"

collections:
  - name: "blog"
    label: "Blog"
    folder: "src/content/blog"
    create: true
    slug: "{{year}}-{{month}}-{{day}}-{{slug}}"
    fields:
      - {label: "Title", name: "title", widget: "string"}
      - {label: "Publish Date", name: "date", widget: "datetime"}
      - {label: "Body", name: "body", widget: "markdown"}
yml

之后来配置 OAuth,这里简单介绍一下。OAuth 的意思是 Open Authentication,允许用户通过 Github、Google 等大平台账号直接登录第三方应用,而无需自建账号密码。但我们这里使用 OAuth 的目的更加特化,当用户通过自己的 Github 账号登录 CMS 后,不仅提供了用户名、头像,还提供了读取用户自己的仓库信息并进行修改的权限,因此 CMS 可以管理用户放在 Github 上的 Astro 仓库。故理论上别人也可以访问我的 CMS,登录自己的 Github 账号,然后管理自己的静态网页仓库(若有)。

然而实际上这是不行的,因为 Decap CMS 要求在 config.yml 中指明要管理的仓库,即使获得了其他仓库的读写权,CMS 仍然会尝试 access 我的仓库,从而无法管理其他用户的项目。从这个角度看,我的 CMS 是我个人独有的,但 Oauth 部分确实可以多用户复用。

具体到实践层面,我先创建了一个 OAuth 应用程序,用于获取 access_token;然后使用 Cloudflare Worker 作为认证后端,调用 Github OAuth 的 API 获得 code,再兑换为 access_token;然后 Decap CMS 登录时跳转 Worker,认证完毕后 Worker 将 access_token 回传给 CMS,完成登录。 以下是较通用的 Worker 代码:

worker.js
// Cloudflare Worker for Decap CMS GitHub OAuth

const GITHUB_BASE = 'https://github.com';
const GITHUB_AUTHORIZE_PATH = '/login/oauth/authorize';
const GITHUB_TOKEN_PATH = '/login/oauth/access_token';

// 生成随机 state(替代 Node 的 randomBytes)
function generateState() {
  const array = new Uint8Array(4);
  crypto.getRandomValues(array);
  return Array.from(array).map(b => b.toString(16).padStart(2, '0')).join('');
}

// 构建 GitHub OAuth 授权 URL
function createOAuthURL(env, redirect_uri) {
  const state = generateState();
  return `${GITHUB_BASE}${GITHUB_AUTHORIZE_PATH}?response_type=code&client_id=${env.CLIENT_ID}&redirect_uri=${encodeURIComponent(redirect_uri)}&scope=repo&state=${state}`;
}

// 获取 access token
async function getAccessToken(env, code, redirect_uri) {
  const res = await fetch(`${GITHUB_BASE}${GITHUB_TOKEN_PATH}`, {
    method: 'POST',
    headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' },
    body: JSON.stringify({
      client_id: env.CLIENT_ID,
      client_secret: env.CLIENT_SECRET,
      code,
      redirect_uri,
      grant_type: 'authorization_code',
    }),
  });
  const json = await res.json();
  return json.access_token;
}

// 返回 HTML 页面,弹窗回传 token 并关闭
function callbackScriptResponse(status, token) {
  return new Response(`
<!DOCTYPE html>
<html>
<head>
  <title>Decap OAuth</title>
  <script>
// IMPORTANT for returning access_token to Decap!
    const receiveMessage = (message) => {
      window.opener.postMessage(
        'authorization:github:${status}:${JSON.stringify({ token })}',
        '*'
      );
      window.removeEventListener("message", receiveMessage, false);
    }
    window.addEventListener("message", receiveMessage, false);
    window.opener.postMessage("authorizing:github", "*");
  </script>
</head>
<body>
  <p>Authorizing Decap...</p>
</body>
</html>
  `, { headers: { 'Content-Type': 'text/html' } });
}

export default {
  async fetch(request, env) {
    const url = new URL(request.url);

    // Step 1: 发起 GitHub OAuth
    if (url.pathname === '/api/auth') {
      const redirect_uri = `https://${url.hostname}/api/auth/callback`;
      const authorizationUri = createOAuthURL(env, redirect_uri);
      return new Response(null, { headers: { location: authorizationUri }, status: 301 });
    }

    // Step 2: GitHub OAuth 回调
    if (url.pathname === '/api/auth/callback') {
      const code = url.searchParams.get('code');
      if (!code) return new Response('Missing code', { status: 400 });

      const redirect_uri = `https://${url.hostname}/api/auth/callback`;
      const token = await getAccessToken(env, code, redirect_uri);
      return callbackScriptResponse('success', token);
    }

    return new Response('Not Found', { status: 404 });
  },
};
js

这个过程中不要忘记给 worker 加路由。

总结一下,通过这种方式在 CMS 上 post 的流程就是:

flowchart TD
    U["Composer(Me)"] -->|Login| A
    A --> |auth backend| W(Cloudflare Worker) -->|redirect to| O(Github OAuth)
    O --> |return \`code\`| W
    W --> |\`code\`| G(Github API)
    G --> |\`access_token\`| W
    W --> |\`access_token\`| A
    U -->|Write a Post| A
    A{Decap CMS} -->|access_token| B(Github Repo for your site)
    A <--> |read and commit|B
    B --> |CD build <br> on repo changes|C(Cloudflare Pages)
    click A href "https://decapcms.org/"
    click O href "https://github.com/settings/developers"
    click G href "https://docs.github.com/zh/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps"
    click W href "https://dash.cloudflare.com/?to=/:account/workers"
    click C href "https://dash.cloudflare.com/?to=/:account/pages"

总结一下未来要改进的部分:这里 OAuth 给的权限稍大,能读写整个账号下的仓库,我会再研究一下如何细化权限;然后会考虑魔改美化一下 Decap,或者接入写作辅助大模型 API;隐藏 config.yml,避免被 client 直接访问;给 OAuth 和 CMS 都加上图标和合适的名字🤣。

建站第一弹
https://blog.leosrealms.top/blog/2025-10-18-first-post
Author CoderXL
Published at 2025年10月18日
Comment seems to stuck. Try to refresh?✨