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\\/.*
# EOFWeb.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>