Hugoにブログカード埋め込みshortcodeを実装する
Tuesday, January 29, 2019
先日書いた記事
で、他に自分が運営している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.icu/" >} }
↑これがこうなる↓
⚠️コードブロックがshortcodeとして解釈されるのを防ぐため波括弧にスペースを入れて対処しています。Seita Uchimura
Software Engineer in Tokyo