Hugoにブログカード埋め込みshortcodeを実装する

先日書いた記事で、他に自分が運営しているUCSB留学ブログを記事に埋め込んで紹介したのですが、実はその埋め込み作業にかなり手間がかかったのでまとめます。

はてなのapiで埋め込めない

最初はこちらの記事にも書かれているように、iframeではてなのapiを使用して手軽に済ませようと思ったのですが(以下例)、なぜかうまく画像が表示されず苦戦。

<iframe class="hatenablogcard" style="width:100%;height:155px;max-width:680px;" title="URLを記入するだけ!はてなブログカード風にWordpress記事も表示させるカスタマイズ方法" src="https://hatenablog-parts.com/embed?url=http://nelog.jp/wordpress-blog-card" width="300" height="150" frameborder="0" scrolling="no"></iframe>

自分のサイトのogpがちゃんと設定されていないのかなとも思いましたが、twitterやfacebook では綺麗に表示されているので大丈夫そう。。

自作しよう

いくら調べても原因がわからなかったのでhtmlをベタ書きして埋め込もうかとも思いましたが、悔しいのできっちり自作してやることにしました。参考にしたのは以下のサイト。

目標としては、hugoのdata driven contentを利用してshortcodeでurlを指定するだけでogpの情報を埋め込める実装を目指しました。

ogpの情報をJSONで返却するapiを実装する

早速cloud functions for firebaseを利用し、ogpの情報をスクレイプしてJSONで結果を返すapiを作ります。以下該当コード。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
exports.ogp = functions.https.onRequest((req, res) => {
    const parser = require("ogp-parser");
    const params = req.query;
    const chacheControl = 'public, max-age=31557600, s-maxage=31557600';
    if (!params.hasOwnProperty('url')) {
        console.error("Error getting ogp data: please provide url");
        return res.json({ error: "Error getting ogp data: please provide url" });
    }
    return parser(encodeURI(params['url']), false)
        .then((data) => {
            console.log(data);
            console.log(params['url']);
            if (!data.hasOwnProperty('title')) {
                console.error("Error getting ogp data: no ogpData returned");
                return res.json({ error: "no ogpData returned" });
            }
            let ogpData = {};
            ogpData['siteName'] = data.title;
            for(let prop in data.ogp) {
                if (/^og:/g.test(prop)) {
                    ogpData[prop.split(':')[1]] = data.ogp[prop][0];
                }
            }
            return res.set('Cache-Control', chacheControl).json(ogpData);
        })
        .catch((err) => {
            console.error("Error getting ogp data: " + err);
            return res.json({ error: err });
        });
});

apiのendpointにクエリパラメーターでurlを付与することでogpを取得できるようになっています。また、何度もapiを叩いて欲しくないのでCache-Controlでcacheを1年間有効にしています。

加えて、9行目でogpのparserにencodeURIしたurlを渡しているのは、素のurlをparserに渡す実装にしていたところ、urlに日本語が入っているページをリクエストしたら 404 page not found でogpの情報が返ってきてしまっていたためです。しばらく原因がわからずハマったので注意してください。

firebase hostingとfunctionsを連携する

このブログではfirebase hostingを採用しているため、同様にfirebaseのサービスであるfunctionsとは簡単に連携することができます。

Cloud Functions による動的コンテンツの配信 | Firebase

上のdocumentに従って、firebase.json にrewritesの設定を追記します。これだけでfirebase hostingのbase domainに関数名を加えるだけでapiを叩けるようになります。

{
  "hosting": {
    "public": "public",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
    "rewrites": [ {
      "source": "/ogp", "function": "ogp"
    } ]
  },
  "functions": {
    "predeploy": [
      "npm --prefix \"$RESOURCE_DIR\" run lint"
    ]
  }
}

hugoのshortcodeを作る

続いてshortcodeを実装します。完成したコードは以下です。自分は web-embed.html としてshortcodeに追加しました。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
{{ $url := .Get "url" }}
{{ $json := getJSON $.Page.Site.Params.OgpApiEndpoint $url }}
{{ $siteName := $json.siteName }}
{{ $title := $json.title }}
{{ $description := $json.description }}
{{ $image := $json.image }}
{{ $urlInfo := urls.Parse $url }}
{{ $host := path.Join $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 hatena-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>

サイトのconfigに設定したOgpApiEndpointにgetJSONした結果を利用しています。faviconに関してはgoogleのapiを利用して取得しています。

上記のshortcodeに対応するcssが以下です。はてなのcssを丸パクリしています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
div.page-embed.hatena-web-card{
    height: 155px;
    border: 1px solid rgba(0, 0, 0, 0.1);
}

div.page-embed.hatena-web-card div.embed-wrapper-inner{
    padding: 12px;
}

div.page-embed.hatena-web-card div.embed-content.with-thumb{
    height: 100px;
    overflow: hidden;
    position: relative;
}

div.page-embed.hatena-web-card div.embed-content.with-thumb .thumb-wrapper{
    position: absolute;
    top: 0;
    right: 0;
    width: 100px;
    height: 100px;
    overflow: hidden;
}
div.page-embed.hatena-web-card div.embed-content.with-thumb .thumb-wrapper .thumb{
    width: auto;
    max-width: 200%;
    height: 100px;
    border: none !important;
    display:block;
    position: relative;
    left: 50%;
    transform: translateX(-50%);
}

div.page-embed.hatena-web-card div.embed-content.with-thumb .entry-body{
    margin-right: 110px;
}
div.page-embed.hatena-web-card div.embed-content.with-thumb .entry-body .entry-title{
    font-size: 17px;
    margin: 0 0 2px;
    line-height: 1.4;
    max-height: 47px;
    overflow: hidden;
    border: none;
}

div.page-embed.hatena-web-card div.embed-content.with-thumb .entry-body .entry-content{
    line-height: 1.5;
    font-size: 12px;
    max-height: 72px;
    overflow: hidden;
    border: none;
    padding-bottom:0;
}

div.page-embed.hatena-web-card div.embed-footer{
    margin-top: 8px;
    height: 15px;
    position: relative;
    font-size: 11px;
}

div.page-embed.hatena-web-card div.embed-footer img.favicon{
    width:16px;
    height: 16px;
    display: inline;
    vertical-align: middle;
    border: none !important;
}

結果

{ {< web-embed url="https://seita.tokyo/" >} }

↑これがこうなる↓

Seita Blog

留学・プログラミング・読書についての記録
⚠️コードブロックがshortcodeとして解釈されるのを防ぐため波括弧にスペースを入れて対処しています。

Share Comments
comments powered by Disqus