Markdownでコンテンツを管理できるWebサイトをLaravelで作ってみた

# はじめに
アドベントカレンダー初参加の@ABCanG1015です。
今回はMarkdownでコンテンツを管理できるWebサイトを作ってみた話をします。
Laravel4を使ってます。
これは今自分のサイトの管理に使ってます。
[abcang.net](http://abcang.net)を見ながら読むと何を書いてるのかわかりやすいと思います。

# コンセプト
1つのページに複数のコンテンツがある場合、コンテンツを追加したり編集したい時は毎回そのページ全体を更新することになります。
名前順に並べたい時も自分で適切な位置に配置してあげなければいけません。
今回はそれを解決できるように以下の3つをコンセプトに作りました。

* 1つのコンテンツにつき1つのMarkdownで管理する
* カテゴリ分けして、名前でソートして表示する
* メニューバーにそれらのコンテンツを表示する

# フォルダ構造
フォルダ構造は以下のように設計しました。

```
site
`-- app
    |-- composers.php
    |-- controllers
    |   `-- ContentController.php
    |-- routes.php
    |-- storage
    |   `-- data
    |       |-- contents.md
    |       |-- hoge
    |       |   |-- about.md
    |       |   |-- bar.md
    |       |   `-- foo.md
    |       |-- huga
    |       |   |-- about.md
    |       |   |-- bar.md
    |       |   `-- foo.md
    |       |-- index.md
    |       `-- updates.md
    `-- views
        `-- content
            |-- index.blade.php
            |-- master.blade.php
            `-- show.blade.php
```

Markdownは`app/storage/data`に配置します。
`contents.md`にはメニューバーに表示するものを記述します。
例としては以下のようにリスト形式でリンクを作成するだけです。

```m
* [ほげ](./hoge)
* [ふが](./huga)
```

`index.md`や`updates.md`はトップページに表示するためのMarkdownになります。

カテゴリ分けはフォルダを作って、その中にMarkdownファイルを入れる形にしました。
`about.md`はそのカテゴリの説明用のMarkdownになっています。
`about.md`以外のMarkdownファイルが1つのコンテンツになります。

# routes.php
以下のようにトップページは`index`を、それ以外のカテゴリは`show`を呼ぶようにしています。

```php
<?php
Route::pattern('content', '[\w\-]+');

Route::get('/', array(
    'as'   => 'content.index',
    'uses' => 'ContentController@index'
));

Route::get('{content}', array(
    'as'   => 'content.show',
    'uses' => 'ContentController@show'
));
```

# ContentController.php
コントローラーは以下のようにしました。
`index`ではただ単に表示しているだけですが、`show`ではフォルダ内のMarkdownファイルを読み込んでソートしています。

```php
<?php
use dflydev\markdown\MarkdownParser;

class ContentController extends BaseController {

    public $mdp;
    public $data_path;

    public function __construct()
    {
        $this->mdp = new MarkdownParser();
        $this->data_path = storage_path().'/data/';
    }

    public function index()
    {
       return View::make('content/index', array(
            'mdp' => $this->mdp,
            'data_path' => $this->data_path
        ));
    }

    public function show($content)
   {
        $content_path = $this->data_path.$content.'/';
        if (is_dir($content_path)) {
            $items = array();
            if ($dir = opendir($content_path)) {
                while (($file = readdir($dir)) !== false) {
                    if (ends_with($file, '.md') && $file != 'about.md') {
                        $items[] = str_replace('.md', '', $file);
                    }
                }
                closedir($dir);
               sort($items);
            }

            return View::make('content/show', array(
                'mdp' => $this->mdp,
                'data_path' => $this->data_path,
                'content' => $content,
                'content_path' => $content_path,
                'items' => $items
            ));
        }
        App::abort(404);
    }
}
```

# composers.php
メニューバーを表示するときに、もしコンテンツがあった場合はそれらもメニューに加える処理をしています。結構強引な処理をしています…
`app/start/global.php`などからロードするようにしておきます。

```php
<?php

View::composer(array('*'), function($view) {
    $data = $view->getData();
    $contents = file_get_contents(storage_path().'/data/contents.md');

    if (isset($data['content'])) {
        $array = explode("\n", $contents);
        foreach ($array as $key => $val) {
            if (ends_with($val, '(./'.$data['content'].')')) {
                $items = array();
               foreach ($data['items'] as $item) {
                    $fp = fopen($data['content_path'].$item.'.md', 'r');
                    $items[] = preg_replace('/^#+ +(.+)\n/', '	* [$1](#'.$item.')', fgets($fp));
                    fclose($fp);
                }
               array_splice($array, $key+1, 0, $items);
                break;
            }
        }
        $contents = implode("\n", $array);
    }
    $view->with('contents', $contents);
});

```

# View
`show.blade.php`では以下のようにページを生成しています。

```php
<div id="about">
    {{ $mdp->transformMarkdown(file_get_contents($content_path.'about.md')); }}
</div>
@foreach($items as $item)
    <div class="item" id='{{$item}}'>
        {{ $mdp->transformMarkdown(file_get_contents($content_path.$item.'.md')); }}
    </div>
@endforeach
```

`index.blade.php`では`index.md`と`updates.md`を読み込むようにしています。
`master.blade.php`ではメニューバーの表示をしています。

# おわりに
今回こんな感じで作ってみましたが、結構管理はしやすいです。
現在はコンテンツの部分だけを別のgitのリポジトリとして管理しています。
しかしリポジトリにpushすると自動的にpullして反映する機能は無いので、現在手動でしています。
また、毎回Markdownからページを生成しているので、コンパイルして静的なページを生成とかしてみたいと思いました。
これらの機能は今後の課題という感じです。

明日は@kavao_jpさんです