June 30, 2020

Простой блог с помощью Swift и Publish

Раньше мой сайт работал с помощью Jekyll и бесплатно хостился на GitHub. В принципе меня это вполне устраивало. Но некоторое время назад я наткнулся на классную утилиту от John Sundell под названием Publish. Это аналог Jekyll и является таким же генератором статических вебстраниц, но написанным на Swift. И в этом посте я постарался описать процесс от создания сайта до деплоя на GitHub Pages.

Итак мои требования к будущему сайту:

  1. Markdown разметка
  2. Блог с тэгами
  3. Подсветка кода, в первую очередь Swift
  4. Бесплатный хостинг на GitHub Pages

Как устроен Publish?

Сайт в Publish представляет собой отдельный Swift Package. Поэтому в качестве IDE придётся использовать знакомый iOS-разработчикам Xcode.

Publish тянет за собой несколько зависимостей. Вот наболее важные из них:

  • Ink — парсер markdown разметки
  • Plot — DSL на Swift для HTML
  • ShellOut — Вызов команд Shell из Swift

Создаем сайт из шаблона

Качаем утилиту с GitHub и устанавливаем.

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

Создаем папку для будущего сайта, генерируем Swift Package и открываем в Xcode.

$ mkdir Blog
$ cd Blog
$ publish new
$ open Package.swift

Структура Swift Package в Xcode выглядит следующим образом:

  • Content (все markdown странички)
  • Output (сгенерированные HTML и ресурсы),
  • Resourses (картинки, видео, аудио и стили)
  • Sources (исходники на Swift)

Xcode

Для генерации сайта есть схема в Xcode.

Чтобы посмотреть локально сгенерированный сайт, нужно воспользоваться командой run в директории проекта.

$ publish run

Вот так выглядит сайт сразу после создания. 👾

Blog

Настраиваем deploy в GitHub Pages

Создаем репозиторий на GitHub и называем его в определённом формате {login}.github.io. Если вы хотите использовать свой домен, то название репозитория может быть любым.

GitHub creating repo

Возвращаемся в Xcode. Теперь нам надо научить Publish деплоить сгенерированные странички в созданный репозиторий.

Для взаимодействия Publish с GitHub у вас заранее должны быть настроены deploy keys. Инструкция о том, как это сделать, есть тут.

В Publish есть структура DeploymentMethod, которая как раз отвечает за деплой сайта. Нам нужно сконфигурировать деплой. Для этого в файле main.swift делаем такие изменения.

struct Blog: Website {
    enum SectionID: String, WebsiteSectionID {
        case posts
    }

    struct ItemMetadata: WebsiteItemMetadata {
        // Add any site-specific metadata that you want to use here.
    }

    var url = URL(string: "https://bestk1ngArthur.github.io")!
    var name = "bestk1ngArthur"
    var description = "My name is Artem, I am an iOS developer and you are on my webpage"
    var language: Language { .russian }
    var imagePath: Path? { nil }
}

try Blog().publish(using: [
    .addMarkdownFiles(),
    .copyResources(),
    .generateHTML(withTheme: .foundation),
    .generateSiteMap(),
    .deploy(using: .git("https://github.com/bestK1ngArthur/bestk1ngArthur.github.io.git"))
])

Теперь при вызове deploy файлы из Output будут заливаться в репозиторий в ветку master.

$ publish deploy

Возвращаемся в GitHub и в настройках репозитория включаем Pages, чтобы после пуша сайта в ветку master автоматически запускался деплой GitHub Pages.

GitHub enabling pages

Ура! Сайт уже должен появиться на домене {login}.github.io. 🚀


Подключаем свой домен

В настройках репозитория в разделе GitHub Pages задаем свой домен. Не забываем сменить DNS для домена на гитхабовские. Как это сделать написано вот тут.

Теперь у нас в репозиторий добавился файл CNAME.

Возвращаемся в Xcode. При стандатном деплое с помощью .git каждый раз происходит очистка папки репозитория. В том числе и удаляется файл CNAME. Нам нужно это исправить. Для этого создаем свой собственный DeploymentMethod.

extension DeploymentMethod {
    static func gitHubPages(_ remote: String) -> Self {
        DeploymentMethod(name: "GitHub Pages (\(remote))") { context in
            let folder = try context.createDeploymentFolder(withPrefix: "Git") { folder in
                if !folder.containsSubfolder(named: ".git") {
                    try shellOut(to: .gitInit(), at: folder.path)

                    try shellOut(
                        to: "git remote add origin \(remote)",
                        at: folder.path
                    )
                }

                try shellOut(
                    to: "git remote set-url origin \(remote)",
                    at: folder.path
                )

                _ = try? shellOut(
                    to: .gitPull(remote: "origin", branch: "master"),
                    at: folder.path
                )

                try folder.empty()
                
                try shellOut(
                    to: "echo \(context.site.url.absoluteString) > CNAME",
                    at: folder.path
                )
            }

            do {
                try shellOut(
                    to: """
                    git add . && git commit -a -m \"🚀 Publish deploy" --allow-empty
                    """,
                    at: folder.path
                )

                try shellOut(
                    to: .gitPush(remote: "origin", branch: "master"),
                    at: folder.path
                )
            } catch let error as ShellOutError {
                throw PublishingError(infoMessage: error.message)
            } catch {
                throw error
            }
        }
    }
}

Я оставил очистку папки, чтобы не возникало мусора и артефактов. Но после очистки добавил команду для создания файла CNAME.

Меняем в файле main.swift метод деплоя на созданный и указываем в конфиге блога новый URL.

struct Blog: Website {
    enum SectionID: String, WebsiteSectionID {
        case posts
    }

    struct ItemMetadata: WebsiteItemMetadata {
        // Add any site-specific metadata that you want to use here.
    }

    var url = URL(string: "https://bestk1ng.ru")!
    var name = "bestk1ngArthur"
    var description = "My name is Artem, I am an iOS developer and you are on my webpage"
    var language: Language { .russian }
    var imagePath: Path? { nil }
}

try Blog().publish(using: [
    .addMarkdownFiles(),
    .copyResources(),
    .generateHTML(withTheme: .foundation),
    .generateSiteMap(),
    .deploy(using: .gitHubPages("https://github.com/bestK1ngArthur/bestk1ngArthur.github.io.git"))
])

Теперь деплой сайта происходит на кастомный домен. 🎉


Делаем подсветку синтаксиса Swift

В Publish есть поддержка плагинов. Чтобы добавить подсветку синтаксиса есть классный плагин Splash.

Добавляем плагин в Package.swift.

import PackageDescription

let package = Package(
    name: "Blog",
    products: [
        .executable(
            name: "Blog",
            targets: ["Blog"]
        )
    ],
    dependencies: [
        .package(name: "Publish", url: "https://github.com/johnsundell/publish.git", from: "0.6.0"),
        .package(name: "SplashPublishPlugin", url: "https://github.com/johnsundell/splashpublishplugin", from: "0.1.0")
    ],
    targets: [
        .target(
            name: "Blog",
            dependencies: ["Publish", "SplashPublishPlugin"]
        )
    ]
)

Теперь в стили нужно добавить цвета для подсветки. Для этого нам придется создать кастомную тему оформления.

Создаем в папке Resourses папку CodeTheme и копируем в неё файл styles.css из стандартной темы.

Добавляем в этот файл цвета из примера.

И создаем новую тему в Sources/Blog/Theme+Code.swift на основе стандартной.

public extension Theme {
    static var code: Self {
        Theme(
            htmlFactory: CodeHTMLFactory(),
            resourcePaths: ["Resources/CodeTheme/styles.css"]
        )
    }
}

private struct CodeHTMLFactory<Site: Website>: HTMLFactory {

    ...
}

Устанавливаем плагин во флоу публикации сайта в main.swift.

import SplashPublishPlugin

...

try Blog().publish(using: [
    .installPlugin(.splash(withClassPrefix: "")),
    .addMarkdownFiles(),
    .copyResources(),
    .generateHTML(withTheme: .code),
    .generateSiteMap()
])

Готово. Теперь код на Swift будет иметь подсветку синтаксиса. 🌈


Пишем первый пост

Посты хранятся в папке Content/posts/ и представляют собой Markdown файлы.

Отредактируем уже созданый пост first-post.

---
date: 2020-06-24 12:00
description: Ребята, у меня теперь есть блог.
tags: introduction, fun
---
# Первый пост на моём блоге

Ребята, у меня теперь есть блог.

И я могу вставлять код на Swift.

````swift
import Blog

let myBlog = Blog()
myBlog.publish()
````swift

Сгенерируем сайт и увидим наш пост с кодом. 😎

Fisrt Post