この記事は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 用にscripts
にheroku-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 をみたので今後に期待です。