惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Attack and Defense Labs
Attack and Defense Labs
N
News and Events Feed by Topic
L
LINUX DO - 热门话题
PCI Perspectives
PCI Perspectives
www.infosecurity-magazine.com
www.infosecurity-magazine.com
爱范儿
爱范儿
D
DataBreaches.Net
Simon Willison's Weblog
Simon Willison's Weblog
S
Secure Thoughts
S
SegmentFault 最新的问题
博客园 - 【当耐特】
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 叶小钗
P
Proofpoint News Feed
The Hacker News
The Hacker News
T
ThreatConnect
N
News and Events Feed by Topic
T
Threatpost
The Register - Security
The Register - Security
WordPress大学
WordPress大学
博客园 - Franky
Recorded Future
Recorded Future
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Project Zero
Project Zero
大猫的无限游戏
大猫的无限游戏
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
罗磊的独立博客
Stack Overflow Blog
Stack Overflow Blog
腾讯CDC
F
Future of Privacy Forum
F
Full Disclosure
Cyberwarzone
Cyberwarzone
J
Java Code Geeks
李成银的技术随笔
Schneier on Security
Schneier on Security
Know Your Adversary
Know Your Adversary
H
Hacker News: Front Page
人人都是产品经理
人人都是产品经理
博客园_首页
Scott Helme
Scott Helme
Google DeepMind News
Google DeepMind News
美团技术团队
Malwarebytes
Malwarebytes
Last Week in AI
Last Week in AI
T
Tailwind CSS Blog
T
The Exploit Database - CXSecurity.com
G
GRAHAM CLULEY
Recent Announcements
Recent Announcements
C
CXSECURITY Database RSS Feed - CXSecurity.com

CSS-Tricks

Revealing Text With CSS letter-spacing | CSS-Tricks Technical Writing in the AI Age | CSS-Tricks Cross-Document View Transitions: Scaling Across Hundreds of Elements | CSS-Tricks Cross-Document View Transitions: Scaling Across Hundreds of Elements | CSS-Tricks The State of CSS Centering in 2026 | CSS-Tricks Stack Overflow: When We Stop Asking | CSS-Tricks Cross-Document View Transitions: The Gotchas Nobody Mentions | CSS-Tricks What’s !important #11: 3D Voxel Scenes, Flying Focus, CSS Syntaxes, and More | CSS-Tricks Computing and Displaying Discounted Prices in CSS | CSS-Tricks rotateX() | CSS-Tricks rotateY() | CSS-Tricks rotateZ() | CSS-Tricks rotate() | CSS-Tricks Soon We Can Finally Banish JavaScript to the ShadowRealm | CSS-Tricks Using CSS corner-shape For Folded Corners | CSS-Tricks A Scrollytelling Gift for Mum on Mother’s Day 2026 | CSS-Tricks Google’s Prompt API | CSS-Tricks Making Zigzag CSS Layouts With a Grid + Transform Trick | CSS-Tricks Fixed-Height Cards: More Fragile Than They Look | CSS-Tricks What’s !important #10: HTML-in-Canvas, Hex Maps, E-ink Optimization, and More | CSS-Tricks The Importance of Native Randomness in CSS | CSS-Tricks contrast() | CSS-Tricks contrast-color() | CSS-Tricks Let’s Use the Nonexistent ::nth-letter Selector Now | CSS-Tricks Quick Hit #126 Recreating Apple’s Vision Pro Animation in CSS | CSS-Tricks Quick Hit #125 Enhancing Astro With a Markdown Component | CSS-Tricks Quick Hit #124 Markdown + Astro = ❤️ | CSS-Tricks Quick Hit #123 What’s !important #9: clip-path Jigsaws, View Transitions Toolkit, Name-only Containers, and More | CSS-Tricks A Well-Designed JavaScript Module System is Your First Architecture Decision | CSS-Tricks hypot() | CSS-Tricks The Radio State Machine | CSS-Tricks 7 View Transitions Recipes to Try | CSS-Tricks Quick Hit #122 Quick Hit #121 Selecting a Date Range in CSS | CSS-Tricks saturate() | CSS-Tricks justify-self | CSS-Tricks Quick Hit #120 Alternatives to the !important Keyword | CSS-Tricks Quick Hit #119 New CSS Multi-Column Layout Features in Chrome | CSS-Tricks Quick Hit #118 Making Complex CSS Shapes Using shape() | CSS-Tricks Quick Hit #117 Front-End Fools: Top 10 April Fools’ UI Pranks of All Time | CSS-Tricks Sniffing Out the CSS Olfactive API | CSS-Tricks What’s !important #8: Light/Dark Favicons, @mixin, object-view-box, and More | CSS-Tricks Quick Hit #116 Form Automation Tips for Happier User and Clients | CSS-Tricks Quick Hit #115 Generative UI Notes | CSS-Tricks Quick Hit #114 Quick Hit #113 Experimenting With Scroll-Driven corner-shape Animations | CSS-Tricks Quick Hit #112 JavaScript for Everyone: Destructuring | CSS-Tricks Quick Hit #111 Quick Hit #110 What’s !important #7: random(), Folded Corners, Anchored Container Queries, and More | CSS-Tricks 4 Reasons That Make Tailwind Great for Building Layouts | CSS-Tricks Quick Hit #109 Quick Hit #108 Abusing Customizable Selects | CSS-Tricks Quick Hit #107 The Value of z-index | CSS-Tricks Quick Hit #106 The Different Ways to Select <html> in CSS Quick Hit #105 Popover API or Dialog API: Which to Choose? Quick Hit #104 What’s !important #6: :heading, border-shape, Truncating Text From the Middle, and More Yet Another Way to Center an (Absolute) Element An Exploit ... in CSS?! Quick Hit #103 A Complete Guide to Bookmarklets Quick Hit #102 Loading Smarter: SVG vs. Raster Loaders in Modern Web Design Potentially Coming to a Browser :near() You Quick Hit #101 Distinguishing "Components" and "Utilities" in Tailwind Quick Hit #100 Spiral Scrollytelling in CSS With sibling-index() Interop 2026 Quick Hit #99 What’s !important #5: Lazy-loading iframes, Repeating corner-shape Backgrounds, and More Quick Hit #98 Making a Responsive Pyramidal Grid With Modern CSS Approximating contrast-color() With Other CSS Features Quick Hit #97 Trying to Make the Perfect Pie Chart in CSS Quick Hit #96 Quick Hit #95 CSS Bar Charts Using Modern Functions Quick Hit #94 No Hassle Visual Code Theming: Publishing an Extension Quick Hit #93
How to Make a Simple CMS With Cloudflare, GitHub Actions and Metalsmith
CSS-Tricks · 2020-05-14 · via CSS-Tricks

Let’s build ourselves a CMS. But rather than build out a UI, we’re going to get that UI for free in the form of GitHub itself! We’ll be leveraging GitHub as the way to manage the content for our static site generator (it could be any static site generator). Here’s the gist of it: GitHub is going to be the place to manage, version control, and store files, and also be the place we’ll do our content editing. When edits occur, a series of automations will test, verify, and ultimately deploy our content to Cloudflare.

You can find the completed code for the project is available on GitHub. I power my own website, jonpauluritis.com, this exact way.

What does the full stack look like?

Here’s the tech stack we’ll be working with in this article:

  • Any Markdown Editor (Optional. e.g Typora.io)
  • A Static Site Generator (e.g. Metalsmith)
  • Github w/ Github Actions (CICD and Deployment)
  • Cloudflare Workers

Why should you care about about this setup? This setup is potentially the leanest, fastest, cheapest (~$5/month), and easiest way to manage a website (or Jamstack site). It’s awesome both from a technical side and from a user experience perspective. This setup is so awesome I literally went out and bought stock in Microsoft and Cloudflare. 

But before we start…

I’m not going to walk you through setting up accounts on these services, I’m sure you can do that yourself. Here are the accounts you need to setup: 

I would also recommend Typora for an amazing Markdown writing experience, but Markdown editors are a very personal thing, so use which editor feels right for you. 

Project structure

To give you a sense of where we’re headed, here’s the structure of the completed project:

├── 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

Step 1: Command line stuff

In a terminal, change directory to wherever you keep these sorts of projects and type this:

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

That will create a new directory, move into it, and initialize the use of npm.

The next thing we want to do is stand on the shoulders of giants. We’ll be using a number of npm packages that help us do things, the meat of which is using the static site generator Metalsmith:

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

Along with Metalsmith, there are a couple of other useful bits and bobs. Why Metalsmith? Let’s talk about that.

Step 2: Metalsmith

I’ve been trying out static site generators for 2-3 years now, and I still haven’t found “the one.” All of the big names — like Eleventy, Gatsby, Hugo, Jekyll, Hexo, and Vuepress — are totally badass but I can’t get past Metalsmith’s simplicity and extensibility.

As an example, this will code will actually build you a site: 

// 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);

Pretty cool right?

For sake of brevity, type this into the terminal and we’ll scaffold out some structure and files to start with.

First, make the directories:

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

Then, create the build file:

$ touch build.js

Next, we’ll create some layout files:

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

And, finally, we’ll set up our content resources:

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

The project folder should look something like this:

├── 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

Step 3: Let’s add some code

To save space (and time), you can use the commands below to create the content for our fictional website. Feel free to hop into “articles” and create your own blog posts. The key is that the posts need some meta data (also called “Front Matter”) to be able to generate properly.  The files you would want to edit are index.md, post1.md and post2.md.

The meta data should look something like this: 

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

Or, if you’re lazy like me, use these terminal commands to add mock content from GitHub Gists to your site:

$ 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

Next, we’ll be creating our layouts and partial layouts (“partials”). We’re going to use Handlebars.js for our templating language in this tutorial, but you can use whatever templating language floats your boat. Metalsmith can work with pretty much all of them, and I don’t have any strong opinions about templating languages.

Build the index layout

<!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>

A couple of notes: 

  • Our “navigation” hasn’t been defined yet, but will ultimately replace the area where {{>navigation }} resides. 
  • {{#each }} will iterate through the “collection” of articles that metalsmith will generate during its build process. 
  • Metalsmith has lots of plugins you can use for things like stylesheets, tags, etc., but that’s not what this tutorial is about, so we’ll leave that for you to explore. 

Build the About page

Add the following to your about.hbs page:

<!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>

Build the Articles layout

<!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>

You may have noticed that this is the exact same layout as the About page. It is. I just wanted to cover how to add additional pages so you’d know how to do that. If you want this one to be different, go for it.

Add navigation

Add the following to the layouts/partials/navigation.hbs file

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

Sure there’s not much to it… but this really isn’t supposed to be a Metalsmith/SSG tutorial.  ¯\_(ツ)_/¯

Step 4: The Build file

The heart and soul of Metalsmith is the build file. For sake of thoroughness, I’m going to go through it line-by-line. 

We start by importing the dependencies

Quick note: Metalsmith was created in 2014, and the predominant module system at the time was common.js , so I’m going to stick with require statements as opposed to ES modules. It’s also worth noting that most of the other tutorials are using require statements as well, so skipping a build step with Babel will just make life a little less complex here.

// 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');

Next, we’ll be including Metalsmith and telling it where to find its compile targets:

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

So far, so good. After we have the source and target set, we’re going to set up the markdown rendering, the layouts rendering, and let Metalsmith know to use “Collections.” These are a way to group files together. An easy example would be something like “blog posts” but it could really be anything, say recipes, whiskey reviews, or whatever. In the above example, we’re calling the collection “articles.”

 // 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/**/*": "**/*",
//   }
// }))

Next, we’re adding the markdown plugin, so we can use markdown for content to compile to HTML.

From there, we’re using the layouts plugin to wrap our raw content in the layout we define in the layouts folder. You can read more about the nuts and bolts of this on the official plugin site but the result is that we can use {{{contents}}} in a template and it will just work. 

The last addition to our tiny little build script will be the build method:

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

Putting everything together, we should get a build script that looks like this:

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!');
    }
  });

I’m a sucker for simple and clean and, in my humble opinion, it doesn’t get any simpler or cleaner than a Metalsmith build. We just need to make one quick update to the package.json file and we’ll be able to give this a run:

 "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
  }
}

If you want to see your handy work, you can uncomment the parts of the build file that will let you serve your project and do things like run npm run build. Just make sure you remove this code before deploying.

Working with Cloudflare

Next, we’re going to work with Cloudflare to get access to their Cloudflare Workers. This is where the $5/month cost comes into play.

Now, you might be asking: “OK, but why Cloudflare? What about using something free like GutHub Pages or Netlify?” It’s a good question. There are lots of ways to deploy a static site, so why choose one method over another?

Well, Cloudflare has a few things going for it…

Speed and performance

One of the biggest reasons to switch to a static site generator is to improve your website performance. Using Cloudflare Workers Site can improve your performance even more.

Here’s a graph that shows Cloudflare compared to two competing alternatives:

Courtesy of Cloudflare

The simple reason why Cloudflare is the fastest: a site is deployed to 190+ data centers around the world. This reduces latency since users will be served the assets from a location that’s physically closer to them.

Simplicity

Admittedly, the initial configuration of Cloudflare Workers may be a little tricky if you don’t know how to setup environmental variables. But after you setup the basic configurations for your computer, deploying to Cloudflare is as simple as wrangler publish from the site directory. This tutorial is focused on the CI/CD aspect of deploying to Cloudflare which is a little more involved, but it’s still incredibly simple compared to most other deployment processes. 

(It’s worth mentioning GitHub Pages, Netlify are also killing it in this area. The developer experience of all three companies is amazing.)

More bang for the buck

While Github Pages and Netlify both have free tiers, your usage is (soft) limited to 100GB of bandwidth a month. Don’t get me wrong, that’s a super generous limit. But after that you’re out of luck. GitHub Pages doesn’t offer anything more than that and Netlify jumps up to $45/month, making Cloudflare’s $5/month price tag very reasonable.

ServiceFree Tier BandwidthPaid Tier PricePaid Tier Requests / Bandwidth
GitHub Pages100GBN/AN/A
Netlify100GB$45~150K / 400 GB
Cloudflare Workers Sitesnone$510MM / unlimited 
Calculations assume a 3MB average website. Cloudflare has additional limits on CPU use. GitHub Pages should not be used for sites that have credit card transactions.

Sure, there’s no free tier for Cloudflare, but $5 for 10 million requests is a steal. I would also be remise if I didn’t mention that GitHub Pages has had a few outages over the last year. That’s totally fine in my book a demo site, but it would be bad news for a business.

Cloudflare offers a ton of additional features for that worth briefly mentioning: free SSL certificates, free (and easy) DNS routing, a custom Workers Sites domain name for your projects (which is great for staging), unlimited environments (e.g. staging), and registering a domain name at cost (as opposed to the markup pricing imposed by other registrars). 

Deploying to Cloudflare

Cloudflare provides some great tutorials for how to use their Cloudflare Workers product. We’ll cover the highlights here.

First, make sure the Cloudflare CLI, Wrangler, is installed:

$ npm i @cloudflare/wrangler -g

Next, we’re going to add Cloudflare Sites to the project, like this:

wrangler init --site cms 

Assuming I didn’t mess up and forget about a step, here’s what we should have in the terminal at this point:

⬇️ 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`

There should also be a generated folder in the project root called /workers-site as well as a config file called wrangler.toml — this is where the magic resides.

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


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

You might have already guessed what comes next… we need to add some info to the config file! The first key/value pair we’re going to update is the bucket property.

bucket = "./public"

Next, we need to get the Account ID and Zone ID (i.e. the route for your domain name). You can find them in your Cloudflare account all the way at the bottom of the dashboard for your domain:

Stop! Before going any further, don’t forget to click the “Get your API token” button to grab the last config piece that we’ll need. Save it on a notepad or somewhere handy because we’ll need it for the next section. 

Phew! Alright, the next task is to add the Account ID and Zone ID we just grabbed to the .toml file:

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.)

Again, those IDs are fake. You may be asked to set up credentials on your computer. If that’s the case, run wrangler config in the terminal.

GitHub Actions

The last piece of the puzzle is to configure GitHub to do automatic deployments for us. Having done previous forays into CI/CD setups, I was ready for the worst on this one but, amazingly, GitHub Actions is very simple for this sort of setup.

So how does this work?

First, let’s make sure that out GitHub account has GitHub Actions activated. It’s technically in beta right now, but I haven’t run into any issues with that so far.

Next, we need to create a repository in GitHub and upload our code to it. Start by going to GitHub and creating a repository.

This tutorial isn’t meant to cover the finer points of Git and/or GitHub, but there’s a great introduction. Or, copy and paste the following commands while in the root directory of the project:

# 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

That should add the project to GitHub. I say that with a little hesitance but this is where everything tends to blow up for me. For example, put too many commands into the terminal and suddenly GitHub has an outage, or the terminal unable to location the path for Python. Tread carefully!

Assuming we’re past that part, our next task is to activate Github Actions and create a directory called .github/workflows in the root of the project directory. (GitHub can also do this automatically by adding the “node” workflow when activating actions. At the time of writing, adding a GitHub Actions Workflow is part of GitHub’s user interface.)

Once we have the directory in the project root, we can add the final two files. Each file is going to handle a different workflow:

  1. A workflow to check that updates can be merged (i.e. the “CI” in CI/CD)
  2. A workflow to deploy changes once they have been merged into master (i.e. the “CD” in CI/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

This is a straightforward workflow. So straightforward, in fact, that I copied it straight from the official GitHub Actions docs and barely modified it. Let’s go through what is actually happening in there:

  1. on: Run this workflow only when a pull request is created for the master branch
  2. jobs: Run the below steps for two-node environments (e.g. Node 10, and Node 12 — Node 12 is currently the recommended version). This will build, if a build script is defined. It will also run tests if a test script is defined.

The second file is our deployment script and is a little more involved.

# 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 }}

Important! Remember that Cloudflare API token I mentioned way earlier? Now is the time to use it. Go to the project settings and add a secret. Name the secret CF_API_TOKEN and add the API token.

Let’s go through whats going on in this script:

  1. on: Run the steps when code is merged into the master branch
  2. steps: Use Nodejs to install all dependencies, use Nodejs to build the site, then use Cloudflare Wrangler to publish the site

Here’s a quick recap of what the project should look like before running a build (sans 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

A GitHub-based CMS

Okay, so I made it this far… I was promised a CMS? Where is the database and my GUI that I log into and stuff?

Don’t worry, you are at the finish line! GitHub is your CMS now and here’s how it works:

  1. Write a markdown file (with front matter).
  2. Open up GitHub and go to the project repository.
  3. Click into the “Articles” directory, and upload the new article. GitHub will ask whether a new branch should be created along with a pull request. The answer is yes. 
  4. After the integration is verified, the pull request can be merged, which triggers deployment. 
  5. Sit back, relax and wait 10 seconds… the content is being deployed to 164 data centers worldwide.

Congrats! You now have a minimal Git-based CMS that basically anyone can use. 

Troubleshooting notes

  • Metalsmith layouts can sometimes be kinda tricky. Try adding this debug line before the build step to have it kick out something useful: DEBUG=metalsmith-layouts npm run build
  • Occasionally, Github actions needed me to add node_modules to the commit so it could deploy… this was strange to me (and not a recommended practice) but fixed the deployment.
  • If you’re looking for an easy way to handle assets (images, svgs, etc) this plugin is awesome: https://github.com/alexgs/metalsmith-assets-improved (thanks to @gofango for pointing out the omission!)
  • Please let me know if you run into any trouble and we can add it to this list!