作成中のアプリケーションで、sitemap.xml を出力する必要はあり、
wicketで作っているので、
なんかないか調べたところ、wicketstuff に sitemap出力用のライブラリがあったので、
それを使ってみた結果を書いておきます。
稼働環境の情報
Java Version 、Wicket の Version は以下の通りです。
- OS
OS X El Capitan
バージョン 10.11.6
- Java
java -version
------------------------------
java version "1.8.0_45"
Java(TM) SE Runtime Environment (build 1.8.0_45-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.45-b02, mixed mode)
------------------------------
- Wicket
<dependency>
<groupId>org.apache.wicket</groupId>
<artifactId>wicket-core</artifactId>
<version>7.4.0</version>
</dependency>
まず、サイトマップに関するガイドラインに記載がある。 一般的なサイトマップXMLの仕様について
サイトマップのガイドラインは、google の Search Console のヘルプ、
サイトマップの仕様を説明しているサイト(これもgoogleが作成?) が参考になりました。
以下、サイトマップの仕様で気になったところ(個人的に重要だと思ったところ)を記載します。
-
1 つのサイトマップにはサイズが 10 MB、 URL は 50,000 件以下にする。
-
サイトマップの形式は、[XML]、[RSS、mRSS、Atom 1.0]、[テキスト]、[Google サイト] の4つ
-
サイトマップ インデックス ファイル を作る場合は、 サイトマップは送信しなくて良い。
-
サイトマップ インデックス ファイルのxmlフォーマットは、 サイトマップのxmlフォーマットと少し違う
-
サイトマップ ファイルを http://example.co.jp/catalog/sitemap.xml に置いた場合は、
http://example.co.jp/catalog/ から始まる URL を含めることができるが、
http://example.co.jp/images/ から始まる URL を含めることはできない。
Example の 挙動の確認
sitemap indexファイルの生成もできるのかわからないので、
実際にwicket stuff の example を参考に挙動を確認してみます。
1. pom.xml に 依存関係を追加
<!-- https://mvnrepository.com/artifact/org.wicketstuff/wicketstuff-sitemap-xml -->
<dependency>
<groupId>org.wicketstuff</groupId>
<artifactId>wicketstuff-sitemap-xml</artifactId>
<version>7.4.0</version>
</dependency>
2. SiteMap.java を作成する
core/ExampleSiteMap.java at master · wicketstuff/coreをコピーして、
SiteMap.javaを作成します。
3. Applicationクラスで、SiteMap を登録
Application#mountResource()
で、SiteMap.javaとsitemap.xmlを紐付けします。
// -------------------------------------------------------------------------------
// SiteMap
// -------------------------------
mountResource("sitemap.xml", new SiteMap());
4. デプロイして、出力を確認
ローカル環境にデプロイして、http://127.0.0.1:18080/sitemap.xml
にアクセスすると、
以下のsitemap.xmlが出力されます。
sitemapというか、sitemap index ファイルが出力されます。
※サーバをポート 18080 で起動させてます。
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<script/>
<sitemap>
<loc>
http://127.0.0.1:18080/sitemap.xml?sourceindex=0&offset=0
</loc>
<lastmod>2016-10-29</lastmod>
</sitemap>
<sitemap>
<loc>
http://127.0.0.1:18080/sitemap.xml?sourceindex=0&offset=1000
</loc>
<lastmod>2016-10-29</lastmod>
</sitemap>
...
</sitemapindex>
続いて、sitemap index ファイルに含まれるURL
http://127.0.0.1:18080/sitemap.xml?sourceindex=0&offset=0
にアクセスすると、
以下のxmlが出力されます。
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<script/>
<url>
<loc>http://127.0.0.1:18080/sitemap.xml?number=0</loc>
<lastmod>2016-10-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.5</priority>
</url>
<url>
<loc>http://127.0.0.1:18080/sitemap.xml?number=1</loc>
<lastmod>2016-10-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.5</priority>
</url>
...
</urlset>
sitemap.xml が出力されました。
sitemap.xml の出力、 sitemap index ファイルの出力ともにサポートされているようです。
※50000件以上のエントリーが存在する場合も、使用できそう。
sitemap インデックスファイルに パラメータを渡すと、sitemap.xml になる動作は少しグレーですが、
サイトマップに関するガイドライン
の求める挙動はしているように思います。
Exmaple実装を、実アプリケーションで使用できるように、 サイトマップURLの出力のされ方、ページの指定方法、エントリーへのリンクの出力方法を変更します。
アプリケーションでサイトマップに出力したいエントリーの前提事項は以下の通りです。
前提事項
-
作ってるのは音楽フェスのサイトで、音楽フェスごとのページと、アーティストページがある
-
フェスのページ、アーティストページとも記事のインデックスIDはRDBに保存している
-
RDBからはJPAでデータを取得する
Example を 変更
ExampleからSitemap.javaは以下の通り変更しています。
1. Sitemap.java
変更点は以下の通りです。
- getDataSources()で IOffsetSiteMapEntryIterable の、2つ実装クラスを返すようにした。
フェスごとのページと、アーティストページがあるので、
フェスページのsitemapを作成するクラスと、
アーティストページのsitemapを作成するクラスのインスタンスを生成して、
返すようにしました。
- getDomain() はオーバーライド
親クラスSiteMapIndex.java の getDomain()の実装は、以下の通りです。
public String getDomain() {
if (domain == null) {
final Request rawRequest = RequestCycle.get().getRequest();
if (!(rawRequest instanceof WebRequest)) {
throw new WicketRuntimeException("sitemap.xml generation is only possible for http requests");
}
WebRequest wr = (WebRequest) rawRequest;
domain = "http://" + ((HttpServletRequest) wr.getContainerRequest()).getHeader("host");
}
return domain;
}
HTTP headerの host から host 名 取得しているので、以下、記事の問題(経験上たまに見舞われる)
nginxでリバースプロキシ先にホスト名が引き継がれない - Fujimura にぶつかりかねないので、
リソースバンドルから、取得するなければhostから取得するように変更しました。
package xyz.monotalk.sitemap;
import com.google.api.client.repackaged.com.google.common.base.Strings;
import org.apache.wicket.extensions.sitemap.IOffsetSiteMapEntryIterable;
import org.apache.wicket.extensions.sitemap.SiteMapIndex;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
/**
* SiteMap
*/
public class SiteMap extends SiteMapIndex {
private static final long serialVersionUID = 7074357449807043532L;
@Override
public IOffsetSiteMapEntryIterable[] getDataSources() {
return new IOffsetSiteMapEntryIterable[]{
new FestivalOffsetSiteMapEntryIterable(getDomain()),
new ArtistOffsetSiteMapEntryIterable(getDomain())
};
}
/**
* getDomain
*
* @return
*/
public String getDomain() {
String domain = null;
try {
domain = ResourceBundle.getBundle("WicketApplication").getString("domainName");
} catch (MissingResourceException e) {
// Do Nothing...
return super.getDomain();
}
if (Strings.isNullOrEmpty(domain)) {
return super.getDomain();
}
return domain;
}
}
続いて、FestivalOffsetSiteMapEntryIterable.java について
2. FestivalOffsetSiteMapEntryIterable.java
以下、変更点の説明になります。
-
RDBからの取得は遅延初期化する
SiteMap Entry クラスでは、何回もEntryを取得する必要はないように思ったので、
1回 ELEMENTS_PER_BLOCK数分のレコードを取得して、そのレコードを使い回すようにしています。 -
Guice の @Transactional アノテーションを付与しているので、close() では何もしない。
@Transactionnal をつけておくと、勝手にEntiryManager.remove()まで実行してくれるので、
close()メソッドでは何もしないように実装しました。 -
BasicSiteMapEntryBuilder を作った。
BasicSiteMapEntry のコンストラクタに4つ引数を渡すのが何かしっくりこなかったので、
Builderクラスを作成しました。
※これは趣味の話かと思います。 -
JPAのリポジトリクラスは、class内でInjector経由でInject
FestivalOffsetSiteMapEntryIterableクラス内で、Injector経由でInjectしています。
@Inject アノテーションでInjectする場合は、mountResourceメソッド実行時に以下のようにインスタンスを、
生成する必要があります。
mountResource("/sitemap.xml", InjectorHolder._new(SiteMap.class));
package xyz.monotalk.sitemap;
import lombok.NonNull;
import org.apache.wicket.extensions.sitemap.IOffsetSiteMapEntryIterable;
import org.apache.wicket.extensions.sitemap.ISiteMapEntry;
import org.apache.wicket.request.cycle.RequestCycle;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import xyz.monotalk.models.rdb.entity.Festival;
import xyz.monotalk.models.rdb.repository.FestivalRepository;
import xyz.monotalk.inject.initialize.InjectorHolder;
import xyz.monotalk.pages.festival.detail.FestivalDetailPage;
import java.util.Date;
import java.util.List;
/**
* FestivalOffsetSiteMapEntryIterable
*/
public class FestivalOffsetSiteMapEntryIterable implements IOffsetSiteMapEntryIterable {
private FestivalRepository repository = InjectorHolder._new(FestivalRepository.class);
private int festivalTotalCount = -1;
private Date changedDate = null;
private String domain = null;
private static final int ELEMENTS_PER_BLOCK = 1000;
/**
* FestivalOffsetSiteMapEntryIterable
*
* @param domain
*/
public FestivalOffsetSiteMapEntryIterable(@NonNull String domain) {
this.domain = domain;
}
@Override
public int getUpperLimitNumblocks() {
if (festivalTotalCount == -1) {
festivalTotalCount = repository.getRecordCount().intValue();
}
return (int) Math.ceil((double) festivalTotalCount / ELEMENTS_PER_BLOCK);
}
/**
* fullUrlFrom
*
* @param charSequence
* @return
*/
private String fullUrlFrom(@NonNull CharSequence charSequence) {
return this.domain + "/" + charSequence.toString();
}
@Override
public int getElementsPerSiteMap() {
return ELEMENTS_PER_BLOCK;
}
@Override
public Date changedDate() {
if (changedDate == null) {
changedDate = repository.getMaxUpdateDate();
}
return changedDate;
}
@Override
public SiteMapIterator getIterator(final int startIndex) {
return new SiteMapIterator() {
int numCalled = 0;
private List<Festival> festivals = null;
/**
* getFestivals
* @param limit
* @param offset
* @return
*/
private List<Festival> getFestivals(int limit, int offset) {
if (festivals == null) {
festivals = repository.findAllByLimitAndOffset(limit, offset);
}
return festivals;
}
/**
* hasNext
* @return
*/
public boolean hasNext() {
return numCalled <= ELEMENTS_PER_BLOCK && numCalled < getFestivals(ELEMENTS_PER_BLOCK, startIndex).size();
}
/**
* next
* @return
*/
public ISiteMapEntry next() {
Festival elem = getFestivals(ELEMENTS_PER_BLOCK, startIndex).get(numCalled);
PageParameters pageParameters = new PageParameters();
pageParameters.add("id", elem.getId());
numCalled++;
final CharSequence url = RequestCycle.get()
.mapUrlFor(FestivalDetailPage.class, pageParameters)
.toString();
return new BasicSiteMapEntryBuilder(fullUrlFrom(url))
.setModified(elem.getUpdateDate())
.setFrequency(ISiteMapEntry.CHANGEFREQ.WEEKLY)
.setPriority(0.5).createBasicSiteMapEntry();
}
/**
* remove
*/
public void remove() {
throw new UnsupportedOperationException("not possible here..");
}
public void close() {
// Do Nothing...
// @Transactional 付与で勝手にcloseされるので何もしない
}
};
}
}
3. BasicSiteMapEntryBuilder.java
これは、説明することはありません。
Builerクラスです。。
package xyz.monotalk.sitemap;
import lombok.NonNull;
import org.apache.wicket.extensions.sitemap.BasicSiteMapEntry;
import org.apache.wicket.extensions.sitemap.ISiteMapEntry;
import java.util.Date;
/**
* BasicSiteMapEntryBuilder
*/
public class BasicSiteMapEntryBuilder {
private String url;
private Date modified;
private double priority;
private ISiteMapEntry.CHANGEFREQ frequency;
private static final double DEFAULT_PRIORITY = 0.5;
public BasicSiteMapEntryBuilder(@NonNull String url) {
this.url = url;
modified = new Date();
priority = DEFAULT_PRIORITY;
frequency = ISiteMapEntry.CHANGEFREQ.WEEKLY;
}
public BasicSiteMapEntryBuilder setModified(@NonNull Date modified) {
this.modified = modified;
return this;
}
public BasicSiteMapEntryBuilder setPriority(double priority) {
if (priority > 1.0) {
priority = 1.0;
}
if (priority < 0.0) {
priority = 0.0;
}
this.priority = priority;
return this;
}
public BasicSiteMapEntryBuilder setFrequency(@NonNull ISiteMapEntry.CHANGEFREQ frequency) {
this.frequency = frequency;
return this;
}
public BasicSiteMapEntry createBasicSiteMapEntry() {
return new BasicSiteMapEntry(this.url, this.modified, this.priority, this.frequency);
}
}
sitemap index ファイル に出力されるsitemap.xml のファイル名が動的パラメータなのが、
少し気に入らないですが、特に問題はなさそうです。
※SiteMapIndex.java に相当するクラスを自作すればそれも解決できます。
補足. Wicket wiki 記載のSitemap.xml 生成方法
Wicket wiki にも、Sitemap.xmlの生成例はあったので、
補足として、記載しておきます。
リンクは以下です。
wicketstuff の sitemap を使うような作りの説明ではなくて、
Wicket の page クラスで、xml を生成するような形で実装しています。
この例だと、sitemap インデックスファイルは生成ではなく、sitemap の出力で、5万件までならば、
この実装方法で問題ないように思います。
また、Enchache2系に含まれている SimplePageCachingFilter を使って、Cache してもいいんじゃない。
って記載してあり、そちらは確かにやっておいた方がいい気がしました。
以上です。
コメント