Swiftで始めるWeb開発(2) 静的サイトジェネレータIgniteを用いてWebページを構築する

前回の[Swiftで始めるWeb開発(1) Swiftで始めるバックエンド開発](https://qiita.com/gdate/items/59e19b458f10b97d1d98)に続き今回はSwiftを使った静的サイトジェネレータIgniteの紹介をしたいと思います。
# Igniteとは
[Ignite](https://github.com/twostraws/Ignite)とは静的サイトジェネレータです。[Paul Hudson](https://twitter.com/twostraws)さんによって作られ、try!Swift 2024にて発表されました。
Igniteは、ResultBuilderによってHTMLやCSSの記述をSwiftUIのような構文に置き換え、結果として得られたHTMLやCSSなどWebページに必要な構成要素を含んだものをBuildフォルダに出力します。
# ResultBuilderとは
resultBuilderとはSwift5.4で追加されたアトリビュートの一つです。
SwiftのDSL(Domain Specific Language)と言えます。DSLとは特定の問題領域またはタスクに特化したプログラミング言語のことです。
resultBuilderを使ってDSLを構築するメリットは以下です。

* 複数要素の組み合わせ作業を命令的ではなく、宣言的に行えるようにできます。
* 要素を収集し、最終的に一つの結果を出力します。
* 軽量で読みやすいコードで複雑なオブジェクトを構築できるようになります。
* 可読性・保守性の向上が見込めます。
* 例としてSwiftUIのレイアウトを記述する際に使われるViewBuilderなどが挙げられます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/457333/860a6e07-0e85-a5ed-95da-7c00c082df1a.png)

より詳しく知りたい方は、以下の資料がわかりやすかったので併せてご覧ください。
https://speakerdeck.com/chocoyama/20fen-dewakaru-su-xi-resultbuilder-iosdc-2022

# 使い方
では、IgniteでどのようにしてWebサイトを構築していくのか見ていきたいと思います。
まずは[Ignite](https://github.com/twostraws/Ignite)をcloneします。
最近CLIが提供されたようなので、そちらを使っていきたいと思います。

## プロジェクトの作成
```
// リポジトリのクローン
$ git clone https://github.com/twostraws/Ignite
// クローンしたディレクトリに移動
$ cd Ignite
// コマンドラインツールをビルドし、インストールします
$ make install
Building the Ignite command-line tool...
...
✅ Success! Run `ignite` to get started.

// ExampleSite というプロジェクトを作成 
$ ignite new ExampleSite
⚙️  Creating a new Ignite site in 'ExampleSite'...
✅ Success!

// 作成したプロジェクトのディレクトリに移動
$ cd ExampleSite
// Xcodeでの編集を開始
$ open Package.swift 
```

## Webページの編集
Xcodeが開いたら Sources -> Pages -> Home.swiftを編集していきます。
以下のようにWebページに必要な要素を記述していき、それぞれの要素に対してSwiftUIのmodifireのように属性を付与していくことで振る舞いを変更することができます。
```Swift
    // Home.swift
    ...
    func body(context: PublishingContext) -> [BlockElement] {
        Text {
            Text("Swift rocks")
                .font(.title1)
            Text(markdown: "Add *inline* Markdown")
                .foregroundStyle(.secondary)
            Link("Swift", target: "https://www.swift.org")
                .linkStyle(.button)
            Divider()
            Image("/images/Swift_logo_color.svg")
                .accessibilityLabel("The Swift logo.")
                .padding()
        }
    }
```

より詳しい記法について知りたい場合はサンプルページが用意されていますので、そちらを参照ください。
https://ignitesamples.hackingwithswift.com/

## Webサイトのジェネレート
Webページが出来上がったら、 Scheme -> IgniteStarter を実行すると以下のようなhtmlやその他必要なファイルが出力されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/457333/571063b5-d9a5-a6a8-e552-792e45667ff4.png)

:::note info
ジェネレート時にファイルアクセス権を聞かれるので許可してください。
![スクリーンショット 2024-04-21 19.21.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/457333/8677b098-5761-f733-124a-76f4759e2353.png)

:::

## プレビュー表示
サイトジェネレートが完了したら、以下のコマンドでサイトをプレビューするために使用するローカルサーバーが起動され、ブラウザで開くことができます。
```
$ ignite run --preview
✅ Starting local web server on http://localhost:8000
Press ↵ Return to exit.
```
自動でブラウザに遷移するので以下表示が確認できるかと思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/457333/46ab3e42-cf57-3332-dab1-9861e4c83ce6.png)



# Vaporでローカルサーバを立ててWebサイトを表示する
続いて前回の[Swiftで始めるWeb開発(1) Swiftで始めるバックエンド開発](https://qiita.com/gdate/items/59e19b458f10b97d1d98)と組み合わせてVaporでローカルサーバーを起動させてページを表示したいと思います。
以下プロジェクトをcloneして動作を確認できます。
https://github.com/gdate/SwiftServerDemo
Schemeから**WebSiteGenerator**というものを選択してDestinationをMy Macにします。
こちらはIgniteを使ってWebサイトを構築するためのターゲットです。
ビルドするとPublicディレクトリの中にサンプルサイトの表示に必要なファイル群が作成されます。
次にSchemeから**WebServer**を選択してDestinationを同じくMy Macにします。
こちらはVaporを使ってWebサーバーを立ち上げるためのターゲットです。
ビルドすると、Xcodeのコンソールにlocalhostのアドレスが表示されますので、それをコピーしてブラウザに貼り付けるとサンプルWebサイトが表示されるかと思います。
![スクリーンショット 2024-04-21 18.32.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/457333/26fceab7-029a-1225-3579-d5a79c35537b.png)
ちいかわのカルーセルが表示されていれば成功です（特に意味はありません笑）
一応メインコンテンツとして本記事の内容と同じものがあります。
![スクリーンショット 2024-04-21 18.34.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/457333/e6d165df-5b3a-30cc-3018-2556241642bc.png)

:::note alert
PublicディレクトリをVaporが認識できるようにカスタムワーキングディレクトリを設定する必要があります。
(WebServer)Edit Scheme -> Run -> Options -> Use custom working directoryをチェックし、プロジェクトのルートフォルダへのパスを入力してください。
![スクリーンショット 2024-04-21 19.37.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/457333/cbbb774c-20dc-0069-2646-d8ac365dda50.png)
:::


# Tips
今回VaporとIgniteを組み合わせてWebサイトを表示するところまで行いました。
これを実現するために必要な設定をVaporとIgniteでどのように行うのか紹介していきます。
## Vaporの設定
VaporでWebサイトを表示するための設定をしていきます。
まずは、configure.swiftの修正をします。
```Swift
// configure.swift
let directory = DirectoryConfiguration.detect()
app.directory = directory
app.directory.publicDirectory = "Build/"
app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory, defaultFile: "index.html"))
```
Vaporの公開ディレクトリはデフォルトでPublicとなっています。しかし、Igniteで出力されるフォルダはBuildになっているため、このままでは毎回ディレクトリのリネームが必要です。
そこで、公開ディレクトリの参照先を変更してあげます。

:::note alert
サンプルを実行するとPublicにIgniteの生成物が出力されます。こちらは次のTipsで理由をお話しします。
:::

また、FileMiddlewareのdefaultFileをindex.htmlにしてあげることで、ルートパスにアクセスした際に自動的にindex.htmlを参照するようにできます。

続いてroutes.swiftを修正してWebサイトのルーティングを設定します。
```Swift
func routes(_ app: Application) throws {
    app.get { req async in
        req.fileio.streamFile(at: "\(req.application.directory.publicDirectory)")
    }
...
```
このコードは、HTTP GETリクエストが来た際に、アプリケーションのパブリックディレクトリ内のファイルをストリーミングしてレスポンスとして返すように設定されています。
```
req.fileio.streamFile(at:)
```

は、ファイルをストリーミングするためのメソッドです。このメソッドは、指定されたファイルの内容をストリーム化し、レスポンスとして返却します。
```
\(req.application.directory.publicDirectory)
```
は、ファイルのパスを指定しています。req.application.directory.publicDirectoryは、Vaporアプリケーションのパブリックディレクトリへのパスを表します。
つまり、このルートは、Vaporアプリケーションのパブリックディレクトリ内のファイルをストリーミングするように設定されています。

続いて、ルート配下のパスにアクセスした際のレスポンスを設定します。
```Swift
    ...
    app.get("**") { req -> EventLoopFuture<Response> in
        // パスを取得
        guard let path = req.url.path.removingPercentEncoding?.replacingOccurrences(of: "/", with: "") else {
            throw Abort(.internalServerError)
        }
        
        // パスにindex.htmlを追加
        let indexPath = path + "/index.html"
        
        // index.htmlのファイルパスを作成
        let publicDirectory = app.directory.publicDirectory
        let indexPathInPublic = publicDirectory + indexPath
        
        // ファイルが存在するか確認
        guard FileManager.default.fileExists(atPath: indexPathInPublic) else {
            throw Abort(.notFound)
        }
        
        // index.htmlを返す
        return req.fileio.streamFile(at: indexPathInPublic)
            .encodeResponse(for: req)
    }
```
まず、app.get("**")は、どのようなパスにもマッチすることを意味します。つまり、どんなパスが来てもこのルートが適用されます。

次に、このルートの処理では、リクエストのパスを取得し、それに"/index.html"を追加して、index.htmlファイルのパスを作成します。

その後、そのファイルが存在するかどうかを確認します。ファイルが存在しない場合は、HTTPステータスコード404（Not Found）を返します。

ファイルが存在する場合は、そのファイルをストリーミングしてレスポンスとして返します。

## Igniteの設定
先程はVaporの公開ディレクトリの場所をIgniteの生成ディレクトリに変更することでサイトの表示を実現しました。
逆にIgniteの生成ディレクトリをVaporの公開ディレクトリである**Public**変更できたらもっと楽ではないでしょうか？
しかし、Igniteにはその機能はありませんでした。
そこで、Igniteの生成ディレクトリを変更できる機能を実装し、PRを出してマージしてもらいました。🎉
![スクリーンショット 2024-04-21 19.01.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/457333/16ccf3c3-74ad-4559-f774-866bd0b566ac.png)
そのため、現在はWebサイトを生成するメソッドに**buildDirectoryPath**の引数に任意のディレクトリを指定することで、生成物の出力先を変更できます。
```Swift
// Site.swift
try site.publish(buildDirectoryPath: "Public")
```
これによってサンプルディレクトリは実行時にPublicディレクトリに生成物が出来上がったわけです。

:::note info
サイトにアクセスした際のルート設定はVaporでする必要があります。
:::

# おわりに
ここまででVaporとIgniteを組み合わせてWebサイトを構築し、ローカルサーバーを起動するところまで完了しました。
次回はこのプロジェクトを Google Cloud Run を用いてデプロイする取り組みについて紹介したいと思います。