LoginSignup
6
6

More than 5 years have passed since last update.

Elixir/Phoenixで、Agentを使用してサイトマップを生成する方法とその考察

Last updated at Posted at 2016-05-11

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モジュールの出番ということになりそうです。

考察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内のコードをそのまま流用しています)

plain_sitemap

補足

検索エンジンへのping処理等は今後追加していきたいと思います。

6
6
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
6