Alternative Architecture DOJO

オルターブースのクラウドネイティブ特化型ブログです。

C# の静的サイトジェネレーター「Statiq」を使ってみた

こんにちは。MLBお兄さんこと松村です。この記事はオルターブース Advent Calendar 2020の2日目の記事です。
adventar.org

1日目は弊社のマーケティング切り込み隊長のよしざっきーが開幕宣言をしてくれましたね!というかもう12月って早すぎませんかね。
aadojo.alterbooth.com

アドベントカレンダーのネタ探しでは「聞いたことあるけどまだ試したこと無いもの」を取り上げるようにしています。
今年は何を書こうかなーと考えていましたが、 C# の静的サイトジェネレーターである「Statiq」を取り上げてみます。

突然ですが私は .NET Foundation のサイトをたまに見るんですが、実はこのサイトのソースコードは GitHub で公開されています。

github.com

で、このリポジトリの README.md を読むとこう書かれています。

This website uses Statiq Web, a flexible and extensible static site generator for .NET.

Statiq というツールがあるのを知ったきっかけですね。


Statiq というのは C# の静的サイトジェネレーターです。記事執筆時点でバージョンが 1.0.0-beta.14 なので、まだ新しいツールですね。

statiq.dev

github.com

ツールの特徴

1. テンプレートが選べる

最終的には HTML ファイルになりますが、もとのページは Markdown と Razor構文で書くことができます。
Razor で書けるというのが C# 感ありますよね。動的コードがどのように静的コードになるのか、気になるところです。

2. データアジリティ

データアジリティをどう訳すか難しいところですが、 Statiq では YAML, JSON, XML をデータフォーマットとして扱うことができるということです。
現に .NET Foundation のリポジトリでは Markdown ファイルをデータとして扱っていることが分かります。

上記のコードで指定されている projects/data/ には Markdown ファイルが多数あります。
データベースから取得したレコードのように、 Markdown ファイルに記載されているタイトルを取得して出力しています。

3. 柔軟性

Statiq は3つのプロジェクトに分かれています。

  • Statiq Web
    • HTML の Web ページを生成することができます
  • Statiq Docs
    • まだこれから開発が行われるようだが、Statiq Web を拡張して API ドキュメントを生成することができるようになるらしいです
  • Statiq Framework
    • Statiq Web や Statiq Docs のベースになっているフレームワークで、100以上のモジュールがあるということです

4. デプロイメントが組み込まれている

Statiq の機能で Netlify, Azure App Service, GitHub Pages にデプロイができるようです。

statiq.dev

statiq.dev

statiq.dev

Hello world

では Quick Start に沿ってサンプルを作ってみます。
Statiq Web といいつつ、コンソールアプリケーションを作ります。

dotnet new console --name MySite
cd MySite
dotnet add package Statiq.Web --version 1.0.0-beta.14

Program.cs をこのように書きます。
Statiq.App.Bootstrapper を使って Web アプリケーションとして実行するというパイプラインを構成します。

using System.Threading.Tasks;
using Statiq.App;
using Statiq.Web;

namespace MySite
{
  public class Program
  {
    public static async Task<int> Main(string[] args) =>
      await Bootstrapper
        .Factory
        .CreateWeb(args)
        .RunAsync();
  }
}

次にページのコンテンツを作ります。
input フォルダーを作成し、そのなかに Markdown ファイルを作成します。

input/index.md

Title: My First Statiq page
---
# Hello World!

Hello from my first Statiq page.

f:id:tech-tsubaki:20201129234200p:plain

Statiq Web を実行して静的ページを出力します。
dotnet run を実行すると input フォルダーの HTML ページが output フォルダーに出力されます。

dotnet run

[INFO] Statiq Framework version 1.0.0-beta.29+a415eded36042aa7385b2f5b05cbf3d45ce2b7c7
[INFO] Statiq Web version 1.0.0-beta.14+cdb07e6364c16d1c6d5787f84bd8bc952e03ae3f
[INFO] Root path:
       D:/src/yuta/netcore/statiq/tmp/MySite
[INFO] Input path(s):
       theme/input
       input
[INFO] Output path:
       output
[INFO] Temp path:
       temp
[INFO] ========== Execution ==========
[INFO] Executing 10 pipelines (AnalyzeContent, Archives, Assets, Content, Data, DirectoryMetadata, Feeds, Inputs, Redirects, Sitemap)
[INFO] Cleaned temp directory: temp
[INFO] Cleaned output directory: output
[INFO] -> Inputs/Input ≫ Starting Inputs Input phase execution... (0 input document(s), 1 module(s))
[INFO] -> DirectoryMetadata/Input ≫ Starting DirectoryMetadata Input phase execution... (0 input document(s), 1 module(s))
[INFO] <- DirectoryMetadata/Input ≫ Finished DirectoryMetadata Input phase execution (0 output document(s), 38 ms)
[INFO] -> DirectoryMetadata/Process ≫ Starting DirectoryMetadata Process phase execution... (0 input document(s), 1 module(s))
[INFO] <- Inputs/Input ≫ Finished Inputs Input phase execution (1 output document(s), 39 ms)
[INFO] <- DirectoryMetadata/Process ≫ Finished DirectoryMetadata Process phase execution (0 output document(s), 1 ms)
[INFO] -> Inputs/Process ≫ Starting Inputs Process phase execution... (1 input document(s), 9 module(s))
[INFO] <- Inputs/Process ≫ Finished Inputs Process phase execution (1 output document(s), 67 ms)
[INFO] -> Assets/Process ≫ Starting Assets Process phase execution... (0 input document(s), 3 module(s))
[INFO] -> Data/Process ≫ Starting Data Process phase execution... (0 input document(s), 5 module(s))
[INFO] <- Assets/Process ≫ Finished Assets Process phase execution (0 output document(s), 4 ms)
[INFO] <- Data/Process ≫ Finished Data Process phase execution (0 output document(s), 6 ms)
[INFO] -> Content/Process ≫ Starting Content Process phase execution... (0 input document(s), 4 module(s))
[INFO] <- Content/Process ≫ Finished Content Process phase execution (1 output document(s), 175 ms)
[INFO] -> Archives/Process ≫ Starting Archives Process phase execution... (0 input document(s), 3 module(s))
[INFO] -> Redirects/Process ≫ Starting Redirects Process phase execution... (0 input document(s), 2 module(s))
[INFO] <- Archives/Process ≫ Finished Archives Process phase execution (0 output document(s), 0 ms)
[INFO] -> Feeds/Process ≫ Starting Feeds Process phase execution... (0 input document(s), 3 module(s))
[INFO] <- Feeds/Process ≫ Finished Feeds Process phase execution (0 output document(s), 0 ms)
[INFO] <- Redirects/Process ≫ Finished Redirects Process phase execution (0 output document(s), 5 ms)
[INFO] -> Redirects/Output ≫ Starting Redirects Output phase execution... (0 input document(s), 1 module(s))
[INFO] -> Data/Output ≫ Starting Data Output phase execution... (0 input document(s), 2 module(s))
[INFO] -> Content/PostProcess ≫ Starting Content PostProcess phase execution... (1 input document(s), 1 module(s))
[INFO] -> Archives/PostProcess ≫ Starting Archives PostProcess phase execution... (0 input document(s), 1 module(s))
[INFO] -> Sitemap/PostProcess ≫ Starting Sitemap PostProcess phase execution... (0 input document(s), 1 module(s))
[INFO] -> Feeds/Output ≫ Starting Feeds Output phase execution... (0 input document(s), 2 module(s))
[INFO] <- Feeds/Output ≫ Finished Feeds Output phase execution (0 output document(s), 2 ms)
[INFO] <- Data/Output ≫ Finished Data Output phase execution (0 output document(s), 2 ms)
[INFO] <- Redirects/Output ≫ Finished Redirects Output phase execution (0 output document(s), 2 ms)
[INFO] -> Assets/Output ≫ Starting Assets Output phase execution... (0 input document(s), 2 module(s))
[INFO] <- Assets/Output ≫ Finished Assets Output phase execution (0 output document(s), 0 ms)
[INFO] <- Archives/PostProcess ≫ Finished Archives PostProcess phase execution (0 output document(s), 3 ms)
[INFO] -> Archives/Output ≫ Starting Archives Output phase execution... (0 input document(s), 2 module(s))
[INFO] <- Archives/Output ≫ Finished Archives Output phase execution (0 output document(s), 0 ms)
[INFO] <- Sitemap/PostProcess ≫ Finished Sitemap PostProcess phase execution (1 output document(s), 4 ms)
[INFO] -> Sitemap/Output ≫ Starting Sitemap Output phase execution... (1 input document(s), 1 module(s))
[INFO] <- Sitemap/Output ≫ Finished Sitemap Output phase execution (1 output document(s), 3 ms)
[INFO] [Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager] User profile is available. Using 'C:\Users\yuta\AppData\Local\ASP.NET\DataProtection-Keys' as key repository and Windows DPAPI to encrypt keys at rest.
[INFO] <- Content/PostProcess ≫ Finished Content PostProcess phase execution (1 output document(s), 1769 ms)
[INFO] -> Content/Output ≫ Starting Content Output phase execution... (1 input document(s), 2 module(s))
[INFO] <- Content/Output ≫ Finished Content Output phase execution (1 output document(s), 1 ms)
[INFO] -> AnalyzeContent/Input ≫ Starting AnalyzeContent Input phase execution... (0 input document(s), 1 module(s))
[INFO] <- AnalyzeContent/Input ≫ Finished AnalyzeContent Input phase execution (1 output document(s), 0 ms)
[INFO] AnalyzeContent/Process ≫ Running 3 analyzers (ValidateAbsoluteLinks, FencedCodeBlocksShouldHaveLanguage, ValidateRelativeLinks)
[INFO] ========== Execution Summary ==========

Number of output documents per pipeline and phase:

 | Pipeline          | Input     | Process    | PostProcess | Output   | Total Time |
 |----------------------------------------------------------------------------------|
 | AnalyzeContent    | 1 (0 ms)  |            |             |          | 0 ms       |
 | Archives          |           | 0 (0 ms)   | 0 (3 ms)    | 0 (0 ms) | 3 ms       |
 | Assets            |           | 0 (4 ms)   |             | 0 (0 ms) | 4 ms       |
 | Content           |           | 1 (175 ms) | 1 (1769 ms) | 1 (1 ms) | 1945 ms    |
 | Data              |           | 0 (6 ms)   |             | 0 (2 ms) | 8 ms       |
 | DirectoryMetadata | 0 (38 ms) | 0 (1 ms)   |             |          | 39 ms      |
 | Feeds             |           | 0 (0 ms)   |             | 0 (2 ms) | 2 ms       |
 | Inputs            | 1 (39 ms) | 1 (67 ms)  |             |          | 106 ms     |
 | Redirects         |           | 0 (5 ms)   |             | 0 (2 ms) | 7 ms       |
 | Sitemap           |           |            | 1 (4 ms)    | 1 (3 ms) | 7 ms       |

Pipeline phase timeline:

 | Pipeline          | Timeline (2067 total ms)                                                             |
 |----------------------------------------------------------------------------------------------------------|
 | AnalyzeContent    |                                                                                    I |
 | Archives          |             PTO                                                                      |
 | Assets            |      P      O                                                                        |
 | Content           |      P------T----------------------------------------------------------------------O |
 | Data              |      P      O                                                                        |
 | DirectoryMetadata | I-P                                                                                  |
 | Feeds             |             PO                                                                       |
 | Inputs            | I-P--                                                                                |
 | Redirects         |             PO                                                                       |
 | Sitemap           |             TO                                                                       |


[INFO] ========== Completed ==========
[INFO] Finished execution in 2092 ms
[INFO] Cleaned temp directory: temp

dotnet run -- preview を実行するとサーバーを起動し、静的ページにアクセスすることができます。
プレビュー実行中であればファイル変更を検知して、自動的に再読込してくれます。いわゆるホットリロードですね。

f:id:tech-tsubaki:20201129234308p:plain


Razor構文のテンプレートも試してみます。
about フォルダーを作成し、ASP.NET Core Razor Pages で使うレイアウトファイル等を置きます。

f:id:tech-tsubaki:20201129234402p:plain

input/about/index.cshtml

@{
    ViewData["Title"] = "This is about page";
}

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>

レイアウトファイル + index.cshtml の HTML ファイルが出力されます。
このときページタイトルは index.cshtml にて動的に設定していますが、きちんとコードが実行されたうえでページタイトルになっています。

ViewData["Title"] = "This is about page";

<title>This is about page - MySite</title>

f:id:tech-tsubaki:20201129234428p:plain


1つのプロジェクトに Markdown 形式と Razor 構文のどちらも含めることができることがわかったので、シンプルな装飾で良ければ Markdown 形式、CSSクラス名を指定したり動的な値設定をしたい場合は Razor 構文にする、といった使い分け方になると思います。