Get Netlify build time down to 0 hours with GitHub Actions and get rid of the month-end hijinks!
Published: 2021-02-28 16:51:49

# Netlify

Netlify is a very useful and appreciated service that you all know and love, but for the poor people who use it for free**, there is a certain problem that arises every month.

*This month's build time is XX minutes left*!

![img](https://i.imgur.com/TSm24w0.png)

Netlify works with GitHub repositories to run front-end builds and deploy them, but there is a time limit on how long it takes to run these builds.

The free version is limited to 300 minutes per month. (If you want to use it more than that, you can charge $19/month for the Pro version. I've already paid for it.)

It's not that I don't think 300 minutes will be enough, but

If you're using Netlify across multiple repositories, if you're using a lot of images in Gatsby.js and it's taking a long time for **Sharp** to resize them, or if **Dependabot** is getting PRs and Preview deploys regularly
You may be surprised to find that you're on the edge.

![img](https://i.imgur.com/y7ixbEG.png)

So, at the end of the month, poor folks like me are so worried about Netlify build time that they stop writing this blog post** and slow down the pace of **Site Refactor**.

So at the end of the month, poor folks like me are so worried about Netlify's build time that they don't write any more posts on this blog or slow down the pace of **Site Refactor**.

For example, the article I'm writing right now is written from my phone on the train to work, so I'd like to hit commit and save it in detail, but if I hit commit and push it, the build will run, so I'm too lazy to commit with WIP, and as a result, I don't have a work space like at home where I can push it all together. As a result, I don't blog anymore unless I'm at home where I have a work space where I can push it all together.
As a result, I don't write blogs anymore unless I am at home where I have a work space where I can push them all together.

## Leave this problem to GitHub Actions!

So, I'd like to solve this problem with GitHub Actions.

## Find out what Netlify is doing when it builds and try to do it yourself.

Basically, what Netlify does when building, for example, Gatsby.js, is to run the gatsby build command and deploy the built JS in a specific directory (usually `./public`) and deploys the pre-built JS placed in a specific directory.
However, there is a pattern that Netlify performs post-processing (PostProcess) on the built JS.

In my case, **Asset optimization**, which optimizes JS and images, and **Form detection**, which creates forms automatically by adding attributes to Form tags, were set.

![img](https://i.imgur.com/ytjbJQA.png)

![img](https://i.imgur.com/LfL70Br.png)

This will no longer be done by Netlify, so you will need to reimplement it here.

## gatsby-plugin-minify

Among asset optimization, JS and CSS minifers can use [gatsby-plugin-minify](https://www.gatsbyjs.com/plugins/gatsby-plugin-minify/) to minify html, JS, and CSS can be minified.

You can install it from NPM (yarn) as usual.

```
npm install gatsby-plugin-minify
```

You can use it by setting the plugins in gatsby-config.js as follows.

```typescript
    {
      resolve: 'gatsby-plugin-minify',
      options: {
        caseSensitive: false,
        collapseBooleanAttributes: true,
        useShortDoctype: false,
        removeEmptyElements: false,
        removeComments: true,
        removeAttributeQuotes: false,
        minifyCSS: true,
        minifyJS: true,
      },
    },
```

By setting minifyCSS and minifyJS to true, the CSS will be minified together using [clean-css](https://github.com/jakubpawlowicz/clean-css) and the JS will be minified together using [UglifyJS](https://github. com/mishoo/UglifyJS) to minify the JS together. Also, the other side of gatsby-plugin-minify is just multiplying [html-minifier](https://github.com/kangax/html-minifier) by postbuild in gatsby-node.js. You can set detailed options in [html-minifier](https://github.com/kangax/html-minifier#options-quick-reference).

By the way, you have to be careful to set the **removeAttributeQuotes** option to false.

If you set this option to true, double-quotes will be removed from the attributes in the HTML tags, and the file will be a little lighter, but it will not load well in systems like [berss.com](https://berss.com/feed/Find.aspx) that retrieve RSS links from sites. I've been using it all day.

Be careful if you are planting RSS links as page links.

## By using imgur, you can do image hosting and resizing at the same time.

There is a service called [imgur](https://imgur.com/).

It is mainly known as an image hosting service for Reddit and Gifs, but I use imgur on this blog because it allows you to easily resize and host images.

This can be done by inserting the keyword that matches the image size after the image URL.

For example, if you have an image with this URL

```
https://i.imgur.com/Wfz9G0B.png
```

To resize to 160x160, attach **b** to the back.

```
https://i.imgur.com/Wfz9G0Bb.png
```

This completes the image optimization.

## getform.io

Getform.io](https://getform.io/) is a great service that provides a backend for forms.

You need to pay for the paid version to use the useful integrations, but if you just want to send a notification email to a specified email address when a form is submitted, you can do that for free.

We will now replace Netlify's Form detection.

First of all, when you create a new form, you can issue an Action URL for the form.

The following blog explains how to create a Form in an easy-to-understand manner.

<https://blog.nakamu.life/posts/getform-io>

Now, once the Form is created, you can follow the tutorial and set this URL to the action of the Form tag, but the free version of GetForm does not allow you to **set the Thanks page after submitting the Form**.

```html
<!--
* Add your getform endpoint into "action" attribute
* Set a unique "name" field
* Start accepting submissions
-->
<form action="{getform-endpoint}" method="POST">

  <input type="text" name="name">
  <input type="email" name="email">
  <button type="submit">Send</button>

</form>
```

![img](https://i.imgur.com/sT5vhFE.png)

Well, this is good enough, but since we are using **React**, let's specify that the URL of getform.io should be POST fetched behind the scenes and sent to your own Thanks URL defined in actions.

First, we need to set **onSubmit** on the Form.

```typescript
        <form
              name="contact"
              method="post"
              action="/thanks/"
              onSubmit={this.handleSubmit}
            >
                <label>
                  <span className="icon-user" />&nbsp;Your name<br />
                  <input
                    type="text"
                    name="name"
                    className="form-control"
                    maxLength="30"
                    minLength="2"
                    required
                    placeholder="Enter your name"
                    onChange={this.handleChange}
                  />
                </label>
              </p>
```

Then, separately define a function that will fire onSubmit.


```typescript
  handleSubmit(e) {
    e.preventDefault();
    const form = e.target;
    fetch('https://getform.io/f/xxxxxxxxxxxxxxxxxxxxxxxxx', {
      method: 'POST',
      body: Contact.encode({
        'form-name': form.getAttribute('name'),
        ...this.state,
      }),
    })
  }
```

Since we are sending a Form, we need to send the elements appended to [FormData](https://developer.mozilla.org/ja/docs/Web/API/FormData) in the fetch.

```
  static encode(data) {
    const formData = new FormData();
    // eslint-disable-next-line no-restricted-syntax
    for (const key of Object.keys(data)) {
      formData.append(key, data[key]);
    }
    return formData;
  }
```

Again, in React, in addition to actions, onSubmit can be a function in a Form.

However, when onSubmit is pressed, the input items of the Form must be passed via POST Fetch, so for each changeEvent that occurs on input of the Form, the value of the Form should be saved as a state.


```typescript
  handleChange(e) {
    this.setState({ [e.target.name]: e.target.value });
  }

  handleAttachment(e) {
    this.setState({ [e.target.name]: e.target.files[0] });
  }
  
  (中略)

                <label>
                  <span className="icon-user" />&nbsp;Your name<br />
                  <input
                    type="text"
                    name="name"
                    className="form-control"
                    maxLength="30"
                    minLength="2"
                    required
                    placeholder="Enter your name"
                    onChange={this.handleChange}
                  />

                </label>
              </p>
```

Also, if you use onSubmit, you will not be able to jump to the Thanks page after the Post process is finished using Gatsby's navigate function.


```typescript
  handleSubmit(e) {
    e.preventDefault();
    const form = e.target;
    fetch('https://getform.io/f/897f187e-876d-42a7-b300-7c235af72e6d', {
      method: 'POST',
      body: Contact.encode({
        'form-name': form.getAttribute('name'),
        ...this.state,
      }),
    })
      .then(() => navigateTo(form.getAttribute('action')))
      .catch((error) => alert(error));
  }
```

Now you can create your own Thanks page even with GetForm free version.

![img](https://i.imgur.com/gumRkbF.png)

## Build and Deploy with GitHub Actions

Once you've done this, all you need to do is build and deploy with GitHub Actions.

We'll create two actions, one for preview deployment with a PR to the master branch, and another for production deployment with a commit to master.

First, let's do Preview deployment.

```yaml
name: DeployToNetlifyPreview
on:
  pull_request:
    branches:
      - master

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout source code
        uses: actions/checkout@v2
      - name: Cache node_modules
        uses: actions/cache@v1
        with:
          path: node_modules
          key: ${{ runner.OS }}-build-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.OS }}-build-
            ${{ runner.OS }}
      - name: Setup Node
        uses: actions/setup-node@v1
        with:
          node-version: 12.x
      - name: npm install and build
        env:
          GATSBY_GITHUB_CLIENT_SECRET: ${{secrets.GATSBY_GITHUB_CLIENT_SECRET}}
          GATSBY_GITHUB_CLIENT_ID: ${{secrets.GATSBY_GITHUB_CLIENT_ID}}
          GATSBY_ALGOLIA_SEARCH_API_KEY: ${{secrets.GATSBY_ALGOLIA_SEARCH_API_KEY}}
          GATSBY_ALGOLIA_INDEX_NAME: ${{secrets.GATSBY_ALGOLIA_INDEX_NAME}}
          GATSBY_ALGOLIA_APP_ID: ${{secrets.GATSBY_ALGOLIA_APP_ID}}
          GATSBY_ALGOLIA_ADMIN_API_KEY: ${{secrets.GATSBY_ALGOLIA_ADMIN_API_KEY}}
          FAUNADB_SERVER_SECRET: ${{secrets.FAUNADB_SERVER_SECRET}}
        run: |
          npm install
          npm run build
      - name: Deploy to netlify
        run: npx netlify-cli deploy --dir=./public > cli.txt
        env:
          NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
          NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
      - name: Cat cli.txt
        run: |
          cat cli.txt
          sed -i -z 's/\n/\\n/g' cli.txt
      - name: Post Netlify CLI Comment
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          URL: ${{ github.event.pull_request.comments_url }}
        run: |
          curl -X POST \
               -H "Authorization: token ${GITHUB_TOKEN}" \
               -d "{\"body\": \"$(cat cli.txt)\"}" \
               ${URL}
```

The node setup, npm install, and build are the same as usual.

In GitHub Actions, you can specify a secret, so API keys for Algolia search and FaunaDB are passed as secrets in the environment variable at build time.

Incidentally, if you set **GATSBY_XXXX** in the environment variable, the environment variable will also be entered in the built JS. (Don't forget to do this when using environment variables from JS. This is a point that gets stuck quite a bit.

To deploy, use [netlify-cli](https://docs.netlify.com/cli/get-started/).

The required environment variables are site ID and AUTH TOKEN.

One of the features of netlify-cli is that when the deployment is successful, the **deploy URL** will appear in the standard output**.

I also send the URL to PR comments.

The great thing about GitHub Actions is that you can retrieve the GITHUB TOKEN by using secrets.GITHUB_TOKEN without setting it, so you can easily send it to PR comments.

```yaml
      - name: Deploy to netlify
        run: npx netlify-cli deploy --dir=./public > cli.txt
        env:
          NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
          NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
      - name: Cat cli.txt
        run: |
          cat cli.txt
          sed -i -z 's/\n/\\n/g' cli.txt
      - name: Post Netlify CLI Comment
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          URL: ${{ github.event.pull_request.comments_url }}
        run: |
          curl -X POST \
               -H "Authorization: token ${GITHUB_TOKEN}" \
               -d "{\"body\": \"$(cat cli.txt)\"}" \
               ${URL}
```

The next step is to deploy to production.

```yaml
name: DeployToNetlifyPRD
on:
  push:
    branches:
      - master

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout source code
        uses: actions/checkout@v2
      - name: Cache node_modules
        uses: actions/cache@v1
        with:
          path: node_modules
          key: ${{ runner.OS }}-build-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.OS }}-build-
            ${{ runner.OS }}
      - name: Setup Node
        uses: actions/setup-node@v1
        with:
          node-version: 12.x
      - name: npm install and build
        env:
          GATSBY_GITHUB_CLIENT_SECRET: ${{secrets.GATSBY_GITHUB_CLIENT_SECRET}}
          GATSBY_GITHUB_CLIENT_ID: ${{secrets.GATSBY_GITHUB_CLIENT_ID}}
          GATSBY_ALGOLIA_SEARCH_API_KEY: ${{secrets.GATSBY_ALGOLIA_SEARCH_API_KEY}}
          GATSBY_ALGOLIA_INDEX_NAME: ${{secrets.GATSBY_ALGOLIA_INDEX_NAME}}
          GATSBY_ALGOLIA_APP_ID: ${{secrets.GATSBY_ALGOLIA_APP_ID}}
          GATSBY_ALGOLIA_ADMIN_API_KEY: ${{secrets.GATSBY_ALGOLIA_ADMIN_API_KEY}}
          FAUNADB_SERVER_SECRET: ${{secrets.FAUNADB_SERVER_SECRET}}
        run: |
          npm install
          npm run build
      - name: Deploy to netlify
        run: npx netlify-cli deploy --prod --dir=./public
        env:
          NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
          NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
```

It is almost the same, but by including the --prod option in the deploy command in netlify-cli, it will be deployed to the production environment.

```yaml
      - name: Deploy to netlify
        run: npx netlify-cli deploy --prod --dir=./public
        env:
          NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
          NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
```

## Conclusion

Now the build time of Netlify is zero, which is a mental relief.

![img](https://i.imgur.com/ugdUr9l.png)

And I can get on with refactoring and writing articles!