ブログカードとは以下のようにリンクをちょっとリッチに表示してくれる機能のことです。以前はこれってどうやって実現しているんだろうと不思議に思っていました。
上記のようなブログカードは「はてなブログ」や「WordPress」等のブログサービスでよく見かけますが、基本的にこれらのブログカードはリンク先のURLを指定するだけで自動的に生成されています。本記事では上記のようなブログカードを支える技術について解説します。
(本記事は自分のブログからの転載記事です。)
はじめに
本記事では、ブログカード1の表示に使われる一般的な技術の解説およびJavaScriptによる実装を行います。普段何気なく見たり使ったりしているブログカードの技術に興味がある人におすすめします。
ブログカードの要素技術
まずはブログカードを実現するための要素技術について解説します。
ブログカードの構成
ブログカードは主に「タイトル」、「説明」、「画像」から成ります。オプションで「favicon」、「サイト名」、「ソーシャルカウント」を表示する場合もあります。
ブログカードの主な情報源
ブログカードの主な情報源は以下の3つになりますが、メインの「タイトル」、「説明」、「画像」といった情報を提供しているのはOpen Graph Protocolになります。
- Open Graph Protocol(OGP)
- favicon
- ソーシャルカウント
Open Graph Protocol
Open Graph Protocolの説明の前提知識として、まずはソーシャルグラフについて説明します。ソーシャルグラフとはFacebookやTwitter等のSNS(ソーシャル・ネットワーク・サービス)において、人と人の繋がりである「ソーシャル・ネットワーク」を点と線で可視化したものです。「Graph」の由来は、点と点の結びつきに関する数学理論であるグラフ理論から来ています。
上記のようなソーシャルグラフの構築には点と点をつないで線にする仕組みが必要です。そのために考案されたのがOpen Graph Protocol(OGP)です。ソーシャルグラフにおける「点」は「人」を表していますが、Webの世界ではWebページを「人」とみなしてソーシャルグラフを構築します。WebページはHTMLで記述されHTTPプロトコルを用いてやり取りされますが、基本的にはOGPもその仕組みの上に成り立っています。具体的には以下の図のように単純な仕組みでメタデータのフォーマットのみがOGPで規定されていて、それ以外は既存のHTTPやHTMLの仕組みをそのまま利用しています。
OGPで必須とされているメタデータはog:title
、og:type
、og:image
、og:url
の4つですが、og:site_name
やog:description
もよく利用されます。具体的なHTMLのヘッダに埋め込まれたメタデータの例は以下のようになります。
<meta property="og:type" content="article">
<meta property="og:title" content="とにかくかっこいいブログの作り方">
<meta property="og:url" content="https://tonikaku-kakkoii-blog.com/articles/how_to_create_kakkoii_blog/index.html">
<meta property="og:site_name" content="tonikaku-kakkoii-blog.com">
<meta property="og:description" content="とにかくかっこいいブログサイトを作るにはどうすればいいのかお悩みの方も多くいると思います。本記事ではデザイン、技術、記事内容の3つの視点で解説します。">
<meta property="og:image" content="https://https://tonikaku-kakkoii-blog.com/ore_kakkoii.png">
favicon
「favicon」はWebサイトのシンボルとして表示される画像のことでブラウザのタブやブックマークで表示されます。もともとInternet Explorer 5で「お気に入り」に画像を表示するための技術で、「favicon」の由来は「favorite icon」だとされています。
faviconの画像形式には特に決まりがなくブラウザ依存ですが、伝統的にはICO形式です。ICO形式はWindowsのアイコン形式で、以下のように正方形の任意の画像サイズを複数格納できるようになっています2。
Webサイトにfaviconを設定する伝統的な手法はWebサイトのルートディレクトリにfavicon.ico
というファイル名でICO形式のファイルを配置することです。しかし近年のブラウザではそれ以外にも以下のようにHTMLのヘッダでfaviconを指定することもできます。
<link rel="icon" href="tonikaku-kakkoii-blog.com/favicon.ico" />
MIMEタイプを指定することによってgifやpngといった画像形式にも対応できます。
<link rel="icon" type="image/png" href="tonikaku-kakkoii-blog.com/favicon.png" />
favicon取得用API
faviconはこれまで説明してきたとおり画像形式も配置場所もばらばらなので、単純にブログカードに表示させることはできません。クライアント側のJavaScriptでもある程度はできるかもしれませんがICO形式等のマルチ画像のフォーマットがあると厳しいです。そこでブログカード用にfavicon取得用のAPIをサーバサイドで実装するのが一般的です。favicon取得用APIではfaviconを取得してブログカードの表示に適切は画像フォーマットとサイズに変換してクライアントに返却します。このようなAPIは自作することも公開されているサービスを利用することもできます。以下にfavicon取得用APIの例を掲載します3。
- GoogleのAPI
- HatenaのAPI
ソーシャルカウント
ソーシャルカウントは一般的にSNSにおける「人気」を表す指標のことで、例えばはてなブックマークの数とかFacebookのいいねの数になります。ソーシャルカウントを取得する方法は、サービスによってそれぞれ異なります。具体例として以下にはてなブックマーク数を取得するAPIを掲載します。
- はてなブックマークの取得API
ブログカードの実装
今回は以下のようなブログカードをサーバサイドJavaScript(Node.js)で実装してみたいと思います。
JavaScriptでHTMLを出力するイメージです。今回はgetTag
関数を実装し、戻り値はPromiseとします。利用方法は以下のとおりです
getTag({url: "https://hinastory.github.io/cats-cats-cats/2019/12/29/visualize_ruby_development_by_file/"}).then(e => console.log(e))
HTMLの骨組み
まずはHTMLを出力するにあたって骨組みを考えます。最初は出力するイメージを再現できる素直な入れ子構造を考えます。ポイントはブログカードをクリックしたらリンク先に飛びたいのでリンクを示すaタグ
でなるべく広く囲むことです。次にスタイル(CSS)を当てることを考えて不足しているレイヤーがあれば調整します。最終的にできた骨組みは以下になりました。aタグと書かれた箇所以外は全てdivタグで、class属性の骨格を示しています。
- hbc-blog-card
- hbc-link-wrap
- hbc-link(aタグ)
- hbc-card
- hbc-info
- hbc-favicon
- hbc-site-name
- hbc-contents
- hbc-thumbnail
- hbc-text
- hbc-title
- hbc-url
- hbc-description
- hbc-info
- hbc-card
- hbc-link(aタグ)
- hbc-link-wrap
JavaScriptにおける実装
実装は以下のとおりです。基本的には上記の骨格どうりにHTMLタグを組み立てているだけです。今回はソーシャルカウントの実装は行っていませんが、実装はそれほど難しくはないはずです。
const util = require('hexo-util');
const ogs = require('open-graph-scraper'); // Open Graph Protocol解析用
const escapeHTML = require('escape-html');
const url = require('url');
const descriptionLength = 140;
const className = 'blog-card';
const faviconAPI = 'http://favicon.hatena.ne.jp/?url=$URL';
function getTag(options){
return ogs(options)
.then(function (result) {
const ogp = result.data;
const info = getInfo(options, ogp);
const contents = getContents(options, ogp);
const card = util.htmlTag('div', { class: 'hbc-card' }, info + contents, false);
const link = util.htmlTag('a', { class: 'hbc-link', href: options.url, target: options.target, rel: options.rel }, card, false);
const linkWrap = util.htmlTag('div', { class: 'hbc-link-wrap' }, link, false);
const tag = util.htmlTag('div', { class: className }, linkWrap, false);
return tag;
})
.catch(function (error) {
console.log('error:', error);
return '';
});
}
function getInfo(options, ogp) {
let name = '';
const urlParsed = url.parse(options.url);
// ogSiteNameがなかった場合にホスト名を表示
if (ogp.hasOwnProperty('ogSiteName')) {
name = ogp.ogSiteName;
} else {
name = urlParsed.hostname;
}
const siteName = util.htmlTag('div', { class: 'hbc-site-name' }, name);
let api = faviconAPI.replace('$DOMAIN', encodeURIComponent(urlParsed.hostname));
api = api.replace('$URL', options.url);
const favicon = util.htmlTag('img', { class: 'hbc-favicon', src: api } , '');
return util.htmlTag('div', { class: 'hbc-info' }, favicon + siteName, false);
}
function getContents(options, ogp) {
let contents = '';
let text = '';
if (ogp.hasOwnProperty('ogImage')) {
const image = util.htmlTag('img', { src: ogp.ogImage.url } , '');
contents = util.htmlTag('div', { class: 'hbc-thumbnail' }, image, false);
}
text += util.htmlTag('div', { class: 'hbc-title' }, escapeHTML(ogp.ogTitle), false);
text += util.htmlTag('div', { class: 'hbc-url' }, options.url, false);
if (ogp.hasOwnProperty('ogDescription')) {
const description = adjustLength(ogp.ogDescription);
text += util.htmlTag('div', { class: 'hbc-description' }, description);
}
contents += util.htmlTag('div', { class: 'hbc-text' }, text, false);
return util.htmlTag('div', { class: 'hbc-contents' }, contents, false);
}
// 内容が長い場合に切り詰める
function adjustLength(description) {
if (description && description.length > descriptionLength) {
description = description.slice(0, descriptionLength) + '…';
}
return description;
}
// 実行したい場合は以下のようにする
// getTag({url: "https://hinastory.github.io/cats-cats-cats/2019/12/29/visualize_ruby_development_by_file/"}).then(e => console.log(e))
実行結果
上記のプログラムの実行にはnode.jsとnpmによるライブラリ(hexo-util
とopen-graph-scraper
)のインストールが必要ですが、興味がある方は実行してみてください。実行結果(HTML)は以下のとおりです。適当にスタイルを当てています。
See the Pen blog_card by hinastory (@hinastory) on CodePen.
まとめ
本記事ではブログカードで使われている技術として以下の3つの技術について解説を行い、JavaScriptによるブログカードの実装例を紹介しました。
- Open Graph Protocol
- favicon
- ソーシャルカウント
本記事がブログカードを支える技術の理解の一助となれば幸いです。
おまけ(1/24追記)
記事本文では触れませんでしたが、ブログカードに使われる技術の一つとしてoEmbedというものもあります。これはWebサイトに動画や写真を埋め込むためのプロトコルで、YouTubeやInstagram等のリッチコンテンツを提供しているサイトが対応しています。一部のブログカードの実装ではWebサイトがoEmbedに対応していたらoEmbedを優先するという動作をします。
現在(2020年)時点では、oEmbedに対応しているサイトはOGPに対応しているサイトと比較するとごく一部であり、oEmbedに対応しているサイトも自分が調べた限りではOGPに対応していたので4、本記事ではOGPメインの構成にしています。oEmbedも非常に興味深い技術なので興味がある方は調べてみてください。
参考文献
- The Open Graph protocol
- Favicon - Wikipedia
- ICO (ファイルフォーマット) - Wikipedia
- ファビコン画像を取得する便利なWebサービス(API) | 俺の開発研究所
- はてなブックマーク件数取得API - Hatena Developer Center