最近は様々な Web サイトで Ajax が当たり前のように使われています。
ページ内のコンテンツをほぼ全てAjaxで別途ロードするようなサイトの場合、検索エンジンのロボットにはすっからかんのページがクロールされてしまいます。

そのために、サーバ側で予めブラウザで画面をロードした後のHTMLを生成して返すのが良いみたいです。
Googleのサイトにも AJAX クロール: ウェブマスターおよびデベロッパー向けガイド - ウェブマスター ツール ヘルプ のようなページがあり、HTML スナップショットをクローラーに提供するように提案されています。

また下記の英語サイトでは具体的な HTML snapshot の実装方法例が出ていて、その中で Java の HtmlUnit というものが紹介されています。
How do I create an HTML snapshot? - Webmasters — Google Developers

これを使って実際に今、Nginx とアプリケーションサーバで動かしている Web サイトに、
クローラーのアクセス時だけ Nginx → HtmlUnit の Java Web アプリサーバ → アプリケーションサーバと経由して HTML スナップショットを返すようにしてみました。

HtmlUnit のサイト: http://htmlunit.sourceforge.net/

サーブレットの実装

HtmlStaticServlet クラスは、GET で受け取ったパスをそのまま本来のアプリケーションサーバにリクエストして返すものです。
getAjaxPage メソッド内で FireFox 3.6 で JavaScript オン、CSS オフでアクセスしてロード後2秒間処理させて結果のXHTMLを返しています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.gargoylesoftware.htmlunit.BrowserVersion;
import com.gargoylesoftware.htmlunit.WebClient;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
public class HtmlStaticServlet extends HttpServlet {
private static final String ROOT_URI = "http://localhost:8081";
private static final long serialVersionUID = 172403081095751054L;
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
String url = ROOT_URI + request.getRequestURI();
System.out.println(url);
String htmlContent = getAjaxPage(url);
response.setContentType("text/html; charset=UTF-8");
PrintWriter writer = response.getWriter();
if (htmlContent != null) {
response.setStatus(HttpServletResponse.SC_OK);
writer.print(htmlContent);
} else {
response.setStatus(HttpServletResponse.SC\_NO\_CONTENT);
}
}
private String getAjaxPage(String url) {
WebClient webClient = new WebClient(BrowserVersion.FIREFOX\_3\_6);
webClient.setJavaScriptEnabled(true);
webClient.setCssEnabled(false);
webClient.setScriptPreProcessor(new IgonoredGAScriptPreProcessor());
try {
HtmlPage page = webClient.getPage(url);
webClient.waitForBackgroundJavaScriptStartingBefore(2000);
return page.getDocumentElement().asXml();
} catch (Exception e) {
return null;
} finally {
webClient.closeAllWindows();
}
}
}

Google Analyticsの無視

このままだとスナップショット生成時にGoogle Anlyticsへのアクセスが発生してしまいます。
こちらを無視するように JavaScritpのファイルロード時の処理を前もって処理する ScriptPreProcessor クラスのサブクラスを作っておきます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import com.gargoylesoftware.htmlunit.ScriptPreProcessor;
import com.gargoylesoftware.htmlunit.html.HtmlElement;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
public class IgonoredGAScriptPreProcessor implements ScriptPreProcessor {
@Override
public String preProcess(HtmlPage htmlPage, String sourceCode,
String sourceName, int lineNumber, HtmlElement htmlElement) {
if (sourceName.startsWith("script in")) {
if (sourceCode.indexOf(".google-analytics.com/ga.js") \> -1)
return "";
}
return sourceCode;
}
}

サーブレットの実行

ここでは htmlsnapshot.war として上記のサーブレットをアーカイブしました。
さらに軽量 Java Web アプリケーションサーバwinstone を使って起動します。
Winstone のサイト: http://winstone.sourceforge.net/

java -jar winstone-lite-0.9.10.jar –warfile=htmlsnapshot.war –httpPort=8082

Nginx のサイト設定

最後に Nginx のサイト設定を変更します。

まず、それぞれのサービスポートをおさらいしておきます。

Nginx

80

アプリケーションサーバ

8081

Winstone

8082

Googlebot と bingbot に対して snapshot を返すように proxy_pass を切り替えます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
upstream example_com {
server 127.0.0.1:8081;
}
upstream example\_com\_bot {
server 127.0.0.1:8082;
}
server {
listen 80;
server_name example.com;
location / {
if ($http\_user\_agent ~* (googlebot|bingbot)) {
proxy\_pass http://example\_com_bot;
break;
}
proxy\_pass http://example\_com;
}
}