Hugoにブログカードを埋め込む

Netlify FunctionsでJSONを返すAPIを立ち上げたらいい感じにできた話

HugoとAcademicテーマを使ってブログを構築したのはいいのですが、すぐに欲しい機能がないことに気付きました。それは「ブログカード」です。ブログカードというのは、以下のようなURLの埋め込み表示形式のことをいい、これがないとURLの文字列の表示だけになってしまい(または マークダウンで文字列リンクとするだけ)、なんとも寂しい表示になってしまいます。

このブログカードを作る方法についてですが、Hugoのデフォルト機能には入っておらず、基本的には自作するほかありません。また自作するにあたっては、Hugoの機能であるShortcodeを用いて解決することが可能です。

Shortcodeとは

Shortcodeというのは、マークダウンテキスト内に書けるSnippetのことで、ビルド時にSnippetのテンプレートからhtmlを生成して埋め込んでくれます。デフォルトでもTwitterのツイートやinstagramの投稿などいくつかのソーシャルメディアに対応するShortcodeがあります。今回はこのShortcodeを使ってブログカードのSnippetを作ろうという試みです。

仕組み

まず、ブログカードを作るために必要なものとして、metaタグの解釈があります。URLそのものから遷移先のページタイトルや画像などを取得する必要があるためです。幸い、Shortcodeに対応するテンプレートhtmlを作る際、テンプレート内で getJSON というJsonリクエストをできる関数が用意されているためこれを使えば特定のURLからogpのmetaタグをパースしたJsonを返してそれを用いることができそうです。

URLにアクセスしてタグをパースしてJsonで返却するAPIは、今回はNetlify Functionsを使うことにしました。理由はデプロイにNetlifyを使っており同じサービス・同じアカウントで機能を使った方が楽だと思ったからです。また、利用するのはビルドする時だけで、一度要素が取得できればキャッシュされるということで、無料枠の範囲内で十分事足ります。

実装手順

本記事では、Hugo+AcademicテーマをNetlifyでデプロイしている前提で説明します。また、執筆時点のNetlify Functionsは NodeJS12.x が対応していますので、NodeJS12 の環境を作っておく必要があります。

Netlify Functions環境のセットアップ

まず、執筆時点でのNetlifyのデフォルト環境では NodeJS10.x で実行されますので、それを12にするために環境変数を設定します。

Netlifyにログインし、Site dashboardから、Settings > Build & deploy > Environment > Environment variables の順に進み、以下の環境変数を入れておきます。

今回は、 src/functions/ にFunctionsのソースファイルを置き、 functions/ にビルドしたFunctionsファイルを置く設定にします。まずはファイルだけ作っておきます。

mkdir -p src/functions functions
#名前はhoge.jsとしていますが、任意のもので構いません。
touch src/functions/hoge.js

ルートディレクトリにある netlify.toml を編集します。以下のようにfunctionsのディレクトリ指定と、hugoコマンドの実行前に後ほど設定する yarn run build を追加します。

[build]
  command = "yarn run build && hugo --gc --minify -b $URL"
  publish = "public"
  functions = "./functions"

Netlify Functionsのビルドと、Fuctions内でOGPパーサーを利用するために netlify-cli と netlify-lambdaopen-graph-scraper をインストールします。

yarn global add netlify-cli 
yarn add --dev netlify-lambda
yarn add open-graph-scraper

さらに package.json に以下のscriptsを追加しておきます。

{
  "scripts": {
    "build": "netlify-lambda build src/functions/",
    "serve": "netlify dev"
  }
}
  • $yarn run build を実行すると、src/functions/ 内のファイルをビルドして functions/ ディレクトリにjsファイルを配置します。
  • $yarn run dev を実行すると、WebサーバとFunctions双方のローカルサーバが立ち上がります
    • Webサーバ: http://localhost:8888
    • Functions: http://localhost:34567/<Function名>
      • 例通りにやると、Function名は hoge.js です。
ちなみにFunctionsローカルサーバの立ち上げ方として netlify-lambda serve という別のコマンドがありますが、legacy commandとされており、現在は上記のやり方が推奨とされています。

Functionを作成する

src/functions/hoge.js を以下のように実装します。

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)}
  const ogs = require("open-graph-scraper");
  ogs(options).then(function(data) {
    const metadata = data.data;
    console.log(metadata);
    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 {
      ogpData['image'] = metadata.ogImage.url;
    }
    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);
  });

};
  • event.queryStringParameters.url でブログカードに利用するURLを取得
  • open-graph-scraperを使ってURLのmetaタグをパース
  • パースしたデータから最低限必要なデータを取得し、Jsonを返す

これで、metaタグを解釈するAPIが完成します。Functionsサーバを立ち上げて、 curl http://localhost:34567/hoge?url=<任意のURL> とするとJsonデータが取得できるようになります。

Shortcodeを作成する

まずはFunctionのエンドポイントを設定しておきます。 config/_default/config.toml に設定しておくのがわかりやすいです。ローカルでのテスト時はlocalhostにしてください。

[params]
  # OgpApiEndpoint = "http://localhost:34567/.netlify/functions/hoge?url="
  OgpApiEndpoint = "https://<netlifyのhost名>/.netlify/functions/hoge?url="

次にacademicテーマ内にある shortcodes ディレクトリにhtmlを書いていきます。ファイル名がShortcode呼び出し時の名前になります。

{{ $url := .Get 0 }}

{{ $jsonData := getJSON $.Page.Site.Params.OgpApiEndpoint $url }}
{{ $siteName := $jsonData.siteName }}
{{ $title := $jsonData.title }}
{{ $description := $jsonData.description }}
{{ $image := $jsonData.image }}
{{ $urlInfo := urls.Parse $url }}
{{ $host := printf "%s" $urlInfo.Host }}
{{ $prefix := "https://www.google.com/s2/favicons?domain=" }}
{{ $favicon := printf "%s%s" $prefix $urlInfo.Host }}

<div class="box">
    <a class="box-content" target="_blank" rel="noopener" href="{{ $url }}">
        <div class="box-content">
            <div class="box-left">
                <h2 class="box-title">{{ $title }}</h2>
                <div class="box-description">{{ $description }}</div>
                <div class="box-host">{{ $host }}</div>
            </div>
            <div class="box-image-container" style="background-image: url('{{ $image }}');" ></div>
        </div>
    </a>
</div>

対応するCSS(SCSS)はこんな感じ。

div.box {
  margin-top: 16px;
  margin-bottom: 16px;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1), inset 0 0 0 1px rgba(0, 0, 0, 0.1);
}

.dark div.box {
  box-shadow: 0 1px 4px rgba(255, 255, 255, 0.1), inset 0 0 0 1px rgba(255, 255, 255, 0.1);
}

a.box-content {
  -webkit-tap-highlight-color: transparent;
  text-decoration: none;
  color: rgba(0, 0, 0, 0.8);
}

.dark a.box-content {
  color: rgba(255, 255, 255, 0.8);
}

a.box-content.hover {
  text-decoration: none;
}

.dark h2.box-title {
  color: rgba(255, 255, 255, 1);
}

div.box-content {
  display: flex;
  align-items: center;
}

div.box-left {
  flex: 1;
  padding: 16px 20px;
}

h2.box-title {
  font-size: 18px;
  font-weight: 600;
  line-height: 20px;
  margin: 0;
}

div.box-description {
  font-size: 16px;
  line-height: 20px;
  margin-top: 8px;
}

div.box-host {
  margin-top: 12px;
  font-size: 15px;
  color: rgba(0, 0, 0, 0.54);
  line-height: 20px;
  margin-top: 12px;
}

.dark div.box-host {
  color: rgba(255, 255, 255, 0.2);
}

div.box-image-container {
  width: 160px;
  height: 167px;
  background-position: center center;
  background-repeat: no-repeat;
  background-size: cover;
}

Shortcodeを利用する

ここまでできたら後は呼び出すだけです。新しく記事を新規作成して、その中で上のshortcodeを呼び出します。仮にファイル名が blogcard.html とすると{ {% blogcard "<URL>" %}}のように書くことで呼び出しができます({}のスペースは無くしてください)。

という感じになります。そしてファイルを保存しビルドするときにHugoサーバがFunctionosをリクエストしてJsonを取得し、ブログカードの表示に置き換われば完成です。

デプロイする

ここまでできたら、コミットしてプッシュします。Netlifyのデプロイタスクが自動的に実行され、Functionsの設定まで行われるので、特別にやることはありません。

終わりに

気になっていることとして、URLによっては望まない画像が表示されてしまうことがあります。これはまた今後Functionsに手を入れてなおしていきたいと思います。

下記のサイトが非常にわかりやすく、参考になりました。

Software Engineer

issueを倒したいエンジニア。

関連項目