Play frameworkでブログを作ってみました

Play! framework Advent Calendar 2011 jp #play_ja : ATND の15日目を担当します。
勢いでAdvent Calendarへ参加をしてみたものの、ブログもネタもなかったので、ブログをネタにすることにしました。

Playの公式にもブログのチュートリアルとサンプルがありますが、それ以外で工夫した箇所などを紹介したいと思います。

Play1.2.4を使用しています。

Routing

最終的なroutesファイルはこんな感じです。

# Static files
GET     /favicon.ico                               staticFile:public/images/favicon.ico
GET     /robots.txt                                staticFile:public/robots.txt

# Static dirs
GET     /public/                                   staticDir:public

# Home page
GET     /                                          Blog.archives(page:'1')
GET     /page/{page}/                              Blog.archives

# For reverse routing
GET     /                                          Blog.index

# Date based
GET     /{<[0-9]{4}>date}/                         Blog.yearly(page:'1')
GET     /{<[0-9]{4}>date}/page/{page}/             Blog.yearly
GET     /{<[0-9]{4}/[0-9]{2}>date}/                Blog.monthly(page:'1')
GET     /{<[0-9]{4}/[0-9]{2}>date}/page/{page}/    Blog.monthly

# Tag based
GET     /tag/{slug}/                               Blog.tag(page:'1')
GET     /tag/{slug}/page/{page}/                   Blog.tag

# Single post
GET     /{<[0-9]{4}/[0-9]{2}>date}/{slug}/         Blog.post

# Feed
GET     /atom.xml                                  Blog.atom(format:'xml')

# Force trailing slash
GET     /[^\.]+[^/]$                               Helper.addSlash

SEOフレンドリーなページング

本当にSEOフレンドリーかどうかは知りませんが、ページングはWordpressっぽい感じにしました。
1ページ目の場合は省略で、2ページ目以降はGETクエリーを使わず/page/2/のような書式にするためにエントリを2パターンに分けています。

controllerpage引数を与えて問題なくページ番号を取得できました。

public static void archives(int page) {

架空エントリを使用してリバースルーティングをシンプルに

先ほどのページング処理の関係でHomeページへのURLを取得するためには以下のようにページ番号を指定する必要がありました。

@{Blog.archives(1)}

これをroutesファイルが上から処理されていく性質を利用して、実際のHomeページBlog.archivesへのルートエントリの後に

GET     /      Blog.index

という使用されない架空のエントリを追加して(Blog.indexという空のアクションは必要)

@{Blog.index}

でHomeへのURLを取得出来るようにしました。

ただ後からrequest.getBase()でルートURLを取得できることを発見しましたが、ぎゅっと目をつぶることにしてます。

URL中の日付をDate型にバインディング

年別、月別アーカイブと個別記事ページに日付を使用しています。これももちろんWordpressからのパクリです。
最初は単純に年と月に分けて、以下のように数値で取得していました。

/{<[0-9]{4}>year}/{<[0-9]{2}>month}/

この方法でもよかったのですが、ドキュメントを眺めていたら@Asアノテーションという便利機能を見つけたので、routes

/{<[0-9]{4}/[0-9]{2}>date}/

のように修正してcontrollerDate型へのバインディングを追加。

public static void monthly(@As("yyyy/MM") Date date, int page) {

こうすることで、2011/13のような存在しない日付を渡された場合、dateNULLで渡されるので検証がだいぶ楽になりました。

URLの末尾にスラッシュを自動追加する

各URLの末尾がスラッシュで終わる形式が良かったのですが、単純にルートエントリの末尾を/にして強制してしまうと、スラッシュを忘れただけでページが表示されないという誰も得をしない結果になってしまいます。

またルートエントリの末尾を/?にしてスラッシュ有り無し両対応をするとURLが2パターン存在してしまう上に、リバースルーティングを使用するとスラッシュなしが採用されてしまい、これも望んでいたものと少し違いました。

そこで各ルートエントリはスラッシュを強制しつつ、末尾がスラッシュでないリクエストかった場合に、スラッシュ有りにリダイレクトさせる方法を考えました。
ルーティングだけでは実現出来そうになかったので、リダイレクトさせるためのアクションを作成してどうにか対応。

routes

GET     /[^\.]+[^/]$    Helper.addSlash

controller

public class Helper extends Controller {
    public static void addSlash() {
        redirect(request.url + "/");
    }
}

ルーティング関係は以上です。

Extensions

以下テンプレート用にいくつかエクステンションを書いたので紹介します。

String.truncate

public static String truncate(String input) {
    return truncate(input, 80);
}

public static String truncate(String input, int length) {
    return truncate(input, length, "...");
}

public static String truncate(String input, int length, String suffix) {
    if (input.length() > length) {
        input = input.substring(0, length - suffix.length()) + suffix;
    }

    return input;
}

文字列を指定文字数に切り詰めて...を追加するありがちなやつです。
ブログ記事の要約を作るのに使用しました。

String.stripTags

public static String stripTags(String input) {
    return Jsoup.parse(input).text();
}

Jsoupを使用して文字列からHTMLタグを取り除きます。
これも記事の要約を作るのに使用しています。

記事自体はmarkdownで書いているので本来ならmarkdownパーサ的なものを利用するのがベストなんですが、今後記事中にHTMLタグも直接書いたりすることもあると思うので

${post.content.markdown().stripTags().truncate(140)}

という感じで一度HTMLに変換してからタグを取り除いて、文字数を切り詰めています。
ちなみに.markdown()は公式にあがってるMarkdown moduleです。

String.mailto

public static String mailto(String email) {
    return mailto(email, email);
}

public static String mailto(String email, String label) {
    return String.format("<script type=\"text/javascript\">document.write(decodeURIComponent('%s'))</script>",
                                  hex(String.format("<a href=\"mailto:%s\">%s</a>", email, label)));
}

プロフィールに載せるメールアドレスをエンコードして、JavaScriptでデコードして表示させています。

${'email@example.com'.mailto().raw()}

Date.rfc3339

public static String rfc3339(Date date) {
    return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).format(date);
}

AtomフィードとHTML5のtimeタグに使用する日付フォーマット。date.format('...')とやってることは同じですが、後学のために書式と書式名を関連付けるために用意しました。Locale.USはコピペに起因するもので正直必要性はわかっていません。

RFC822版もあったりします。

public static String rfc822(Date date) {
    return new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US).format(date);
}

Deployment

いきなり話は飛んでデプロイについて。

本環境のサーバは既にnginx+PHPのサイトで使用していたので、nginxの後ろで走らせることにしました。
vhostを追加して以下のように設定。

server {
    listen 80;
    server_name blog.arahaya.com;
    ...

    location / {
        proxy_pass http://127.0.0.1:9000;
        proxy_set_header XRealIP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
    }
}

サービスの起動スクリプトはこちらのスニペットを若干修正して使用しました。


以上です!

だいぶ省略しましたが、一応ソースも上げておきましたので、興味があればmodel等、その他の部分も見てみてください。
https://github.com/arahaya/playblog

管理画面もcrudsecureモジュールを使用すれば簡単に作れるし、
慣れれば簡易的なブログなら数時間で作れてしまうのではないでしょうか。

面倒くさいJavaで面倒くさくないフレームワークを実現したPlayさんは本当に素晴らしいと思います。まだの方は是非試してみてください!

さて、Play! framework Advent Calendar 2011 jp #play_ja : ATND
明日は@genki_さんです!よろしくお願いします!