CakePHP に自動で OGP タグを埋め込んで Facebook に対応させるためのヘルパーを作ったよ

※ これは CakePHP 1.3 時代の記事です。2.0以降だとこのままでは動かないと思います。

OGP (Open Graph Protocol) という約束事がありますね。
例えばウェブページで Facebook の「いいね!」(Like) ボタンが押されたとき

どんな画像や説明文を表示さるかを指定するのに使われるやつ。
このブログの <head /> 内にも埋め込んであります。

本来は誰か(何か)と誰か(何か)の関係の集まりであるソーシャルグラフの中で
それは誰(何)なのかを表現するための決め事と理解してますけど、
まあ実質「いいね!」のためにウェブサイトで使われることが多いので。

実際には何も書いておかなくても
Facebook が「これでしょ」と適当に画像や文字列を拾ってくれるんだけど
結果を見たら「それじゃない!」と思うことが多いので
こっちから指定しておいた方がいいですね。

ブログだけじゃなくてあらゆるウェブサイトに使えるから
CakePHP のサイトにも埋め込んでおきたい。
最近作ったサービスにはだいたい入れてあります。

ただ、OGP のタグ自体は <meta /> 内に情報を書いていくだけで
特に複雑なものではないんだけど
毎日書くようなものじゃないからお作法をすぐ忘れちゃう。

というわけで、
CakePHP で作られたサイトに自動で OGP タグを埋め込んで
Facebook の「いいね!」やシェアに対応させる
OgpHelper を作りました。

コードは長くなっちゃうから後で。

環境

  • ここでは CakePHP 1.3 を前提としています。
  • 1.2 で使う場合は View からの呼び出しを $this->Ogp->func() ではなく $ogp->func() にしてください。
  • 2.0 対応はこれからやります。あるいは誰かやってください。

設置

OgpHelper が書かれた ogp.php
app/view/helpers/ に放り込んで、
あとは AppController かどっかで読み込み。

public $helpers = array('Ogp', 'Html', 'Form');

とりあえず何も設定しなくても <head /> 内にこういうものが追加されます。

<meta property="og:url" content="&#91;現在の URL&#93;" />
<meta property="og:type" content="website" />
<meta property="og:title" content="&#91;ビュー変数の $title_for_layout&#93;" />
<meta property="og:locale" content="ja_JP(言語設定による)" />
<meta property="og:site_name" content="&#91;レイアウトの title&#93;" />

基本的な設定

上記だけでは情報が足りないので、
次のどこかの場所に初期設定を配列で書きます。
まあヘルパーを直接いじるより Configure した方がいいと思います。

  • OgpHelper::$defaults
  • Configure::write(‘OGP’, array(…));
  • View 変数の $ogp_params
    (Controller から $this->set() で指定したもの)

設定できる内容はこんな感じ。
もっといろいろ設定できるんだけど、
「いいね!」ボタンやコメントボックスを設置するときに必要か、
あるいは設定してないと Facebook の Debugger でおこられるのは以下。

array(
    'fb:admins'      => '[FB のユーザーID]',
    'fb:app_id'      => '[FB のアプリケーション ID'],
    'og:url'         => '[設定したい URL]',
    'og:type'        => '[ページのタイプ]',
    'og:title'       => '[ページのタイトル]',
    'og:locale'      => '[言語_地域コード]',
    'og:site_name'   => '[サイトの名前]',
    'og:image'       => '[ページ内容を表す画像]',
    'og:description' => '[ページの説明]',
)  

設定すれば自動で埋め込まれるので、
テンプレートファイルを変更する必要はありません。

false に設定すると、そのタグは埋め込まれません。
ただし後述の自動取得設定が true になっていたら
それらしい値を自動で挿入します。

ヒント

fb:admins
https://graph.facebook.com/(ユーザー名) で調べられます。
fb:app_id
https://developers.facebook.com/apps で新規作成/確認。
og:type
使えるタイプ名一覧は https://developers.facebook.com/docs/opengraph/#types
og:locale
日本語なら ja_JP, 米語なら en_US.
一覧は XML で提供されてます。 https://www.facebook.com/translations/FacebookLocales.xml

なお、Configure のキー名と view 変数の名前は変更することができます。
手っ取り早いのは view から

$this->Ogp->configName = '[Configure のキー名]';
$this->Ogp->varName = '[ビュー変数の名前]';

プロパティ名の自動修正

プロパティ名は og:url とか og:site_name とか決まってるんだけど
appid だったか app_id だったか、
siteName だったか site_name だったかわからなくなるとか
いちいち og: って書くのがめんどくさいとかありそうなので
別名で書いても正しく修正されるようにしてあります。

'admin'       => 'fb:admins',
'admins'      => 'fb:admins',
'app_id'      => 'fb:app_id',
'appid'       => 'fb:app_id',
'appId'       => 'fb:app_id',
'appID'       => 'fb:app_id',
'url'         => 'og:url',
'URL'         => 'og:url',
'type'        => 'og:type',
'title'       => 'og:title',
'locale'      => 'og:locale',
'site_name'   => 'og:site_name',
'sitename'    => 'og:site_name',
'siteName'    => 'og:site_name',
'image'       => 'og:image',
'img'         => 'og:image',
'description' => 'og:description',

その他のメソッド

set($property, $content = false, $id = false)

View のテンプレートファイル内からタグの設定を書き換えることができます。

$this->Ogp->set('image', 'http://www.msng.info/wp-content/themes/msng/img/oboete.png');

また set() は与えられた文字列をそのまま返すので、
HTML に文字列を出力しつつ、同時に OGP の値を設定することができます。

<p><?php echo $this->Ogp->set('description', 'ほげほげについて説明します。');" /></p>

既に設定したプロパティを持つタグを set() した場合、
og:image は追記され、それ以外のプロパティは上書きされます。

ただし $id を指定すると、同じプロパティ名を持つ別のタグを追加します。
image 以外のものを複数入れることってなさそうな気もしますけど一応。

$this->Ogp->set('description', 'これも説明文ですお見逃しなく!', 2);

$contentfalse を指定すると、
既に設定したプロパティ名を持つタグを削除することができます。

$this->Ogp->set('title', false);

$id を指定して削除することもできます。

$this->Ogp->set('image', false, 'logo');

ただし削除には次の delete() を使った方が自然だと思います。

delete($property, $id = 0)

指定したプロパティ名を持つタグを削除します。
set() で設定した $id を指定して削除することもできます。

set()$id が無指定の場合は 0 になるので、
$id 無指定で set() したものは $id 無指定で削除できます。

ns()

OgpHelper 自体はレイアウトやテンプレートを変更しなくても動くんだけど
<html /> 要素に OGP と FB の namespace 属性を指定するのが本当なので
それを出力するためのメソッドです。

<html<?php echo $this->Ogp->ns(); ?> />

で、次のように出力されます。

<html xmlns:og="http://ogp.me/ns#" xmlns:fb="http://www.facebook.com/2008/fbml">

その他の設定

$autoGet

各値の自動取得を行うかどうかの設定です。

public $autoGetUrl = true;
public $autoGetTitle = true;
public $autoGetSiteName = true;
public $autoGetLocale = true;

いずれも、その値がどこにも設定されていない場合に作動します。

$autoGetUrl
現在リクエストされている URL を出力します。
$autoGetTitle
現在の View レイアウトでの $title_for_layout を出力します。
$autoGetSiteName
現在の View レイアウトの <title /> 内の文字列を出力します。__() やグローバル変数などの出力も評価します。ただし少々強引に切り取っていますし、サイト名なんてアプリケーション全体で変わらないものだから素直に Configure した方が賢いと思います。
$autoGetLocale
アプリケーションでの現在の設定から言語を拾い、og:locale の値にマッピングできる場合は自動で出力します。次項を参照してください。

$locales

CakePHP で使われる言語コードと
og:locale コードをマッピングします。
主に $autoGetLocale の設定でロケールを自動取得する場合に使用されます。

これらは完全に1対1で対応させることができないので、
初期設定では次のものだけが指定してあります。

array(
    'ja' => 'ja_JP',
    'en' => 'en_US',
)

これ以外のロケール値を指定する場合は
アプリケーションのどこか (bootstrap.php など) で
次のように指定してください。

Configure::write('OGPlocales', array(
    'fr' => 'fr_FR',
    'it' => 'it_IT',
));

ここでの設定と初期設定はマージされ、
Configure されたものが優先されます。

また Configure のキー名 OGPlocales
次項の $configLocaleName で変更できます。

$configLocaleName

前項の $locales にマージされる Configure のキー名を指定します。
初期設定は ‘OGPlocales‘ です。

コード

以下に貼りますけど、直接 gist で見た方がはやいかも。

<?php
/**
* Ogp Helper class file
*
* Adds OGP elements in <head />
*
* Licensed under The MIT License
*
* @author Masunaga Ray (http://www.msng.info/)
* @link http://www.msng.info/archives/2011/12/cakephp-ogp-helper.php
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class OgpHelper extends AppHelper
{
public $helpers = array('Html');
/**
* Decides whether or not each property should be taken automatically
*
* Works only when the property is not configured anywhere.
**/
public $autoGetUrl = true;
public $autoGetTitle = true;
public $autoGetSiteName = true;
public $autoGetLocale = true;
/**
* Names for configurations
**/
public $configName = 'OGP';
public $configLocaleName = 'OGPlocales';
public $varName = 'ogp_params';
/**
* Default values for OGP are read from this property,
* Configure::read['OGP'] and View variable $ogp_params (by default)
* and the former is overwritten by the latter.
* Set to false to skip adding the property.
**/
public $defaults = array(
'fb:admins' => false,
'fb:app_id' => false,
'og:url' => false,
'og:type' => 'website',
'og:title' => false,
'og:locale' => false,
'og:site_name' => false,
'og:image' => false,
'og:description' => false,
);
/**
* Map for CakePHP's config.language with Facebook's locale code
* used by _getLocale method.
*
* Since they can not be associated one to one,
* these values are supposed to be overwritten by Configure::write('OGPlocales').
**/
public $locales = array(
'ja' => 'ja_JP',
'en' => 'en_US',
);
/**
* Namespace attributes for OGP and Facebook
**/
protected $_ns = ' xmlns:og="http://ogp.me/ns#" xmlns:fb="http://www.facebook.com/2008/fbml"';
/**
* Map used to correct irregular property names
**/
protected $_propertyNames = array(
'admin' => 'fb:admins',
'admins' => 'fb:admins',
'app_id' => 'fb:app_id',
'appid' => 'fb:app_id',
'appId' => 'fb:app_id',
'appID' => 'fb:app_id',
'url' => 'og:url',
'URL' => 'og:url',
'type' => 'og:type',
'title' => 'og:title',
'locale' => 'og:locale',
'site_name' => 'og:site_name',
'sitename' => 'og:site_name',
'siteName' => 'og:site_name',
'image' => 'og:image',
'img' => 'og:image',
'description' => 'og:description',
);
protected $_properties = array();
public function __construct() {
$this->view =& ClassRegistry::getObject('view');
}
/**
* Sets default values
**/
public function beforeRender() {
$this->defaults = $this->_normalize($this->defaults);
$conf = $this->_normalize(Configure::read($this->configName));
$viewVar = $this->_normalize($this->view->getVar($this->varName));
$ogp = array_merge($this->defaults, $conf, $viewVar);
if ($this->autoGetUrl && empty($ogp['og:url'])) {
$ogp['og:url'] = $this->url(null, true);
}
if ($this->autoGetTitle && empty($ogp['og:title'])) {
$ogp['og:title'] = $this->_getTitle();
}
if ($this->autoGetSiteName && empty($ogp['og:site_name'])) {
$ogp['og:site_name'] = $this->_getSiteName();
}
if ($this->autoGetLocale && empty($ogp['og:locale'])) {
$ogp['og:locale'] = $this->_getLocale();
}
foreach ($ogp as $property => $content) {
$this->set($property, $content);
}
}
/**
* Sets a meta tag in the view layout's <head />
**/
public function set($property, $content = false, $id = false) {
if ($property) {
$property = $this->_getPropertyName($property);
if (empty($this->_properties[$property]) || !in_array($content, $this->_properties[$property])) {
if ($id === false) {
if ($property == 'og:image' && isset($this->_properties['og:image'])) {
$id = count($this->_properties['og:image']);
} else {
$id = 0;
}
}
if ($content === false) {
return $this->delete($property, $id);
} else {
$propertyId = $this->_getPropertyId($property, $id);
$meta = $this->Html->meta(null, null, array(
'property' => $property,
'content' => $content,
'inline' => true,
));
$this->view->addScript($propertyId, $meta);
$this->_properties[$property][$id] = $meta;
}
}
}
return $content;
}
/**
* Deletes a tag associated with property name and the ID.
**/
public function delete($property, $id = 0) {
if ($property) {
$property = $this->_getPropertyName($property);
$propertyId = $this->_getPropertyId($property, $id);
if (isset($this->view->__scripts[$propertyId], $this->_properties[$property][$id])) {
unset($this->view->__scripts[$propertyId]);
unset($this->_properties[$property][$id]);
return true;
}
}
return false;
}
/**
* Returns namespace attributes for OGP and Facebook.
**/
public function ns() {
return $this->_ns;
}
/**
* Normalizes property names set as keys of an array
**/
protected function _normalize($ogp) {
if (is_array($ogp)) {
foreach ($ogp as $property => $content) {
$propertyName = $this->_getPropertyName($property);
if ($propertyName != $property) {
unset($ogp[$property]);
$ogp[$propertyName] = $content;
}
}
} else {
$ogp = array();
}
return $ogp;
}
/**
* Returns a proper property name for an OGP tag
**/
protected function _getPropertyName($property) {
if (isset($this->_propertyNames[$property])) {
$property = $this->_propertyNames[$property];
}
return $property;
}
/**
* Returns the $title_for_layout for the current view
**/
protected function _getTitle() {
if (!$title = $this->view->getVar('title_for_layout')) {
$title = Inflector::humanize($this->view->viewPath);
}
if ($title) {
return __($title, true);
} else {
return false;
}
}
/**
* Returns the <title /> in the layout
**/
protected function _getSiteName() {
$layoutFileName = $this->view->_getLayoutFileName();
if ($layoutFileName && file_exists($layoutFileName)) {
if ($layout = file_get_contents($layoutFileName)) {
$layout = str_replace(array("\r", "\n"), ' ', $layout);
$pattern = '#<title.*?>(.+?)</title.*?>#i';
preg_match($pattern, $layout, $matches);
$title = $matches[1];
$pattern = '/<\?(?:php|=)(.*?)\?>/i';
preg_match_all($pattern, $title, $codes);
if (is_array($codes[1]) && $codes[1]) {
$debug = Configure::read('debug');
Configure::write('debug', 0);
foreach ($codes[1] as $key => $code) {
ob_start();
eval($code);
$result = ob_get_contents();
ob_end_clean();
$title = str_replace($codes[0][$key], $result, $title);
}
Configure::write('debug', $debug);
}
$title = trim($title);
if ($title) {
return $title;
}
}
}
return false;
}
/**
* Returns a Facebook locale code associated with Config.language
**/
protected function _getLocale() {
if (!class_exists('L10n')) {
App::import('Core', 'l10n');
}
$l10n = new L10n;
$l10n->get();
$language = Configure::read('Config.language');
if (!empty($_SESSION['Config']['language'])) {
$language = $_SESSION['Config']['language'];
}
if ($language) {
$this->locales = array_merge($this->locales, (array)Configure::read($this->configLocaleName));
if (!empty($this->locales[$language])) {
return $this->locales[$language];
}
}
return false;
}
/**
* Creates a property ID string
**/
protected function _getPropertyId($property, $id) {
return $property . '-' . $id;
}
}
view raw ogp.php hosted with ❤ by GitHub

利用しているサービス

OGP Helper は先日公開したこのサービスで使っています。

もう終了したけどな。

次の人

CakePHP Advent Calendar 2011
明日は @papettoTV さんですお楽しみに!