SEOをしっかり考慮したWebサービスを開発する場合、サイトマップの出力は必須になりますが、Rubyのsitemap_generatorのようなElixirの便利パッケージは現時点で存在しません。
パッケージを使わずにゴリゴリ書いて生成することも可能なのですが、他のプロジェクトでも共用することを考慮して、ここはパッケージを作ってしまいましょう。
考察1
まずはsitemap_generator
の動作を調査してみましょう。DSLは下記のようになっています。
SitemapGenerator::Sitemap.default_host = "http://www.example.com"
SitemapGenerator::Sitemap.create(:include_root => false) do
add '/', changefreq: 'daily', priority: 1.0
add '/welcome', changefreq: 'weekly', priority: 0.5
end
このDSLによって出力されるXMLは下記のようになります。
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns:xsi=...>
<url>
<loc>http://www.example.com/</loc>
<lastmod>2011-05-21T00:03:38+00:00</lastmod>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>http://www.example.com/welcome</loc>
<lastmod>2011-05-21T00:03:38+00:00</lastmod>
<changefreq>weekly</changefreq>
<priority>0.5</priority>
</url>
</urlset>
sitemap_generator
には他にも色々な機能が用意されていますが、最低限とりあえず上記の動作が実現出来れば良さそうです。
考察2
sitemap_generator
と似た感じのDSLを定義してみましょう。とりあえず下記のようなDSLでオッケーなのではないでしょうか。
defmodule MyApp.Sitemap do
use MySitemap, default_host: "http://www.example.com"
urlset do
add "/", changefreq: "daily", priority: 1.0
add "/welcome", changefreq: "weekly", priority: 0.5
Entry |> Repo.all |> Enum.each(fn(entry) ->
add "/entries/#{entry.id}", lastmod: entry.updated_at, changefreq: "daily", priority: 1.0
end)
end
end
"/"
と"/welcome"
に加えて、記事詳細ページ用の定義が追加されていますが、ここで一つの問題が発生します。
"/"
と"/welcome"
は固定URLなので特に問題無いのですが、記事詳細ページ用の"/entries/:id"
という形式のURLを生成するには、データベースの参照処理が必要になります。
つまり、上記のurlset
マクロに渡されるdo
ブロック内の処理は、コンパイル時ではなく実行時に呼び出される必要があるということになりますが、これはどのような方法で実現すれば良いのでしょうか?
考察3
いくつか方法はあると思いますが、最もシンプルなのは「do
ブロックを関数定義の内部で展開する」という方法だと思いますので、urlset
マクロとadd
マクロを下記のように定義してみます。
defmodule MySitemap do
defmacro urlset([do: block]) do
quote do
def render do
unquote(block)
end
end
end
defmacro add(path, opts \\ []) do
quote do
...
end
end
end
関数内で展開されたdo
ブロック内の処理は、その関数が呼び出されるまでは実行されませんので、とりあえずこの問題に関してはこの対応でオッケーそうです。
考察4
しかしここでまた新たな疑問が生じます。add
マクロ内の処理でurlの情報を追加していきたいわけですが、Elixirは関数型言語ですので、MySitemap
モジュールのメンバ変数にurlの情報を追加していくような処理は行えません。
コンパイル時であればModule attributeをtemporary storageとして使用可能ですが、上記のrender
関数は実行時に呼び出されますので、Module attribute
は使用出来ません。
ということで、ここはstate管理機能を提供しているAgentモジュールの出番ということになりそうです。
- (2016/05/13追記) Erlangの提供しているProcess Dictionaryを使用する方法でも良さそうです。Processモジュールに
put
やget
関数が用意されています。
考察5
Agent
を使用して、下記のようなモジュールを定義してみましょう。
defmodule MySitemap.Generator do
def start_link do
Agent.start_link(fn -> [] end, name: __MODULE__)
end
def add(path, opts) do
entry = {path, opts}
Agent.update(__MODULE__, &([entry | &1]))
end
def urlset do
Agent.get(__MODULE__, &(&1 |> Enum.reverse))
end
end
MySitemap.add/2
マクロは、上記のMySitemap.Generator.add/2
関数を下記のように呼び出せば良さそうです。
defmodule MySitemap do
...
defmacro add(path, opts \\ []) do
quote do
MySitemap.Generator.add(unquote(path), unquote(opts))
end
end
end
これで、MySitemap.add/2
内の処理が実行される度に、Agent
の管理しているリストにurl情報が追加され、MySitemap.Generator.urlset
関数を使用してurlのリストを取得出来るようになりました。
まとめ
上記までの処理に、supervisor
によるプロセス管理や、gzip圧縮されたサイトマップファイルの出力処理等を追加したパッケージが下記になります。(使用方法はREADME.md
に記載されています。urlset
の各属性値に関してはsitemap_generator
内のコードをそのまま流用しています)
補足
検索エンジンへのping処理等は今後追加していきたいと思います。