Posted at
Next.jsDay 11

Next.js と Puppeteer を heroku で動かす

この記事はNext.js Advent Calendar 2018の 11 日目です。

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


ソースコード


ファイルツリー

.

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


package.json

heroku 用にscriptsheroku-postbuildを追加しています。

後、puppeteerを使うので、enginesを追加しています。

  "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

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

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

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 を使っています。

$ 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