Quantcast
Channel: Node.jsタグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 8934

Githubプロフィールに貼れる画像を動的に生成して返すエンドポイントを作る

$
0
0

巷ではGithubのプロフィールをデコるのが流行っています。
具体的にはGitHub Readme Stats を利用してGitHubプロフィールをカッコよくするのような記事を読んでもらえばいいんですが、ここで紹介されている物以外を載せたい場合があると思います。

そこで、今回は試しにTwitterのプロフィール情報を取得し画像にして返すtwitter-profile-cardというものを書きました。
Vercelにデプロイされており、URLにidのクエリを付けて叩くとTwitterのプロフィールの画像が返ってきます。
Twitterの情報を取得する手順で他のデータを取得すれば応用が利くはずです。

記事では書いたCSSの詳細は省いていますが、この様な感じで表示されます。(画像はpngにした物ですが、リンク先は生成された物になっています。)
詳しくはリポジトリを見て下さい。

twitter-profile-card-default

やりたいこと

  1. Twitter APIを叩いて、プロフィールを取得
  2. HTMLElementにデータを埋め込んでスタイリング1
  3. PuppeteerでHTMLElementのスクリーンショットを撮る
  4. svgの配下のimageタグにスクリーンショットを埋め込んだレスポンスを返す
  5. これらをデプロイ

構成

.
├── README.md
├── package.json
├── vercel.json
├── api
│   └── index.ts
├── src
│   ├── createCard.ts
│   ├── createElement.tsx
│   └── getTwitterData.ts
└── tsconfig.json
  • /apiがエンドポイントになります

データの取得

好きなように取得してください。
今回はTwitterのプロフィール情報を取得し返す、と言う事で適当に取得します。

src/getTwitterData.ts
exportfunctiongetTwitterData({id})=>{constheaders={Authorization:`Bearer ${process.env.TWITTER_BEARER_TOKEN}`}constparams={screen_name:id}constuserShowEndoPoint='https://api.twitter.com/1.1/users/show.json'returnnewPromise((resolve,reject)=>{axios.get(userShowEndoPoint,{headers,params}).then((response)=>resolve(response.data)).catch(async(err)=>{returnreject(err.response)})})}

データをスタイリングし、Puppeteerでスクリーンショットを撮る

VercelAWS Lambda上で動いてる為、デプロイパッケージのサイズに制限があります。
Puppeteerに同梱さているChrome単体パッケージが単体で250MBもあるので、そのままVercelにデプロイしてしまうと、サイズ上限に引っかかってしまいます。

そこで、サイズ上限を回避するためにchrome-aws-lambdapuppeteer-coreを利用します。2
この2つはバージョンを合わせる必要があるので注意が必要です。

src/createCard.ts
importchromefrom'chrome-aws-lambda'importpuppeteerfrom'puppeteer-core'

また、ローカル環境ではchrome-aws-lambdaが働いてくれないので、サーバー上で動いているかを条件分けし、ローカルでは自分のPCに入っているChromeを使うようにします。

src/createCard.ts
constbrowser=awaitpuppeteer.launch(process.env.AWS_REGION?{args:chrome.args,executablePath:awaitchrome.executablePath,headless:chrome.headless}:{args:[],executablePath:process.platform==='win32'?'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe':process.platform==='linux'?'/usr/bin/google-chrome':'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'})

データを表示する為に、htmlを書きます。
スタイリングにインテリセンスが効かないと辛いので、.tsx | .jsx拡張子のファイルを作り、JSX.Elementを返す関数を書きます。
全てを書くと長いので、ここではヘッダーを表示する部分だけ書きます。

src/createElement.tsx
import*asReactfrom'react'exportfunctioncreateElement(tweetData){constheader={height:'33%',width:'100%',overflow:'hidden'}constheaderImage={height:'100%',width:'100%',objectFit:'cover'}return(<divstyle={header}><imgsrc={tweetData.profile_banner_url}alt="header image"height="100px"width="300px"style={headerImage}/></div>)}

この様にinline-styleでスタイリングしていきます。

このJSX.Elementreact-dom/serverrenderToStringを使う事でstring型に変換でき、それをPuppeteerで読み込み、スクリーンショットを撮ります。

src/createCard.ts
import{renderToString}from'react-dom/server'import{createElement}from'./createElement'
src/createCard.ts
constelement=createElement(tweetData)constpage=awaitbrowser.newPage()awaitpage.setContent(`<html>
      <head>
        <style>
          body {
            width: "${width}";
            height: "${height}";
          }
        </style>
      </head>
      <body>${renderToString(element)}</body>
    </html>
  `)constimage=awaitpage.$('body')constbuffer=awaitimage.screenshot({encoding:'base64'})

補足

日本語が混じったDOMをそのままスクリーンショットしてしまうと、日本語のフォントがPuppeteerに存在しないので文字化けしてしまいます。
ですのでgooglefontsなどのcdnからfontを読み込む様にします。

src/createCard.ts
awaitchrome.font('https://rawcdn.githack.com/googlefonts/noto-cjk/be6c059ac1587e556e2412b27f5155c8eb3ddbe6/NotoSansCJKjp-Regular.otf')awaitchrome.font('https://rawcdn.githack.com/googlefonts/noto-fonts/ea9154f9a0947972baa772bc6744f1ec50007575/hinted/NotoSans/NotoSans-Regular.ttf')

撮ったスクリーンショットをクライアントに返す

上述しましたが、クライアント側のエンドポイントは/apiになります。
(いくつかのレポジトリでこうなってたので参考にしました。)
./api/index.tsで、リクエストを受けレスポンスを返す処理を行います。

exportdefaultasync(req,res)=>{const{id}=req.queryres.send(id)}

req.queryに叩いたURLのqueryが入ります。
(例えば/api?id=Twitterとすると、req.query.idにTwitterが入ります。)
res.sendでクライアントにレシポンスを返すことができ、res.setHeaderでHeaderを設定します。

先ほど撮ったスクリーンショットをsvgに埋め込み、クライアントに返します。

src/createCard.ts
return`<svg
    xmlns="http://www.w3.org/2000/svg"
    width="${width}"
    height="${height}"
    viewport="0 0 ${width}${height}"
    fill="none"
  >
    <image href="data:image/jpeg;base64,${buffer}" x="0" y="0" width="100%" height="100%"/>
  </svg>
`
api/index.ts
import{createCard}from'../src/createCard'import{getTwitterData}from'../src/getTwitterData'exportdefaultasync(req,res)=>{constresult=awaitgetTwitterData(req.query)// Twitterのデータ取得constsvgImage=awaitcreateCard()// svg画像のHTMLelementを取得res.setHeader('Content-Type','image/svg+xml')// svgを指定res.setHeader('Cache-Control',`public, max-age=${60*60*12}`)// データの変化があまりないのでキャッシュを12時間にres.send(svgImage)// データを返す}

これでクライアントにsvg画像を返すことができました。

確認、デプロイ

vercel.jsonを設定し、Vercel devでローカルサーバーを起動できます。
http://localhost:3000/api?id=Twitterで画像が表示されれば完成です。

あとはvercelコマンドでデプロイできます、簡単ですね。

まとめ

参考


  1. スタイリングにインテリセンスが効かないと辛いので(1敗)、スタイリングはjsxで書いていき、ReactDOMServer.renderToString()を使って変換します 

  2. chrome-aws-lambdaの配下にPuppeteerが存在し、それを利用すれば一見動きそう何ですが、puppeteer-coreが依存関係にあり、両方必要です 


Viewing all articles
Browse latest Browse all 8934

Trending Articles