Having a powerful theme system is one of the most important reasons for the success of a static website generator. Publish uses Plot as a topic development tool, allowing developers to efficiently write topics while enjoying the advantages of Swift type safety. This article starts with Plot, so you can finally learn how to create Publish topics.
Plot
Introduction to the
To develop a Publish Theme, you have to start with Plot.
Plot is one of several excellent projects in the Swift community dedicated to HTML generation using Swift: Vapor Leaf, Point-free Swift-HTML, etc. Plot was originally written by John Sundell as part of the Publish suite and focuses on HTML generation for the Swift static web site as well as the creation of documents in other formats required for the site, including RSS, podcast, and Sitemap. It is tightly integrated with Publish but also exists as a separate project.
Plot uses a technique called Phantom Types, which uses Types as “markers” for the compiler, enabling type safety to be enforced through generic constraints. Plot uses a very lightweight API design that minimizes external parameter tags, thus reducing the amount of syntax required to render the document and rendering it a “DSL-like” code representation.
use
basis
-
Node
Is the core building block for all elements and attributes in any Plot document. It can represent elements and attributes, as well as text content and node groups. Each node is bound to a Context type, which determines which DSL API it can access (for example html.bodyContext is used for nodes placed in the HTML page ).
-
An Element represents an Element that can be opened and closed with two separate tags (such as ) or closed on its own (such as ). When using Plot, you usually don’t need to interact with this type; instances of it are created in the base Node.
-
Attribute
Represents an attribute attached to an element, such as the href of the element or the SRC of the element. You can construct the Attribute value through its initializer, or through the DSL, using the.attribute() command.
-
The Document and DocumentFormat
Documents in a given format, such as HTML, RSS, and PodcastFeed. These are the highest level types, and you can use the Plot DSL to start a document building session.
Type of DSL grammar
import Plot
let html = HTML(
.head(
.title("My website"),
.stylesheet("styles.css")
),
.body(
.div(
.h1("My website"),
.p("Writing HTML in Swift is pretty great!"))))Copy the code
The Swift code above generates the following HTML code. The code form is very similar to a DSL, with minimal code contamination.
<! DOCTYPEhtml>
<html>
<head>
<title>My website</title>
<meta name="twitter:title" content="My website"/>
<meta name="og:title" content="My website"/>
<link rel="stylesheet" href="styles.css" type="text/css"/>
</head>
<body>
<div>
<h1>My website</h1>
<p>Writing HTML in Swift is pretty great!</p>
</div>
</body>
</html>
Copy the code
Sometimes it feels like Plot just maps each function directly to an equivalent HTML element — at least that’s what the above code looks like — but Plot also automatically inserts a lot of very valuable metadata, and we’ll see more of that later.
attribute
Attributes can be applied in exactly the same way as child elements, simply by adding another entry to the element’s comma-separated list of contents. For example, here’s how to define an anchor element with both a CSS class and a URL. Attributes, elements, and inline text are all defined in the same way, which not only makes the Plot API easier to learn, but also makes the input experience very fluid — as you can simply type in any context. To continually define new attributes and elements.
let html = HTML(
.body(
.a(.class("link"), .href("https://github.com"), "GitHub")))Copy the code
Type safety
Plot makes extensive use of Swift’s advanced generics capabilities, not only to make it possible to write HTML and XML in native code, but to achieve complete type safety in the process. All elements and attributes of Plot are implemented as context-bound nodes, which both enforces valid HTML semantics and allows Xcode and other ides to provide rich auto-completion information when writing code using Plot’s DSL.
let html = HTML(.body(
.p(.href("https://github.com"))))Copy the code
For example,
cannot be placed directly in
, auto-completion will not be prompted when typing.p (because of context mismatch), and the code will report errors during compilation.
This high level of type safety both makes for a very pleasant development experience and makes it much more likely that HTML and XML documents created with Plot will be semantically correct — especially when compared to writing documents and tags using raw strings.
For those of you who know very little about HTML, there is no way I can write the following error code under Plot.
let html = HTML(.body)
.ul(.p("Not allowed"))))Copy the code
Custom components
Similarly, the context-bound Node architecture not only gives Plot a high degree of type security, but also allows for the definition of more and higher level components, which can then be flexibly mixed with elements defined by Plot itself.
For example, we want to add an Advertising component to our web site that is bound to the context of an HTML document.
extension Node where Context: HTML.BodyContext { // Strict context binding
static func advertising(_ slogan: String.herf:String) -> Self {
.div(
.class("avertising"),
.a(
.href(herf),
.text(slogan)
)
)
}
}
Copy the code
Advertising can now be used using exactly the same syntax as the built-in elements.
let html = HTML(
.body(
.div(
.class("wrapper"),
.article(
.
),
.advertising("Elbow's Swift Notepad", herf: "https://fatbobman.com"))))Copy the code
Control process
Although Plot focuses on static site generation, it comes with several control flow mechanisms that let you use the inline logic of its DSL. Currently supported control commands include.if(),.if(_,else:), unwrap(), and forEach().
var books:[Book] = getbooks()
let show:Bool = true
let html = HTML(.body(
.h2("Books"),
.if(show,
.ul(.forEach(books) { book in
.li(.class("book-title"), .text(book.title))
})
,else:
.text("Please add stacks"))))Copy the code
Using the above control flow mechanism, especially when used in conjunction with custom components, allows you to build truly flexible themes and create the documents and HTML pages you need in a type-safe manner.
Customize elements and attributes
Although Plot aims to cover as many standards as it can in relation to the document formats it supports, you may still come across elements or attributes of some form that Plot does not yet have. It is very easy to customize elements and attributes in Plot, which is particularly useful when generating XML.
extension Node where Context= =XML.ProductContext {
static func name(_ name: String) -> Self {
.element(named: "name", text: name)
}
static func isAvailable(_ bool: Bool) -> Self {
.attribute(named: "available", value: String(bool))
}
}
Copy the code
Document rendering
let header = Node.header(
.h1("Title"),
.span("Description"))let string = header.render()
Copy the code
Output indentation can also be controlled
html.render(indentedBy: .tabs(4))
Copy the code
Other support
Plot also supports generating RSS feeds, Podcasting, Site maps, and more. The corresponding part of Publish is also implemented by Plot.
The Publish theme
Before reading the following, you should have read Publish Blog Creation (part 1) – Getting Started.
The article mentions that sample templates are available for download at GIthub.
Custom themes
In Publish, topics need to follow the HTMLFactory protocol. To define a new topic:
import Foundation
import Plot
import Publish
extension Theme {
public static var myTheme: Self {
Theme(
htmlFactory: MyThemeHTMLFactory<MyWebsite>(),
resourcePaths: ["Resources/MyTheme/styles.css"])}}private struct MyThemeHTMLFactory<Site: Website> :HTMLFactory {
/ /... Specific page, need to achieve six methods
}
private extension Node where Context= =HTML.BodyContext {
// Node definition, such as header, footer, etc
}
Copy the code
The topic is specified in the Pipeline using the following code
.generateHTML(withTheme:.myTheme ), // Use a custom theme
Copy the code
HTMLFactory protocol requires that we must implement all six methods, corresponding to the six pages, respectively:
-
makeIndexHTML(for index: Index,context: PublishingContext<Site>)
The home page of the site, usually recent articles, hot recommendations, etc., is an explicit list of all items in the default theme
-
makeSectionHTML(for section: Section<Site>,context: PublishingContext<Site>)
The page when the Section is an Item container. Usually displays a list of items that belong to the Section
-
makeItemHTML(for item: Item<Site>, context: PublishingContext<Site>)
The display page for a single article (Item)
-
makePageHTML(for page: Page,context: PublishingContext<Site>)
The display Page of a free article (Page). When the Section is not a container, its index.md is rendered as a Page
-
makeTagListHTML(for page: TagListPage,context: PublishingContext<Site>)
Tag list page. This is typically where all tags that have appeared in the site’s articles are displayed
-
makeTagDetailsHTML(for page: TagDetailsPage,context: PublishingContext<Site>)
Usually a list of items that have the Tag
In each method of the MyThemeHTMLFactory, just write the Plot representation described above. Such as:
func makePageHTML(for page: Page.context: PublishingContext<Site>) throws -> HTML {
HTML(
.lang(context.site.language),
.head(for: page, on: context.site),
.body(
.header(for: context, selectedSection: nil),
.wrapper(.contentBody(page.body)),
.footer(for: context.site)
)
)
}
Copy the code
Header, wrapper, and footer are all custom nodes
Generation mechanism
Publish uses workflow mechanism, through sample code to understand how data is in the Pipeline operation.
try FatbobmanBlog().publish(
using: [
.installPlugin(.highlightJS()), // Add the syntax highlighting plugin. This plug-in is called when MarkDown parses
.copyResources(), // Copy the resources required by the website, files in the Resource directory
.addMarkdownFiles(),
/* Read the markdown file one by one under the Content, parse the markdown file, 1: parse the metadata, save the metadata in the corresponding Item 2: parse the Markdown paragraphs one by one and convert them into HTML data 3: Plugin 4 is called when it encounters text blocks (codeBlocks) that highlightJS requires processing: all processed content is saved to PublishingContext */
.setSctionTitle(), // Change the section's display title
.installPlugin(.setDateFormatter()), // Set the time display format for the HTML output
.installPlugin(.countTag()), // Add a tagCount attribute to the tag to count the number of articles in each tag
.installPlugin(.colorfulTags(defaultClass: "tag", variantPrefix: "variant", numberOfVariants: 8)), // Add a colorfiedClass attribute to each tag by injection, returning the corresponding color definition in the CSS file
.sortItems(by: \.date, order: .descending), // All articles are in descending order
.generateHTML(withTheme: .fatTheme), // Specify a custom theme and generate an HTML file in the Output directory
/* Use the theme template to call the page generation method one by one. Depending on the parameters required by each method, pass the corresponding PublishingContext, Item, Scetion topic methods and so on to the data, use Plot to render HTML such as makePageHTML, The content of a page article is obtained through the page. Body */
.generateRSSFeed(
including: [.posts,.project],
itemPredicate: nil
), // Generate RSS using Plot
.generateSiteMap(), // Use Plot to generate Sitemap])Copy the code
As you can see from the code above, using the topic template to generate HTML and save it is at the end of the entire Pipeline, and usually the data given is ready when the topic method calls it. However, since the subject of Publish is not a description file but standard program code, we can still reprocess the data before render.
Although Publish currently offers a limited variety of pages, we can still render different content completely differently using only these categories. Such as:
func makeSectionHTML(for section: Section<Site>,
context: PublishingContext<Site>) throws -> HTML {
// If the section is posts, show a completely different page
if section.id as! Myblog.SectionID = = .posts {
return HTML(
postSectionList(for section: Section<Site>,
context: PublishingContext<Site>)}else {
return HTML(
otherSctionList(for section: Section<Site>,
context: PublishingContext<Site>)}}Copy the code
This can also be done using the control commands provided by Plot, and the following code is equivalent to the above
func makeSectionHTML(for section: Section<Site>,
context: PublishingContext<Site>) throws -> HTML {
HTML(
.if(section.id as! Myblog.SectionID = = .posts,
postSectionList(for section: Section<Site>,
context: PublishingContext<Site>)
,
else:
otherSctionList(for section: Section<Site>,
context: PublishingContext<Site>)))}Copy the code
In a word, Publish uses the idea of writing ordinary programs to deal with web pages, themes are not just description files.
Work with CSS
The theme code defines the basic layout and logic of the corresponding page, and the more specific layout, size, color, effects and so on are set in the CSS file. The CSS file is specified when the theme is defined (there can be more than one).
If you’re an experienced CSS user, it’s usually easy. But I don’t use CSS at all, and spent the most time and effort rebuilding my Blog with Publish.
Please recommend a tool or VScode plug-in that can tidy up CSS. Since I have no experience in CSS, the code is very messy. Is it possible to automatically adjust the same level or similar tag classes together for easy search?
In actual combat
The next step is to experience the development process by modifying the two theme approaches.
The preparatory work
It’s not practical to completely rebuild all the theme code at first, so I recommend starting with Foundation, the default theme that comes with Publish.
Complete the setup in Publish Create blog (1) – Getting Started
Modify the main. Swift
enum SectionID: String.WebsiteSectionID {
// Add the sections that you want your website to contain here:
case posts
case about // Add an item to illustrate the top navigation bar
}
Copy the code
$http://cdn myblog
$publish run
Copy the code
Go to http://localhost:8000, and the page looks something like this
Create the MyTheme directory in the Resource directory. Copy styles. CSS and Theme+Foundation. Swift from Publish library to MyTheme directory in XCode, or paste the code after creating a new file in MyTheme directory.
Publish--Resources--FoundatioinTheme-- styles.css
Copy the code
Publish--Sources--Publish--API-- Theme+Foundation.swift
Copy the code
Rename Theme+ foundation. swift to myTheme.swift and edit the content
Will:
private struct FoundationHTMLFactory<Site: Website> :HTMLFactory {
Copy the code
To:
private struct MyThemeHTMLFactory<Site: Website> :HTMLFactory {
Copy the code
will
static var foundation: Self {
Theme(
htmlFactory: FoundationHTMLFactory(),
resourcePaths: ["Resources/FoundationTheme/styles.css"])}Copy the code
Instead of
static var myTheme: Self {
Theme(
htmlFactory: MyThemeHTMLFactory(),
resourcePaths: ["Resources/MyTheme/styles.css"])}Copy the code
In the main. In the swift
will
try Myblog().publish(withTheme: .foundation)
Copy the code
Instead of
try Myblog().publish(withTheme: .myTheme)
Copy the code
Feel free to create a couple of.md files under the Content posts directory. Such as
-- Date: 2021-01-30 19:58 Description: Second tags: Articletitle: My second post
---
hello world
...
Copy the code
At this point, the page looks something like this, with the makeIndexHTML method creating the currently displayed page.
Example 1: Change the display of Item Row in makeIndexHTML
The current code for makeIndexHTML looks like this:
func makeIndexHTML(for index: Index.context: PublishingContext<Site>) throws -> HTML {
HTML(
.lang(context.site.language), //< HTML lang="en"> Language can be changed in main.swift
.head(for: index, on: context.site), // Content, title and meta
.body(
.header(for: context, selectedSection: nil), // Site name and nav navigation SectionID
.wrapper(
.h1(.text(index.title)), // Welcome to MyBlog! Corresponds to the title of Content--index.md
.p(
.class("description"), // In styels.css correspond to.description
.text(context.site.description) // Corresponds to site.description in main.swift
),
.h2("Latest content"),
.itemList( MakeIndex makeSection makeTagList makeIndex makeSection makeTagList makeIndex makeSection makeTagList
for: context.allItems(
sortedBy: \.date, // In descending order by creation time, according to metatData date
order: .descending
),
on: context.site
)
),
.footer(for: context.site) // Customize Node to display the copyright information below))}Copy the code
Make the following changes in makeIndexHTML
.itemList(
Copy the code
Instead of
.indexItemList(
Copy the code
Add.h2(“Latesht content”) later, which becomes the following code
.h2("Latesht content"),
.unwrap(context.sections.first{ $0.id as! Myblog.SectionID = = .posts}){ posts in
.a(
.href(posts.path),
.text("Show all articles"))},Copy the code
Extension Node where Context == html.bodyContext
static func indexItemList<T: Website> (for items: [Item<T>].on site: T) -> Node {
let limit:Int = 2 // Set the maximum number of Item items displayed on the index page
let items = items[0.min((limit - 1),items.count)]
return .ul(
.class("item-list"),
.forEach(items) { item in
.li(.article(
.h1(.a(
.href(item.path),
.text(item.title)
)),
.tagList(for: item, on: site),
.p(.text(item.description)),
.p(item.content.body.node) // Add display Item full text))})}Copy the code
Index now changes to the following state:
Example 2: Add navigation to adjacent articles for makeItemHTML
For this example, we’ll add article navigation to our makeItemHTML, which will look like this:
Click on any Item (article)
func makeItemHTML(for item: Item<Site>,
context: PublishingContext<Site>) throws -> HTML {
HTML(
.lang(context.site.language),
.head(for: item, on: context.site),
.body(
.class("item-page"),
.header(for: context, selectedSection: item.sectionID),
.wrapper(
.article( / / < article > tag
.div(
.class("content"), //css .content
.contentBody(item.body) //.raw(body.html) displays the item.body.html text body
),
.span("Tagged with: "),
.tagList(for: item, on: context.site) // forEach(item.tags)
)
),
.footer(for: context.site)
)
)
}
Copy the code
Before the code HTML(add the following:
var previous:Item<Site>? = nil // Previous article Item
var next:Item<Site>? = nil // Next article Item
let items = context.allItems(sortedBy: \.date,order: .descending) // Get all items
/* Let items = context.allitems (sortedBy: \.date,order: .descending) .filter{$0.tags.contains(Tag("article"))} */
// The index of the current Item
guard let index = items.firstIndex(where: {$0 = = item}) else {
return HTML()}if index > 0 {
previous = items[index - 1]}if index < (items.count - 1) {
next = items[index + 1]}return HTML(
.
Copy the code
Add before.footer
.itemNavigator(previousItem:previous,nextItem:next),
.footer(for: context.site)
Copy the code
Add a custom NodeitemNavigator to extension Node WHERE Context == html.bodyContext
static func itemNavigator<Site: Website> (previousItem: Item<Site>? .nextItem: Item<Site>? -> Node{
return
.div(
.class("item-navigator"),
.table(
.tr(
.td(
.unwrap(previousItem){ item in
.a(
.href(item.path),
.text(item.title)
)
}
),
.td(
.unwrap(nextItem){ item in
.a(
.href(item.path),
.text(item.title)
)
}
)
)
)
)
}
Copy the code
Add in styles.css
.item-navigator table{
width:100%;
}
.item-navigator td{
width:50%;
}
Copy the code
The above code is intended as a demonstration of concept only. Here are the results:
conclusion
If you have experience with SwiftUI, you will find that the usage is very similar. In the Publish topic, you have plenty of tools to organize, process data, and lay out views (think of Node as a View).
Publish’s FoundationHTMLFactory currently defines only six page types. There are two ways to add new types:
-
Fork Publish, directly extending its code
This way is the most thorough, but maintenance is more troublesome.
-
After Pipeline has executed.generateHTML, the custom generate Step is executed
No changes to the core code. There may be redundant actions, and we need to do a little work in the FoundationHTMLFactory built-in methods to connect to our newly defined pages. For example, both index and section lists currently do not support paging (they only output an HTML file). We can regenerate a set of paging indexes after the built-in makeIndex and overwrite the original one.
In this article, we’ve shown how to use Plot and how to customize your own topics in Publish. In Publish blogging (iii) – plug-in development, we’ll look at ways to add functionality (not just plugins) without changing the core Publish code.
My personal blog, Swift Notepad, has more on Swift, SwiftUI, and CoreData.