D.I.O URL Shortener

 

開発経緯

仕事柄、よくQRコードも作成するし、職業柄、流入経路の判別は重要ですから、UTMパラメーターをつけます。普段はエクセルのシートに関数入れたものを部分的に書き換えて作成しているのですが、書き換え→セルの入力値を合体(&でつなぐ関数?)させたものをURL短縮サービスに入れて、そこで発行された短縮URLをQRコード生成サービスに入れて、QRコードを発行しています。手間というほどじゃないですけど、出来れば、URLを入力して、そのまま一気にQRコード発行まで行きたいな、と思いまして。

ためしにClaudeさんに聞いたら、割とシンプルに出来ますよ、なんて言われたので、「なんだ簡単なのか。それなら作ってしまおう」と思ったわけですが、「では、MySQLでDB作りましょう」的なことを言われて、「え?いきなり、そのレベルさわるの?」と。なんとなく、MySQLは怖い感じがして、今まで一切触れずにいましたが、勉強を兼ねて、やってみるか、と。

あと経緯としては、パラメーター付与機能はGoogleのキャンペーンパラメーターのサービスがあり、使い勝手いいのですが、QRコードまでは発行出来ないですからね。短縮URLもbit.lyだけになったし、登録必要になったし。気軽に使えて、危険性のない、欲しい機能が揃っている、そんなものがあるといいなぁ、ということで作ってみました。

 

そもそも、なんで短縮URL+QRコード発行機能がセットで必要なのか。

雑誌やチラシ、名刺など、QRコードを貼っておけばスマホから読み取ってもらって自社のWEBサイトやキャンペーンのLPなどに素早く誘導することが出来ます。そんなQRコードは非常に便利な機能ではありますが、QRコードからのアクセスはGoogleAnalytics上では、direct/none(その他・直接・不明みたいな感じ)と呼ばれる種類にカテゴライズされて他の流入と判別がつかなくなってしまいます。流入経路をきっちり判別させようとする場合、URLにはGoogleが指定する「UTMパラメーター」と呼ばれる「しるし」をつける必要があります。

UTMパラメータとは、Google アナリティクスなどの解析ツールでどこから・どんな方法で訪問者が来たかを計測するためのURLの付加情報です。SNS投稿・メルマガ・広告など、流入経路ごとに設定することで、施策の効果を正確に把握できます。

そのUTMパラメーターをつけることで流入経路がGA上できっちりと分類され判別できるようになるのですが、URLが非常に長くなってしまいます。URLが非常に長くなると、情報量が増えるためQRコードの「マス」が非常に小さく(白黒のマスが細かくなる)なります。そうすると何が起こるのか、というとQRコードの読込エラーが起きやすくなります。

理由として、マス目が小さくなると画面上では見えていて読込が出来ていても印刷で「マス目」が潰れてしまったり、PDFや画像で拡大した際にQRのマスが滲んだり、粗で読込みできなかったりすることが増えてきます。そこで、QRコード化する際に情報量(=文字数)を減らし、QRコードのマスを出来るだけ大きくするため「URLを短縮化」する必要があります。

UTMパラメーター+短縮URL+QRコードのセットはQRコードからのアクセスをGAで正しく計測するために必須の設定なんです。

 

完成したもの

URL短縮サービス「D.I.O URL Shortener」として mutsu.me/shorten/ で公開しています。主な機能は以下の通りです。
・URLの短縮(mutsu.me/xxxxx 形式で発行)
・UTMパラメーター(source / medium / campaign / content)の付与
・QRコードの生成(PNG・SVG形式で別タブ表示)
・Google Safe Browsing APIによる悪意あるURLのチェック
・短縮URLのコピーボタン

 

技術スタック

サーバーはエックスサーバーの共用レンタルサーバーを使用しています。既存のWordPressサイトと同居させる形での構築です。
言語:PHP 8.3
データベース:MySQL(MariaDB 10.5)
サーバー:エックスサーバー共用サーバー
QRコード生成:goqr.me API(外部API)
スパム対策:Google Safe Browsing API v4

 

ファイル構成

WordPressが /blog/ 以下に入っているため、ルート直下に独立したファイル群を配置しました。

public_html/
├── .htaccess          ← ルーティング制御
├── redirect.php       ← リダイレクト処理
├── shorten/
│   ├── index.php      ← URL作成ページ(メイン)
│   └── css/
│       └── shorten.css ← スタイルシート
└── blog/              ← 既存WordPress(触らない)

※ db.php は public_html の一つ上の階層(ブラウザからアクセス不可の場所)に設置

 

データベース設計

エックスサーバーのサーバーパネルから新規データベース(xs197646_short)を作成し、phpMyAdminでテーブルを構築しました。テーブル設計は以下の通りです。

CREATE TABLE `url_shortener` (
  `id`           INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  `code`         VARCHAR(10)  NOT NULL UNIQUE,
  `original`     TEXT         NOT NULL,
  `full_url`     TEXT         NOT NULL,
  `utm_source`   VARCHAR(255) DEFAULT NULL,
  `utm_medium`   VARCHAR(255) DEFAULT NULL,
  `utm_campaign` VARCHAR(255) DEFAULT NULL,
  `utm_content`  VARCHAR(255) DEFAULT NULL,
  `created_at`   DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

original(元URL)と full_url(UTMパラメーター付きの完全URL)を別カラムで保存しています。リダイレクト先は full_url を使用するため、UTMパラメーターがそのまま計測に活きる設計です。

 

短縮コードの生成ロジック

短縮コードは英数字小文字の5文字ランダム文字列で生成しています。生成後にDBで重複チェックを行い、衝突した場合は再生成するループ構造です。

do {
    $code = substr(str_shuffle('abcdefghijklmnopqrstuvwxyz0123456789'), 0, 5);
    $exists = $pdo->prepare('SELECT id FROM url_shortener WHERE code = ?');
    $exists->execute([$code]);
} while ($exists->fetch());

5文字・英数字36種の組み合わせは36の5乗=約6,000万通りあるため、小規模運用では重複の心配はほぼありません。

 

UTMパラメーターの付与

フォームに入力されたUTMパラメーターは、PHPの http_build_query() でクエリ文字列に変換し、元URLに結合します。元URLにすでにクエリ文字列がある場合は & で、ない場合は ? で繋ぐ処理を入れています。

$separator = strpos($original, '?') !== false ? '&' : '?';
$full_url  = $params ? $original . $separator . http_build_query($params) : $original;

 

リダイレクト処理

mutsu.me/xxxxx へのアクセスは .htaccessredirect.php に振り、DBから対応する full_url(UTMパラメーター付き完全URL)を取得して301リダイレクトします。

# .htaccess(抜粋)
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^([a-z0-9]{5})$ /redirect.php?code=$1 [L,QSA]
<?php
// redirect.php(抜粋)
$stmt = $pdo->prepare('SELECT full_url FROM url_shortener WHERE code = ?');
$stmt->execute([$code]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);

if ($row) {
    header('Location: ' . $row['full_url'], true, 301);
    exit;
}

 

Google Safe Browsing APIによるスパム対策

公開サービスのため、悪意あるURLが登録されないようGoogle Safe Browsing API v4を組み込んでいます。フォーム送信時にURLを検査し、マルウェア・フィッシング・不審なソフトウェアが含まれると判定された場合は登録を拒否します。

function isMaliciousUrl(string $url): bool {
    $api_key  = 'ここには取得したGoogle Safe BrowsingのAPIキーを記述';
    $endpoint = 'https://safebrowsing.googleapis.com/v4/threatMatches:find?key=' . $api_key;
    $body = json_encode([
        'client'     => ['clientId' => 'mutsu-me-shortener', 'clientVersion' => '1.0.0'],
        'threatInfo' => [
            'threatTypes'      => [
                'MALWARE',
                'SOCIAL_ENGINEERING',
                'UNWANTED_SOFTWARE',
                'POTENTIALLY_HARMFUL_APPLICATION'
            ],
            'platformTypes'    => ['ANY_PLATFORM'],
            'threatEntryTypes' => ['URL'],
            'threatEntries'    => [['url' => $url]],
        ],
    ]);

    $ch = curl_init($endpoint);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => $body,
        CURLOPT_HTTPHEADER     => ['Content-Type: application/json'],
        CURLOPT_TIMEOUT        => 5,
    ]);
    $response = curl_exec($ch);
    curl_close($ch);

    if (!$response) return false; // API失敗時は通す
    $data = json_decode($response, true);
    return !empty($data['matches']);
}

APIのタイムアウトを5秒に設定し、API側の障害時はURLを通す(false返却)設計にしています。サービスの可用性を優先した判断です。Google Cloud Consoleでプロジェクトを作成し、Safe Browsing APIを有効化してAPIキーを取得する必要があります。月1万件まで無料枠で利用できます。

 

QRコード生成

QRコードの生成には外部API(goqr.me)を使用しています。サーバーサイドでの処理は不要で、imgタグのsrc属性にAPIのURLを指定するだけで表示できます。

<img src="https://api.qrserver.com/v1/create-qr-code/?size=180x180&margin=20&data=" alt="QRコード">

パラメーターの margin=20 はQRコード規格で推奨されているクワイエットゾーン(静止域)の確保のために設定しています。QRコードは周囲に一定の余白がないと読み取りエラーが起きるため、このパラメーターは重要です。ダウンロード用は size=400x400&margin=40 と大きめのサイズで別タブ表示する設計にしています。

 

管理画面とセキュリティ

自分用の管理画面(mutsu.me/admin/)を別途用意し、発行した短縮URLの一覧確認と削除ができるようにしています。管理画面はIDとパスワードによるセッション認証を設けており、さらに admin/.htaccess で自宅IPアドレスからのアクセスのみを許可するIP制限を追加しています。

# admin/.htaccess
order deny,allow
deny from all
allow from (自宅IPアドレスを記述)

また、db.php(DB接続情報を含むファイル)は public_html の一つ上の階層に設置し、ブラウザから直接アクセスできない場所に置くことでセキュリティを確保しています。

 

WordPressとの共存

既存のWordPressが /blog/ 以下に入っているため、.htaccess/blog//shorten/ へのアクセスはそのまま通し、5文字の英数字コードのみ redirect.php に振る設計にしています。

RewriteEngine On

# HTTP → HTTPS
RewriteCond %{HTTPS} !on
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]

# www → non-www
RewriteCond %{HTTP_HOST} ^www\.(.*)$ [NC]
RewriteRule ^(.*)$ https://%1/$1 [R=301,L]

# /shorten/ はそのまま通す
RewriteRule ^shorten(/.*)?$ - [L]

# /blog/ はそのまま通す
RewriteRule ^blog(/.*)?$ - [L]

# 既存ファイル・フォルダはそのまま
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d

# 5文字コードをredirect.phpへ
RewriteRule ^([a-z0-9]{5})$ /redirect.php?code=$1 [L,QSA]

 

工夫した点・詰まった点

db.phpのパス問題

最初は dirname(__DIR__) を使った相対的なパス指定を試みましたが、エックスサーバーのディレクトリ構造との兼ね合いでうまく動作しませんでした。エラーログからサーバーの実際のパスを確認し、/home/xs197646/mutsu.me/db.php のようにフルパスで指定することで解決しました。

フォームの二重構造バグ

最初のバージョンでは、formタグの外にinputを配置してJavaScriptでhiddenフィールドに値を渡す設計にしていたため、送信しても何も起きないバグが発生しました。フォームタグ内にすべてのinputを収める構造に修正し解決しています。

MySQLは思ったより怖くなかった

今まで避けていたMySQLですが、エックスサーバーのphpMyAdminを使えばGUIで操作できるため、SQLコマンドを直接叩く必要もなく、意外とハードルは低かったです。既存のWordPress用DBとは完全に独立した新規DBを作成したため、既存サイトへの影響もゼロでした。過去にクライアントのサーバーメンテナンスを頼んだ外注パートナー企業がテストサーバーのMySQLのバージョンを勝手に変更して真っ白にして年末年始に雲隠れするという荒業をかましてくれたこともあり、MySQLにはトラウマがあります。

 

まとめ

「URL短縮・UTMパラメーター付与・QRコード発行」という3つの作業を一つのツールで完結させることができました。Claudeと対話しながら少しずつ構築していく形をとったので、MySQLやPHPの処理の流れも理解しながら進められたのが良かったです。

Google Safe Browsing APIの組み込みや管理画面の実装など、「作ってみたら意外と本格的なものができてしまった」という感じです。今後はクリック数の計測機能なども追加していきたいと考えています。

短縮URLとQRコードをまとめて発行したい方はぜひ使ってみてください。
D.I.O URL Shortener

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA


このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください