静的サイトジェネレーターでC#を使いたかったからWyamを試したら最高だった

Github Pagesでちょっとしたサイトを公開するのにJekyll(Github側でビルドしてたからプラグインがほとんど使えない)を使っていたのですがやっぱりC#使いたくなったのでC#を使える静的サイトジェネレーターを探したら[Wyam](https://wyam.io/)がありました

試したら最高だったので紹介します

*当記事では現状最新の0.15.\*-betaをもとにしています*

# Wyamについて

## 特徴

- C#をRoslynでスクリプトとして動かす
- [モジュール](https://wyam.io/docs/concepts/modules)・[パイプライン](https://wyam.io/docs/concepts/pipelines)形式
- 簡単なことならC#のコーディングをしなくてもいい(後述)
- ASP .NETでお馴染みのRazorが使える
- Front matterとしてYAMLが使えるほかMarkdown,Lessなどにも対応
- ぶっちゃけモジュールさえ書けばなんでもできる(用意されてるモジュールもたくさんある)
- そのモジュールさえ書かなくてもC#のスクリプトで拡張できる
- もちろんNuGetに対応し.NETの資産を使える

あと今後.NET Coreとしてクロスプラットフォームにする予定みたいです

## ダメなところ

- ビルドに時間がかかる(ビルド毎にNuGetパッケージをインストールしてる)
- beta版(普通に使えるけど保守性を考えると個人の趣味程度しか使えなさそう)
- 使ってる人が少ない、日本語圏は皆無、英語圏でも少ない、よって使うときは公式ドキュメントとソースとにらめっこになる

## つまり

![wyam_1.PNG](https://qiita-image-store.s3.amazonaws.com/0/51176/835b0da5-3235-68d4-edf4-336a7e254ab4.png)
*[https://wyam.ioトップのスクショです](https://wyam.io/)*

*レシピとテーマがありますが使ってないので当記事では紹介しないです*

# とりあえず動かしてみる

## 環境の用意
[リリースページ](https://github.com/Wyamio/Wyam/releases)
[ここ](https://wyam.io/docs/usage/obtaining)を読んでから入れろと書いてありますがWindowsのインストーラーのSetup.exeを動かせばWyamのコマンドプロンプトがインストールされます

## VisualStudioプロジェクトの用意
C#使うからにはやっぱりVisualStudioの補完が使いたい！ってなるのでVS先輩使います

まずプロジェクト作成ですがASP .NET(.NET Framework)プロジェクトにします
![wyam_2.PNG](https://qiita-image-store.s3.amazonaws.com/0/51176/e32a7cc2-627b-1b87-f6b0-a582045737a7.png)
テンプレートですが空のテンプレートでMVCのところにチェックを入れてください(チェックを入れなくても自分で用意することもできますが)
![wyam_3.PNG](https://qiita-image-store.s3.amazonaws.com/0/51176/66e63de4-a150-ac94-7443-3a514dc90c19.png)

作成されたらプロジェクトのプロパティで**必ず対象フレームワークを.NET Framework 4.6**にしてください
4.5のままだとNuGetのパッケージが用意されていません
![wyam_4.PNG](https://qiita-image-store.s3.amazonaws.com/0/51176/d1eb221b-66cf-6ad1-53fb-9d630d98a7c1.png)

次にVSのビルドでWyamのビルドをできるようにしたら楽なのでWyamのパッケージを[NuGet](https://www.nuget.org/packages/Wyam/)からインストールします

Preview版なのでパッケージマネージャーコンソールからインストールすることにします

```
Install-Package Wyam -Pre
```

*Wyamのバージョンはすべて同じ系統で統一したほうがいいです*

こんな感じでソリューションフォルダ直下のpackagesフォルダにWyamのツールが追加されるのでこれをビルドイベントに追加します
![wyam_5.PNG](https://qiita-image-store.s3.amazonaws.com/0/51176/c7441f5d-e3a3-0e51-eae1-6621dbade031.png)

これをビルドイベントに書き足すのですがバージョンとコンフィグファイルは各自合わせてください

```
"$(SolutionDir)\packages\Wyam.0.15.6-beta\tools\wyam.exe" "$(SolutionDir)\config.wyam"
```

![wyam_6.PNG](https://qiita-image-store.s3.amazonaws.com/0/51176/981223e3-a376-dccb-0b8e-00a2b55b0284.png)

VS先輩の補完に使うために[Wyam.Common](https://www.nuget.org/packages/Wyam.Common/)をプロジェクトに追加しておきます

こちらもパッケージマネージャーコンソールからインストールします

```
Install-Package Wyam.Common -Pre
```

## configファイルの用意

先ほどビルドイベントで指定したようにソリューションフォルダ直下にconfig.wyamを追加します
\#nの行はNuGetパッケージです(-pはプレリリース版) 使うモジュールなどに合わせて追記してください
FileSystem.InputPaths.Add(string);で用意するファイル群ディレクトリを追加します(デフォルトではinput,theme)
最初はFront matter YAMLとRazorを使って直下のindex.htmlを生成するコードにします

```:(SolutionDir)/config.wyam
#n Wyam.Markdown -p
#n Wyam.Razor -p
#n Wyam.Yaml -p
#n Wyam.Html -p
#n Wyam.Less -p
#n Wyam.Minification -p

FileSystem.InputPaths.Add("Site/Views");

Pipelines.Add("Index",
	ReadFiles("Index.cshtml"),
	FrontMatter(Yaml()),
	Razor(),
	WriteFiles((doc,ctx) => new FilePath("index.html"))
);
```

C#風な書き方ですが
> The configuration file is evaluated as C# code, so you can make use of the full C# language and the entire .NET ecosystem. However, it's not necessary to know C# to write Wyam configuration files. The syntax has been carefully crafted to be usable by anyone no matter their level of programming experience. Some extra pre-processing is also done to the file to make certain code easier to write (which actually makes the syntax a superset of C#, though this extra magic is entirely optional).
[https://wyam.io/docs/usage/configurationより](https://wyam.io/docs/usage/configuration)
>> 構成ファイルはC＃コードとして評価されるため、完全なC＃言語と.NETエコシステム全体を利用できます。ただし、Wyamの設定ファイルを書き込むためにC＃を知る必要はありません。構文は、プログラミング経験のレベルに関係なく誰もが使用できるように慎重に作られています。いくつかの余分な前処理もファイルに対して行われ、特定のコードを簡単に書くことができます（この余分なマジックは完全にオプションですが、実際にはC＃のスーパーセットになります）。
Google翻訳より

とあるように知識がなくても書けて、知識があったらやりたいように書けるという風です

## ファイルの用意

VS先輩の補完を使うには以下のものをcshtmlファイルに追加するといいです
下記ではModel.MetadataからメタデータメソッドにアクセスしてますがModelにも生えてるのでわざわざMetadataにアクセスしなくていいです(小声)

```csharp
@using Wyam.Common.Documents;
@using Wyam.Common.Execution;
@model IDocument
@{
    var context = Context as IExecutionContext;
    var globalMetadata = context.GlobalMetadata;
    var metadata = Model.Metadata;
}
```


```html:Views/Shared/_Layout.cshtml
@using Wyam.Common.Documents;
@using Wyam.Common.Execution;
@model IDocument
@{
    var context = Context as IExecutionContext;
    var globalMetadata = context.GlobalMetadata;
    var metadata = Model.Metadata;
}
<!DOCTYPE html>
<html>
<head>
    <title>@Html.Raw(metadata.String("Title",string.Empty))</title>
</head>
<body>
    @RenderBody()
</body>
</html>
```

```csharp:Views/_ViewStart.cshtml
@{ 
    Layout = "../Shared/_Layout";
}
```

```html:Views/Index.cshtml
---
Title: Titleだよーん
---
<p>Helloooooooooooooooooo</p>
```

## ビルド
先ほどVS先輩のビルドイベントに追加していたらVisualStudioのビルドメニューから該当プロジェクトをビルドするだけでWyamのビルドがされます
少し(自分の環境で30秒ぐらい)時間がかかるのでTwitterでも見ながら暇つぶしするといいです

ビルドが成功したらソリューション直下のoutputフォルダに生成されるはずなので確認しましょう

```html:(SolutionDir)/output/index.html
<!DOCTYPE html>
<html>
<head>
    <title>Titleだよーん</title>
</head>
<body>
    <p>Helloooooooooooooooooo</p>
</body>
</html>

```

こんな感じのが生成されていたら成功です
もしビルドエラーがでたらVS先輩からじゃ詳細がわからないのでWyam.exeのパスが通ったコマンドプロンプトで```wyam build```をして確認してください

## ローカル環境で確認
Wyam.exeのパスが通ったコマンドプロンプトで```wyam preview```するとlocalhostでブラウザから確認できます

# カスタマイズ

## View

これ以降各cshtmlファイルで以下のものが宣言されてる前提にします

```csharp
@using Wyam.Common.Documents;
@using Wyam.Common.Execution;
@model IDocument
@{
    var context = Context as IExecutionContext;
    var globalMetadata = context.GlobalMetadata;
    var metadata = Model.Metadata;
}
```

以下が参考になります

- [IDocumentインターフェース](https://github.com/Wyamio/Wyam/blob/develop/src/core/Wyam.Common/Documents/IDocument.cs)
- [IExecutionContextインターフェース](https://github.com/Wyamio/Wyam/blob/develop/src/core/Wyam.Common/Execution/IExecutionContext.cs)

各ViewのモデルにはIDocumentインターフェースを実装したもの(おおかた[Documentクラス](https://github.com/Wyamio/Wyam/blob/develop/src/core/Wyam.Core/Documents/Document.cs)だろうけども)が来ます
ASP .NETでのMVCパターンのModelとViewの部分が表面に出てきてControllerはWyamが勝手にするという感じですかね

### メタデータを取得

ジェネリクスなGetメソッドが用意されています
以下はTitleキーのstring型を取ってくる例です

```csharp
Model.Get<string>("Title");
metadata.Get<string>("Title");

Model.Get("Title",string.Empty);// default value付き
metadata.Get("Title",string.Empty);// default value付き
```
string型用のStringメソッドも用意されています

```csharp
Model.String("Title");
metadata.String("Title");

Model.String("Title",string.Empty);// default value付き
metadata.String("Title",string.Empty);// default value付き
```

グローバルメタデータの場合はconfig.wyamに以下のように書きます

```:config.wyam
GlobalMetadata["Title"] = "Titleだよーーん";
```

アクセス方法はglobalMetadata変数から通常のメタデータを取ってくるのと同じ方法です

[IMetadataインターフェース](https://github.com/Wyamio/Wyam/blob/develop/src/core/Wyam.Common/Meta/IMetadata.cs)が参考になります
補足ですがIDocumentインターフェースはIMetadataインターフェースを継承してるのでModelからでもアクセスできます


### リンクを取得

IExecutionContextインターフェースにはModelと関連付けられたViewへのリンク(実際の相対URL)を取得するメソッドが用意されています(引数はIMetadataインターフェースでいいです)

```csharp
context.GetLink(IDocument);//IDocument型を渡す方が多いと思います
context.GetLink(IMetadata);
```

ただ、この方法はメタデータにある、後述するWriteFilesモジュールなどでセットされるFilePathを利用するのでそのあとの処理でしか使えないです

他には、FilePath型のIDocument.Sourceプロパティから出力されるパスに変換するという手もあります
[FilePathクラス](https://github.com/Wyamio/Wyam/blob/develop/src/core/Wyam.Common/IO/FilePath.cs)参考

### 他のViewのModelを取得

使い道はページ一覧とかタグ一覧とかになりますがちゃんと他のViewのModelを取得することができます(※ただし処理済みのものに限る)

IEnumerable<IDocument>を継承した[IDocumentCollection型](https://github.com/Wyamio/Wyam/blob/develop/src/core/Wyam.Common/Documents/IDocumentCollection.cs)を```context.Documents```で取得できます
パイプラインによる絞り込みも```FromPipeline(string)```メソッドで行えます

そこからメタデータなどによって絞り込みをすれば一覧ページなどを作ることができますが、処理済みのModelしか取得できないのでconfig.wyamに書くパイプラインの順番には気を付けたほうがいいです

## config.wyam

どう説明したらいいかわからないので作ったサイトのconfig.wyamを上から補足するように説明します

### [Markdownモジュール](https://wyam.io/modules/markdown)を使う

[Razorモジュール](https://wyam.io/modules/razor)のように書くだけです

```:config.wyam
Pipelines.Add("Archives",
	ReadFiles("Archive/*.md"),
	FrontMatter(Yaml()),
	Markdown(),
	WriteFiles((doc,ctx) => $"Archive/{doc.Source.FileNameWithoutExtension}/index.html".ToLower())	
);
```

Razorモジュールと組み合わせて使うこともできます、また同じフォルダにcshtmlファイルとmdファイルを共存させることもできます
その場合は[Concatモジュール](https://wyam.io/modules/concat)を使います

```:config.wyam
Pipelines.Add("Archives",
	ReadFiles("Archive/{!_,}*.cshtml"),
	FrontMatter(Yaml()),
	Concat(
		ReadFiles("Archive/*.md"),
		FrontMatter(Yaml()),
		Markdown()
	),
	Razor(),
	WriteFiles((doc,ctx) => $"Archive/{doc.Source.FileNameWithoutExtension}/index.html".ToLower())	
);
```

### [Metaモジュール](https://wyam.io/modules/meta)を使う
パイプライン内でもModelのメタデータを追加できます

自分の場合Jekyllで書いたファイルをそこまで手直ししたくなかったのでファイル名が```(Year)-(Month)-(Day)-(Title).md```形式になっているのですがここから時刻をメタデータに格納して出力パスも```(Year)/(Month)/(Day)/(Title)```にします

```config.wyam
	public static class DateHelper {

        public static DateTime FromFileName(string fileName) {
            string[] ar = fileName.Split('-');
            if(ar.Length < 4) {
                return DateTime.MinValue;
            }
            int year;
            int month;
            int day;
            if(int.TryParse(ar[0],out year) && int.TryParse(ar[1],out month) && int.TryParse(ar[2],out day)) {
                return new DateTime(year,month,day);
            }
            return DateTime.MinValue;
        }
    }

Pipelines.Add("Archives",
	ReadFiles("Archive/{!_,}*.cshtml"),
	FrontMatter(Yaml()),
	Concat(
		ReadFiles("Archive/*.md"),
		FrontMatter(Yaml()),
		Markdown()
	),
	Meta("Date",DateHelper.FromFileName(@doc.Source.FileNameWithoutExtension.ToString())),
	Razor(),
	WriteFiles((doc,ctx) => $"Archive/{doc.Source.FileNameWithoutExtension.ToString().Replace("-","/")}/index.html".ToLower())	
);
```

### [Excerptモジュール](https://wyam.io/modules/excerpt)を使う
サイトの概要を自動で生成したくなりますが、そのときはExcerptモジュールを使います
デフォルトでは一番最初に現れたpタグをメタデータにキーをExcerptとして格納します

またデフォルトではpタグ自身を含めたり、内部のHTMLタグをそのまま入れたりするので[Ifモジュール](https://wyam.io/modules/if)とMetaモジュールを使い再格納します

```:config.wyam
Pipelines.Add("Archives",
	ReadFiles("Archive/{!_,}*.cshtml"),
	FrontMatter(Yaml()),
	Concat(
		ReadFiles("Archive/*.md"),
		FrontMatter(Yaml()),
		Markdown()
	),
	Meta("Date",DateHelper.FromFileName(@doc.Source.FileNameWithoutExtension.ToString())),
	Excerpt().WithOuterHtml(false),
	If(
		(doc,ctx) => doc.String("Excerpt",null) != null,
		Meta("Excerpt",System.Text.RegularExpressions.Regex.Replace(@doc.String("Excerpt"), "<.*?>", string.Empty))
	),
	Razor(),
	WriteFiles((doc,ctx) => $"Archive/{doc.Source.FileNameWithoutExtension.ToString().Replace("-","/")}/index.html".ToLower())	
);
```

### [WriteFilesモジュール](https://wyam.io/modules/writefiles)を使う
基本的なモジュールですが出力したいパスが各自違うと思うのでまとめておきます


そのまま出力する

```
WriteFiles()
```


相対パスはそのままで拡張子だけ変える

```
WriteFiles(".html")
```

相対パスをstring型で返す
(例はブラウザでアクセスするときファイル名を隠せるようにindex.htmlファイルにする奴)

```
WriteFiles((doc,ctx) => $"Archive/{doc.Source.FileNameWithoutExtension}/index.html")	
```

相対パスをFilePath型で返す
(例は出力直下にindex.htmlファイルを置く奴)

```
WriteFiles((doc,ctx) => new FilePath("index.html")

相対パスをstring型で返す方法はたまにうまくいかないパターンがあるのでFilePath型を返す方が確実です
```

### [Executeモジュール](https://wyam.io/modules/execute)を使う
ここでWyamの真骨頂Executeモジュールです
やりたいことが用意されているモジュールにないとき、わざわざモジュールを作らなくてもC#をスクリプトとして動かしてやりたいようにできるモジュールです

やりたいことの例として、すべての(処理済みの)ドキュメントからTagsというキーのメタデータがあるModelを集めてそのタグごとのModel一覧ページを作るです

通常は[ReadFilesモジュール](https://wyam.io/modules/readfiles)からコンテンツを作成していきますが、元ファイルがないところからでも作れます

ただパイプライン上で各Viewを表現するのは鬼畜の所業なのでRazorさんを使ってテンプレートからViewを作成します
また、ファイルがないとこからの処理なのでRazorのとこでWithViewStart(FilePath)メソッドでViewStartを指定します
(View側は_ViewStart.cshtmlが参照してるLayoutでメタデータ中心のレイアウト組めばいいです)

```:config.wyam
Pipelines.Add("Tags",
	Execute(@ctx.Documents
		.WhereContainsKey("Tags")
		.SelectMany(x => x.Get<IEnumerable<string>>("Tags"))
		.Distinct()
		.Select(x => @ctx.GetDocument("",new Dictionary<string, object>()
			{
				{ "Title", $"{x} Tag" },
				{ "Description", $"{x}タグに関するページを表示します" },
				{ "Tag", x},
				{ "TagPages", @ctx.Documents.WhereContainsKey("Tags").Where(y => y.Get<IEnumerable<string>>("Tags").Contains(x)) }
			})
		)
	),
	Razor().WithViewStart(new FilePath("/Tag/_ViewStart.cshtml")),
	WriteFiles((doc,ctx) => $"Tag/{doc.String("Tag",string.Empty).Replace("#","Sharp")}/index.html".ToLower())
);
```

### [Paginateモジュール](https://wyam.io/modules/paginate)を使う

ページネーションもちゃんとできます
最初の引数で1ページごとのコンテンツ数を指定し、そのあとの引数で処理するコンテンツをモジュールで引き出します
[Documentsモジュール](https://wyam.io/modules/documents)ではReadFilesのようにパイプラインからコンテンツを引き出せます
[OrderByモジュール](https://wyam.io/modules/orderby)はその名の通りソートしてます

メタデータのキーについては[Paginateモジュール](https://wyam.io/modules/paginate)を参照してください

```:config.wyam
Pipelines.Add("Archive",
	ReadFiles("Page/Archive.cshtml"),
	Paginate(5,
		Documents("Archives"),
		OrderBy((doc, ctx) => doc["Date"]).Descending().ThenBy((doc,ctx) => doc.String("Title",string.Empty))	
	),
	FrontMatter(Yaml()),
	Razor(),
	WriteFiles((doc,ctx) => "Archive/index"+(doc.Get<int>("CurrentPage") == 1 ? "" : ("-"+doc.Get<int>("CurrentPage")))+".html".ToLower())
);
```

### Asset

特に説明することはないですが[CopyFilesモジュール](https://wyam.io/modules/copyfiles)でそのままコピーしたり、[Lessモジュール](https://wyam.io/modules/less)でLessを処理したり、[MinifyCSSモジュール](https://wyam.io/modules/minifycss)でCSSを圧縮したりできます

```:config.wyam
Pipelines.Add("Asset",
	CopyFiles("Asset/**/*")
);

Pipelines.Add("Less",
	ReadFiles("Asset/Less/*.less"),
	Less(),
	MinifyCss(),
	WriteFiles((doc,ctx) => $"Asset/Css/{doc.Source.FileNameWithoutExtension}.css")
);
```

# 最後に

以上のようにやりたいことは何でもできるようになっています(知識が必要でないとは言っていない)
[紹介してないモジュールもたくさんあります](https://wyam.io/modules)のでぜひ使ってみてください

あといつまでbeta版なんだろ…

### Wyamで作ったもの

- [サイト](https://blog.meilcli.net/)
- [生成物](https://github.com/MeilCli/MeilCliNet/tree/master/docs)
- [ソース](https://github.com/MeilCli/MeilCliNet)