アプリ開発ときどきアウトドア

主にJavaを使ったアプリ開発やトラブルシューティング等のノウハウ、キャンプや登山の紹介や体験談など。

1. システムエンジニアリング 9. その他

Webアプリ用のサイトマップ生成

投稿日:2019年6月30日 更新日:

Googleの検索結果への表示を促進するためにGoogleに対してサイトマップを提供する必要がある。
WordPressでは様々なプラグインを使ってサイトマップを作成できるが、Webアプリの場合は独自に提供する必要がある。
ここでは、Webアプリでサイトマップを生成するためのサーブレットについて説明します。

前提

  • JavaEEを使ったWebアプリを想定しています。ここでは、Javaでサイトマップを生成するためのライブラリであるsitemapgen4jを使用します。
  • Googleに提供するサイトマップファイルには、通常当該サイトで公開対象のURLが複数含まれる単一ファイルになりますが、このファイルのサイズが 50 MB(圧縮しない状態で)を超過する場合や、URL数 が5万件を超える場合、ファイルを分割する必要があります。(サイトマップファイルと、これらのファイルへの参照を定義するサイトマップインデックスファイル)
  • サイトマップの詳細については、Googleのヘルプを参照のこと。

仕様

  • Webアプリで検索して見つかったディレクトリやファイルからサイトマップに含めるURLを構成します。
  • 例えば、ベースのURLが”https://example.com/example-web/”、見つかったファイルが/pages/index.xthml”だった場合、これらを結合した”https://example.com/example-web/pages/index.xthml”というURLをサイトマップファイルに追加します。
  • ベースURLやディレクトリ・ファイルの定義はプロパティファイルに定義します。プロパティファイルの配置場所はクラスパス上の任意の場所を想定します。
    プロパティ名 説明 値の例
    sitemap.baseurl ベースとなるURL https://example.com/example-web/
    sitemap.scanMM.path 検索ディレクトリであり、このディレクトリを再帰的に検索する。コンテキストからのパスです。MMは任意の整数です。 /testdir1
    sitemap.scanMM.patternNN 上記のscanMM.pathを検索する際のファイル名(正規表現)。NNは整数であり、NNの値を01, 02,…等のように変更することで複数指定可能。 .*\\.xhtml$
    sitemap.exclude.patternZZ 上記の検索から除外するディレクトリ・ファイル名であり、ZZを変更することで複数指定が可能。 ^\\/WEB-INF\\/.*
  • JavaEEベースのWebアプリに組み込みやすいよう、サーブレット単体で実現します。
  • sitemapgen4jの仕様上、ファイルシステムへの出力が必要となります。本プログラムでは一時ディレクトリを作成(Files#createTempDirectory)してサイトマップファイルを出力し、その内容をHTTP応答として返却後に一時ディレクトリとサイトマップファイルを削除しています。

モジュール

関連するファイルは下記の通り。

SimpleSitemapGenerator.java サイトマップ作成サーブレット
sitemap-config.properties プロパティファイル
web.xml Webアプリの構成ファイル
pom.xml mavenファイル

サーブレットプログラム

メインとなるサーブレットのソースコード(抜粋)です。

package example.web.servlet;
...
public class SimpleSitemapGenerator extends HttpServlet {
...
	@Override
	public void init() throws ServletException {

		logger.trace("init(): start");

		// コンテキストの物理パス
		Path contextPath = Paths.get(getServletContext().getRealPath(File.separator));

		// プロパティのロード
		Properties prop = new Properties();
		ClassLoader cl = getClass().getClassLoader();
		try (InputStream is = cl.getResourceAsStream(PROP_FILENAME)) {
			prop.load(is);
		} catch (IOException e) {
			throw new ServletException("missing or invalid properties: " + PROP_FILENAME);
		}

		// ベースURLの取得
		URL baseUrl;
		try {
			String baseUrlStr = prop.getProperty(PROPKEY_BASE_URL, "");
			baseUrl = new URL(stripRight(baseUrlStr, URL_SEP));
		} catch (MalformedURLException e) {
			throw new ServletException("invalid baseurl", e);
		}

		// スキャンエントリの取得
		List<SitemapScan> scanList = parseScanEntries(prop);

		// 除外パターンの取得
		List<Pattern> exPatternList = parseExcludeEntries(prop);

		this.contextPath = contextPath;
		this.baseUrl = baseUrl;
		this.scanList = scanList;
		this.exPatternList = exPatternList;

		logger.debug("contextPath: {}", this.contextPath);
		logger.debug("baseUrl: {}", this.baseUrl);
		logger.debug("scanList: {}", this.scanList);
		logger.debug("exPatternList: {}", this.exPatternList);

		logger.trace("init(): end");
	}

	@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

		logger.trace("doGet(): start");

		// sitemapgen4jのAPIはメモリ上にサイトマップを作成できないので
		// 一時ディレクトリにサイトマップファイルを作成し、その内容をレスポンスに設定する。
		Path tmpDirPath = null;
		try {

			// サイトマップファイルを格納するためのディレクトリを作成
			tmpDirPath = Files.createTempDirectory("sitemap");
			logger.debug("tmpdir: {}", tmpDirPath);

			// ファイル一覧からURLを生成してサイトマップを作成
			WebSitemapGenerator wsg = new WebSitemapGenerator(this.baseUrl, tmpDirPath.toFile());
			for (SitemapScan scan : this.scanList) {
				loadUrl(wsg, scan);
			}
			wsg.write();

			// サイトマップが作成されているかを検証
			Path sitemapFilePath = tmpDirPath.resolve(SITEMAP_FILENAME);
			if (!Files.isReadable(sitemapFilePath)) {
				throw new ServletException("no sitemap file found: " + sitemapFilePath);
			}

			// サイトマップファイルの内容を返却
			resp.setContentType(CONTENT_TYPE);
			Files.copy(sitemapFilePath, resp.getOutputStream());
			resp.flushBuffer();

		} finally {
			cleanup(tmpDirPath);
		}

		logger.trace("doGet(): end");
	}

	/**
	 * サイトマップスキャン情報に基づいてURLをロードする。
	 * 
	 * @param wsg  サイトマップジェネレータ
	 * @param scan サイトマップスキャン
	 * @throws IOException ファイル一覧取得失敗
	 */
	protected void loadUrl(WebSitemapGenerator wsg, SitemapScan scan) throws IOException {
		if (scan == null || scan.scanPath == null) {
			return;
		}

		// 走査パス配下のファイル一覧を取得し、マッチング判定してファイル一覧を作成
		Path scanPath = Paths.get(this.contextPath.toString(), scan.scanPath.toString());
		try (Stream<Path> walk = Files.walk(scanPath).filter(Files::isRegularFile)) {

			Iterator<Path> it = walk.iterator();
			while (it.hasNext()) {
				Path path = it.next();

				// ルートを基準としたパスに変換する。
				// 併せてセパレータをURLの"/"に置き換え
				// 例) C:\path\to\scan, C:\path\to\scan\path\to\page.xhtml -> /path/to/page.xhtml
				Path relPath = this.contextPath.relativize(path);
				String relPathStr = URL_SEP + relPath.toString().replace(File.separatorChar, URL_SEP);

				// 除外条件にマッチすれば除外
				if (regexesMatched(this.exPatternList, relPathStr)) {
					logger.trace("exclude path: {}", relPathStr);
					continue;
				}

				// 対象条件にマッチしない場合は除外
				if (!regexesMatched(scan.patternList, relPathStr)) {
					logger.trace("no include path: {}", relPathStr);
					continue;
				}

				// エントリとなるWebSitemapUrlを生成
				String url = this.baseUrl + relPathStr;
				Options opts = new Options(url);
				opts = opts.changeFreq(ChangeFreq.HOURLY);
				opts = opts.lastMod(new Date(Files.getLastModifiedTime(path).toMillis()));
				opts = opts.priority(1.0);
				WebSitemapUrl wsu = opts.build();

				wsg.addUrl(wsu);
				logger.info("add: {}", wsu.getUrl());
			}
		}
	}
...

プロパティファイル

上記のサーブレットで使用するプロパティファイルです。

#
# sitemap-config.properties
#

# ベースURL
sitemap.baseurl=https://example.com/example-web/

# スキャン対象ディレクトとファイルパターンを定義
# ・定義ルール
#   sitemap.scanMM.path     : スキャンフォルダ
#   sitemap.scanMM.patternNN: ファイルパターン
# ・正規表現のエスケープを使う場合は"\\"と表記
# ・意図しないファイルが出力されないことをログで確認!

# /testdir1
sitemap.scan01.path=/testdir1
sitemap.scan01.pattern01=.*\\.xhtml$
sitemap.scan01.pattern02=.*\\.html$

# /testdri2
sitemap.scan02.path=/testdir2
sitemap.scan02.pattern01=.*\\.xhtml$
sitemap.scan02.pattern02=.*\\.html$

# サイトマップに出力しないファイルパターン
# ・全てのスキャンディレクトリを対象
# ・「サイトマップに出力するファイルパターン」より優先
# ・正規表現のエスケープを使う場合は"\\"と表記
sitemap.exclude.pattern01=^\\/WEB-INF\\/.*
sitemap.exclude.pattern02=^\\/META-INF\\/.*

# EOF

Web.xml

上記のサーブレットをWebアプリに登録します。

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
...
	<!-- Googleサイトマップ作成用サーブレット -->
	<servlet>
		<servlet-name>sitemapgen</servlet-name>
		<servlet-class>example.web.servlet.SimpleSitemapGenerator</servlet-class>
	</servlet>
	<servlet-mapping>
		<servlet-name>sitemapgen</servlet-name>
		<url-pattern>/sitemap</url-pattern>
	</servlet-mapping>
...
</web-app>

pom.xml

mavenを使ってライブラリを指定する場合は次のようにpom.xmlを定義します。
mavenを使わない場合は、/WEB-INF/lib等のクラスパス上にsitemapgen4jのjarを配置します。

<project xmlns="http://maven.apache.org/POM/4.0.0"
...
	<dependencies>
...
		<dependency>
			<groupId>com.google.code</groupId>
			<artifactId>sitemapgen4j</artifactId>
			<version>1.0.1</version>
		</dependency>
...
	</dependencies>
...

動作確認

web.xmlで定義したURLをブラウザで開きます。
Googleのサイトマップとして登録する場合は、Google Search ConsoleのサイトマップでURLを登録します。

下記はブラウザで開いた場合の応答結果です。

<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>
https://example.com/example-web/testdir1/test1-1.xhtml
</loc>
<lastmod>2019-06-23T17:48:36.931+09:00</lastmod>
<changefreq>hourly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>
https://example.com/example-web/testdir1/test1-2.xhtml
</loc>
<lastmod>2019-06-23T17:48:36.931+09:00</lastmod>
<changefreq>hourly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>
https://example.com/example-web/testdir1/test1-4.html
</loc>
<lastmod>2019-06-23T17:48:36.931+09:00</lastmod>
<changefreq>hourly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>
https://example.com/example-web/testdir2/test2-1.xhtml
</loc>
<lastmod>2019-06-23T17:48:36.931+09:00</lastmod>
<changefreq>hourly</changefreq>
<priority>1.0</priority>
</url>
</urlset>


(adsbygoogle = window.adsbygoogle || []).push({});


(adsbygoogle = window.adsbygoogle || []).push({});

-1. システムエンジニアリング, 9. その他

執筆者:


comment

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

関連記事

CentOS7のマルチホーム化

サイトの存在を隠しつつも、sftpサーバを公開し、後輩と1G以上のファイルのやりとりしたい。 パブリック側のIPアドレスを教えてしまうと、どこのサーバだろうかとブラウザで開いたりするとサイトの存在がわ …

Webアプリテスト用のHTTPヘッダの追加

フロントに配置されたリバースプロキシサーバやロードバランサで設定されたHTTPヘッダを使用するWebアプリを開発することが多々あります。 このようなシナリオでは、設計に基づいて実装することはできますが …

技術検証

Linuxでの gdrive の使い方

wordpressのデータをgoogle driveにバックアップするための基礎調査を行っている。 google driveに接続可能なライブラリは幾つかあるようだが、ここでは無料でお手軽に始められそ …

マスタデータ生成ツール

開発や結合試験、本番環境等で使用するマスタデータをExcelで管理することがあります。 そのようなExcelファイルからDBに登録するためのインサート文を作成するために、いつもツールに悩むので作成して …

Windows10のインストール場所を選べない

DELLのノートPCであるVostro 5370を購入しました。 既定ではWindows 10 Homeがインストールされていましたが、会社用のボリュームライセンスのWindows10 Enterpr …

プロフィール ゆっきーです。
都内でシステムエンジニアをやっています。
もっと詳細を見る