Netlifyで展開しているHugoのサイトでブログカードを利用できるようにする。
Netlify Functionsを利用してブログカードを利用できるようにした
WordPressから移行するときに、色々と調べて回った中の一つに「ブログカード」という概念があった。
こういうものだ↓
バイバイ、WordPress。ハロー Hugo
調べていく中で「これは是非最初から導入しておこう」と思って導入したものの、詰まった点や、参考にしたサイトには記載のなかったものなどを忘れないうちにまとめておく。
仕組み
細かい内容は、参考元の以下の2つのサイトにも記載されている。ブログカードを作るためには、以下のことを行なっている。
- 挿入したいサイトからOGPのmetaタグをJSON形式で取得するAPIを作成する
- APIはNetlifyが提供しているNetlify FunctionsというAWS上で稼働するサービス上で稼働させる
- APIはHugoサイトと同じリポジトリで管理する
- 記事内で利用するときは、HUGOのショートコード機能を利用する
- この記事の時点では、125,000リクエスト、月100時間まで無料。
APIとショートコード、2つの実装をする必要があるが、先達の方々がおられるので、あまり悩む必要はなかった。
それでも色々と変更した点や修正した内容があるので、改めてまとめる。
参考としたサイト
ページが見つかりませんでした
</div>
</div>
</div>
<div class="embed-footer">
<a href="https://blog.a-1.dev" target="_blank">
<img src="https://www.google.com/s2/favicons?domain=blog.a-1.dev" alt="" title="ページが見つかりませんでした" class="favicon">
https://blog.a-1.dev
</a>
</div>
</div>
</div>
ページが見つかりませんでした
</div>
</div>
</div>
<div class="embed-footer">
<a href="https://wada.page" target="_blank">
<img src="https://www.google.com/s2/favicons?domain=wada.page" alt="" title="ページが見つかりませんでした" class="favicon">
https://wada.page
</a>
</div>
</div>
</div>
実装
Netlify Functionsのセットアップ
NPMはNodeJS14系を利用することにした。
Netlifyの設定画面でも設定することは可能だが、できるだけ設定はソース管理したい。
そこでリポジトリルートに、.nvmrcファイルを作成してNetlifyに対してNodeのバージョンを指定する。
.nvmrcファイル
| |
このように設定することで、Netlifyが利用するNode JSを14.17.4に固定化できる。
Netlify Functionsを作成する
src/functions/にFunctionsのソースファイルを置き、 functions/ にビルドしたFunctionsファイルを置く設定にする。
| |
ルートディレクトリにあるnetlify.tomlを編集する。
また、functionsのディレクトリ指定と、hugoコマンドの実行前netlify functionsのビルドを設定する。
| |
ローカル環境のHUGO環境でNetlify Functionsをデバッグできるようにする
自分のローカル環境はDockerで構築しているため、netlify-lambdaや、今回のAPIで利用するためのnpmライブラリをインストールするようにする
| |
また、package.jsonにscriptsの記述を設定する
| |
npm run buildを実行すると、src/functions/ 内のファイルをビルドしてfunctions/hoge.jsが作成される。
npm run serveを実行すると、HUGOサーバとFunctions双方のローカルサーバが立ち上がる。
- Webサーバ: http://localhost:8888
- Functions: http://localhost:9000/functions/<Function名>
流れ通りにすると、Function名は hogeとなる。
Functionsを実装する
src/functions/hoge.js を以下のように実装する。
特にここの部分がポイントとなっているが、例えばKickstarterなどは、一定の範囲のUserAgent以外は403エラーが戻ってくる。おそらくはBot対策だろう。
AWS起点でアクセスしているということや、NPMライブラリ、open-graph-scraperでアクセスしていること、さらには、timeout設定をつけないことには不必要にダンマリとなってしまうことが想定される。
URLによってはリンク先が消失していることも考えられるため、200レスポンス以外の想定も入れたい考えているが、今のところ200か403、404以外のレスポンスを確認できていない。
| exports.handler = (event, _context, callback) => { | |
| if ('url' in event.queryStringParameters === false) { | |
| console.error("parameter 'url' is necessary!!"); | |
| return; | |
| } | |
| const url = event.queryStringParameters.url; | |
| const options = { | |
| 'url': encodeURI(url), | |
| 'timeout': 10000, | |
| 'headers': { | |
| 'user-agent': 'My-Functions', | |
| }, | |
| } | |
| const ogs = require("open-graph-scraper"); | |
| ogs(options).then(function(data) { | |
| const metadata = data.result; | |
| let ogpData = {}; | |
| ogpData['siteName'] = metadata.ogSiteName; | |
| ogpData['title'] = metadata.ogTitle; | |
| ogpData['description'] = metadata.ogDescription; | |
| if (Array.isArray(metadata.ogImage)) { | |
| const jpgUrl = metadata.ogImage.find((image) => image.url.endsWith('.jpg') || image.url.endsWith('.jpeg')).url | |
| ogpData['image'] = jpgUrl | |
| } else if (typeof metadata.ogImage !== 'undefined'){ | |
| ogpData['image'] = metadata.ogImage.url; | |
| } | |
| console.log(ogpData); | |
| console.log(JSON.stringify(ogpData)); | |
| callback(null, { | |
| statusCode: 200, | |
| "headers": { "Content-Type": "application/json; charset=utf-8"}, | |
| body: JSON.stringify(ogpData) | |
| }); | |
| }).catch(function(error) { | |
| console.error(error); | |
| let ogpData = {}; | |
| ogpData['status'] = "404"; | |
| return; | |
| }); | |
| }; |
他にもopen-graph-scraperのオプションパラメータがあると思うので、今後も必要に応じてアップデートしたいと思う。
ショートコードの作成を行う
Functionのエンドポイントを設定する。
HUGOは環境に応じて設定ファイルの読み替えをすることが可能なので以下のように設定する
| |
基本的な設定は_default/config.tomlに記載し、ローカル専用の設定はdevelopment/config.tomlの内容で上書きを行う
例えばそれぞれ以下のようにすると、Netlifyが利用する設定は、
| |
としておき、ローカルの環境は
| |
と設定しておけば良い。
次に自分のテンプレート内のshortcodesディレクトリにhtmlを増やす。ファイル名が{{}}で呼び出すショートコードの名前となる。
| {{ $url := .Get 0 }} | |
| {{ $jsonData := getJSON $.Page.Site.Params.OgpApiEndpoint $url }} | |
| {{ if $jsonData }} | |
| {{ $siteName := $jsonData.siteName }} | |
| {{ $title := $jsonData.title }} | |
| {{ $description := replace $jsonData.description "\n" ""}} | |
| {{ $image := (replace $jsonData.image "http://" "https://")}} | |
| {{ $urlInfo := urls.Parse $url }} | |
| {{ $host := printf "%s://%s" $urlInfo.Scheme $urlInfo.Host }} | |
| {{ $prefix := "https://www.google.com/s2/favicons?domain=" }} | |
| {{ $favicon := printf "%s%s" $prefix $urlInfo.Host }} | |
| <div class="body-iframe page-embed blog-web-card"> | |
| <div class="embed-wrapper"> | |
| <div class="embed-wrapper-inner"> | |
| <div class="embed-content with-thumb" > | |
| <div class="thumb-wrapper"> | |
| <a href="{{ $url }}" target="_blank"> | |
| <img src="{{ $image }}" class="thumb"> | |
| </a> | |
| </div> | |
| <div class="entry-body"> | |
| <h2 class="entry-title"> | |
| <a href="{{ $url }}" target="_blank"> | |
| {{ $title }} | |
| </a> | |
| </h2> | |
| <div class="entry-content"> | |
| {{ $description }} | |
| </div> | |
| </div> | |
| </div> | |
| <div class="embed-footer"> | |
| <a href="{{ $host }}" target="_blank"> | |
| <img src="{{ $favicon }}" alt="" title="{{ $title }}" class="favicon"> | |
| {{ $host }} | |
| </a> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {{else}} | |
| <a href="{{$url}}">リンク先が取得できませんでした。</a> | |
| {{end}} |
サイトによっては、OGPのimageメタタグがhttpのURLスキームを利用しているため、強制的にhttpsのURLスキームに変換するようにして、miedコンテンツではないようにしている。
対応するCSSは以下となった。
| .blog-web-card { | |
| height: 155px; | |
| border: 1px solid rgba(0, 0, 0, 0.1); | |
| margin-top: 30px; | |
| margin-bottom: 30px; | |
| background: #FFF; | |
| .embed-wrapper{ | |
| .embed-wrapper-inner{ | |
| padding: 12px; | |
| .with-thumb{ | |
| height: 100px; | |
| overflow: hidden; | |
| position: relative; | |
| .thumb-wrapper{ | |
| position: absolute; | |
| top: 0; | |
| right: 0; | |
| width: 100px; | |
| height: 100px; | |
| overflow: hidden; | |
| .thumb{ | |
| width: auto; | |
| max-width: 200%; | |
| height: 100px; | |
| border: none; | |
| display: block; | |
| position: relative; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| } | |
| }.entry-body{ | |
| margin-right: 110px; | |
| .entry-title{ | |
| font-size: 17px; | |
| margin: 0 0 2px; | |
| line-height: 1.4; | |
| max-height: 47px; | |
| overflow: hidden; | |
| border: none; | |
| padding: 0; | |
| } | |
| } | |
| } | |
| }.embed-footer{ | |
| margin-top: 8px; | |
| height: 15px; | |
| position: relative; | |
| font-size: 11px; | |
| .favicon{ | |
| display: inline; | |
| vertical-align: middle; | |
| border: none; | |
| } | |
| } | |
| } | |
| } |
Netlify Functionsのデプロイを行う
Dockerを利用したローカル環境で何事もなく動作していたため、既存のWPサイトのブログカードをいくつかNetlify FunctionsにデプロイするAPIが利用できることを前提に記事の内容を修正して、初回のデプロイを試みたのが問題となった。
ローカル環境ではサイトがデプロイされているのに、Netlifyではデプロイされない。デプロイエラーになる。という現象が発生した。
原因としては、Netlifyのデプロイ中、hoge.jsのビルドが終わる前にショートコードが実行、JSONファイルの取得を試みた結果、JSON(API)のレスポンスがないために、ビルドエラーとなる、というものであった。
このことに気がつくまでに結構時間をかけてしまったので、試す際は、まずショートコードを利用しない状態でNetlifyにデプロイを行い、デプロイが成功したら、ショートコードを使う、という段階を踏んだデプロイをお勧めする。





