Hugoでブログを書いていてずっと欲しかったブログカードをshortcodeで実装してみました。
はてなブログでURLを「埋め込み」形式で貼り付けたときのイメージを目指します。
以下の流れでやっていきます。
- 指定したURLのogp情報を取得するNetlify Functionを作成
- Netlify Functionから取得したogp情報を元にブログカードを生成するshortcodeを作成
- ブログ記事からshortcodeを呼び出す
取得する情報
当ブログにも設置していますがheadタグ下のmetaタグから必要な情報を取得します。
記事の「タイトル」「アイキャッチ画像」「記事の説明」が今回取得したいものになります。
Twitter Cardからも欲しい情報は取得できますが今回はogpタグから取得します。
<meta name="description" content="Mac OSのDockerで/etc/localtimeがマウントできなくなって困った話">
<meta property="og:title" content="Mac OSのDockerで/etc/localtimeがマウントできなくなって困った話 - A1 Blog">
<meta property="og:type" content="article">
<meta property="og:url" content="https://blog.a-1.dev/post/2019-04-13-docker-mount/">
<meta property="og:image" content="https://blog.a-1.dev/images/docker-mount-error/title.png">
<meta property="og:site_name" content="A1 Blog">
<meta property="og:description" content="Mac OSのDockerで/etc/localtimeがマウントできなくなって困った話">
<meta property="og:locale" content="ja_JP">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="A1 Blog">
<meta name="twitter:url" content="https://blog.a-1.dev/post/2019-04-13-docker-mount/">
<meta name="twitter:title" content="Mac OSのDockerで/etc/localtimeがマウントできなくなって困った話 - A1 Blog">
<meta name="twitter:description" content="Mac OSのDockerで/etc/localtimeがマウントできなくなって困った話">
<meta name="twitter:image" content="https://blog.a-1.dev/images/docker-mount-error/title.png">
Netlify Functionsを実装する
NetlifyにはAWS Lambdaが無料で使えるFunctionsという機能があります。
執筆時点ではNode.jsとGoしか使えませんが、125,000アクセス/月、100時間/月まで無料で利用できるという神サービスです。
今回は、Functionsを使用して指定したURLのogp情報をJSONで返すAPIを実装します。
Netlify Functionsを有効化
functions="./functions"
をnetlify.tomlに追加してFunctionsを配置するフォルダを指定します。
指定するフォルダはデフォルトの./functionsフォルダとしました。
あと、commandに後ほど設定するnpm run build
をhugoコマンドの実行前に追加しておきます。
[build]
command = "npm run build && hugo --gc --minify --enableGitInfo"
functions="./functions"
netlify-lambdaのインストール
Functionをローカルでデバッグするため、netlify-lambdaをインストールします。
$ npm install --save-dev netlify-lambda
- netlify-lambda serve
$(npm bin)/netlify-lambda serve <ソースフォルダ>
を実行するとローカルにサーバーが立ち上がりhttp://localhost:9000/<Function名>
でデバッグできるようになります。
- netlify-lambda build
$(npm bin)/netlify-lambda build <ソースフォルダ>
を実行すると内蔵されているwebpackが実行され./functions
フォルダにjsファイルが作られます。
それぞれpackage.jsonのscriptsに追加しておきましょう。
"scripts": {
"build": "netlify-lambda build src/functions/",
"serve": "netlify-lambda serve src/functions/"
}
ogp-parserをインストール
今回、ogp-parserというライブラリを使用させてもらってogp情報を取得しますのでインストールしておきます。
$ npm install --save ogp-parser
ogp-parserを実行するとこんな感じのJSONが取得できます。
{ title: 'Mac OSのDockerで/etc/localtimeがマウントできなくなって困った話 - A1 Blog',
ogp:
{ 'og:title': [ 'Mac OSのDockerで/etc/localtimeがマウントできなくなって困った話 - A1 Blog' ],
'og:type': [ 'article' ],
'og:url': [ 'https://blog.a-1.dev/post/2019-04-13-docker-mount/' ],
'og:image':
[ 'https://blog.a-1.dev/images/docker-mount-error/title.png' ],
'og:site_name': [ 'A1 Blog' ],
'og:description': [ 'Mac OSのDockerで/etc/localtimeがマウントできなくなって困った話' ],
'og:locale': [ 'ja_JP' ] },
seo:
{ pinterest: [ 'nopin' ],
viewport: [ 'width=device-width,minimum-scale=1,initial-scale=1' ],
'theme-color': [ '#263238' ],
generator: [ 'Hugo 0.55.0' ],
description: [ 'Mac OSのDockerで/etc/localtimeがマウントできなくなって困った話' ],
'twitter:card': [ 'summary_large_image' ],
'twitter:site': [ 'A1 Blog' ],
'twitter:url': [ 'https://blog.a-1.dev/post/2019-04-13-docker-mount/' ],
'twitter:title': [ 'Mac OSのDockerで/etc/localtimeがマウントできなくなって困った話 - A1 Blog' ],
'twitter:description': [ 'Mac OSのDockerで/etc/localtimeがマウントできなくなって困った話' ],
'twitter:image':
[ 'https://blog.a-1.dev/images/docker-mount-error/title.png' ]
}
}
<meta property=...>
で定義されたものはogp
というキーに情報が格納されるので基本的にこの情報を使用します。
かたやseo
というキーには<meta name=...>
で定義されたものが格納されるようなので、viewportなどSEOと関係ない情報も格納されるようです。<meta name="og:image"...>
という変則的(バグ?)なサイトもあったので、seoキーの方からもog:xxxx
にマッチする情報を取得していきます。
Functionを実装
src/functions
に任意の名前のファイル名でFunctionsを実装します。ファイル名がそのままFunctionの名前になります。
exports.handler = (event, context, callback) => {
if ('url' in event.queryStringParameters === false) {
console.error("parameter 'url' is necessary!!");
return;
}
const url = event.queryStringParameters.url;
const parser = require("ogp-parser");
parser(encodeURI(url), true).then(function(data) {
console.log(data);
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];
}
}
for (let prop in data.seo) {
if (/^og:/g.test(prop)) {
ogpData[prop.split(':')[1]] = data.seo[prop][0];
}
}
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);
});
};
やっていることは以下の通り。
- GETパラメータでurlを取得
- opg-parserでopg情報を取得
- 取得した情報から必要な情報に絞ったJSONを返却
Shortcodeの実装
事前に、config.tomlにFunctionのURLを登録して$.Page.Site.Params.OgpApiEndpoint
で取得できるようにしておきます。ちなみにローカルと違って、/.netlify/functions/<Function名>
のように長い形式でしかアクセスできないようです。
[params]
OgpApiEndpoint = "https://<netlifyのURL>/.netlify/functions/<Function名>?url="
shortcodeを実装してogp.htmlとして保存します。
まず、getJSON
でFunctionを呼び出し、取得したJSONからHTMLを生成します。
HTMLタグも、CSSも、はてなブログのものを殆どそのまま真似させてもらいました。
{{ $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://%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 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>
<img src="https://s.st-hatena.com/entry.count.image?uri={{ $url }}" alt="" class="star-count">
<a href="http://b.hatena.ne.jp/entry/{{ $url }}" target="_blank">
<img src="https://b.hatena.ne.jp/entry/image/{{ $url }}" class="bookmark-count">
</a>
</div>
</div>
</div>
</div>
div.page-embed.hatena-web-card {
height: 155px;
border: 1px solid rgba(0, 0, 0, 0.1);
margin-bottom: 10px;
}
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;
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;
background: #FFF;
padding: 0;
}
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 {
display: inline;
vertical-align: middle;
border: none;
}
div.page-embed.hatena-web-card div.embed-footer img {
box-shadow: none;
}
ブログから呼び出す
{{% ogp "<URL>" %}}
でshortcodeを呼び出します。
完成イメージ
やっと完成した!はてなスターやブックマーク数も良い感じに表示されています。
ブログに写真や動画を貼り付ける「リンク挿入」機能を使いやすくしました URLをペーストするだけでコンテンツを埋め込めます - はてなブログ開発ブログ
※amp版は対応していないので、ただのリンクとして表示されています。