静的サイトジェネレータ「Publish」のセットアップ&使い方(Swift)

「PublishでWebサイトを構築する」は6部構成です。
記事を順番に読み進めると、PublishでWebサイトが構築できるようになります。

第1部: セットアップ&使い方　__←イマココ__
第2部: [カスタムテーマの作成](https://qiita.com/uhooi/items/111d4439273775586149)
第3部: [プラグインのインストール](https://qiita.com/uhooi/items/bf0ff67d687b53a11c5b)
第4部: プラグインの作成（未投稿）
第5部: [ツイートボタンの表示](https://qiita.com/uhooi/items/9aa046f0a42e5afbd7c3)
第6部: [はてなブックマークボタンの表示](https://qiita.com/uhooi/items/40cb71ac3aca57853544)

## 「Publish」とは？

Swift製の静的サイトジェネレータです。
SwiftとMarkdownファイルからHTMLやCSSを生成し、GitHub Pagesなどへデプロイできます。

## 環境

- OS：macOS Monterey 12.2.1
- Xcode：13.3 (13E113)
- Swift：5.6
- Publish：0.8.0

## セットアップ

### CLIツールのインストール

Publishで静的サイトを生成するには、CLIツールとライブラリの両方を使います。
どちらも1つのパッケージで管理されています。

```swift:PublishのPackage.swift
let package = Package(
    // ...
    products: [
        .library(name: "Publish", targets: ["Publish"]),
        .executable(name: "publish-cli", targets: ["PublishCLI"])
    ],
    // ...
)
```

引用：https://github.com/JohnSundell/Publish/blob/9e1b20a23c73ba078966a4fcce23146088d0de63/Package.swift#L14-L15

私はCLIツールとライブラリでバージョンを揃えたい、かつどちらもSwiftPMで管理したいため、まず以下のような `Package.swift` を用意しました。

後述しますが、あとで捨てるので丁寧に作る必要はありません。

```swift:Package.swift   
// swift-tools-version:5.6

import PackageDescription

let package = Package(
    name: "Tools",
    platforms: [
        .macOS(.v12),
    ],
    dependencies: [
        .package(url: "https://github.com/johnsundell/publish.git", from: "0.8.0"),
    ],
    targets: [.target(name: "Tools", path: "")]
)
```

`Package.swift` を作成したら、PublishのCLIツールをリリースビルドします。

```shell-session
$ swift build -c release --product publish-cli
```

グローバル環境を汚したくないため、パスを通さずに使うことにします。
リリースビルドしたCLIツールは `.build/release/` フォルダに生成されます。

```shell-session
$ .build/release/publish-cli help
Publish Command Line Interface
------------------------------
Interact with the Publish static site generator from
the command line, to create new websites, or to generate
and deploy existing ones.

Available commands:

- new: Set up a new website in the current folder.
- generate: Generate the website in the current folder.
- run: Generate and run a localhost server on default port 8000
       for the website in the current folder. Use the "-p"
       or "--port" option for customizing the default port.
- deploy: Generate and deploy the website in the current
       folder, according to its deployment method.
```

`publish-cli help` を実行して、`new` `generate` `run` `deploy` と、コマンドが4つあることがわかります。

これでインストールは完了です。

### Webサイトの基盤生成

`publish-cli new` を実行し、Webサイトの基盤を生成します。

`Package.swift` などが残っていると生成できないので、削除します。

```shell-session:❌ファイルが残っていると生成できない
$ .build/release/publish-cli new
❌ New projects can only be generated in empty folders.
```

```shell-session:✅フォルダが空だと生成できる
$ rm -f Package.swift Package.resolved
$ .build/release/publish-cli new
✅ Generated website project for 'WebsiteSample'
Run 'open Package.swift' to open it and start building
```

成功すると `Package.swift` やソースなどが生成されます。（すごい！）
`tree` コマンドだと見えないですが `.gitignore` も生成されています。

```shell-session
$ tree
.
├── Content
│   ├── index.md
│   └── posts
│       ├── first-post.md
│       └── index.md
├── Package.resolved
├── Package.swift
├── Resources
└── Sources
    └── WebsiteSample
        └── main.swift

5 directories, 6 files
```

プロジェクト名は現在のフォルダ名をアッパーキャメルケースに変換した文字列が使われます。
私は `website-sample` フォルダで実行したため、プロジェクト名が `WebsiteSample` になりました。
必要に応じてパッケージ名などを変更してください。

指示通り `open Package.swift` を実行し、生成されたパッケージをXcodeで開いて開発します。

### プロジェクトの更新

Publish 0.8.0現在、生成されるパッケージのswift-tools-versionが古かったりするので、更新します。

```diff_swift:Package.swift
- // swift-tools-version:5.2
+ // swift-tools-version:5.6

import PackageDescription

let package = Package(
    name: "WebsiteSample",
    products: [
        .executable(
            name: "WebsiteSample",
            targets: ["WebsiteSample"]
        )
    ],
    dependencies: [
-        .package(name: "Publish", url: "https://github.com/johnsundell/publish.git", from: "0.7.0")
+        .package(url: "https://github.com/johnsundell/publish.git", from: "0.8.0")
    ],
    targets: [
-        .target(
+        .executableTarget(
            name: "WebsiteSample",
-            dependencies: ["Publish"]
+            dependencies: [
+                .product(name: "Publish", package: "publish")
+            ]
        )
    ]
)
```

成果物（HTMLなど）が生成される `Output` フォルダが `.gitignore` に記述されていないため、成果物をGit管理しない場合は追記します。

```diff:.gitignore
+ Output
```

実装によりますが、成果物の生成時に `Resources` フォルダが存在しないとエラーが発生するため、フォルダ内に `.gitkeep` を追加しておくといいです。

```shell-session:❌Resourcesフォルダがないとエラーになることがある
$ .build/release/publish-cli generate
[1/1] Planning build
Building for debugging...
[1/3] Emitting module WebsiteSample
[2/3] Compiling WebsiteSample main.swift
[3/3] Linking WebsiteSample
Build complete! (0.51s)
Swift/ErrorType.swift:200: Fatal error: Error raised at top level: Publish encountered an error:
[step] Copy 'Resources' files
[path] Resources
[info] Folder not found
/bin/bash: line 1: 77958 Trace/BPT trap: 5       swift run
❌ ShellOut encountered an error
Status code: 133
Message: "[1/1] Planning build
Building for debugging...
[1/3] Emitting module WebsiteSample
[2/3] Compiling WebsiteSample main.swift
[3/3] Linking WebsiteSample
Build complete! (0.51s)
Swift/ErrorType.swift:200: Fatal error: Error raised at top level: Publish encountered an error:
[step] Copy 'Resources' files
[path] Resources
[info] Folder not found
/bin/bash: line 1: 77958 Trace/BPT trap: 5       swift run"
Output: ""
make: *** [generate] Error 1
```

`.copyResources()` を実行しなければエラーは発生しないので、実装でも防げます。

```diff_swift:✅.copyResources()を実行しなければResourcesフォルダがなくてもエラーにならない
try WebsiteSample().publish(using: [
    .addMarkdownFiles(),
-   .copyResources(),
    .generateHTML(withTheme: .foundation),
    .generateRSSFeed(including: [.posts]),
    .generateSiteMap(),
    .deploy(using: .gitHub("uhooi/uhooi.github.io", useSSH: false))
])
```

これらの更新はすべてPublishにPRを作成したので、マージされれば不要になります。

- https://github.com/JohnSundell/Publish/pull/126
- https://github.com/JohnSundell/Publish/pull/127
- https://github.com/JohnSundell/Publish/pull/128
- https://github.com/JohnSundell/Publish/pull/129

### publish-cliコマンドのラップ

Webサイトの動作確認のため、 `publish-cli run` を頻繁に使います。
毎回 `.build/release/publish-cli` と入力するのは手間なので、 `Makefile` に隠蔽すると便利です。

```makefile:Makefile
PUBLISH_CLI_PATH := ./.build/release/publish-cli

.PHONY: setup
setup:
	$(MAKE) build-cli-tool

.PHONY: build-cli-tool
build-cli-tool:
	swift build -c release --product publish-cli

.PHONY: help
help:
	${PUBLISH_CLI_PATH} help

.PHONY: run
run:
	${PUBLISH_CLI_PATH} run

.PHONY: generate
generate:
	${PUBLISH_CLI_PATH} generate
```

これで `make run` や `make generate` と実行するだけで `publish-cli` を使えます。
私は `new` は最初の1回のみ、 `deploy` は使わないため、 `Makefile` に定義していません。

ついでに `make setup` でCLIツールをビルドできるようにしています。
ローカルやCIで開発環境を構築する際に便利です。

## 使い方

### Webサイトのプレビュー

`publish-cli run` を実行すると、Webサイトのプレビューを見れます。

```shell-session
$ make run
./.build/release/publish-cli run
Building for debugging...
Build complete! (0.10s)
Publishing MyWebsite (6 steps)
[1/6] Copy 'Resources' files
[2/6] Add Markdown files from 'Content' folder
[3/6] Sort items
[4/6] Generate HTML
[5/6] Generate RSS feed
[6/6] Generate site map
✅ Successfully published WebsiteSample
🌍 Starting web server at http://localhost:8000

Press ENTER to stop the server and exit

```

Enterキーを押下するまでローカルでWebサイトを参照できます。
http://localhost:8000

![スクリーンショット 2022-03-28 13.39.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138245/d8bdfc62-93e7-e8e5-0418-5c07f8b55272.png)

Webサイトが表示されました。
デフォルトのテーマ（ `foundation` ）がシンプルで、なんとなくAppleみを感じて好きです。

### 成果物の生成

`publish-cli generate` を実行すると、 `Output` フォルダに成果物が生成されます。

```shell-session
$ make generate 
./.build/release/publish-cli generate
Building for debugging...
Build complete! (0.09s)
Publishing WebsiteSample (5 steps)
[1/5] Add Markdown files from 'Content' folder
[2/5] Copy 'Resources' files
[3/5] Generate HTML
[4/5] Generate RSS feed
[5/5] Generate site map
✅ Successfully published WebsiteSample
```

HTMLやCSS、RSSなどが生成されていることがわかります。

```shell-session
$ tree Output/
Output/
├── feed.rss
├── index.html
├── posts
│   ├── first-post
│   │   └── index.html
│   └── index.html
├── sitemap.xml
├── styles.css
└── tags
    ├── article
    │   └── index.html
    ├── first
    │   └── index.html
    └── index.html

5 directories, 9 files
```

`publish-cli run` でも生成されますが、ターミナルを止めてしまうため、CIで使えません。
成果物を生成するのみなら `publish-cli generate` を使うべきです。

### Webサイトのデプロイ

`publish-cli deploy` を実行すると、Webサイトをデプロイします。

ただGitHub Actionsでデプロイを自動化したらエラーが発生しました。
おそらく認証周りが原因ですが、解決できませんでした。

そのため私は `publish-cli deploy` を使わず `publish-cli generate` で成果物を生成し、それをサードパーティ製のアクションでGitHub Pagesへデプロイしています。

以下がワークフローの例です。

```yaml:deploy.yml
name: Deploy

on:
  push:
    branches: [ main ]

jobs:
  deploy-github-pages:
    runs-on: ubuntu-latest
    container: "swift:5.6"
    steps:
    - uses: actions/checkout@v3

    - name: Setup
      run: make setup

    - name: Generate the website
      run: make generate

    - name: Deploy to GitHub Pages
      uses: peaceiris/actions-gh-pages@v3
      with:
        personal_token: ${{ secrets.PERSONAL_TOKEN }}
        external_repository: uhooi/uhooi.github.io
        publish_branch: master
        publish_dir: ./Output
```

`peaceiris/actions-gh-pages` アクションの詳細は、GitHubのREADMEをご参照ください。
https://github.com/peaceiris/actions-gh-pages

## 実装

### メインファイルの更新

基本的には `Sources/{パッケージ名}/main.swift` の1ファイルのみ更新します。

GitHub Pagesへデプロイする場合は、以下のように変更します。
GitHub PagesのURLは `http` だとエラーになるので `https` で指定します。

```diff_swift:main.swift
struct WebsiteSample: Website {
    enum SectionID: String, WebsiteSectionID {
        case posts
    }

    struct ItemMetadata: WebsiteItemMetadata {
    }

-   var url = URL(string: "https://your-website-url.com")!
+   var url = URL(string: "https://uhooi.github.io")!
    var name = "WebsiteSample"
    var description = "A description of WebsiteSample"
    var language: Language { .english }
    var imagePath: Path? { nil }
}

- try WebsiteSample().publish(withTheme: .foundation)
+ try WebsiteSample().publish(using: [
+   .addMarkdownFiles(),
+   .copyResources(),
+   .generateHTML(withTheme: .foundation),
+   .generateRSSFeed(including: [.posts]),
+   .generateSiteMap(),
+   .deploy(using: .gitHub("uhooi/uhooi.github.io", useSSH: false))
+ ])
```

`.deploy(using:useSSH:)` は `branch:` も指定できるのですが、エラーになる不具合があるため、デフォルト（ `master` ）のブランチにしかプッシュできません。
参考：https://github.com/JohnSundell/Publish/pull/116

`useSSH:` は `true` が推奨ですが、CIでの設定がわからないため `false` にしてしまっています。
私は `publish-cli deploy` を使っていないので、おそらく `false` でも問題ありません。

必要に応じて名前や概要、言語なども変更します。

```diff_swift:main.swift
struct WebsiteSample: Website {
    enum SectionID: String, WebsiteSectionID {
        case posts
    }

    struct ItemMetadata: WebsiteItemMetadata {
    }

    var url = URL(string: "https://uhooi.github.io")!
-   var name = "WebsiteSample"
-   var description = "A description of WebsiteSample"
-   var language: Language { .english }
+   var name = "THE Uhooi"
+   var description = "Uhooi's Website"
+   var language: Language { .japanese }
    var imagePath: Path? { nil }
}
```

`foundation` テーマでは、名前や概要は以下に反映されます。

![スクリーンショット_2022-03-28_16_34_57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138245/95512745-18d0-723c-8286-f81324a488bf.png)

### 記事の追加や更新

記事は `.md` ファイルにMarkdown記法で書きます。
デフォルトでは `Content/posts` の中に `.md` ファイルを格納して `run` や `generate` するだけで記事がHTMLで生成されます。（簡単！）

記事の形式は以下の通りです。

```markdown:first-post.md
---
date: 2022-03-28 12:40
description: A description of my first post.
tags: first, article
---
# My first post

My first post's text.

...
```

ヘッダーに `date` `description` `tags` を付けます。
`date` は `YYYY-MM-DD hh:mm` フォーマット、 `description` は文字列、 `tags` はカンマ区切りの文字列で記述します。

ヘッダー以下が本文になります。
Markdown記法を使って記述します。

例えば以下のMarkdownをもとにHTMLを生成します。

````markdown:first-post.md
---
date: 2022-03-28 12:40
description: A description of my first post.
tags: first, article
---
# My first post

My first post's text.

## foo

foo.

### Swift

```swift
let foo = Foo()
```

```diff
- foo
+ bar
```

## bar

`bar` is code span.
_bar_ is italic.
__bar__ is bold.

- foo
- bar
  - test
  - test2

https://uhooi.github.io/

[https://uhooi.github.io/](https://uhooi.github.io/)
````

`foundation` テーマだとこのように変換されます。

![スクリーンショット 2022-03-29 10.59.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138245/c97b3052-2208-24af-cd42-f440953bbd05.png)

いい感じですが、デフォルトのテーマなので、いくつか気になる箇所もあります。

- コードがシンタックスハイライトされない
- コードスパンとコードブロックの文字サイズが小さい
- 箇条書きがインデントされない
- コードブロックと箇条書きの前後にマージンがない
- URLがリンクにならない

これらはCSS修正のPRを作成するのもありだと思いますが、「プラグイン」という仕組みで解決できる可能性があります。
プラグインについてはまだ勉強不足であり、本記事とは別のタイミングで紹介したいです。

## おわりに

これでSwiftとMarkdownを使ってWebサイトを構築できました！
ぜひみなさんも試してみてください :relaxed:

## 参考リンク

- [JohnSundell/Publish: A static site generator for Swift developers](https://github.com/JohnSundell/Publish)
- [Swiftでブログを作ってGitHub Pagesにデプロイしてみる | DevelopersIO](https://dev.classmethod.jp/articles/generate-static-site-in_swift/)