XMLサイトマップ(sitemap.xml)を毎日自動で最新に!自動生成に加えて自動更新するプログラムを開発してみた

こんにちは。制作部開発グループの古川です。

Webサイト制作後のGoogle Search Console(サーチコンソール)へのサイト登録に欠かせないのが、
XMLサイトマップ(sitemap.xml)です。

これまでXMLサイトマップ(sitemap.xml)の作成は、
無料ツールを利用させて頂いておりました。
しかしながら、ページを追加・削除するたびに作り直すのは大変で、
手間が掛かる作業でした。

この作業を自動化できないか、という声が社内に上がっていたため、
今回、自動生成するプログラムを開発いたしました。

XMLサイトマップ(sitemap.xml)とは

XMLサイトマップ(sitemap.xml)とは、
検索エンジンにクロールしてほしいURLと各URLに関する追加のメタデータ(最終更新日、更新頻度、重要度)を、
一覧表示するXMLファイルです。

XMLサイトマップ(sitemap.xml)のメリット

XMLサイトマップ(sitemap.xml)をサーバー上に設置することで、
検索エンジンのクローラーにサイト内の構造やコンテンツを早く正確に伝えることができます。

追加・更新した画面の情報をXMLサイトマップ(sitemap.xml)に記載すると、
より早く検索エンジンにインデックスされ、検索順位に反映されます。

検索エンジンにインデックスされていないページの有無を把握することができます。
インデックスされていない画面は何かしらの違反がある場合があるため、早めに対応をしないと、WEBサイト全体に悪影響を及ぼしてしまいます。

XMLサイトマップ(sitemap.xml)を使用することで、検索順位を上げられるわけではありませんが、
間接的にSEOに貢献することが出来ます。

XMLサイトマップ(sitemap.xml)の実装方法

作成したコード

https://www.php.net/manual/ja/class.domdocument.php

xmlファイルの生成は、DOMDocumentを使用しました。
こちらのファイルを毎日0時にCronで定期実行を行います。

※再帰処理をするため、ループ回数は1000回までとしています。
※現時点ではGoogleはpriorityの値を使用していません。

サイト内には、様々なリンク表記(「./」「../」「/sampledir/」etc.)があり、
それらを正しく「https://~」(または「https://~」)に正規化するのが難しかったです。

ソースコード
<?php
//******************
// 必須設定
//******************
// トップページのURL
// ※末尾スラッシュ必須
define("HOMEPAGE", "https://www.akindo2000.net/");

// ドキュメントルート
// ※絶対パスで指定
// 末尾スラッシュ必須
define("DOCUMENT_ROOT", "/var/www/html/");

// 出力するXMLサイトマップのファイルパス
// ※絶対パスで指定
define("OUTPUT",   DOCUMENT_ROOT . "sitemap.xml");

//******************
// 以下任意の設定
//******************

// サイトマップに登録しない拡張子
// ※ドット「.」必須
$ignore_types = array(".jpg", ".png", ".gif", ".dat", ".log", ".jpeg", ".pdf");

// サイトマップに登録しないファイル名
$ignore_files = array();

// // サイトマップに登録しないディレクトリ名(例:認証ありのURL)
$ignore_dirs  = array();

// 省略可能なファイル名の優先順位
// (サーバの設定と同じにしないと更新日時が違う可能性アリ)
$index_name   = array("index.html", "index.php", "index.cgi");

// 先頭に空の値を追加
array_unshift($index_name, "");

// 優先度の設定
// ディレクトリが深くなるごとの数値を決める (0.0~1.0)
// ※現時点ではGoogleはpriorityの値を使用していない
$priority_set = array(1.0, 0.8, 0.5, 0.2);

// 更新頻度目安の設定
// 動的ページは随時(always)に設定
// always, hourly, daily, weekly, monthly, yearly, never
define("FREQ_SET",   "weekly");

// 実行開始時刻
// 現在時刻をUNIX時間で取得 (1970年1月1日0時0分0秒からの秒数)
$start_time = date("U");

// URLを保存する変数
$URLs[md5(HOMEPAGE) . "0"] = [
    'url' => HOMEPAGE,
    'lastmod' => false,
    'priority_flg' => 0,
    'freq_set' => FREQ_SET,
];
foreach ($index_name as $index) {
    if (is_file(DOCUMENT_ROOT . $index)) {
        $file_path = DOCUMENT_ROOT . $index;
        break;
    }
}
if (isset($file_path)) {
    $modified_time = @filemtime($file_path);
    $URLs[md5(HOMEPAGE) . "0"]['lastmod'] = date(DATE_W3C, $modified_time);
}

// 登録しない拡張子とファイル名を連結する
$ignores = array_merge($ignore_types, $ignore_files);

// 無限ループ回避用
$infinity = 0;

/**
 * WEBページをHTTP通信で取得して、クロールしながら取得
 * @param string $URL
 */

function getSiteURLs($URL)
{
    global $infinity;
    // 無限ループに陥っても終了するようにクロールするページ数のMAXを設定する
    // 最大ページ数ではないので注意すること (初期値 : 1000)
    if ($infinity ]*?href="([^"]+?)"[^>]*>/i', $html, $anchors);
            foreach ($anchors[1] as $a) {
                $flag = true;

                // URLからページ内リンク(アンカー)を削除
                $a = preg_replace('/([^#]+)(#.*){0,1}/', '$1', $a);
                $anchor = $a;

                // ルートパス(/)にドメインを付与
                if (preg_match('/^\/(.*)$/', $a, $m)) {
                    $anchor = HOMEPAGE . $m[1];
                }
                // http・httpsなしURLにhttpまたはhttpsを付与
                //https通信が使われている場合、全てにhttpsを付与するが、一部httpにする必要あり?
                if (preg_match('/^\/\/(.*)$/', $a, $m)) {
                    $anchor = 'https://' . $m[1];
                }

                $URL_buff = $URL;
                if (strpos($anchor, "..") === 0) {
                    // 文頭に相対パスの記述(../)をURLに変換
                    $m = substr_count($anchor, "../") + 1;
                    array_splice($URL_buff, (0 - $m), $m);
                    $next = implode("/", $URL_buff) . '/' . preg_replace("/^(\.\.\/)+/", "", $anchor);
                } else if (strpos($anchor, ".") === 0) {
                    // 文頭に相対パスの記述(./の)をURLに変換
                    $URL_buff[count($URL_buff) - 1] = "";
                    $next = implode("/", $URL_buff) . preg_replace("/^\.\//", "", $anchor);

                } else if (strpos($anchor, HOMEPAGE) === 0) {
                    // 内部サイトURL
                    $next = $anchor;
                } 

                // 除外する拡張子・ファイルがあるか確認する
                foreach ($ignores as $ignore) {
                    $i = explode($ignore, $next);
                    if (empty(end($i))) {
                        $flag = false;
                    }
                }
                // 除外するディレクトリか確認する
                foreach ($ignore_dirs as $ignore) {
                    $i = explode(HOMEPAGE . $ignore, $next); 
                    if (count($i) > 1) {
                        $flag = false;
                    }
                }
                // ルートパスを取得
                $root_path_data = explode(HOMEPAGE, $next);
                $root_path = end($root_path_data);

                // URLをスラッシュ「/」区切りし、末尾の情報を取得
                $next_hierarchy_data = explode("/", $next); 
                $end_url = end($next_hierarchy_data); 

                if($flag){
                    if (!isset($URLs[md5($next) . "0"]['url'])) {
                        // 新しいURLの時は登録する
                        // priority_flgは、HOMEPAGEを0, 1階層目を1, 2階層目を2...と設定していく。
                        $URLs[md5($next) . "0"] = [
                            'url' => $next,
                            'lastmod' => false,
                            'priority_flg' => count(explode("/", $root_path)),
                            'prev_url' => implode('/', $URL),
                            'freq_set' => FREQ_SET,

                        ];

                        // ファイルの更新を取得・設定
                        foreach ($index_name as $index) {
                            if (is_file(DOCUMENT_ROOT . $root_path . $index)) {
                                $file_path = DOCUMENT_ROOT . $root_path . $index;
                                break;
                            }
                        }
                        if(isset($file_path)){
                            $modified_time = @filemtime($file_path);
                            $URLs[md5($next) . "0"]['lastmod'] = date(DATE_W3C, $modified_time);
                        }

                        // urlの末尾がスラッシュ「/」の場合、end(explode("/", $next))の値が""となり、本来のpriority_flgより1多く設定される。
                        // 本来の階層と合わせるため、1減算する。
                        if ($end_url === "") {
                            $URLs[md5($next) . "0"]['priority_flg']--;
                       }

                        getSiteURLs($next);
                    } else {
                        if ($URLs[md5($next) . "0"]['url'] === $next) {
                            // 既にURLが追加済みの場合、終了する
                        } else {
                            // 同じMD5値が登録されているか確認する
                            // 且つ、そのキーの配列が現在のURLでないことを確認する
                            $i = 1;
                            while (isset($URLs[md5($next) . $i]['url']) && $URLs[md5($next) . $i]['url'] !== $next) {
                                $i++;
                            }
                            if (!isset($URLs[md5($next) . $i]['url'])) {
                                $URLs[md5($next) . $i] = [
                                    'url' => $next,
                                    'lastmod' => false,
                                    'priority_flg' => count(explode("/", $root_path)),
                                    'prev_url' => implode('/', $URL),
                                    'freq_set' => FREQ_SET,
                                ];
                                // ファイルの更新を取得・設定
                                foreach ($index_name as $index) {
                                    if (is_file(DOCUMENT_ROOT . $root_path . $index)) {
                                        $file_path = DOCUMENT_ROOT . $root_path . $index;
                                        break;
                                    }
                                }
                                if(isset($file_path)){
                                    $modified_time = @filemtime($file_path);
                                    $URLs[md5($next) . "0"]['lastmod'] = date(DATE_W3C, $modified_time);
                                }
                                if ($end_url === "") {
                                    $URLs[md5($next) . "0"]['priority_flg']--;
                                }
                                getSiteURLs($next);
                            }
                        }
                    }
                }
            }
        }
    }
}

//******************
// MAIN
//******************

getSiteURLs(HOMEPAGE);

$xml = new DOMDocument("1.0", "UTF-8");
$xml->formatOutput = TRUE;
$xml_root = $xml->createElement("urlset");
foreach ($URLs as $urlinfo) {
    // 存在しないURLか確認する
    $fp = @fopen($urlinfo['url'], 'r');
    if (!$fp) {
        continue;
    }
    $xml_row = $xml->createElement("url");
    foreach ($urlinfo as $key => $value) {
        switch ($key) {
            case 'url':
                $xml_field = $xml->createElement('loc', $value);
                break;
            case 'lastmod':
                if ($value) {
                    $lastmod = $value;
                    $xml_field = $xml->createElement('lastmod', $lastmod);
                }
                break;
            case 'priority_flg':
                if ($value createElement('priority', $priority);
                break;
            case 'freq_set':
                $xml_field = $xml->createElement('changefreq', $value);
                break;
            default:
                break;
        }
        $xml_row->appendChild($xml_field);
    }
    $xml_root->appendChild($xml_row);
    $xml_root->setAttribute("xmlns", "http://www.sitemaps.org/schemas/sitemap/0.9");
    $xml_root->setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
}
$xml->appendChild($xml_root);
$xml->save(OUTPUT);

exit;

設定

アクセス制限

ブラウザからのアクセスを制限するため、.htaccessでアクセス制限を設定しています。

Order allow,deny
Deny from all

まとめ

自社サイトに実装したところ、全てのページがインデックスされ、
自動でXMLサイトマップ(sitemap.xml)を生成することができました。

さらに、毎日0時に自動更新してくれるため、
ページの追加や削除があった場合にも、
最新情報をクローラーに伝えることができます。

今回作成したプログラムは、HTMLサイトのみ対応可能なため、
今後は、HTMLとWordpressが混在したサイトに対応したツールの開発をしていきたいと思います。