Next.js と Puppeteer を heroku で動かす

この記事は[Next.js Advent Calendar 2018](https://qiita.com/advent-calendar/2018/next-js)の 11 日目です。

Next.js の SSR 時に puppeteer を実行して入力した URL のスクリーンショットを撮るのを作ってみました。

## ソースコード

### ファイルツリー

```zsh
.
├── pages
│   ├── index.js
│   └── screenshot.js
├── .gitignore
├── README.md
├── package.json
├── server.js
└── yarn.lock
```

### package.json

heroku 用に`scripts`に`heroku-postbuild`を追加しています。
後、puppeteerを使うので、`engines`を追加しています。

```json
  "scripts": {
    "dev": "node server.js",
    "build": "next build",
    "heroku-postbuild": "next build",
    "start": "NODE_ENV=production node server.js"
  },
  "dependencies": {
    "express": "^4.16.4",
    "next": "^7.0.2",
    "puppeteer": "^1.11.0",
    "react": "^16.6.3",
    "react-dom": "^16.6.3",
    "valid-url": "^1.0.9"
  },
  "engines": {
    "node": ">8.4.0"
  }
```

### server.js

```javascript
const express = require('express');
const next = require('next');
const puppeteer = require('puppeteer');
const validUrl = require('valid-url');

const port = parseInt(process.env.PORT, 10) || 3000;
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();

app
  .prepare()
  .then(() => {
    const server = express();

    server.get('/screenshot/:url', (req, res) => {
      let { url } = req.params;

      if (!url && !validUrl.isWebUri(url)) {
        return app.render(req, res, '/screenshot', { url: undefined });
      }
      url = decodeURIComponent(url);
      console.log(`[Server] /screenshot/${url} requested!`);
      (async () => {
        const browser = await puppeteer.launch({
          args: ['--no-sandbox', '--disable-setuid-sandbox']
        });

        const page = await browser.newPage();
        await page.goto(url);
        const img = await page.screenshot({
          path: '',
          fullPage: true,
          encoding: 'base64'
        });

        await browser.close();

        return app.render(req, res, '/screenshot', { url, img });
      })();
    });

    server.get('*', (req, res) => {
      return handle(req, res);
    });

    server.listen(port, err => {
      if (err) throw err;
      console.log(`> Ready on http://localhost:${port}`);
    });
  })
  .catch(ex => {
    console.error(ex.stack);
    process.exit(1);
  });
```

### index.js

```javascript
import React, { Component } from 'react';
import Router from 'next/router';
import validUrl from 'valid-url';
export default class extends Component {
  constructor(props) {
    super(props);
  }

  handleSubmit = e => {
    e.preventDefault();

    const urlToScreenshot = decodeURIComponent(this.refs.url.value);
    if (validUrl.isWebUri(urlToScreenshot)) {
      console.log('OK:' + urlToScreenshot);
      Router.push(`/screenshot/${encodeURIComponent(urlToScreenshot)}`);
    } else {
      console.log('NG:' + urlToScreenshot);
    }
  };

  render() {
    return (
      <div>
        スクショ撮るくん
        <form onSubmit={this.handleSubmit}>
          <input type="text" ref="url" />
          <input type="submit" value="Submit" />
        </form>
      </div>
    );
  }
}
```

### screenshot.js

```javascript
import React, { Component } from 'react';

export default class extends Component {
  static getInitialProps({ query: { url, img } }) {
    return { postUrl: url, base64img: img };
  }

  render() {
    return (
      <div>
        <h1>スクショURL: {this.props.postUrl}</h1>
        <img src={'data:image/png;base64,' + this.props.base64img} />
      </div>
    );
  }
}
```

## heroku にデプロイ

heroku で puppeteer を動かすには、`node.js`の buildpack と、スクリーンショットを撮るので Noto フォントを使ってる puppeteer 用の buildpack を使っています。

```zsh
$ heroku create MYAPP
$ heroku buildpacks:set heroku/nodejs
$ heroku buildpacks:add https://github.com/operando/puppeteer-heroku-buildpack.git
$ git push heroku master
```

## その他、あとがき

- スクリーンショットを撮ること自体はできるのですが、Server 側でエラーが出てしまっています。。。
- やり方的には、puppeteer を api 化して本来はやるべきかなと思いながら、動くものを作ろうと作ってしまいました。
- heroku ではなく NOW を使いたかったのですが、NOW v2 ではまだ puppeteer 対応していないようで、今後対応していくみたいな isuue をみたので今後に期待です。


## 参考 URL

- [Next.js w/ Custom Express Server example for Heroku](https://github.com/mars/heroku-nextjs-custom-server-express)
- [Heroku に Puppeteer の実行環境を構築する](https://qiita.com/jerrywdlee/items/ffc988956eb75a99bc3c)
- [日本語フォント「東青梅ゴシック」を同梱した puppeteer-heroku-buildpack](https://github.com/Arika0093/puppeteer-heroku-buildpack/blob/master/README.md)
- [operando/puppeteer-heroku-buildpack](https://github.com/operando/puppeteer-heroku-buildpack)