uehaj's blog

Grな日々 - GroovyとかGrailsとかElmとかRustとかHaskellとかReactとかFregeとかJavaとか -

Grails/Groovyのサイトを構築している、名もなき静的サイトジェネレータ

Grails/Groovyのサイトは、静的サイトジェネレータで作成されています。Groovyサイトは去年ぐらいからそうだったのですが、最近Grailsもそうなりました*1

しかしこの静的サイトジェネレータの名前がわかりません。ソースコード上は、単に「generator/SiteGenerator.groovy」で、独立したgithub projectもありません。groovy-websitegrails-static-websiteというプロジェクトの一部としてそれぞれでカスタム化されて機能しているというだけです。

SiteGenerator.groovyは139行ぐらいでやってることもシンプルで自明なので、独立にするまでもない、ということでしょうか。処理の実行はgradleタスクでguild.graldeに書いてあります。

gradle genrateSite

こんな感じでsite/build/site配下にHTMLなどを含むサイト全体の情報が生成されます。gradle webzipでリンク切れチェックなどが実行された上で、サイト内容をzipで固めたものが生成されます。デプロイ機能とかはないみたいです*2

この名もなき「サイトジェネレータ」がたいへん気に入ったので紹介します。

Markup Template Engineでページを作成する

Markup Template Engineは、Groovy 2.3で導入された、HTMLを書くためのDSLであり、HTML生成を実行するGroovy標準クラスライブラリです。ドキュメントはこちらAPIリファレンスはこちら

たとえばGrailsサイトの、pages配下にあるトップページindex.groovyは以下のような感じで記述されます。

import model.Event

layout 'layouts/main.groovy', true,
        pageTitle: 'The Grails Framework',
        mainContent: contents {
            div(id: 'band', class: 'band') {

            }
            div(id: 'content') {
                include unescaped: 'html/index.html'

                int eventsPerRow = 3
                def allEventNames = allEvents.keySet() as List
                int rows=allEventNames.size()/eventsPerRow + (allEventNames.size()%eventsPerRow != 0 ? 1 : 0)
                for(int rownum = 1; rownum <= rows; rownum++) {
                  int offset = (rownum-1) * eventsPerRow
                  def cssClasses = "row colset-3-article"
                  if(rownum==1) {
                    cssClasses += " first-event-row"
                  }
                  if(rownum==rows) {
                    cssClasses += " last-event-row"
                  }
                  section(class: cssClasses) {
                      if(rownum == 1) {
                        h1 { strong "Groovy and Grails events you shouldn't miss!" }
                      }
                      allEventNames[offset..(Math.min(offset + (eventsPerRow-1), allEventNames.size()-1))].each { String eventName ->
                          Event event = allEvents[eventName]
                          article {
                              div(class: 'content') {
                                  // Note that the event image should be 257x180 to look nice
                                  div(class: 'event-img', style: "background-image: url(${event.logo})") {}
                                  h1 {
                                      a(href: event.url) {
                                          strong eventName
                                          br()
                                          em event.location
                                      }
                                  }
                                  time event.date
                                  yieldUnescaped event.description
                              }
                          }
                      }
                  }
                }
            }
            include unescaped: 'html/they-use-groovy.html'
        }

要するに、HTMLをプログラムとして書くわけです。抽象化、モジュール化、共通部分の括りだし、合成、型チェックなどなど、プログラミング言語が持つフル機能を享受してサイトというシステムの提供機能を記述することができます。

Markup Template Engineについてはこちらの資料もどうぞ。

大規模なサイトは、構造をもったソフトウェアなのであり、「プログラムのように」ビルドやチェックができるのが利点です。さらに静的サイトジェネレータ一般の利点として、ソースコード管理システムで管理できるのがうれしいです。データをDBに保存するCMSとかは個人的には捨てるしかないです。バックアップとか面倒すぎるし、履歴を辿れないのもアレだし。かといってGititとか使ってみたこともありますが、結局私には耐えられなかったとです。

生のHTMLを使える

先のindex.groovyでは、include unescaped "html/index.html"のようにHTML断片を使えます。

これは実際問題としては現実的です。Excelをたとえば XLS2HTMLTABLEアドインなどでテーブルに変換したりしたものをページの一部に貼る、みたいな状況ではHTMLを「そのまま」使いたいときがあり、必須です。半端なwikiとか、「不完全なHTML」だけしかつかえないCMSなどは、動的だろうが静的だろうが捨てるしかないと私は思うわけです。

「 データ」をモデルクラスとして定義、使用する

わたしはこれが一番面白いと思ったのですが、サイトで使用するデータを「モデル」として定義します。たとえば以下が「モデル」です。

  • メニューの選択項目(リンクであったり、サイト内のページであったり)
  • 「書籍」一覧を含むのであれば、書籍クラス(Book.groovy)
  • サイトで配布物jarを列挙しているのであれば、配布物のクラス(Distribution.groovy)

さらにこれらのモデルは、sitemap.groovyというDSLで、その値を簡潔に定義できます。 sitemap.groovyの例はこちら

Webサイトが大規模でも大規模じゃなくても、静的であっても、それがデータを扱っていることは間違いありません。静的データをHTMLのULやliでベタに書くということは、もしやたいへんに愚かしいことではなかったのか、と考え込まずにはいられません。JSで書くのもアレだし。

こちらのサイトによれば、sitemap.groovyとモデルクラスで設定するモデル群は、「低コストなデータベース」だそうです。 まさしく。コンパイル時に使用したい、データベースか。うーん、この発想はなかったな。

もちろん、既存の静的ジェネレータでも「ブログエントリ」とかはデータなんですが、もっと汎用的なもの。

リンク切れチェック

checkDeadLinksタスクでサイト全体のリンク切れのチェックが実行されます。

事前インストールフリー

Gradleだから当たり前なんですが、サイトのプロジェクトソースさえ入手すれば、Java以外のインストールなしに、GroovyやGradle、gvmやらの明示的なインストールなしで、即刻ページ生成が可能となります。gradle wrapperってやつです。

その他

font-awesomeとかtwitter-bootstrapとかをデフォルトでだいたい含んでます。

まとめ

Spud-CMSGrailsプラグインとかも遍歴しましたが、社内のサイトとかは、これからは当面これで作っていこうと思います。apacheだけでいいです。tomcatとかもういいです。

*1:ただし、一部はGrailsアプリケーションとして動作しているそうです。たぶんプラグインのページ。

*2:もちろんすぐ作れます。Gradleだし!