LoginSignup
2
2

More than 1 year has passed since last update.

Githubにツイッターのプロフィールを表示できる画像を生成するAPIを作る

Last updated at Posted at 2021-01-22

巷では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
export function getTwitterData({ id }) => {
  const headers = {
    Authorization: `Bearer ${process.env.TWITTER_BEARER_TOKEN}`
  }

  const params = {
    screen_name: id
  }

  const userShowEndoPoint = 'https://api.twitter.com/1.1/users/show.json'

  return new Promise((resolve, reject) => {
    axios
      .get(userShowEndoPoint, { headers, params })
      .then((response) => resolve(response.data))
      .catch((err) => {
        return reject(err.response)
      })
  })
}

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

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

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

src/createCard.ts
import chrome from 'chrome-aws-lambda'
import puppeteer from 'puppeteer-core'

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

src/createCard.ts

  const browser = await puppeteer.launch(
    process.env.AWS_REGION
      ? {
          args: chrome.args,
          executablePath: await chrome.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 * as React from 'react'

export function createElement(tweetData) {

  const header = {
    height: '33%',
    width: '100%',
    overflow: 'hidden'
  }

  const headerImage = {
    height: '100%',
    width: '100%',
    objectFit: 'cover'
  }
  
  return (
    <div style={header}>
        <img
          src={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

const element = createElement(tweetData)

const page = await browser.newPage()
await page.setContent(
  `<html>
      <head>
        <style>
          body {
            width: "${width}";
            height: "${height}";
          }
        </style>
      </head>
      <body>${renderToString(element)}</body>
    </html>
  `
)

const image = await page.$('body')
const buffer = await image.screenshot({ encoding: 'base64' })

補足

日本語のフォントがPuppeteerに存在しないので文字化けしてしない為、elementに日本語が混じっていると文字化けしてしまうのでgooglefontsなどのcdnからfontを読み込む様にします。

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

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

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

export default async (req,res) => {
  const { id } = req.query
  res.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'

export default async (req,res) => {
  const result = await getTwitterData(req.query) // Twitterのデータ取得
  const svgImage = await createCard()  // 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コマンドでデプロイできます、簡単ですね。

まとめ

フォロワーが居ないアカウントでTwitterのプロフィールを表示させると悲しくなるので注意が必要

参考

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

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

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2