Publish 사용하기 part 2

Swift publish 커스텀하기

Publish 구조 작성하기

struct PublishHTMLFactory: HTMLFactory {
    typealias Site = Example

    func makeIndexHTML(for index: Publish.Index, context: Publish.PublishingContext<Example>) throws -> Plot.HTML {
        <#code#>
    }
    
    func makeSectionHTML(for section: Publish.Section<Example>, context: Publish.PublishingContext<Example>) throws -> Plot.HTML {
        <#code#>
    }
    
    func makeItemHTML(for item: Publish.Item<Example>, context: Publish.PublishingContext<Example>) throws -> Plot.HTML {
        <#code#>
    }
    
    func makePageHTML(for page: Publish.Page, context: Publish.PublishingContext<Example>) throws -> Plot.HTML {
        <#code#>
    }
    
    func makeTagListHTML(for page: Publish.TagListPage, context: Publish.PublishingContext<Example>) throws -> Plot.HTML? {
        <#code#>
    }
    
    func makeTagDetailsHTML(for page: Publish.TagDetailsPage, context: Publish.PublishingContext<Example>) throws -> Plot.HTML? {
        <#code#>
    }
}

이전 포스트에서 만든 PublishHTMLFactory 부분 부터 보도록 하겠습니다.

Publish에서 HTML 로 구조를 작성하기 위해서, Plot이라는 JohnSundell이 만든 라이브러리를 사용합니다.

기존에는

let html = HTML(
    .head(
        .title("My website"),
        .stylesheet("styles.css")
    ),
    .body(
        .div(
            .h1("My website"),
            .p("Writing HTML in Swift is pretty great!")
        )
    )
)

이런 방식으로 작성했지만, Plot이 업데이트 되면서, Component 프로토콜을 사용해서 Component 요소들을 SwiftUI와 유사한 방식으로 작성할 수 있습니다.

struct NewsArticle: Component {
    var imagePath: String
    var title: String
    var description: String

    var body: Component {
        Article {
            Image(url: imagePath, description: "Header image")
            H1(title)
            Span(description).class("description")
        }
        .class("news")
    }
}

이런 방식으로 말이죠

간단하게 알아 봤으니 한번 Publish에서 사용해보겠습니다. 먼저 가장 먼저 보일 부분인 Index 부분을 커스텀해보겠습니다.

func makeIndexHTML(for index: Index, context: PublishingContext<Example>) throws -> HTML {
    <#code#>
}

먼저 HTML이라는 구조체를 makeIndexHTML 메서드에 추가해줍니다.

func makeIndexHTML(for index: Index, context: PublishingContext<Example>) throws -> HTML {
    /// HTML 구조체 내에 node를 추가할 수 있는 형태
    HTML()

    /// HTML 구조체에 들어갈 head 부분이랑 body 부분을 init에서 분리해주는 형태
    HTML(head: [], body: {})
}

위 두가지 HTML 에서 하나를 선택해주시면 됩니다. 저는 위 방식으로 진행하도록 하겠습니다.

먼저 사용할 언어를 선택해줍니다.

func makeIndexHTML(for index: Index, context: PublishingContext<Example>) throws -> HTML {
    HTML(
        .lang(context.site.language) /// <html lang="$(main에서 설정한 언어)">
    )
}

Head에 필요한 정보들을 넣어줍니다. 저는 publish에서 제공해주는 head static 메서드를 통해서 구성해주도록 하겠습니다.

func makeIndexHTML(for index: Index, context: PublishingContext<Example>) throws -> HTML {
    HTML(
        .lang(context.site.language),
        .head(for: index, on: context.site)
    )
}

빌드를 돌리게 되면

이런식으로 Head 부분이 쉽게 구축이 된것을 확인 할 수 있습니다. publish에서 제공하는 head 메서드는 저희가 따로 node로 넣어줄 필요없이 가장 자주 사용되는것들로 구성해 줍니다.

하지만 웹에서는 /style.css에러가 발생합니다. 이 부분은

static func head<T: Website>(
    for location: Location,
    on site: T,
    titleSeparator: String = " | ",
    stylesheetPaths: [Path] = ["/styles.css"],
    rssFeedPath: Path? = .defaultForRSSFeed,
    rssFeedTitle: String? = nil
) -> Node {}

head static 메서드가 선언된 부분을 보면 알 수 있습니다.

stylesheetPath에 내용이 들어가지 않기 때문에, 자동으로 style.css 부분을 넣어줘서, 현재는 style.css 파일이 없기 때문에 에러가 발생하는 것입니다.

.head(for: index, on: context.site, stylesheetPaths: [])

그래서 아직 stylesheet 넣지 않을것이라면 이런식으로 빈 배열로 값을 넣어주면 에러가 발생하지 않습니다.

그 다음은 Body 입니다.
Body 같은 경우도 node로 추가하는 방법도 있지만, 앞에서 말했던 Component를 사용해봅시다.

func makeIndexHTML(for index: Index, context: PublishingContext<Example>) throws -> HTML {
    HTML(
        .lang(context.site.language),
        .head(for: index, on: context.site),
        .body { 
            // Body Code
        }
    )
}

Component 사용법

Component를 사용하기 위해서는 Component 라는 프로토콜을 상속 받아줍니다.

struct ComponentName: Component 

이렇게 상속을 받아주면

struct ComponentName: Component {
    var body: Component {}
}

이런식으로 SwiftUI와 비슷한 스타일로 Component body 부분에서 기존에 사용하던 Node 또는 다른 Component를 이곳에 넣을 수 있습니다.

struct ComponentName: Component {
    var body: Component {
        Div {
            ...
        }
        .class("site-div")
    }
}

이런식으로 Component를 제작할 수 있습니다.

현재 Plot에서 제공하고 있는 Component에서 사용할 수 있는 HTMLComponent 입니다.

/// A container component that's rendered using the `<article>` element.
public typealias Article = ElementComponent<ElementDefinitions.Article>
/// A container component that's rendered using the `<aside>` element.
public typealias Aside = ElementComponent<ElementDefinitions.Aside>
/// A container component that's rendered using the `<button>` element.
public typealias Button = ElementComponent<ElementDefinitions.Button>
/// A container component that's rendered using the `<div>` element.
public typealias Details = ElementComponent<ElementDefinitions.Details>
/// A container component that's rendered using the `<div>` element.
public typealias Div = ElementComponent<ElementDefinitions.Div>
/// A container component that's rendered using the `<fieldset>` element.
public typealias FieldSet = ElementComponent<ElementDefinitions.FieldSet>
/// A container component that's rendered using the `<footer>` element.
public typealias Footer = ElementComponent<ElementDefinitions.Footer>
/// A container component that's rendered using the `<h1>` element.
public typealias H1 = ElementComponent<ElementDefinitions.H1>
/// A container component that's rendered using the `<h2>` element.
public typealias H2 = ElementComponent<ElementDefinitions.H2>
/// A container component that's rendered using the `<h3>` element.
public typealias H3 = ElementComponent<ElementDefinitions.H3>
/// A container component that's rendered using the `<h4>` element.
public typealias H4 = ElementComponent<ElementDefinitions.H4>
/// A container component that's rendered using the `<h5>` element.
public typealias H5 = ElementComponent<ElementDefinitions.H5>
/// A container component that's rendered using the `<h6>` element.
public typealias H6 = ElementComponent<ElementDefinitions.H6>
/// A container component that's rendered using the `<header>` element.
public typealias Header = ElementComponent<ElementDefinitions.Header>
/// A container component that's rendered using the `<li>` element.
public typealias ListItem = ElementComponent<ElementDefinitions.ListItem>
/// A container component that's rendered using the `<main>` element.
public typealias Main = ElementComponent<ElementDefinitions.Main>
/// A container component that's rendered using the `<nav>` element.
public typealias Navigation = ElementComponent<ElementDefinitions.Navigation>
/// A container component that's rendered using the `<p>` element.
public typealias Paragraph = ElementComponent<ElementDefinitions.Paragraph>
/// A container component that's rendered using the `<span>` element.
public typealias Span = ElementComponent<ElementDefinitions.Span>
/// A container component that's rendered using the `<summary>` element.
public typealias Summary = ElementComponent<ElementDefinitions.Summary>
/// A container component that's rendered using the `<caption>` element.
public typealias TableCaption = ElementComponent<ElementDefinitions.TableCaption>
/// A container component that's rendered using the `<td>` element.
public typealias TableCell = ElementComponent<ElementDefinitions.TableCell>
/// A container component that's rendered using the `<th>` element.
public typealias TableHeaderCell = ElementComponent<ElementDefinitions.TableHeaderCell>

이부분을 통해서 Component를 제작하실때 필요한 HTML Element를 사용할 수 있습니다.

Component가 잘 제작이 됬다면

func makeIndexHTML(for index: Index, context: PublishingContext<Example>) throws -> HTML {
    HTML(
        .lang(context.site.language),
        .head(for: index, on: context.site),
        .body { 
            HeaderComponent() // 제작한 Component
            PostsLayout()     // 제작한 Component
            FooterComponent() // 제작한 Component
        }
    )
}

이런식으로 body에 넣어서 추가가 가능하죠. 간단하게 Component를 추가하는 방법에 대해서 알아보았습니다.

그럼 한번 제대로된 Publish 프로젝트 하나를 만들어보면서 어떻게 사용해야하는지 감을 잡아봅시다.

Example

간단하게 디자인 한 이미지를 토대로 개발해보도록 하겠습니다.

먼저 기존에 만들어둔 Example에서 진행을 해보겠습니다.

- Components
- Layouts
- Pages
- Utils

Website를 만들때 이렇게 3가지로 폴더를 분리하였습니다.

  • Components: ComponentsHeaderFooter 같은 페이지에 필요한 컴포넌트의 집합
  • Layouts: Layouts 은 page template와 같은 재사용 가능한 UI 구조를 제공합니다.
  • Pages: Pages는 웹사이트의 모든 페이지에 대한 레이아웃을 처리합니다.
  • Utils: 코드에 추가적으로 필요한 작업이나 확장 관련된 내용이 포함이 됩니다.

예시 프로젝트의 폴더에 대한 정보를 봤으니

일단 모든 페이지에서 공통적으로 사용되는 HeaderFooter부터 작업해보도록 하겠습니다.

먼저 Header입니다.

Header부분을 보면 이런식으로 구성되어 있습니다.
Logo를 눌렀을때 index 페이지로 이동하게 하고, section을 눌렀을 때 각각 section에 맞는 페이지로 이동을하는 기능이 필요합니다.

struct HeaderComponent: Component

먼저 Components 부분에서 HeaderComponent라는 것을 추가하고 Component 프로토콜을 상속 받게 되면

import Publish
import Plot

struct HeaderComponent: Component {
    var body: Component {}
}

이런식으로 body가 자동으로 만들어지고 저희는 site에 어떤 Section이 있고 사용자가 클릭한 section에 대한 정보를 가져와야 하기 때문에

var context: PublishingContext<Example>

Publish 에 있는 PublishingContext를 통해서 정보를 가져올 수 있게 합니다.

struct HeaderComponent: Component {
    var context: PublishingContext<Example>

    var body: Component {
        Header {
            Link("Blog", url: "/")
                .class("header-logo")
            Div {
                Navigation {
                    List(Example.SectionID.allCases) { sectionID in
                        Link(
                            context.sections[sectionID].title,
                            url: context.sections[sectionID].path.absoluteString
                        )
                        .class("header-nav-menu-link")
                    }
                }
                .class("header-nav")
            }
            .class("content")
        }
        .class("site-header")
    }
}

Link 같은 경우는 Html에서 <a> 태그를 담당합니다. Blog 라는 Text를 누르면 기존 타 사이트와 마찬가지로 index 페이지로 이동하도록 만들었고

Section 부분은 List를 사용해서 저희가 main.swift 에 등록한 Section들을 모두 보여주게 만들었습니다.

그리고 기존 디자인에 맞춰서 코드를 넣어주고 styles.css 에서 style 관련 css 코드를 넣어주었습니다.

그렇게 하면 이렇게 디자인 했던 것처럼 결과 값을 받아보실 수 있습니다.

그 다음은 Footer 입니다.

굉장하게 간단한 Footer 입니다.

Text로 구성하고 싶고, Copyright Link 부분을 누르면 저의 깃허브로 이동하도록 만들고 싶네요.

struct FooterComponent: Component {
    var body: Component {
        Footer {
            // code
        }
        .class("site-footer")
    }
}

Component를 선언후에 Footer를 넣어주고

struct FooterComponent: Component {
    var body: Component {
        Footer {
            Paragraph("Made with Publish")
            Paragraph {
                Text("Copyright © ")
                Link("Jihoonahn", url: "https://github.com/jihoonahn")
            }
            .class("copyright")
        }
        .class("site-footer")
    }
}

이렇게 간단하게 구축했습니다.

Paragraph 가 HTML에서는 <p> 태그의 역할을 하고, 저희는 copyright 부분중에 Jihoonahn 이라고 적힌 부분을 눌렀을 때 깃허브로 이동시키고 싶으니 이렇게 Paragraph 내부에 TextLink 를 넣어줍니다.

이렇게 되면 "Copyright © " 부분을 눌렀을 때는 링크로 이동하지 않지만, Jihoonahn 을 눌렀을때만 이동을 하게 됩니다.

마찬가지로 기존 디자인에 맞춰서 코드를 넣어주고 styles.css 에서 style 관련 css 코드를 넣어주었습니다.

간단하게 Header 와 Footer Component를 만들었고 한번 이제 페이지를 구축해볼까요?

Index

func makeIndexHTML(for index: Index, context: PublishingContext<Example>) throws -> HTML {
    HTML(
        .lang(context.site.language),
        .head(for: index, on: context.site, stylesheetPaths: []),
        .body {
            // Index Page
        }
    )
}

기존의 HTMLFactory 에서 index부분 부터 한번 작업을 해보도록하겠습니다.

Index의 Page부분을 만들기 위해서 Pages 폴더에서 IndexPage.swift 파일을 만들었습니다.

struct IndexPage: Component {
    var context: PublishingContext<Example>

    var body: Component {
        ComponentGroup {
            HeaderComponent(context: context)
            FooterComponent()
        }
    }
}

여러개의 Component가 동시에 들어가기 때문에 ComponentGroup 로 묶어 두고 기존에 만들어둔 Header와 Footer을 넣어둡니다.

그리고 Layouts 폴더에 PostsLayout.swift 란 파일을 만들어서

struct PostsLayout: Component

PostsLayout 이라는 Component를 만들고

다시 IndexPage로 돌아가서

struct IndexPage: Component {
    var context: PublishingContext<Example>

    var body: Component {
        ComponentGroup {
            HeaderComponent(context: context)
            PostsLayout()
            FooterComponent()
        }
    }
}

이런식으로 PostsLayout 도 추가로 넣어둡니다.

그리고 IndexPageHTMLFactorymakeIndexHTML method에 넣어줍니다.

func makeIndexHTML(for index: Index, context: PublishingContext<Example>) throws -> HTML {
    HTML(
        .lang(context.site.language),
        .head(for: index, on: context.site),
        .body {
            IndexPage(context: context)
        }
    )
}

그럼 이제 Index 부분을 완성하기 위해서 PostsLayout 부분을 채워볼까요?

Posts 같은 경우

이 부분이 반복됩니다. 전체가 링크로 감싸져 있는 형태이죠.

시작하기 전에 Publish에서 확장이 되어 있지 않는 Component가 몇가지 있습니다. 그중에 PostsLayout에서 사용하고 싶은 section 도 아직은 존재하지 않죠

그러므로 만약 없는 Component들은 어떻게 해야하는지 보도록 하겠습니다.

Utils/Plot/ElementDefinitions.swift 파일을 보시면 됩니다.

Publish 의 HTML 부분을 담당하는 Plot 에서 SectionNode에서는 Component 타입으로 존재하지 않는것이기 때문에

extension ElementDefinitions {
    enum Section: ElementDefinition { public static var wrapper = Node.section }
}

typealias Section = ElementComponent<ElementDefinitions.Section>

이런 방식으로 기존의 Plot의 ElementDefinitions 부분을 참고하여 확장하시면 됩니다.

이제 PostsLayout 을 제작해보겠습니다.

let items: [Item<Example>]
  • items 는 Post들을 가져오는 역할을 합니다.

이 프로퍼티를 추가해주고 IndexPage.swift 를 수정해줍니다.

struct IndexPage: Component {
    var context: PublishingContext<Example>

    var body: Component {
        ComponentGroup {
            HeaderComponent(context: context)
            PostsLayout(items: context.allItems(sortedBy: \.date))
            FooterComponent()
        }
    }
}

이런 식으로 Context의 모든 Item을 들고오면서 날짜 순서대로 정렬이 되도록 해뒀습니다.

다시 PostsLayout 부분으로 돌아가서

struct PostsLayout: Component {
    let items: [Item<Example>]
    
    var body: Component {
        Section {
            Div {
                List(items) { item in
                    Article {
                        Link(url: item.path.absoluteString) {
                            Paragraph(item.tags.map{ $0.string }.joined(separator: ", "))
                                .class("posts-tag")
                            H3(item.title)
                                .class("posts-title")
                            Paragraph(item.description)
                                .class("posts-description")
                        }
                        .class("posts-link")
                    }
                    .class("posts-article")
                }
                .class("posts-list")
            }
            .class("site-posts-inner")
        }
        .class("site-posts")
    }
}

items 값을 List(<ul>)로 보여줄 수 있도록 제작하고 styles.css 에 css 코드를 넣어주면 됩니다.

실행을 해보면 위 Index 디자인한것과 같은 결과물을 얻을 수 있습니다.

그 다음으로는 Section 부분을 처리해보겠습니다.

Section

func makeSectionHTML(for section: Publish.Section<Example>, context: Publish.PublishingContext<Example>) throws -> HTML {
    HTML(
        .lang(context.site.language),
        .head(for: section, on: context.site),
        .body {
            // Section Code
        }
    )
}

HTMLFactory 부분에서 makeSectionHTML 메서드에 Section 코드를 추가하면 됩니다.

struct SectionPage: Component

Pages/Section/SectionPage.swift 파일에 SectionPage라는 Component를 만들고

var section: Publish.Section<Example>
var context: PublishingContext<Example>

Example의 sectionContext 를 가져와 줍니다.

struct SectionPage: Component {
    var section: Publish.Section<Example>
    var context: PublishingContext<Example>

    var body: Component {
        switch section.path.string {
        case Example.SectionID.blog.rawValue:
            return IndexPage(context: context)
        case Example.SectionID.about.rawValue:
            // About Page
        default: return Div()
        }
    }
}

이렇게 section의 path가 blog이면 IndexPage를 보여주게 하고 about 이라면 AboutPage를 보여주게 만듭니다.

struct AboutPage: Component {
    var context: PublishingContext<Example>
    
    var body: Component {
        ComponentGroup {
            HeaderComponent(context: context)
            Div {
                Image("/images/AboutPageImage.svg")
                H2("Publish Example")
                Paragraph("Jihoonahn’s Blog Example")
            }
            .class("site-about")
            FooterComponent()
        }
    }
}

그리고 SectionPage에 넣어둘 AboutPage가 필요하기 때문에 간단하게 제작하고,

struct SectionPage: Component {
    var section: Publish.Section<Example>
    var context: PublishingContext<Example>

    var body: Component {
        switch section.path.string {
        case Example.SectionID.blog.rawValue:
            return IndexPage(context: context)
        case Example.SectionID.about.rawValue:
            return AboutPage(context: context)
        default: return Div()
        }
    }
}

이렇게 넣어두면

이렇게 Header에 있는 About Section을 누르게 되면 AboutPage 로 이동이 되게 만들 수 있습니다.

Post

그 다음으로는 Index에서 Post중 하나를 눌렀을 때 그 Post의 내용을 볼 수 있도록 만들겠습니다.

func makeItemHTML(for item: Item<Example>, context: PublishingContext<Example>) throws -> HTML {
    HTML(
        .lang(context.site.language),
        .head(for: item, on: context.site),
        .body {
            /// Post
        }
    )
}

이곳에 post 들의 item을 가져올 수 있습니다.

struct PostLayout: Component

Layouts/PostLayout.swift 에서 위 Index와 비슷하게 PostLayout 이라는 Component를 만들어주겠습니다.

그리고 PostLayout 부분에서 Item 내용을 가져오기 위해서

var item: Item<Example>
var context: PublishingContext<Example>
  • item 프로퍼티를 통해서 가져올 수 있도록 하였습니다.
  • context는 tag의 url을 가져오기 위해서 사용하였습니다.
struct PostLayout: Component {
    var item: Item<Example>
    var context: PublishingContext<Example>

    var body: Component {
        Section {
            Article {
                Div {
                    for tag in item.tags {
                        Link(tag.string, url: context.site.url(for: tag))
                            .class("post-tag")
                    }
                    H2(item.title)
                        .class("post-title")
                    Paragraph(DateFormatter.time.string(from: item.date))
                        .class("post-date")
                }
                .class("site-post-header")
                Div {
                    Div {
                        Node.contentBody(item.body)
                    }
                }
                .class("site-post-content")
            }
            .class("site-post-article")
        }
        .class("site-post")
    }
}

디자인 대로 구축한 PostLayout 코드입니다.

for tag in item.tags {
    Link(tag.string, url: context.site.url(for: tag))
        .class("post-tag")
}

item에 있는 tag들을 가져와서 눌렀을때 Tag에 관련된 Post를 찾을 수 있는 페이지로 이동시킵니다.

Node.contentBody(item.body)

그리고 저희가 Content 파일에 markdown으로 추가한 내용을 볼 수 있도록 NodecontentBody 메서드를 사용해주시면 됩니다.

struct PostPage: Component {
    var item: Item<Example>
    var context: PublishingContext<Example>

    var body: Component {
        ComponentGroup {
            HeaderComponent(context: context)
            PostLayout(item: item, context: context)
            FooterComponent()
        }
    }
}

그리고 PostLayout을 만들었으니, PostPagePostLayout을 가져와주시면 됩니다.

func makeItemHTML(for item: Item<Example>, context: PublishingContext<Example>) throws -> HTML {
    HTML(
        .lang(context.site.language),
        .head(for: item, on: context.site),
        .body {
            PostPage(item: item, context: context)
        }
    )
}

다시 HTMLFactory 부분으로 돌아가서 PostPage 부분을 body에 넣어주시면

이렇게 Markdown에 있는 Post가 잘 작동하는 것을 확인할 수 있습니다.

Page

makePageHTML 부분은 특별하게 예시에서는 사용하고 있지 않기 때문에

func makePageHTML(for page: Page, context: PublishingContext<Example>) throws -> HTML {
    HTML(
        .lang(context.site.language),
        .head(for: page, on: context.site),
        .body {
            page.body
        }
    )
}

page.body 만 보이게 해뒀습니다.

Tag List

그 다음은 TagList 입니다. Post에 있는 모든 Tag들을 한번에 가져와서 볼 수 있게 하는 역할을 합니다.

func makeTagListHTML(for page: Publish.TagListPage, context: PublishingContext<Example>) throws -> HTML? {
    HTML(
        .lang(context.site.language),
        .head(for: page, on: context.site),
        .body {
            // TagList Code                
        }
    )
}

HTMLFactory 부분에서 makeTagListHTML 에서 처리할 수 있습니다.

struct TagListPage: Component

먼저 TagListPage 에서 마찬가지로 Component를 생성하고

let tags: Set<Tag>

TagListPage에서는 Publish.TagListPage에 있는 모든 태그를 들고 올 수 있게 Set<Tag> 타입을 사용합니다.

struct TagListPage: Component {
    let tags: [Tag]
    let context: PublishingContext<Example>
    
    var body: Component {
        ComponentGroup {
            HeaderComponent(context: context)
            List(tags) { tag in
                ListItem {
                    Link(tag.string, url: context.site.url(for: tag))
                }
                .class("site-tag")
            }
            .class("site-tagList")
            FooterComponent()
        }
    }
}

그리고 tags 를 List에 넣어서 보여주고 styles.css 에서 간단한 css 코드만 추가해주었습니다.

func makeTagListHTML(for page: Publish.TagListPage, context: PublishingContext<Example>) throws -> HTML? {
    HTML(
        .lang(context.site.language),
        .head(for: page, on: context.site),
        .body {
            TagListPage(tags: page.tags, context: context)
        }
    )
}

다시 HTMLFactory 코드로 돌아와서 TagListPage를 넣어줍니다.

이렇게 디자인과 같은 TagList 페이지를 얻을 수 있습니다.

TagDetail

마지막 TagDetail 부분입니다. 지정된 tag를 눌렀을 때 이 tag를 가지고 있는 post를 보여주는 역할을 합니다.

func makeTagDetailsHTML(for page: Publish.TagDetailsPage, context: PublishingContext<Example>) throws -> HTML? {
    HTML(
        .lang(context.site.language),
        .head(for: page, on: context.site),
        .body {
            // Tag Details Code
        }
    )
}

HTMLFactory에서 마지막 하나 남은 메서드인 makeTagDetailsHTML 에서 작업을 하실 수 있습니다.

struct TagDetailPage: Component

TagDetailPage라는 Component를 선언하고

let items: [Item<Example>]
let selectedTag: Tag
  • items: Tag에 포함된 post들을 가져옵니다.
  • selectedTag: 선택된 Tag의 정보를 알려줍니다.

위의 프로퍼티들을 추가해주시고

struct TagDetailsPage: Component {
    let items: [Item<Example>]
    let context: PublishingContext<Example>
    let selectedTag: Tag

    var body: Component {
        ComponentGroup {
            HeaderComponent(context: context)
            Div {
                H2(selectedTag.string)
                PostsLayout(items: items)
            }
            .class("site-tagDetails")
            FooterComponent()
        }
    }
}

기존의 PostsLayout 을 가져오는 방식으로 간단하게 처리하고

func makeTagDetailsHTML(for page: Publish.TagDetailsPage, context: PublishingContext<Example>) throws -> HTML? {
    HTML(
        .lang(context.site.language),
        .head(for: page, on: context.site),
        .body {
            TagDetailsPage(
                items: context.items(
                    taggedWith: page.tag,
                    sortedBy: \.date
                ),
                context: context,
                selectedTag: page.tag
            )
        }
    )
}

이렇게 HTMLFactorymakeTagDetailsHTML에 넣어주시면 되는데,

items: context.items(
    taggedWith: page.tag,
    sortedBy: \.date
),

items를 가져오는 부분을 보면 context의 items를 가져오는데 page.tag가 포함되어 있는 item 만 가져오게 해준다라고 생각하시면 될것 같습니다.

이런식으로 Tag를 눌렀을때 잘 조회가 되는것을 확인할 수 있습니다. 이렇게 해서 저희는 Publish로 웹사이트 하나를 뚝딱 만들어 봤습니다.

위에서 진행한 내용은 예시코드 를 확인할 수 있습니다.


이번 글에서는 Publish에서 HTML을 작성하는 방법과, 직접 예제를 만들어보며 Publish를 이용해서 실질적인 웹사이트를 만들어 봤습니다.

다음글에서는 Publish로 만든 웹사이트를 배포하는 법에 대해서 작성할 예정입니다.