如何使用 Cloudflare、GitHub Actions 和 Metalsmith 创建一个简单的 CMS

Avatar of Jon Paul Uritis
Jon Paul Uritis

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 $200 免费积分!

让我们自己构建一个 CMS。但我们不会构建 UI,而是会以 GitHub 的形式免费获得该 UI!我们将利用 GitHub 作为管理静态网站生成器内容的方式(它可以是 _任何_ 静态网站生成器)。以下是其要点:GitHub 将成为管理、版本控制和存储文件的地方,同时也是我们进行内容编辑的地方。当发生编辑时,一系列自动化将测试、验证并最终将我们的内容部署到 Cloudflare。

您可以在 GitHub 上找到该项目的完整代码。我使用这种确切的方式来运行自己的网站 jonpauluritis.com

完整堆栈是什么样子的?

以下是我们将在本文中使用的技术堆栈

  • 任何 Markdown 编辑器(可选。例如 Typora.io
  • 静态网站生成器(例如 Metalsmith)
  • 带 GitHub Actions 的 Github(CICD 和部署)
  • Cloudflare Workers

为什么您应该关心这种设置?这种设置可能是管理网站(或 Jamstack 网站)最精简、最快、最便宜(约 5 美元/月)且最简单的方式。从技术方面和用户体验方面来说,它都非常棒。这种设置非常棒,我直接去购买了微软和 Cloudflare 的股票。

但在我们开始之前……

我不会引导您完成在这些服务上设置帐户的过程,我相信您自己可以做到。以下是您需要设置的帐户:

我还建议您使用 Typora 获得绝佳的 Markdown 写作体验,但 Markdown 编辑器是非常个人化的东西,因此请使用您觉得合适的编辑器。

项目结构

为了让您了解我们将要做什么,以下是完成项目的结构

├── build.js
├── .github/workflows
│   ├── deploy.yml
│   └── nodejs.js
├── layouts
│   ├── about.hbs
│   ├── article.hbs
│   ├── index.hbs
│   └── partials
│       └── navigation.hbs
├── package-lock.json
├── package.json
├── public
├── src
│   ├── about.md
│   ├── articles
│   │   ├── post1.md
│   │   └── post2.md
│   └── index.md
├── workers-site
└── wrangler.toml

步骤 1:命令行操作

在终端中,将目录更改为您保存此类项目的位置,并键入以下内容

$ mkdir cms && cd cms && npm init -y

这将创建一个新目录,进入该目录,并初始化 npm 的使用。

接下来我们要做的是站在巨人的肩膀上。我们将使用许多 npm 包来帮助我们完成操作,其中最重要的是使用静态网站生成器 Metalsmith

$ npm install --save-dev metalsmith metalsmith-markdown metalsmith-layouts metalsmith-collections metalsmith-permalinks handlebars jstransformer-handlebars

除了 Metalsmith 之外,还有其他一些有用的组件。为什么要使用 Metalsmith?我们来谈谈这个。

步骤 2:Metalsmith

我已经尝试使用静态网站生成器 2-3 年了,但我仍然没有找到“那个”。所有的大牌 — 例如 EleventyGatsbyHugoJekyllHexoVuepress — 都非常棒,但我无法克服 Metalsmith 的简单性和可扩展性。

例如,这段代码实际上会构建一个网站:

// EXAMPLE... NOT WHAT WE ARE USING FOR THIS TUTORIAL
Metalsmith(__dirname)         
  .source('src')       
  .destination('dest')     
  .use(markdown())             
  .use(layouts())           
  .build((err) => if (err) throw err);

很酷吧?

为了简洁起见,将此代码键入终端,我们将构建一些结构和文件作为起点。

首先,创建目录

$ mkdir -p src/articles &&  mkdir -p layouts/partials 

然后,创建构建文件

$ touch build.js

接下来,我们将创建一些布局文件

$ touch layouts/index.hbs && touch layouts/about.hbs && touch layouts/article.hbs && touch layouts/partials/navigation.hbt

最后,我们将设置我们的内容资源

$ touch src/index.md && touch src/about.md && touch src/articles/post1.md && touch src/articles/post1.md touch src/articles/post2.md

项目文件夹应如下所示

├── build.js
├── layouts
│   ├── about.hbs
│   ├── article.hbs
│   ├── index.hbs
│   └── partials
│       └── navigation.hbs
├── package-lock.json
├── package.json
└── src
    ├── about.md
    ├── articles
    │   ├── post1.md
    │   └── post2.md
    └── index.md

步骤 3:添加一些代码

为了节省空间(和时间),您可以使用以下命令创建我们虚构网站的内容。您可以随意进入“articles”并创建自己的博文。关键是这些文章需要一些元数据(也称为“前置 matter”)才能正确生成。您需要编辑的文件是 index.mdpost1.mdpost2.md

元数据应如下所示:

---
title: 'Post1'
layout: article.hbs 
---
## Post content here....

或者,如果您像我一样懒,可以使用以下终端命令将 GitHub Gists 的模拟内容添加到您的网站

$ curl https://gist.githubusercontent.com/jppope/35dd682f962e311241d2f502e3d8fa25/raw/ec9991fb2d5d2c2095ea9d9161f33290e7d9bb9e/index.md > src/index.md
$ curl https://gist.githubusercontent.com/jppope/2f6b3a602a3654b334c4d8df047db846/raw/88d90cec62be6ad0b3ee113ad0e1179dfbbb132b/about.md > src/about.md
$ curl https://gist.githubusercontent.com/jppope/98a31761a9e086604897e115548829c4/raw/6fc1a538e62c237f5de01a926865568926f545e1/post1.md > src/articles/post1.md
$ curl https://gist.githubusercontent.com/jppope/b686802621853a94a8a7695eb2bc4c84/raw/9dc07085d56953a718aeca40a3f71319d14410e7/post2.md > src/articles/post2.md

接下来,我们将创建布局和部分布局(“partials”)。在本教程中,我们将使用 Handlebars.js 作为我们的模板语言,但您可以使用任何您喜欢的模板语言。Metalsmith 可以与几乎所有模板语言配合使用,我对模板语言没有强烈的偏好。

构建索引布局

<!DOCTYPE html>
<html lang="en">
  <head>
    <style>
      /* Keeping it simple for the tutorial */
      body {
        font-family: 'Avenir', Helvetica, Arial, sans-serif;
        -webkit-font-smoothing: antialiased;
        -moz-osx-font-smoothing: grayscale;
        text-align: center;
        color: #2c3e50;
        margin-top: 60px;
      }
      .navigation {
        display: flex;
        justify-content: center;
        margin: 2rem 1rem;
      }
      .button {
        margin: 1rem;
        border: solid 1px #ccc;
        border-radius: 4px;        
        padding: 0.5rem 1rem;
        text-decoration: none;
      }
    </style>
  </head>
  <body>
    {{>navigation }}
    <div>
       {{#each articles }}
        <a href="{{path}}"><h3>{{ title }}</h3></a>
        <p>{{ description }}</p>
       {{/each }}
    </div>
  </body>
</html>

几点说明:

  • 我们的“导航”尚未定义,但最终将替换 {{>navigation }} 所在的区域。
  • {{#each }} 将遍历 metalsmith 在其构建过程中生成的“collection”文章。
  • Metalsmith 有 _很多_ 插件 可用于样式表、标签等,但这并不是本教程的重点,因此我们将其留给您探索。

构建关于页面

将以下内容添加到您的 about.hbs 页面

<!DOCTYPE html>
<html lang="en">
  <head>
    <style>
      /* Keeping it simple for the tutorial */
      body {
        font-family: 'Avenir', Helvetica, Arial, sans-serif;
        -webkit-font-smoothing: antialiased;
        -moz-osx-font-smoothing: grayscale;
        text-align: center;
        color: #2c3e50;
        margin-top: 60px;
      }
      .navigation {
        display: flex;
        justify-content: center;
        margin: 2rem 1rem;
      }
      .button {
        margin: 1rem;
        border: solid 1px #ccc;
        border-radius: 4px;        
        padding: 0.5rem 1rem;
        text-decoration: none;
      }    
    </style>
  </head>
  <body>
    {{>navigation }}
    <div>
      {{{contents}}}
    </div>
  </body>
</html>

构建文章布局

<!DOCTYPE html>
<html lang="en">
  <head>
    <style>
      /* Keeping it simple for the tutorial */
      body {
        font-family: 'Avenir', Helvetica, Arial, sans-serif;
        -webkit-font-smoothing: antialiased;
        -moz-osx-font-smoothing: grayscale;
        text-align: center;
        color: #2c3e50;
        margin-top: 60px;
      }
      .navigation {
        display: flex;
        justify-content: center;
        margin: 2rem 1rem;
      }
      .button {
        margin: 1rem;
        border: solid 1px #ccc;
        border-radius: 4px;        
        padding: 0.5rem 1rem;
        text-decoration: none;
      }
    </style>
  </head>
  <body>
    {{>navigation }}
    <div>
      {{{contents}}}
    </div>
  </body>
</html>

您可能已经注意到,这与关于页面的布局完全相同。是的,就是这样。我只是想说明如何添加其他页面,以便您知道如何操作。如果您想让这个页面与众不同,请随意进行更改。

添加导航

将以下内容添加到 layouts/partials/navigation.hbs 文件

<div class="navigation">
  <div>
    <a class="button" href="/">Home</a>
    <a class="button" href="/about">About</a>
  </div>
</div>

当然,它并没有什么特别的……但这并不是关于 Metalsmith/SSG 的教程。 ¯\_(ツ)_/¯

步骤 4:构建文件

Metalsmith 的核心是构建文件。为了完整起见,我将逐行介绍它。

我们首先导入依赖项

快速提示: Metalsmith 创建于 2014 年,当时主要的模块系统是 common.js,所以我将坚持使用 require 语句而不是 ES 模块。值得注意的是,大多数其他教程也使用 require 语句,因此跳过使用 Babel 进行构建步骤只会让这里的生活稍微不那么复杂。

// What we use to glue everything together
const Metalsmith = require('metalsmith');


// compile from markdown (you can use targets as well)
const markdown = require('metalsmith-markdown');


// compiles layouts
const layouts = require('metalsmith-layouts');


// used to build collections of articles
const collections = require('metalsmith-collections');


// permalinks to clean up routes
const permalinks = require('metalsmith-permalinks');


// templating
const handlebars = require('handlebars');


// register the navigation
const fs = require('fs');
handlebars.registerPartial('navigation', fs.readFileSync(__dirname + '/layouts/partials/navigation.hbt').toString());


// NOTE: Uncomment if you want a server for development
// const serve = require('metalsmith-serve');
// const watch = require('metalsmith-watch');

接下来,我们将包含 Metalsmith 并告诉它在哪里找到它的编译目标

// Metalsmith
Metalsmith(__dirname)            
  // where your markdown files are
  .source('src')      
  // where you want the compliled files to be rendered
  .destination('public')

到目前为止,一切都很好。在设置了源和目标之后,我们将设置 markdown 渲染、布局渲染,并让 Metalsmith 知道使用“集合”。这些是将文件组合在一起的一种方式。一个简单的例子可能是“博文”,但实际上它可以是任何东西,比如食谱、威士忌评论或任何其他东西。在上面的例子中,我们称这个集合为“文章”。

 // previous code would go here


  // collections create groups of similar content
  .use(collections({ 
    articles: {
      pattern: 'articles/*.md',
    },
  }))
  // compile from markdown
  .use(markdown())
  // nicer looking links
  .use(permalinks({
    pattern: ':collection/:title'
  }))
  // build layouts using handlebars templates
  // also tell metalsmith where to find the raw input
  .use(layouts({
    engine: 'handlebars',
    directory: './layouts',
    default: 'article.html',
    pattern: ["*/*/*html", "*/*html", "*html"],
    partials: {
      navigation: 'partials/navigation',
    }
  }))


// NOTE: Uncomment if you want a server for development
// .use(serve({
//   port: 8081,
//   verbose: true
// }))
// .use(watch({
//   paths: {
//     "${source}/**/*": true,
//     "layouts/**/*": "**/*",
//   }
// }))

接下来,我们将添加 markdown 插件,这样我们就可以使用 markdown 来编写内容并将其编译成 HTML。

从那里,我们使用 layouts 插件 将我们的原始内容包裹在我们在 layouts 文件夹中定义的布局中。您可以在官方插件网站上了解有关此功能的更多信息,但结果是,我们可以在模板中使用 {{{contents}}},它会正常工作。

我们这个小型构建脚本的最后一个添加项将是构建方法

// Everything else would be above this
.build(function(err) {
  if (err) {
    console.error(err)
  }
  else {
    console.log('build completed!');
  }
});

将所有内容组合在一起,我们应该得到一个如下所示的构建脚本

const Metalsmith = require('metalsmith');
const markdown = require('metalsmith-markdown');
const layouts = require('metalsmith-layouts');
const collections = require('metalsmith-collections');
const permalinks = require('metalsmith-permalinks');
const handlebars = require('handlebars');
const fs = require('fs');


// Navigation
handlebars.registerPartial('navigation', fs.readFileSync(__dirname + '/layouts/partials/navigation.hbt').toString());


Metalsmith(__dirname)
  .source('src')
  .destination('public')
  .use(collections({
    articles: {
      pattern: 'articles/*.md',
    },
  }))
  .use(markdown())
  .use(permalinks({
    pattern: ':collection/:title'
  }))
  .use(layouts({
    engine: 'handlebars',
    directory: './layouts',
    default: 'article.html',
    pattern: ["*/*/*html", "*/*html", "*html"],
    partials: {
      navigation: 'partials/navigation',
    }
  }))
  .build(function (err) {
    if (err) {
      console.error(err)
    }
    else {
      console.log('build completed!');
    }
  });

我喜欢简单干净的东西,在我看来,没有比 Metalsmith 构建更简单干净的了。我们只需要对 package.json 文件进行一个小更新,我们就可以运行它了

 "name": "buffaloTraceRoute",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "node build.js",
    "test": "echo \"No Tests Yet!\" "
  },
  "keywords": [],
  "author": "Your Name",
  "license": "ISC",
  "devDependencies": {
    // these should be the current versions
    // also... comments aren't allowed in JSON
  }
}

如果您想看看自己的作品,您可以取消注释构建文件中允许您为项目提供服务并执行诸如运行 npm run build 之类操作的部分。只需确保在部署之前删除此代码。

使用 Cloudflare

接下来,我们将使用 Cloudflare 来获取对他们的 Cloudflare Workers 的访问权限。这是每月 5 美元的成本发挥作用的地方。

现在,您可能会问:“好吧,但为什么是 Cloudflare?为什么不使用 GutHub Pages 或 Netlify 之类的免费服务?”这是一个好问题。有很多方法可以部署静态站点,那么为什么要选择一种方法而不是另一种方法呢?

好吧,Cloudflare 有一些优势...

速度和性能

切换到静态网站生成器最大的原因之一是为了提高网站性能。使用 Cloudflare Workers Site 可以进一步提高您的性能。

以下是将 Cloudflare 与两个竞争对手进行比较的图表

Cloudflare 提供

Cloudflare 速度最快的简单原因是:网站部署到 全球 190 多个数据中心。由于用户将从物理上更靠近他们的位置获取资源,因此这可以减少延迟。

简单性

诚然,如果您不知道 如何设置环境变量,Cloudflare Workers 的初始配置可能有点棘手。但是,在您为计算机设置基本配置后,部署到 Cloudflare 就和从站点目录中运行 wrangler publish 一样简单。本教程重点介绍了部署到 Cloudflare 的 CI/CD 方面,这涉及更多内容,但与大多数其他部署流程相比,它仍然非常简单。

(值得一提的是,GitHub Pages 和 Netlify 在这方面也做得很好。这三家公司的开发者体验都非常棒。)

物超所值

虽然 Github Pages 和 Netlify 都有免费层,但您的使用量(软)限制为每月 100 GB 带宽。别误会我的意思,这是一个非常慷慨的限制。但超过这个限制,你就走运了。GitHub Pages 不提供超过这个限制的任何东西,而 Netlify 则跃升至每月 45 美元,使 Cloudflare 的每月 5 美元的价格标签非常合理。

服务免费层带宽付费层价格付费层请求 / 带宽
GitHub Pages100GBN/AN/A
Netlify100GB$45~150K / 400 GB
Cloudflare Workers Sites$510MM / 无限
计算假设平均网站为 3MB。Cloudflare 对 CPU 使用有额外的限制。GitHub Pages 不应用于进行信用卡交易的网站。

当然,Cloudflare 没有免费层,但 5 美元可获得 1000 万次请求,这太便宜了。如果我没有提到 GitHub Pages 在过去一年中 经历了几次中断,那我会感到疏忽。在我的演示网站中,这完全没问题,但对于企业来说,这将是一个坏消息。

Cloudflare 为此提供了许多额外的功能,值得简要提及:免费 SSL 证书、免费(且易于使用)的 DNS 路由、用于项目的自定义 Workers Sites 域名(非常适合暂存)、无限环境(例如暂存)以及以成本注册域名(而不是其他注册商强加的加价定价)。

部署到 Cloudflare

Cloudflare 提供了一些很棒的教程,介绍如何使用他们的 Cloudflare Workers 产品。我们将在本文中介绍要点。

首先,确保已安装 Cloudflare CLI(Wrangler)

$ npm i @cloudflare/wrangler -g

接下来,我们将像这样将 Cloudflare Sites 添加到项目中

wrangler init --site cms 

假设我没有弄错并忘记了某个步骤,那么此时我们在终端中应该看到以下内容

⬇️ Installing cargo-generate...
🔧   Creating project called `workers-site`...
✨   Done! New project created /Users/<User>/Code/cms/workers-site
✨  Succesfully scaffolded workers site
✨  Succesfully created a `wrangler.toml`

项目根目录中还应该有一个名为 /workers-site 的生成文件夹,以及一个名为 wrangler.toml 的配置文件——这就是神奇所在。

name = "cms"
type = "webpack"
account_id = ""
workers_dev = true
route = ""
zone_id = ""


[site]
bucket = ""
entry-point = "workers-site"

您可能已经猜到接下来会发生什么……我们需要在配置文件中添加一些信息!我们要更新的第一个键值对是 bucket 属性。

bucket = "./public"

接下来,我们需要获取帐户 ID 和区域 ID(即您域名的路由)。您可以在 Cloudflare 帐户中找到它们,它们位于您域名仪表板最底部

停止! 在继续之前,请不要忘记单击“获取您的 API 令牌”按钮,以获取我们将需要的最后一个配置部分。将它保存在记事本或某个方便的地方,因为我们将在下一部分中使用它。

呼!好了,一步是将我们刚刚获取的帐户 ID 和区域 ID 添加到 .toml 文件中

name = "buffalo-traceroute"
type = "webpack"
account_id = "d7313702f333457f84f3c648e9d652ff" # Fake... use your account_id
workers_dev = true
# route = "example.com/*" 
# zone_id = "805b078ca1294617aead2a1d2a1830b9" # Fake... use your zone_id


[site]
bucket = "./public"
entry-point = "workers-site"
(Again, those IDs are fake.)

同样,这些 ID 是假的。您可能会被要求在计算机上设置凭据。如果出现这种情况,请在终端中运行 wrangler config

GitHub Actions

拼图的最后一块是配置 GitHub 为我们执行自动部署。之前我曾多次尝试进行 CI/CD 设置,因此这次我做好了最坏的准备,但令人惊讶的是,对于这种设置,GitHub Actions 非常简单。

那么这如何运作呢?

首先,让我们确保我们的 GitHub 帐户 已激活 GitHub Actions。从技术上讲,它现在处于测试阶段,但我到目前为止还没有遇到任何问题。

接下来,我们需要在 GitHub 中创建一个存储库并将我们的代码上传到其中。从访问 GitHub 并创建一个存储库开始。

本教程并不打算涵盖 Git 和/或 GitHub 的更精细方面,但有一个 很棒的介绍。或者,在项目根目录中复制粘贴以下命令

# run commands one after the other
$ git init
$ touch .gitignore && echo 'node_modules' > .gitignore
$ git add .
$ git commit -m 'first commit'
$ git remote add origin https://github.com/{username}/{repo name}
$ git push -u origin master

应该将项目添加到 GitHub 中。我有点犹豫地说,但这是我总是遇到问题的地方。例如,将太多命令放入终端,突然 GitHub 出现故障,或者终端无法找到 Python 的路径。小心行事!

假设我们已经完成了这一部分,我们的下一步任务是激活 Github Actions 并在项目目录的根目录中创建一个名为 .github/workflows 的目录。(GitHub 也可以通过在激活操作时添加“node”工作流来自动执行此操作。在撰写本文时,添加 GitHub Actions 工作流是 GitHub 用户界面的一个组成部分。)

一旦我们在项目根目录中拥有该目录,我们就可以添加最后的两个文件。每个文件将处理不同的工作流

  1. 一个工作流来检查更新是否可以合并(即 CI/CD 中的“CI”)
  2. 一个工作流,在更新合并到 master 后部署更改(即 CI/CD 中的“CD”)
# integration.yml
name: Integration


on:
  pull_request:
    branches: [ master ]


jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [10.x, 12.x]
    steps:
    - uses: actions/checkout@v2
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v1
      with:
        node-version: ${{ matrix.node-version }}
    - run: npm ci
    - run: npm run build --if-present
    - run: npm test
      env:
        CI: true

这是一个简单的流程。事实上,它非常简单,我直接从官方 GitHub Actions 文档中复制了它,并且几乎没有修改。让我们一起了解一下其中发生了什么。

  1. on: 仅在为 master 分支创建拉取请求时运行此工作流程。
  2. jobs: 针对双节点环境(例如,Node 10 和 Node 12 - Node 12 是当前推荐的版本)运行以下步骤。如果定义了构建脚本,它将进行构建。如果定义了测试脚本,它还会运行测试。

第二个文件是我们的部署脚本,它更复杂一些。

# deploy.yml
name: Deploy


on:
  push:
    branches:
      - master


jobs:
  deploy:
    runs-on: ubuntu-latest
    name: Deploy
    strategy:
      matrix:
        node-version: [10.x]


    steps:
      - uses: actions/checkout@v2
      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v1
        with:
          node-version: ${{ matrix.node-version }}
      - run: npm install
      - uses: actions/checkout@master
      - name: Build site
        run: "npm run build"
      - name: Publish
        uses: cloudflare/[email protected]
        with:
          apiToken: ${{ secrets.CF_API_TOKEN }}

重要!还记得我之前提到的 Cloudflare API 令牌吗?现在是使用它的时机。转到项目设置并添加一个秘密。将秘密命名为 CF_API_TOKEN 并添加 API 令牌。

让我们看看这个脚本中发生了什么。

  1. on: 当代码合并到 master 分支时运行这些步骤。
  2. steps: 使用 Nodejs 安装所有依赖项,使用 Nodejs 构建网站,然后使用 Cloudflare Wrangler 发布网站。

以下是运行构建之前项目应该具有的外观(不包括 node_modules): 

├── build.js
├── dist
│   └── worker.js
├── layouts
│   ├── about.hbs
│   ├── article.hbs
│   ├── index.hbs
│   └── partials
│       └── navigation.hbs
├── package-lock.json
├── package.json
├── public
├── src
│   ├── about.md
│   ├── articles
│   │   ├── post1.md
│   │   └── post2.md
│   └── index.md
├── workers-site
│   ├── index.js
│   ├── package-lock.json
│   ├── package.json
│   └── worker
│       └── script.js
└── wrangler.toml

基于 GitHub 的 CMS

好的,我已经做到这一步了……我被承诺了一个 CMS?数据库和登录的 GUI 在哪里?

别担心,你已经到达终点了!GitHub 现在就是你的 CMS,它是这样工作的。

  1. 编写一个 Markdown 文件(带有前置信息)。
  2. 打开 GitHub 并转到项目仓库。
  3. 点击“Articles”目录,并上传新文章。GitHub 会询问是否应该创建一个新的分支并附带一个拉取请求。答案是是。 
  4. 在集成验证后,可以合并拉取请求,这会触发部署。 
  5. 坐下来放松,等待 10 秒钟……内容正在部署到全球 164 个数据中心。

恭喜!你现在拥有一个最小化的基于 Git 的 CMS,几乎任何人都可以使用。 

故障排除说明

  • Metalsmith 布局有时可能有点棘手。尝试在构建步骤之前添加此调试行,以便它能输出一些有用的信息:DEBUG=metalsmith-layouts npm run build
  • 有时,Github Actions 需要我将 node_modules 添加到提交中才能部署……这对我来说很奇怪(而且不推荐),但解决了部署问题。
  • 如果你正在寻找一种简单的方法来处理资产(图像、SVG 等),这个插件很棒:https://github.com/alexgs/metalsmith-assets-improved(感谢 @gofango 指出这个遗漏!)
  • 如果你遇到任何问题,请告诉我,我们可以将其添加到此列表中!