In this tutorial, we'll build a static site generator using elem-go
for HTML templating and goldmark
for Markdown processing. This generator will convert Markdown files into HTML and create an index page listing all posts.
Note: You can clone this template to use or follow along with the tutorial.
Explaining the Dependencies
-
elem-go
: A Go library for type-safe HTML element creation. It simplifies the process of building HTML templates in Go. -
goldmark
: A Markdown parser and renderer for Go. It converts Markdown files into HTML.
Creating Our Project
We'll structure our project with a main
Go file, a posts
directory for Markdown, a public
directory for the generated HTML, and a Go module file. In the end, it will look like this:
my-static-site/
│
├── main.go # Your main Go file
├── posts/ # Directory containing Markdown files
│ ├── post1.md
│ ├── post2.md
│ └── ...
├── public/ # Directory where generated HTML files will be stored
└── go.mod # Go module file
To begin, let's create our project directory, navigate to it, and initialize a new Go project:
mkdir my-static-site
cd my-static-site
go mod init my-static-site
Next, we need to install our dependencies:
go get github.com/chasefleming/elem-go
go get github.com/yuin/goldmark
Adding Our Content
Next, create a posts
directory and a hello-world.md
Markdown file within it. This file will serve as your first post on the site.
mkdir posts
touch posts/hello-world.md
Let's also add some content:
# Hello, World!
This is a sample post to get you started. You can edit or delete this post and begin adding your content.
Building Our Generator
Our generator code will take the Markdown files we have and convert them into HTML pages. The conversion of Markdown to HTML will happen with the goldmark
library, but the templating of the pages will happen in elem-go
. It enables writing type-safe Go code to generate HTML, eliminating the need to parse separate template files.
Let's first create our main.go
file:
touch main.go
In main.go
, let's import the dependencies we need:
package main
import (
"github.com/chasefleming/elem-go"
"github.com/chasefleming/elem-go/attrs"
"github.com/chasefleming/elem-go/styles"
"github.com/yuin/goldmark"
)
You'll notice the elem-go
has two subpackages. These come with elem-go
and provide type-safe ways of handling attributes and styles. They help avoid having to write common strings and come with other utility methods.
Next, let's make a layout
function that takes a title
and content
and returns the html structure of our page using the arguments passed in:
func layout(title string, content elem.Node) string {
htmlPage := elem.Html(nil,
elem.Head(nil,
elem.Title(nil, elem.Text(title)),
), elem.Body(nil,
elem.Header(nil, elem.H1(nil, elem.Text(title))),
elem.Main(nil, content),
elem.Footer(nil, elem.Text("Footer content here")),
), )
return htmlPage.Render()
}
You'll notice that instead of having to write HTML, you can use elem-go
to generate HTML with methods like elem.Main
which generates a <main>
element.
After that, let's create another function to actually write the HTML generated with layout
to a file based on the title
. Since we generated a chunk of HTML using goldmark
and it's not elem-go
syntax, we'll pass it into elem.Raw
which takes raw HTML and returns an elem.Node
type which is compatible with the library:
func createHTMLPage(title, content string) string {
htmlOutput := layout(title, elem.Raw(content))
postFilename := title + ".html"
filepath := filepath.Join("public", postFilename)
os.WriteFile(filepath, []byte(htmlOutput), 0644)
return postFilename
}
We also need a function that will take our Markdown and use goldmark
to convert it to HTML. Let's add that:
func markdownToHTML(content string) string {
var buf bytes.Buffer
md := goldmark.New()
if err := md.Convert([]byte(content), &buf); err != nil {
log.Fatal(err)
} return buf.String()
}
Now that we have that, we'll additionally need a function that will walk through all of our posts, read the file, and pass the found markdown to the markdownToHTML
function we just created. The return of the function should be an array of all the converted posts:
func readMarkdownPosts(dir string) []string {
var posts []string
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if !info.IsDir() {
content, err := os.ReadFile(path)
if err != nil {
return err
}
htmlContent := markdownToHTML(string(content))
title := strings.TrimSuffix(info.Name(), filepath.Ext(info.Name()))
postFilename := createHTMLPage(title, htmlContent)
posts = append(posts, postFilename)
} return nil
})
if err != nil {
log.Fatal(err)
} return posts
}
All of our posts pages aren't great if we don't have an index page to find them all from. Again using elem-go
, make a simple list of items with elem.A
, elem.Ul
, and elem.Ul
and pass it to our layout. We also use the attrs
subpackage to add the href
using attrs.Href
inside of attrs.Props{}
to the <a>
element. After that we'll save it to public/index.html
:
func createIndexPage(postFilenames []string) {
listItems := make([]elem.Node, len(postFilenames))
for i, filename := range postFilenames {
link := elem.A(attrs.Props{attrs.Href: "./" + filename}, elem.Text(filename))
listItems[i] = elem.Li(nil, link)
}
indexContent := elem.Ul(nil, listItems...)
htmlOutput := layout("Home", indexContent)
filepath := filepath.Join("public", "index.html")
os.WriteFile(filepath, []byte(htmlOutput), 0644)
}
When we run our project remember we'll not have created the posts
or public
directories yet, so let's make a function to create those directories if they don't exist:
func createDirIfNotExist(dir string) {
if _, err := os.Stat(dir); os.IsNotExist(err) {
err := os.Mkdir(dir, 0755) // or 0700 if you need it to be private
if err != nil {
log.Fatal(err)
} }}
Now in our main
function we can put it all together to first create the required directories, then read and create all the posts, then create our index page:
func main() {
createDirIfNotExist("posts")
createDirIfNotExist("public")
posts := readMarkdownPosts("posts")
createIndexPage(posts)
}
Styling Our Site
elem-go
's styles
subpackage lets you create groups of style properties in a type-safe way. Let's create some styles to make our site look better:
headerStyle := styles.Props{
styles.BackgroundColor: "lightblue",
styles.Color: "white",
styles.Padding: "10px",
styles.TextAlign: "center",
}
footerStyle := styles.Props{
styles.BackgroundColor: "lightgrey",
styles.Color: "black",
styles.Padding: "10px",
styles.TextAlign: "center",
}
mainStyle := styles.Props{
styles.Padding: "20px",
}
Then we'll use the attrs
subpackage to add them as an inline-style and convert the style object to an inline style string using ToInline()
:
elem.Body(nil,
elem.Header(attrs.Props{attrs.Style: headerStyle.ToInline()},
elem.H1(nil, elem.Text(title)),
), elem.Main(attrs.Props{attrs.Style: mainStyle.ToInline()},
content,
), elem.Footer(attrs.Props{attrs.Style: footerStyle.ToInline()},
elem.Text("Footer content here"),
),),
Opening Our Site
We did it! We now have a static site. Open public/index.html
in your browser to see it working:
open public/index.html
Or if you're on Windows, run:
start public\index.html
Top comments (1)
I'm really impressed. Using the Markdown pages I have to hand, it seems to work really well.