DEV Community

Chase Fleming
Chase Fleming

Posted on • Updated on

Building a Go Static Site Generator Using elem-go and goldmark

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Next, we need to install our dependencies:

go get github.com/chasefleming/elem-go
go get github.com/yuin/goldmark
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
)
Enter fullscreen mode Exit fullscreen mode

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()  
}
Enter fullscreen mode Exit fullscreen mode

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  
}
Enter fullscreen mode Exit fullscreen mode

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()  
}
Enter fullscreen mode Exit fullscreen mode

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  
}
Enter fullscreen mode Exit fullscreen mode

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)  
}
Enter fullscreen mode Exit fullscreen mode

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)  
       }    }}
Enter fullscreen mode Exit fullscreen mode

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)  
}
Enter fullscreen mode Exit fullscreen mode

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",  
}
Enter fullscreen mode Exit fullscreen mode

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"),  
    ),),
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Or if you're on Windows, run:

start public\index.html
Enter fullscreen mode Exit fullscreen mode

Top comments (0)