TRY AND ERROR

気になったこと、勉強したこと、その他雑記など色々メモしていきます。。Sometimes these posts will be written in English.,

AMP HTML触ってみた

AMPってなに?

「Accelerated Mobile Pages」略してAMP。
モバイルサイトの UX 改善(ユーザー体験)を目的とした Google 主体のプロジェクトで、 画像やスマートアド等、ロードコストのかかるコンテンツを即座にロードすることにより、 UX を改善しようというのが主な目的。
AMP HTML というオープンフレームワークが発表されており、この仕様に沿ってモバイルサイトを作成すると高速で読み込まれるサイトが実現できるというもの。
Amp が用意した javascript(https://cdn.ampproject.org/v0.js)を読み込むことで、AMP仕様のHTMLタグ(AMP Components)が使用可になるので、これを使ってマークアップする形になります。


特徴

とりあえずファーストビューを表示する

AMP で作成したサイトでは、ファーストビューをできるだけ早く、かつ CPU 負荷の高く ないものという条件で プレレンダリングするようブラウザに伝えられるので、サイトを訪れた際のファーストビ ューがかなり早い。

キャッシュ

AMP の仕様で作成されたサイトは GoogleTwitter 側にキャッシュされ、 このキャッシュからコンテンツを返すことによって、高速表示を可能にしている。 (Google が提供する CDN 経由での配信可能)

Javascript

ロードを遅くする要因の一つとして javascript を挙げており、
AMP が用意している javaScript 以外の javaScript は”一切含めない”としている。 広告を表示したり解析したりする場合に javascript が使用されるが、
AMP ではこれらの javascript を許可せずに、今後の課題としている。
現状、iframe で広告を表示できる。

コンテンツサイズ

ブラウザは画像ファイル等を読み込む前にあらかじめ設定されたサイズを参照し、表示領域を確保することができる。
例えば、300×200 のバナーがあったとき、実際の画像サイズと同じ値を指定するとレンダリングが速くなる。
AMPでは、このようにサイズ指定を必須としている。


必須なこと

・ HTML5 を使用

・ html タグに以下の文字列を記載すること <html ⚡> または <html amp>
(イナズママーク?とかめんどくさw)

・ <head>と<body>を記載すること(まあこれは今まで通り。。)

・ <canonical>を記載すること
AMP で書いたページ(モバイルサイト)以外の通常ページがある場合はそこに、 なければ AMP ページ自身に標準化するように記載する

・ <meta charset="utf-8">を記載すること

・ <meta name="viewport" content="width=device-width,initial-scale=1,minimum- scale=1,maximum-scale=1,user-scalable=no,minimal-ui">を記載すること

・ <script async src="https://cdn.ampproject.org/v0.js"></script>を記載すること
ここに AMP のカスタムタグの定義等が書かれている模様。

・ <style>body {opacity: 0}</style><noscript><style>body {opacity: 1}</style></noscript> を記載すること

AMP Components

AMP が用意した HTML のカスタムタグのこと。
・画像を表示するAMP Component

<amp-img width=500 height=500 src="URL"></amp-img>
※サイズ指定は必須で、ないと表示されない。

これ以外にも多数の AMP Components が用意されている。

amp-audio
amp-anim
amp-ad
amp-pixel
amp-video
amp-carousel
amp-lightbox
amp-iframe
amp-instagram
amp-twitter
amp-youtube

今後の展望

現状 javascript 等に課題が残るものの、Google 主体ということから今後はモバイルサイト コーディングのデファクトスタンダードになっていくと思われる。ちなみに、WP の AMP プラグインも開発中とのこと。

最近話題の広告ブロックについて

面白い記事を見かけたので勝手に持論を展開!!
広告ブロッカーの検知と計測について - クックパッド開発者ブログ


WEB上のあらゆる所で目にする広告ですが、
これをブロックする機能を有したツールが出回っていますね。

WEB広告を生業とする企業にとって有害なものであることに
間違いありませんが、現在の日本の法律ではどうなるのか
気になって少し調べてみました。

広告ブロックは法的にまずい?

現在、日本には広告ブロックを直接的に禁止する法律はなさそうですね。
言うとすれば、著作権関連で同一性保持権(著作物の無断改変)があり、
これに該当するしないで争うことになるのではないかと勝手に想像。。

同一性保持権(どういつせいほじけん)は、著作者人格権の一種であり、著作物及びその題号につき著作者(著作権者ではないことに注意)の意に反して変更、切除その他の改変を禁止することができる権利のことをいう(日本の著作権法20条1項前段。以下、特に断らない限り、引用法令は日本のもの)。
同一性保持権 - Wikipedia

ただ、過去に判例がなさそうなのでなんとも言えませんが、
少なくとも刑事罰が課せられるということは考えにくいのでは??

民事責任を問われるか?

刑事罰の対象にならずとも、民事責任を問われるケースもありますね。
(犯罪じゃないけど違法です!的な...)
広告ビジネスを展開する企業にとって広告ブロックは死活問題と成り得るため、
サイトやサービスの利用規約に広告ブロックを禁止しているものも少なくありません。

また損害額を立証しやすいと思うので、
広告ブロックが使用されたことと売り上げ低下の因果関係が認められれば、
利用規約違反ということで民事上の損害賠償とかで刺されるケースも有り得るのではないでしょうか。


まあ何にせよコンテンツを見る見ないはユーザーの自由ですし、
フリーの動画やWEBサイトでは対価を支払っていないわけだから、「ウザイから広告をブロックします!」というのは倫理に反している気もしますね。。

PHPでGoogle Search ConsoleのAPI使ってみた

題名の通り、使ってみたのでめもします。。

やったこと

PHPのサーバアプリでGoogleSearchConsoleの検索アナリティクスのデータを取得。
google-api-php-clientを利用
複数アカウントのGoogleSearchConsoleに登録されている全てのURLの
データを取得
・アクセストークンとリフレッシュトークンをサーバに保存
・認証(アプリ使用許可)は初回のみ


スニペット

getData.php(検索アナリティクスデータ取得、表示)

これが実行ファイル。
初回認証画面で保存したアカウント毎にGoogleSearchConsoleのデータを
取得して表示する。

<?php

set_include_path(get_include_path() . PATH_SEPARATOR . "/path/to/google-api-php-client/src");
require_once __DIR__ ."/google-api-php-client/src/Google/autoload.php";
require(__DIR__ ."/Auth.php");

//3日前くらいのデータじゃないと反映されていないため
$targetDate = date("Y-m-d",strtotime("-3 day"));

try {
	$redirect_uri = "XXXX";

	$client = new Google_Client();
	$client->setAccessType('offline');
	$client->setApprovalPrompt('force');
	$client->setClientId('XXXX');
	$client->setClientSecret('XXXX');
	$client->setApplicationName("XXXX");
	$client->setDeveloperKey("XXXX"); //server key
	$client->addScope(Google_Service_Webmasters::WEBMASTERS_READONLY); //権限の許容範囲("webmasters.readonly")

	$googleUsers = array_filter(scandir(__DIR__ ."/auth"), function($file){
		return !in_array($file, array(".", "..", ".json"));
	});


	foreach ($googleUsers as $filename) {
		$auth = new AuthInfo($filename);
		$client->setAccessToken($auth->getAccessToken());

		//アクセストークンが有効期限切れの場合
		if($client->isAccessTokenExpired()) {
			$NewAccessToken = json_decode($auth->getAccessToken());	//アクセストークンをデコード
			$client->refreshToken($NewAccessToken->refresh_token);	//リフレッシュトークンを使ってアクセストークンの取り直し
			$auth->saveAccessToken($client->getAccessToken());		//外部ファイルに保存

		}

		//search anaryticsデータ取得
		if ($client->getAccessToken()) {
			getSearchAnalytics($client);
		}
	}



} catch (Google_Service_Exception $e) {
	echo "【Google_Service_Exception】”;
    var_dump($e);

} catch (Google_Exception $e) {
	echo "【Google_Exception】";
    var_dump($e);
}


function getSearchAnalytics($client) {
	global $targetDate;
	$service = new Google_Service_Webmasters($client);
	$searchAnalytics = $service->searchanalytics;

	$targetSites = $service->sites->listSites()->siteEntry;

	$analyticsDatas = array();
	
	foreach ($targetSites as $targetSite) {
		$request = new Google_Service_Webmasters_SearchAnalyticsQueryRequest;
		$request->setStartDate($targetDate); //yyyy-mm-dd
		$request->setEndDate($targetDate); //yyyy-mm-dd
		$request->setDimensions(array("query"));
		$request->setRowLimit(5000);

		$q = $searchAnalytics->query($targetSite->siteUrl, $request);
		$rowData = $q->getRows();

		output(getTodaysFile($targetSite->siteUrl), $rowData);
	}

}


function output($rowData) {
	$cnt = 1;
    //kw,click数、impression数
	foreach ($rowData as $row) {
		echo $cnt ."    |    " .$row['keys'][0] ."    |    " .$row['clicks'] ."    |    "  .$row['impressions'] ."<br>";
		$cnt++;
	}
}

?>
Auth.php(認証情報クラス)

アクセストークンやリフレッシュトークンなどの認証情報を扱うクラス

<?php
class Auth {
	private $filename;
	private $accessToken;
	private $security;

	public function __construct($fnm) {
		$this->security = new Security();
		$this->accessToken = "";
		$this->filename = __DIR__ ."/auth/{$fnm}";
		if(file_exists($this->filename)){
			$this->accessToken = file_get_contents($this->filename);
		}
	}

	public function saveAccessToken($token) {
		if(file_exists($this->filename)){
			unlink($this->filename);
		}
		touch($this->filename);
		file_put_contents($this->filename, $token);
		$this->accessToken = $token;

	}

	public function getAccessToken() {
		return $this->accessToken;
	}
}
regist.php(初回認証画面)

初回のアカウント認証(アプリ使用許可)を行うGUI
認証情報をサーバにjsonで保存する。

<?php

set_include_path(get_include_path() . PATH_SEPARATOR . "/path/to/google-api-php-client/src");
require_once __DIR__ ."/google-api-php-client/src/Google/autoload.php";
require(__DIR__ ."/Auth.php");

session_start();

try {
	$users = getUsers();
	$redirect_uri = "XXXX";
	$client = new Google_Client();
	$client->setAccessType('offline');
	$client->setApprovalPrompt('force');
	$client->setClientId('XXXX');
	$client->setClientSecret('XXXX');
	$client->setApplicationName("XXXX");
	$client->setRedirectUri($redirect_uri);
	$client->setDeveloperKey("XXXX"); //server key
	$client->addScope(Google_Service_Webmasters::WEBMASTERS_READONLY); //権限の許容範囲("webmasters.readonly")

    $isAuthenticated = false;

    if (isset($_POST['userID'])) {
        //初回認証の場合
        $_SESSION['userID'] = $_POST['userID'];
        header('Location: ' . $client->createAuthUrl());

    } else {

        //アプリ使用許可からリダイレクトされてきた場合、認証コードがリクエストパラメータにセットされている
        if (isset($_GET['code'])) {
    		$client->authenticate($_GET['code']);	//アクセストークンと交換する
    		$auth = new AuthInfo($_SESSION['userID']);
    		$auth->saveAccessToken($client->getAccessToken());	//アクセストークンをファイルに保存
            $isAuthenticated = true;
     		unset($_SESSION['userID']);
     	}
    }

} catch (Google_Service_Exception $e) {
	echo "【Google_Service_Exception】”;
    var_dump($e);

} catch (Google_Exception $e) {
	echo "【Google_Exception】";
    var_dump($e);
}

function getUsers() {
	$googleUsers = array_filter(scandir(__DIR__ ."/auth"), function($file){
		return !in_array($file, array(".", "..", ".json"));
	});
	return $googleUsers;
}

?>

<html>
<head>
</head>
<body>
    <div>
        <div>
            <?php
                if (!$isAuthenticated) {
                    echo "<form id='fm' action='./' method='POST'>";
                    echo "<input type='text' id='userID' name='userID'>";
                    echo "<input type='submit' id='btn' value='認証'>";
                    echo "</form>";
                } 
            ?>
        </div>
    </div>
</body>


</html>

↑はスニペットなのでざっくりとしか書いていませんが、
本来は認証情報を可逆暗号化して保存したりデータを日次で取得してCSV
はきだしたりといったことをやりました。


というか検索アナリティクスのデータは2〜3日前くらいのものしか
とれないというのは改善されないのかなあ。。。

古いバージョンのPHPでついやってしまうあの構文

wordpressのテーマを作成する案件があり、テストサーバで動作確認したものを

クライアントのサーバに導入しテーマを適用ところ、画面が真っ白に。。。


とりあえずini_set()でエラーを確認してみると、
どうやら配列を返すメソッドをコールしたところで落ちていました。


原因はPHPバージョン差異による構文エラー。


クライアントサーバのPHPは5.3.3で、
PHP5.4より前のバージョンでは以下のようにメソッドの戻り値の配列
にそのままアクセスするような書き方はできないとのことです。
なので、一度変数に入れてからアクセスすることで解決。

echo func()[0]; //PHP5.4より前のバージョンでこれはsyntax errorになります

$arr = func(); //一度変数に格納すればおk
echo $arr[0];

function func() {
  return 配列;
}


PHPのバージョン上げろや!って簡単に言えない場面では
この手の構文エラーにも注意しておかなきゃですね~

cronを実行するときのユーザー

先日、GoogleAPIを使って取得した検索アナリティクスのデータを
サーバに保存するphpを書きました。

ブラウザでphpを叩いて一通りテストした後、
cronで日次で処理しようとしたのですが上手くいかず。。。


ログを見てみるとファイルやディレクトリのパーミッション関連で
エラーが出ている模様。


PHPで作成、編集しようとするファイルやディレクトリやらの所有者がapacheになっており、
cronでは一般ユーザで実行していたためエラーが出ていました。

なので、apacheユーザーでcrontabを設定しなおすことで解決。

crontab -u apache task.txt


余談ですが、crontabのオプションで-e(エディタ起動)と-r(タスク削除)があるのですが、
エディタなど使用率の高いオプションをeにバインドし、キーボード上隣り合うrに削除系のオプションをバインドしておくのは危険だなあと思いました。。

(´・ω・`)ヤレヤレ

PHPでrequireしたファイルが読み込めない時!

とあるPHPの実行ファイルがあって、その中で別のクラスを読み込んでいます。
読み込んだクラスの中でこれまた別のjsonファイルを読み込もうとするが、
なぜか読み込めずにちょいハマり。。。


ディレクトリ構造はこんな感じ

[DocumentRoot]/index.php
[DocumentRoot]/auth/Auth.php  ...クラス
[DocumentRoot]/json/data.json  ...jsonファイル


index.php(実行ファイル)

require("./auth/Auth.php");

Auth.php

var_dump(file_exist("../json/data.json"));

実行するとfalseが出力され、ファイルが存在しないことになっていました。


理由は簡単、PHPでは実行ファイルのディレクトリがカレントになるというルールがあるからです。
file_existはAuth.phpのディレクトリではなく実行ファイルであるindex.phpのディレクトリを基に、[DocumentRoot]/../json/data.jsonを参照しようとしてnon existになっていた、ということでした。


なので、理屈上はこのように書かないとダメみたいです。
Auth.php

var_dump(file_exist("./json/data.json"));

※includeでも同じ


ただしクラスの性質を考えると、__DIR__やdirname(__FILE__) といったPHPのコアで定義されている定数を使って上手く
表現したほうがベターだと思います。
PHP: 自動的に定義される定数 - Manual


ちょっと前に職場の上司が言っていた「cronでPHPを叩くときは絶対パスで書けよー」という言葉を思い出したのが
解決のきっかけだったのですが、何がともあれ基礎って大事だなあとつくづく思いましたね(´・ω・`)ノ

wordpressのカスタムメニューをjqueryで開閉したい!

wordpressのサイドバーにカスタムメニューを置いたとき、
ネストしているメニューの親にマウスオーバーすると子が開くようにしたかった
というのが今回の話題。

※実際にはUXを考慮してマウスエンターイベントに変更しています。


実際に生成されるHTMLはこんな感じ。

<li class="menu-item menu-item-type-custom menu-item-object-custom current-menu-item current_page_item menu-item-home menu-item-has-children menu-item-120">
    <a>親</a>
    <ul>
        <li class="menu-item menu-item-type-post_type menu-item-object-page menu-item-149">子</li>
        <li class="menu-item menu-item-type-post_type menu-item-object-page menu-item-150">子</li>
        <li class="menu-item menu-item-type-post_type menu-item-object-page menu-item-160">子</li>
    </ul>
</li>

親のliにマウスエンターすると中身のulが表示され、再度親部分のみマウスエンターすると子が閉じる、ということがやりたかったのですが、中身のulにマウスエンターしても開閉してしまうので、中身のulのマウスエンターイベントで何もさせないようにしました。

jQuery(function($) {
	$("li.menu-item-has-children").mouseenter(
		function(){
			if ($(this).children("ul.sub-menu").is(":hidden")) {
				$(this).children("ul").slideDown();
			} else {
				$(this).children("ul").slideUp();
			}
		}
	);

        //↓これで子要素の動作をキャンセル
	$("li.menu-item-has-children ul").mouseenter(
		function(){
			return false;
		}
	);

});

このようなネスト関係がある場合、子要素のイベントが親要素に伝播するイベントバブリングが発生するので、適宜カットする必要があります。


バブリングの対処には以下の方法があり、適宜使い分ける必要があります。

・EventObject.preventDefault()
対象要素のイベントをキャンセル

・EventObject.stopPropagation()
親要素への伝播をキャンセル

・return false
上記2つの両方を行う