一見すると楽だけど実はめんどくさい開発ランキング
— 統合開発環境 (@sadnessOjisan) March 17, 2020
1位: Safariのバウンス対応
2位: SPAにおけるOGPの動的生成
3位: Safariのモーダル背景スクロールさせない対応
マジな話、思ってるよりかなりめんどくさいです
OGP芸、2020年フロントエンドエンジニアの試金石になるんじゃないでしょうか
SPAで動的OGP生成要件が出た
困りますよね。CGMやってると8割ぐらいで発生しそう
CGMってGCMとタイポするとGoogleの新製品みたいですよね。
構成
ちょっとググってみると分かる通り、色々な構成が考えられます。
例えば
- Node.js側で生成し、ストレージにあげ、metaタグにURLをセット
- UserAgentを見て、botならCloud Functionsに飛ばして、Cloud Functionsがmetaタグ付きのHTMLを返す
今回は
- metaタグにCloud FunctionsのURLを渡す
- 例えば
https://asia-northeast1-hoge.cloudfunctions.net/ogp?text=${fuga}
- 例えば
- Cloud FunctionsではStorageに該当画像があればリダイレクト、なければ生成してアップロードしてからリダイレクトする
で行こうと思います
これが一番シンプルなのでは?でもやっている人いなかった、なんか問題あるのか?
関係ないですが、Firebase*Nuxt.js関連の記事がめちゃくちゃ多くて、日本だと本当にVueが多いんだなと思いました
Firebase Cloud Functionsのセットアップ
「爆速で〜」とか「3分で出来る〜」とか全部フェイクで、セットアップだけで1時間ぐらい潰れます
代表的な罠を列挙しておきます
- Nodeは8系が推奨されている(10系は使えない)のでnodebrewなりnodenvなりで用意しておく必要がある
- デフォルトでTSLintを使おうとしてくる(コマンドライン対話時に拒否できる)のでESLint使う人は注意
- デフォルトでnpmを使おうとしてくる(コマンドライン対話時に拒否できる)のでyarn使う人は注意
やることは
mkdir hoge
cd hoge
firebase init functions
で後は対話環境なんでよしなにやって下さい。
10分以内にリンタ環境まで整えられれば、あなたはFirebaseマスターです
ローカルデバッグ
firebase functions:shell
でローカル対話環境が開きます
そこで${function名}:get('?text=太郎')
等でデバッグ出来るので、活用していきましょう
注意点として、本番環境だと書き込み可能なストレージはtmp/
ディレクトリのみです
ローカルだとどこでも書き込めると思うので、いざデプロイするとこれで例外発生したりします
実行ファイル
node-canvasでやります
node.jsではてなブログ風アイキャッチ画像を動的に生成するを大いに参考にしています
import * as fs from 'fs'
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'
import { createCanvas, loadImage } from 'canvas'
admin.initializeApp(functions.config().firebase)
const bucket = admin.storage().bucket() // デフォルトバケットじゃない人はここで指定
言うまでもないですが、権限はGOOGLE_APPLICATION_CREDENTIALS
を見ています
ローカルで実行する際はよしなに設定して下さい
exports.ogp = functions
.region('asia-northeast1')
.https.onRequest(async (req, res) => {
const { text } = req.query
const isExists = await checkIsExists(text)
if (isExists) {
const url = getUrl(`ogps/${text}.png`)
res.redirect(url)
return
}
const url = await createOgp(text)
res.redirect(url)
})
これが関数の本体部分になります
見れば分かるので次に行きましょう
const checkIsExists = async (text: string): Promise<boolean> => {
const filePath = `ogps/${text}.png`
const isExists = await bucket.file(filePath).exists()
return isExists[0]
}
const getUrl = (targetPath: string): string =>
`https://firebasestorage.googleapis.com/v0/b/hoge/o/${encodeURIComponent(
targetPath
)}?alt=media`
bucketのogps/${text}.png
に画像を格納している想定ですね
getUrl
はいちいちgetDownloadURL()
するのダルいので使っています
publicに公開されているなら、たぶん動くはずです
const fontStyle = {
font: 'bold 85px "Noto Sans CJK JP"',
lineHeight: 100,
color: '#FFFFFF'
}
// Return result url
const createOgp = async (text: string): Promise<string> => {
const loaclTargetPath = `/tmp/target.png`
const localBasePath = '/tmp/base.png'
const targetPath = `ogps/${text}.png`
const basePath = 'ogps/base.png'
// 背景画像のダウンロード
await bucket.file(basePath).download({ destination: localBasePath })
const canvas = createCanvas(1280, 670)
const ctx = canvas.getContext('2d')
// 背景画像の描画
const baseImage = await loadImage(localBasePath)
ctx.drawImage(baseImage, 0, 0, 1280, 670)
// 文字列の書き込み
ctx.font = fontStyle.font
ctx.fillStyle = fontStyle.color
ctx.textBaseline = 'top'
const topPadding = 60
const leftPadding = 50
const rightPadding = 600
const lineWidth = 1280 - leftPadding - rightPadding
const lines = splitByMeasureWidth(name, lineWidth, ctx)
const lineCount = lines.length
for (let index = 0; index < lineCount; index++) {
const element = lines[index]
ctx.fillText(
element,
leftPadding,
topPadding + fontStyle.lineHeight * index
)
}
// tmpディレクトリへの書き込み
const buf = canvas.toBuffer()
fs.writeFileSync(loaclTargetPath, buf)
// Storageにアップロード
await bucket.upload(loaclTargetPath, { destination: targetPath })
// tmpファイルの削除
fs.unlinkSync(localBasePath)
fs.unlinkSync(loaclTargetPath)
return getUrl(targetPath)
}
const splitByMeasureWidth = (
str: string,
maxWidth: number,
context: { measureText: CallableFunction }
): Array<string> => {
// サロゲートペアを考慮した文字分割
const chars = Array.from(str)
let line = ''
const lines = []
for (let index = 0; index < chars.length; index++) {
if (maxWidth <= context.measureText(line + chars[index]).width) {
lines.push(line)
line = chars[index]
} else {
line += chars[index]
}
}
lines.push(line)
return lines
}
この部分はほとんどnode.jsではてなブログ風アイキャッチ画像を動的に生成するに依っています
paddingやフォントはよしなに変更して下さい
Cloud Functionsで日本語フォントがちゃんと動くのか心配だったんですが、問題なく動いているようです
結果
良かったですね。
「下線を引きたい」「文字量に応じてフォントサイズを可変にしてほしい」などの要件が来た人は、もっとがんばってくださいね
いかがでしたか?
このブログは業務のデザイン上がってくるの待ちの時間に書いただけなので、コードや完成品をGitHubで共有したりしません。
皆さんもがんばってOGP芸を身に着け、2020年を生き延びましょうね