Publish

Swift Package Manager Mac + Linux Twitter: @johnsundell

欢迎使用 Publish,这是一个专为 Swift 开发者构建的静态站点生成器。它使整个网站能够使用 Swift 构建,并支持主题、插件和大量其他强大的自定义选项。

Publish 用于构建 swiftbysundell.com 的所有内容。

将网站作为 Swift 包

当使用 Publish 时,每个网站都被定义为一个 Swift 包,它充当关于如何生成和部署网站的配置——全部使用原生的、类型安全的 Swift 代码。例如,以下是一个美食食谱网站的配置示例

struct DeliciousRecipes: Website {
    enum SectionID: String, WebsiteSectionID {
        case recipes
        case links
        case about
    }

    struct ItemMetadata: WebsiteItemMetadata {
        var ingredients: [String]
        var preparationTime: TimeInterval
    }

    var url = URL(string: "https://cooking-with-john.com")!
    var name = "Delicious Recipes"
    var description = "Many very delicious recipes."
    var language: Language { .english }
    var imagePath: Path? { "images/logo.png" }
}

每个使用 Publish 构建的网站都可以自由决定它想要支持的部分和元数据的种类。在上面,我们添加了三个部分——食谱链接关于——它们可以包含任意数量的项目。我们还通过 ItemMetadata 类型添加了对我们自己的、特定于站点的项目元数据的支持,我们将在整个发布过程中以完全类型安全的方式使用它。

从简单开始,并在需要时自定义

虽然 Publish 提供了非常强大的 API,几乎可以自定义和调整网站生成过程的每个方面,但它也附带了一套便捷的 API,旨在使入门尽可能快速和容易。

要开始生成我们上面定义的美味食谱网站,我们只需要一行代码,告诉 Publish 使用哪个主题来生成我们网站的 HTML

try DeliciousRecipes().publish(withTheme: .foundation)

上面的调用不仅渲染了我们网站的 HTML,还生成了 RSS 订阅源、站点地图等等。

上面我们使用了 Publish 内置的 Foundation 主题,这是一个非常基础的主题,主要作为起点提供,以及作为如何构建 Publish 主题的示例。我们当然可以随时用我们自己的自定义主题替换该主题,其中可以包含我们想要的任何类型的 HTML 和资源。

默认情况下,Publish 将基于放置在项目 Content 文件夹中的 Markdown 文件生成网站内容,但也可以以编程方式添加任意数量的内容项和自定义页面。

Publish 支持三种类型的内容

部分,根据每个网站的 SectionID 枚举的成员创建。每个部分都有自己的 HTML 页面,并且还可以充当 项目 列表的容器,这些项目代表该部分内的嵌套 HTML 页面。最后,页面 提供了一种构建自定义自由格式页面的方法,这些页面可以放置到任何类型的文件夹层次结构中。

每个 SectionItemPage 都可以定义自己的一组内容——内容范围可以从文本(如标题和描述)到 HTML、音频、视频和各种类型的元数据。

以下是我们如何扩展之前的基本 publish() 调用以注入我们自己的自定义发布管道——这使我们能够定义新项目、修改部分等等

try DeliciousRecipes().publish(
    withTheme: .foundation,
    additionalSteps: [
        // Add an item programmatically
        .addItem(Item(
            path: "my-favorite-recipe",
            sectionID: .recipes,
            metadata: DeliciousRecipes.ItemMetadata(
                ingredients: ["Chocolate", "Coffee", "Flour"],
                preparationTime: 10 * 60
            ),
            tags: ["favorite", "featured"],
            content: Content(
                title: "Check out my favorite recipe!"
            )
        )),
        // Add default titles to all sections
        .step(named: "Default section titles") { context in
            context.mutateAllSections { section in
                guard section.title.isEmpty else { return }
                
                switch section.id {
                case .recipes:
                    section.title = "My recipes"
                case .links:
                    section.title = "External links"
                case .about:
                    section.title = "About this site"
                }
            }
        }
    ]
)

当然,在一个地方定义程序的所有代码很少是一个好主意,因此建议将网站的各种生成操作拆分为明确分离的步骤——可以通过使用静态属性或方法扩展 PublishingStep 类型来定义这些步骤,如下所示

extension PublishingStep where Site == DeliciousRecipes {
    static func addDefaultSectionTitles() -> Self {
        .step(named: "Default section titles") { context in
            context.mutateAllSections { section in
                guard section.title.isEmpty else { return }

                switch section.id {
                case .recipes:
                    section.title = "My recipes"
                case .links:
                    section.title = "External links"
                case .about:
                    section.title = "About this site"
                }
            }
        }
    }
}

每个发布步骤都传递一个 PublishingContext 实例,它可以用来改变网站正在发布的当前上下文——包括其文件、文件夹和内容。

使用上述模式,我们可以实现任意数量的自定义发布步骤,这些步骤将与 Publish 附带的所有默认步骤完美契合。这使我们能够构建真正强大的管道,其中每个步骤都执行生成过程的单个部分

try DeliciousRecipes().publish(using: [
    .addMarkdownFiles(),
    .copyResources(),
    .addFavoriteItems(),
    .addDefaultSectionTitles(),
    .generateHTML(withTheme: .delicious),
    .generateRSSFeed(including: [.recipes]),
    .generateSiteMap()
])

上面我们通过调用 publish(using:) API 构建了一个完全自定义的发布管道。

要了解有关 Publish 内置发布步骤的更多信息,请查看此文件

构建 HTML 主题

Publish 使用 Plot 作为其 HTML 主题引擎,这使得可以使用 Swift 定义整个 HTML 页面。当使用 Publish 时,建议您构建自己特定于网站的主题——它可以充分利用您自己的自定义元数据,并完全定制以适应您网站的设计。

主题使用 Theme 类型定义,该类型使用 HTMLFactory 实现来创建网站的所有 HTML 页面。以下是上面使用的自定义 .delicious 主题的实现摘录

extension Theme where Site == DeliciousRecipes {
    static var delicious: Self {
        Theme(htmlFactory: DeliciousHTMLFactory())
    }

    private struct DeliciousHTMLFactory: HTMLFactory {
        ...
        func makeItemHTML(
            for item: Item<DeliciousRecipes>,
            context: PublishingContext<DeliciousRecipes>
        ) throws -> HTML {
            HTML(
                .head(for: item, on: context.site),
                .body(
                    .ul(
                        .class("ingredients"),
                        .forEach(item.metadata.ingredients) {
                            .li(.text($0))
                        }
                    ),
                    .p(
                        "This will take around ",
                        "\(Int(item.metadata.preparationTime / 60)) ",
                        "minutes to prepare"
                    ),
                    .contentBody(item.body)
                )
            )
        }
        ...
    }
}

在上面,我们能够访问内置的项目属性,以及我们之前在网站的 ItemMetadata 结构中定义的自定义元数据属性,所有这些都以保留完全类型安全的方式进行。

关于如何构建 Publish 主题的更全面的文档,以及一些推荐的最佳实践,将很快添加。

构建插件

Publish 还支持插件,插件可用于在各种项目之间共享设置代码,或以各种方式扩展 Publish 的内置功能。就像发布步骤一样,插件通过修改当前的 PublishingContext 来执行其工作——例如,通过添加文件或文件夹,通过修改网站的现有内容,或通过添加 Markdown 解析修饰符。

以下是一个插件示例,该插件确保网站的所有项目都具有标签

extension Plugin {
    static var ensureAllItemsAreTagged: Self {
        Plugin(name: "Ensure that all items are tagged") { context in
            let allItems = context.sections.flatMap { $0.items }

            for item in allItems {
                guard !item.tags.isEmpty else {
                    throw PublishingError(
                        path: item.path,
                        infoMessage: "Item has no tags"
                    )
                }
            }
        }
    }
}

然后,通过将 installPlugin 步骤添加到任何发布管道来安装插件

try DeliciousRecipes().publish(using: [
    ...
    .installPlugin(.ensureAllItemsAreTagged)
])

如果您的插件托管在 GitHub 上,您可以使用 publish-plugin 主题,以便可以在 社区插件 的其余部分中找到它。

有关 Publish 插件的真实示例,请查看 官方 Splash 插件,它使将 Splash 语法高亮器 与 Publish 集成变得非常容易。

系统要求

为了能够成功使用 Publish,请确保您的系统已安装 Swift 5.4 版本(或更高版本)。如果您使用的是 Mac,还要确保 xcode-select 指向包含所需 Swift 版本的 Xcode 安装,并且您正在运行 macOS Big Sur (11.0) 或更高版本。

请注意,Publish 官方支持任何形式的 beta 软件,包括 Xcode 和 macOS 的 beta 版本,或未发布的 Swift 版本。

安装

Publish 使用 Swift Package Manager 分发。要将其安装到项目中,请将其作为依赖项添加到您的 Package.swift 清单中

let package = Package(
    ...
    dependencies: [
        .package(url: "https://github.com/johnsundell/publish.git", from: "0.1.0")
    ],
    ...
)

然后在您想要使用它的任何地方导入 Publish

import Publish

有关如何使用 Swift Package Manager 的更多信息,请查看 这篇文章,或 其官方文档

Publish 还附带一个命令行工具,可以轻松设置新的网站项目,以及生成和部署现有的网站项目。要安装该命令行工具,只需在 Publish 仓库的本地副本中运行 make

$ git clone https://github.com/JohnSundell/Publish.git
$ cd Publish
$ make

然后运行 publish help 以获取有关如何使用它的说明。

Publish 命令行工具也可通过 Homebrew 获得,如果您安装了 Homebrew,可以使用以下命令安装

brew install publish

但是,请注意 Homebrew 支持并非由 John Sundell 官方维护,因此您在使用 Homebrew 时可能会安装旧版本的 Publish 命令行工具。如上所述,使用 make 是安装 Publish 命令行工具的首选方法。

运行和部署

由于所有 Publish 网站都作为 Swift 包实现,因此只需在 Xcode 中打开网站的包(通过打开其 Package.swift 文件),然后使用 Product > Run 命令(或 ⌘+R)运行它即可生成它们。

Publish 还可以通过其 DeploymentMethod API 促进网站到外部服务器的部署,并附带用于基于 Git 和 GitHub 部署的内置实现。要为网站定义部署方法,请将 deploy 步骤添加到您的发布管道

try DeliciousRecipes().publish(using: [
    ...
    .deploy(using: .gitHub("johnsundell/delicious-recipes"))
])

即使添加到管道中,部署步骤默认情况下也是禁用的,并且仅当传递 --deploy 命令行标志时才执行(可以通过 Xcode 的 Product > Scheme > Edit Scheme... 菜单添加),或者通过使用 publish deploy 运行命令行工具。

Publish 还可以通过使用 publish run 命令启动 localhost Web 服务器以进行本地测试和开发。要在服务器运行时重新生成站点内容,请在 Xcode 中对您站点的包使用 Product > Run。

快速入门

要快速开始使用 Publish,请首先克隆此仓库以安装命令行工具,然后在克隆的文件夹中运行 make

$ git clone https://github.com/JohnSundell/Publish.git
$ cd Publish
$ make

注意:如果在运行 make 时遇到错误,请确保您已从 Xcode 的偏好设置中设置了命令行工具位置。它位于“Preferences” > “Locations” > “Locations” > “Command Line Tools”中。如果尚未设置,则下拉列表将为空白。

然后,为您的新网站项目创建一个新文件夹,只需在其内运行 publish new 即可开始。

$ mkdir MyWebsite
$ cd MyWebsite
$ publish new

最后,运行 open Package.swift 以在 Xcode 中打开项目,开始构建您的新网站。

其他文档

您可以在 Documentation 文件夹 中找到关于 Publish 的各种功能和特性的不断增长的附加文档集合。

设计和目标

Publish 最初也是最重要的是被设计为一个强大且高度可定制的工具,用于在 Swift 中构建静态网站——从 Swift by Sundell 开始,这是一个拥有超过 300 个独立页面和包含超过 25 个发布步骤的管道的网站。

虽然目标绝对也是使 Publish 尽可能易于访问和使用,但它很可能仍然是一个相当底层的工具,它倾向于代码级别的控制而不是文件系统配置文件,以及可定制性而不是严格遵守的约定。

该设计的主要权衡是 Publish 可能会比大多数其他静态站点生成器具有更陡峭的学习曲线,但希望它也能因此提供更大的 power、灵活性和类型安全性。随着时间的推移,并在社区的帮助下,我们应该能够使学习曲线变得不那么陡峭——通过更全面的文档和示例,以及通过共享工具和便捷 API。

Publish 在设计时也考虑了代码重用,并且希望社区随着时间的推移会开发出更多的主题、工具、插件和其他扩展。

贡献和支持

Publish 是完全开放开发的,非常欢迎您的贡献。

在您开始在任何项目中使用 Publish 之前,强烈建议您花几分钟时间熟悉其文档和内部实现,以便您准备好解决可能遇到的任何问题或边缘情况。

由于这是一个非常年轻的项目,因此很可能存在许多限制和缺少的功能,只有当更多人开始使用它时才能发现和解决这些问题。虽然 Publish 在生产中使用以构建 Swift by Sundell 的所有内容,但建议您首先针对您的特定用例试用它,以确保它支持您需要的功能。

本项目不提供基于 GitHub Issues 的支持,而是鼓励用户积极参与其持续开发——通过修复他们遇到的任何错误,或通过改进发现不足的文档。

如果您希望进行更改,请打开一个拉取请求——即使它仅包含您计划进行的更改草案,或重现问题的测试——我们可以在此基础上进一步讨论。

希望您喜欢使用 Publish!