スタッフブログ Staff Blog

gtag.jsでPDFファイルの閲覧数やバナーのクリック数の計測

先月より私はとあるWebサイトの更新作業(運用)に携わらせていただくことになりました。この度の豪雨災害が起こる直前にレスポンシブWebデザイン化を完了させていたため、緊急のお知らせがスマートフォンで閲覧しやすかったのではないかと考えております。

さて、Webサイト更新作業(運用)では、依頼を受けた情報を正しく・見やすく掲載(新規ページの追加もしくは既存ページの更新)する作業がメインとなりますが、その他にもWebサイトの改善提案などもできたらと考えております。(もちろん、費用等の兼ね合いもありますので詳細は契約時に協議することとなります。)

Webサイトの改善提案においてはまず「アクセス解析」が重要になると考えます。アクセス解析については現状ほとんどのサイトが「Google アナリティクス」を導入されているでしょう。Google アナリティクスの管理画面でトラッキングコードを取得してHTMLに挿入するだけですので簡単ですね。どのページがどのぐらい閲覧されているかすぐにデータが集まります。

しかし、WebサイトにはHTMLで作成したページだけではなく、PDFファイルやZipファイルなどもあります。また、バナー画像を設置することも多いでしょう。これらの閲覧数・ダウンロード数・クリック数の取得は一工夫する必要があります。そこでこの記事では、PDFファイルの閲覧数計測やバナーのクリック数計測のコードをご紹介したいと思います。

なお、今回は「グローバル サイトタグ(gtag.js)」を利用している前提とします。(6月に新規導入する際表示されたコードがグローバル サイトタグでした。)

PDFファイル・Zipファイル等の閲覧数計測

PDFファイルやZipファイル内にGoogle アナリティクスのタグは設置できないため、「リンクをクリックした際にページビューとしてカウントする」というのが基本的な考え方となります。クリックした際に何かをするにはJavaScriptを利用することになります。(もっともグローバル サイトタグがJavaScriptなのです)

リンクをクリックした際に何かを動作させるには<a href="/path/to/file.pdf" onclick="〜">のようにonclick属性にコードを記述すれば良いのですが、全てのa要素に都度記述するのはなかなか大変です。そこで、「href属性値が.pdfで終わる全てのa要素を対象にする」方法をとりました。この方法であればあらゆるサイトで同じコード(自動トラッキングを設定したファイル)を流用してすぐに計測を始めることができます。

ページビューのトラッキング」の解説を参照しコードを設計すると、以下のようになりました。バニラJSの場合はdocument.querySelectorAll('a[href$=".pdf"], a[href$=".zip"]')で要素を収集し、forEach()でループしてイベントを付けることになるでしょう。

(function () {
    $('a[href$=".pdf"], a[href$=".zip"]').on('click', function () {
        const $this = $(this);
        const filePath = $(this).attr('href');
        gtag('config', GA_TRACKING_ID, {
            'page_title': $this.text(),
            'page_path': filePath
        });
    });
}(jQuery));

ここで注意しなければならないのが、ページビューを送信する際にはトラッキングコードが必要になることです。そこで、head要素内に挿入したグローバル サイトタグを少し改変し、どのサイトに上記のコードを導入しても誤りなくデータが収集できるようにします。

const GA_TRACKING_ID = 'UA-123456789-1';
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());

gtag('config', GA_TRACKING_ID, { 'anonymize_ip': true });

トラッキングIDを定数(constGA_TRACKING_IDで宣言します。定数は最代入による変更・再定義が認められないので安心でしょう。

このコードで得られたPDFファイルの閲覧数などは以下のように表示されます。WordPressでアップロードしたPDFファイルの閲覧数が表示されていますね。
画面キャプチャ: Google アナリティクスのページ画面でPDFファイルの閲覧数を表示した画面

なお、このコードではパラメータpage_titleはa要素に含まれるテキストを取得して設定しています。必要に応じてdata-*属性を使うなどのカスタマイズも行ってください。

バナークリック数計測

今回担当しているWebサイトにもいくつかのバナーが掲載されています。私もWeb担当者様もどのバナーが効果を発揮しているか知りたくなるものです。

コード設計の大まかな考え方は、先に紹介した「PDFファイル・Zipファイル等の閲覧数計測」と同じになります。Google アナリティクスにはページビューとしてではなくイベントとしてデータを送信する点が異なります。

イベントのトラッキングについては公式サイトに解説ページ「Google アナリティクスのイベントをトラッキングする」があるので、これを参考にコードを設計しました。結果、以下のようなコードになりました。

(function ($) {
    $('.Banner img').on('click', function () {
        const $this = $(this);

        if ($this.get(0).tagName.toLowerCase() === 'img') {
            gtag('event', 'クリック', {
                'event_category': 'バナーの位置など',
                'event_label': $this.attr('alt')
            });
        }
    });
}(jQuery));

パラメータevent_categoryには、バナーの設置位置などを設定すると良いでしょう。またパラメータevent_labelは、PDFファイル等の場合と同じくimg要素のalt属性を取得して設定するようにしました。

Google アナリティクスでの表示を掲載したいのですが、どこのサイトか分かってしまうため今日は控えさせて頂きます。

なお、「拡張リンクのアトリビューション」を使用する方法もあることを付記しておきます。

さらなる発展

バナークリック数計測で紹介したコードは、バナーの設置箇所毎に似たようなコードを設置することになります。そのため汎用的なコードに改変すると共に、またPDFファイルの閲覧数などのコードもまとめた一つのJavaScriptファイルとし、これを設置・設定するだけでさまざまな計測ができるようにしたいと考えています。

スタッフの誕生日会。

6月生まれのスタッフの誕生日会を行いました。

前回(3月)と同じお店でしたが、メニューが変わっていました。


お魚のランチ

お肉のランチ

デザート2種

どこか似ている二人

全員で

そして今回の主役

彼と一緒に仕事をし始めたのが2011年頃で、その頃と比べると体重も増えたようですが、笑顔も格段に増えて、心身ともに健康そうで安心しています。

人の笑っている顔は良いですね。見ているこちらも自然と笑顔になります。


誕生日おめでとうございます!

GoPro Fusionを使ってみました

最近、当社の取り組みで360°コンテンツの実装を掲げているのですが、そこで強力な助けとなる名機 GoPro Fusion を使った様子をお伝えします。

動画は5K、画像は18Mの高画質

GoPro Fusionは日本では2018年4月に発売。お値段も10万円近くと高価ですが、それに見合った性能をもっています。

上記の画像は朝方撮影した福山城です。パソコンではマウスを使って、スマートフォンでは実機を動かすことでも向きを変えられます。

また右下のメガネボタンを押すと、iPhoneではVRモードで表示されます!
私はVRゴーグルを持っていなかったので、昨日100均で買ってきました。

使いやすいと噂のキャンドゥ産VRメガネ。
特に不足なく綺麗に見えました。お手軽に持ち運びできるのもいいですね!

スマートフォンとの連動

ところで上記の360°画像、撮影者(私)が見当たりませんがどこにいるでしょうか?
パッと見分かりづらいですが、実は後方の岩影からひっそりと撮っています。


(本人は完全に隠れたつもりだった・・・)

これはBluetoothを使ってペアリングしてやることで実現できます。

ペアリングは単に遠隔操作だけではなく、本機単体では不可能なプレビューもできるなど、いろいろ補完的な役割を果たしてくれます。

対応機種は多くありませんが、質の良いコンテンツ作成の為には、積極的に使っていきたいですね。

動画の編集比較

撮影した画像・動画は、"Fusion Studio"という専用ソフトで編集できます。
書き出し時にいくつかのオプションがあり、比較しやすい動画も撮れたのでご紹介します。

形式や画質など4つのオプションがありますが、今回比較するのは下記の2項目です。

  • Spatial Audio(空間オーディオ)
  • D.Warp(スティッチング)

まずは、Spatial Audio = Stereo、D.Warp = ON で設定した動画1です。

次は、Spatial Audio = 360 Audio、D.Warp = OFF で設定した動画2です。

音の違いがよく分かると思います。360°オーディオの方は「空間音声」とも呼ばれており、より立体的に音が聞こえます。

また、小鳥のおかげで、D.Warp(スティッチング)の違いも分かりやすくなりました。
スティッチとは日本語で縫い目のことであり、360°動画のつなぎ目の箇所に当たります。


D.Warpなし

D.Warpあり

なしの方は補正が掛かってないので、境目がくっきりしてますね。逆にありの方は、自然な感じで補正されています。

※ちなみにこちらの動画、iPhoneでは公式アプリで見ないと360°にはなりません。
Androidと比べると少し手間ですが、ご覧の際はお試しください。

Webサイトへの取り入れ

最近は質の高いライブラリ(静止画で使っているA-Frameなど)の登場で、比較的VR系のコンテンツも実装しやすくなっています。サイトの1つのアクセントとして、また訪問者へインパクトを与える手段として、こういった新しい技術をどんどん取り入れたいですね。

PowerCMSのスニペット・フィールドで種類の異なるカスタムフィールドをまとめる

通常 Movable Type で種類(テキスト・複数行テキスト・アイテム・画像など)の異なるカスタムフィールドを作成する際は別々のフィールドとして作成する事になりますが、別々のカスタムフィールドとして作成する場合は以下のような問題があります。

  • フィールド数が多くなると編集画面が縦長になる
  • フィールド数が多く、また記事数も増えた時に保存/再構築処理が重くなる

これらの問題を解決する一つの方法として PowerCMS のスニペット・フィールドでまとめる方法をご紹介します。

スニペット・フィールドを作成する

今回は以下のフィールドを持ったスニペット・フィールドを作成します。

  • タイトル(テキスト)
  • 概要(複数行テキスト)
  • サムネイル(画像)

  • システムオブジェクト: ブログ記事
  • 種類: スニペット
  • オプション: title,summary,customfield_thumb
  • 規定値: 後述します
  • ベースネーム: entry_snippet01
  • テンプレートタグ: EntrySnippet01

ここでは上記のように入力してスニペット・フィールドを保存します。 オプション欄はテンプレートタグでスニペット・フィールドの値を取り出す際のキーとして利用しますが、画像フィールドは Movable Type のアイテム選択ダイアログを流用するため必ず「customfield_」で始まる必要があります。

規定値にスニペット・フィールドのテンプレートを入力する

一度スニペット・フィールドを保存すると規定値が入力可能になりますので以下のように入力します。

<mt:Ignore>アイテム・画像を表示するためのテンプレート</mt:Ignore>
<mt:SetVarTemplate name="tmpl_file_field">
    <mt:SetVarBlock name="_name">customfield_<$mt:Var name="_key"$></mt:SetVarBlock>
    <mt:SetVarBlock name="__at"><$mt:Var name="asset_type" _default="image"$></mt:SetVarBlock>
    <mt:SetVarBlock name="__atl"><$mt:Var name="asset_type_label" _default="ファイル"$></mt:SetVarBlock>
    <mt:SetVarBlock name="__w"><$mt:Var name="thumbnail_width" _default="150"$></mt:SetVarBlock>
    <mt:SetVarBlock name="__h"><$mt:Var name="thumbnail_height" _default="150"$></mt:SetVarBlock>

    <mt:SetVarBlock name="_"><$mt:Var name="_name"$>_original</mt:SetVarBlock>
    <mt:SetVarBlock name="asset_id"><$mt:Var name="$_" regex_replace='/\A(?:__snippet_upload_asset__(\d+)\z|.*)/','$1'$></mt:SetVarBlock>
    <input type="hidden" name="<$mt:Var name="_" escape="html"$>" value="<$mt:Var name="$_" escape="html"$>">

    <$mt:SetVar name="_cf_preview_html" value=""$>
    <mt:If name="asset_id" like='/\A[1-9]\d*\z/'>
        <mt:Asset id="$asset_id" setvar="_cf_preview_html">
            <mt:If tag="AssetMIMEType" like="/\A(?:image\/|\z)/">
                <a href="<$mt:AssetURL$>" target="_blank" title="<$mt:Trans phrase="View image" escape="html"$>"><img src="<$mt:AssetThumbnailURL height="$__h" width="$__w"$>" alt=""></a>
            <mt:Else>
                <a href="<$mt:AssetURL$>" target="_blank"><$mt:AssetFileName escape="html"$></a>
            </mt:If>
        </mt:Asset>
    <mt:Else>
        <$mt:Var name="$_name"$>
    </mt:If>
    <mt:Unless name="_cf_preview_html" like="/\S/">
        <$mt:SetVar name="asset_id" value=""$>
    </mt:Unless>

    <input type="hidden" name="<$mt:Var name="_name"$>" id="<$mt:Var name="_name"$>" data-asset-chooser="true" value="<$mt:Var name="$_name" escape="html"$>">

    <div id="<$mt:Var name="_name"$>_preview" class="customfield_preview" data-preview-width="<$mt:Var name="__w"$>" data-preview-height="<$mt:Var name="__h"$>">
        <$mt:Var name="_cf_preview_html"$>
    </div>
    <div class="actions-bar" style="clear: none;">
        <div class="actions-bar-inner pkg actions">
            <a href="<$mt:Var name="script_url"$>?__mode=list_asset&_type=asset&blog_id=<$mt:Var name="blog_id"$>&dialog_view=1&filter=class&filter_val=<$mt:Var name="__filter_val"$>&require_type=<$mt:Var name="__filter_val"$>&edit_field=<$mt:Var name="_name"$>&asset_select=1" class="mt-open-dialog">
                <$mt:Var name="__file_label" escape="html"$>を選択
            </a>
            <a href="#" id="<$mt:Var name="_name"$>_remove_asset" class="<mt:Unless name="asset_id">hidden</mt:Unless>" type="submit" onclick="insertCustomFieldAsset('', '<$mt:Var name="_name"$>'); return false;" style="margin-left: 10px;">
                <$mt:Var name="__file_label" escape="html"$>を削除する
            </a>
        </div>
    </div>
    <mt:SetVarBlock name="__selector" append="1"><mt:If name="__selector">, </mt:If>#<$mt:Var name="_name"$>_remove_asset</mt:SetVarBlock>
</mt:SetVarTemplate>

<div id="<$mt:Var name="basename"$>-content">
    <div class="SnippetTable">
        <div class="SnippetTable__row">
            <div class="SnippetTable__title">タイトル</div>
            <div class="SnippetTable__content">
                <mt:SetVarBlock name="_key">title</mt:SetVarBlock>
                <input type="text" name="<$mt:Var name="_key"$>" class="text" value="<$mt:Var name="$_key" escape="html"$>">
            </div>
        </div>
        <div class="SnippetTable__row">
            <div class="SnippetTable__title">概要</div>
            <div class="SnippetTable__content">
                <mt:SetVarBlock name="_key">summary</mt:SetVarBlock>
                <textarea name="<$mt:Var name="_key"$>" class="text medium"><$mt:Var name="$_key" escape="html"$></textarea>
            </div>
        </div>
        <div class="SnippetTable__row">
            <div class="SnippetTable__title">サムネイル</div>
            <div class="SnippetTable__content">
                <mt:SetVarBlock name="_key">thumb</mt:SetVarBlock>
                <$mt:Var name="tmpl_file_field" __file_label="画像" __file_val="image"$>
                <mt:Ignore>アイテムを利用する場合は書きを使用してください。</mt:Ignore>
                <mt:Ignore><$mt:Var name="tmpl_file_field" __file_label="ファイル" __file_val="file"$></mt:Ignore>
            </div>
        </div>
    </div>
</div>

<mt:Ignore>アイテム・画像の選択動作</mt:Ignore>
<mt:If name="__selector">
<script>
    (function($) {
        $(function() {
            $('<$mt:Var name="__selector" escape="js"$>').on("click", function() {
                return insertCustomFieldAsset("", $(this).attr("id").replace(/_remove_asset$/, ""))
            });

            window.insertCustomFieldAsset = function(html, id, preview_html) {
                var $asset = $("#" + id);
                var AssetFieldName = $asset.attr("name");
                var $original = $('input[name="' + AssetFieldName + '_original"]');

                if ($asset.data("asset-chooser")) {
                    var $remove = $('input[name="' + AssetFieldName + '_remove"]');
                    var m = html.match(/^[<]form [m]t:asset-id="(\d+)"[^<]+<a href="([^"]*)/);
                    if (m) {
                        var assetID = m[1];
                        var assetSrc = m[2];
                        $asset.val(assetSrc);
                        $original.val("__snippet_upload_asset__" + assetID);
                        $remove.val("");
                    } else {
                        $asset.val("");
                        $original.val();
                        if ($remove.length > 0) {
                            $remove.val("1");
                        } else {
                            $asset.after($('<input type="hidden" name="' + id + '_remove" value="1">'));
                        }
                    }
                } else {
                    $asset.val(html);
                }
                preview_html = preview_html || html || '';

                var $form = $("<div>" + preview_html + "</div>").find("form:first");
                var $preview = $("#" + id + "_preview");

                if ($form.length !== 1) {
                    var preview_w = $preview.data("preview-width") * 1;
                    var preview_h = $preview.data("preview-height") * 1;
                    var m = preview_html.match(/ src="([^<">]+)/);
                    if (m) {
                        var assetSrc = m[1];
                        var img = new Image();
                        img.src = assetSrc;
                        $(img).on("load", function() {
                            var w = img.width || 0;
                            var h = img.height || 0;
                            if (w >= h) {
                                h = "" + Math.round(h / w * preview_w);
                                w = preview_w;
                            } else {
                                w = "" + Math.round(w / h * preview_h);
                                h = preview_h;
                            }
                            $preview.html(preview_html.replace(/(<img\s(?![^>]*\swidth[\s=])(?![^>]*\sheight[\s=]))/, '$1 width="' + w + '" height="' + h + '"'));
                        });
                    } else {
                        $preview.html(preview_html.replace(/(<img\s(?![^>]*\swidth[\s=])(?![^>]*\sheight[\s=]))/, '$1 width="' + preview_w + '"'));
                    }
                } else {
                    $form.find("a[href]").each(function() {
                        $(this).attr("target", "_blank");
                    });
                    $preview.html($form.html());
                }
                $("#" + id + "_remove_asset")[html ? "removeClass" : "addClass"]("hidden");
                return false;
            };
        });
    }(jQuery));
</script>
</mt:If>

<style>
    .SnippetTable {
        display: table;
        width: 100%;
    }
    .SnippetTable__row {
        display: table-row;
    }
    .SnippetTable__title,
    .SnippetTable__content {
        display: table-cell;
        padding: 10px 0;
        border-bottom: 1px solid #ccc;
        vertical-align: top;
    }
    .SnippetTable__row:first-child .SnippetTable__title,
    .SnippetTable__row:first-child .SnippetTable__content {
        padding-top: 0;
    }
    .SnippetTable__row:last-child .SnippetTable__title,
    .SnippetTable__row:last-child .SnippetTable__content {
        padding-bottom: 0;
        border-bottom: 0;
    }
    .SnippetTable__title {
        padding-right: 10px;
        width: 5em;
        text-align: right;
    }
    .SnippetTable__content .medium {
        height: 7.5em;
    }
</style>

スニペット・フィールドの値を取り出すテンプレートタグ

画像とそれ以外のフィールドで取り出し方が異なります。 画像フィールドはスニペット・フィールドの「テンプレートタグ」として設定したタグ名に「Asset」を繋げたブロックタグと key モディファイアの組み合わせで画像のコンテキストを得ることができます。

画像以外の値の取り出し方

<mt:Ignore>タイトル</mt:Ignore>
<mt:EntrySnippet key="title">

<mt:Ignore>概要</mt:Ignore>
<mt:EntrySnippet key="summary">

画像の値の取り出し方

<mt:Ignore>画像</mt:Ignore>
<mt:EntrySnippetAsset key="customfield_thumb">
    // do something 
</mt:EntrySnippetAsset>

まとめ

PowerCMS のスニペット・フィールドを活用する事で編集画面をすっきりさせる事ができました。 フィールド数が増えた場合はさらに作り込む事で更新しやすいUIを作る事が可能です。

「Webアクセシビリティの学校 in 広島」に参加

2018年5月19日(土)に広島市で開催された「Webアクセシビリティの学校 in 広島」に参加してきました。私は2015年11月8日に岡山市で開催された「Webアクセシビリティの学校 in 岡山」に続き2度目の参加でした。(個人ブログに記事「『Webアクセシビリティの学校 in 岡山』に参加」を残しています。)
写真:Webアクセシビリティの学校 in 広島のオープニング風景

セミナーの内容は定期的にリニューアルされているようで、2回目の参加でも新鮮な気分で聴くことができました。今回は「Webアクセシビリティとは」「なぜWebアクセシビリティに取り組む必要があるのか」の部分が手厚く解説されたような印象がしています。

セミナー後半で解説された「Webアクセシビリティ」の基本の「キ」10項目は、理解しているつもりでも慣れてくると例えば画像の代替テキストの付け方が少しぶれてしまうなど自分でも「あらあら」と思うことがあるので、改めてポイントを確認することができて有意義な時間でした。基本の「キ」10項目は、会社に新しく入社したメンバーや広島県内で同じ勉強会に参加される方々に知ってもらえたらなと考えています。

障害者・高齢者のためだけの取り組みではない事を伝えたい

「『Webアクセシビリティ』は障害者・高齢者のためだけに取り組むものではない」という点がまだまだ理解されていないと思うことがあります。先月も「W3Q - Web制作者のためのQ&Aサイト」に「すべての商用サイトは8341に対応すべき?」という質問が上がり私が5番目に回答したのですが、どうも上手く伝わらなかったような気がしており未だに回答内容について自問自答することがあります。

今回のセミナーで植木さんは以下のような例を挙げ、バリアフリーの施策が障害者など一部の方だけでなくユーザー全般の利便性向上につながっていることを解説されましたのですが、これは理解しやすいなと感じました。

交差点付近の歩道のカーブカット(角を丸く取り段差をなくす)
車椅子の方がスムーズに通行できるだけでなく、セグウェイでも通行できる。また、キャリーバッグや高齢者が利用する手押し車(シルバーカー)もスムーズに通行できる。
駅のエスカレーター
高齢者の方だけでなく、老若男女あらゆる人が上下の階へ楽に移動できる。重い荷物を持っている人でも階段を安全かつ楽に登り降りできる。

手を怪我してマウスが利用できないなど一時的に障害がある場合の例を出しても納得して頂けないことがあるので、あえてWeb以外の例を挙げるのも良いのかもしれない、と考えました。(Webの話をしているのでWebの例で理解して頂きたい思いはありますが...。)

デザイン? アート?

リンクテキストを色の変化のみで表現すると何らかの理由で色が判別しにくい場合にリンクと認識しづらいので常に下線を付けたい派なのですが(先に紹介したブログ記事「『Webアクセシビリティの学校 in 岡山』に参加」を読み返すと、セミナーでこのことに気付いたことを記録していました)、デザイナーさんから「下線を消したい」と言われることがしばしばあり、懇親会でどのようにしたら良いか植木さんに質問をしました。ここで話題に出たのは「Webサイト(Webデザイン)はアートではない」という趣旨のお話でした。この「Webサイトはアートではない」というのは、先月参加した「CSUN 2018 参加報告セミナー」の懇親会でも弁護士ドットコムの太田さんが話題に出されハッとしたものでした。

前後の文脈を含め一言一句記憶できていないのですが、私は今のところ次のように理解しています。

"Webサイトは美術館などに飾って眺めてもらったり批評してもらったりするような「作品」ではなく、Webサイト構築時に定義された何らかの目的を達成するための「インターフェース」である。したがって、ユーザーがコンテンツを十分に理解できるビジュアルデザイン、また容易に操作できるビジュアルデザインでなければならないし、ワイヤーフレームをベースに慣習やWCAG 2.0のようなアクセシビリティガイドラインを前提条件として取り入れてデザインしなければならない。"

この「デザイン? アート?」の問いやリンクの下線について昨晩考えていたのですが、リンクの下線については以下のような記事を見つけたので書き留めておきます。アクセシビリティだけでなくUXデザインにも関係するなと感じました。

お問い合わせ Contact

制作のご依頼、ご相談などは下記のフォームからご連絡ください。