1. Code
  2. Coding Fundamentals

Building a CMS: goPress

With Virtual Private Servers getting more affordable, it is easier than ever to set up a unique Content Management System for a web site. In this tutorial, I will use the site layout and theming given in the first tutorial to make a simple Markdown, HTML, or Amber powered, flat file system CMS using the Go language from Google.
Scroll to top
34 min read
This post is part of a series called Building a CMS.
Building a CMS: Structure and Styling

Go is a network-centric programming language developed by Google that makes writing network related programs easy. With the many great libraries from which to choose, getting a web application running is a snap.

In this tutorial, I am going to create a Content Management System (CMS) using Go and some helper libraries. This CMS will use the site data structure as laid out in the first tutorial, Building a CMS: Structure and Styling.

Development Setup With Go

The easiest way to install the go programming language on a Mac is with Homebrew. If you haven't installed Homebrew yet, the tutorial Homebrew Demystified: OS X's Ultimate Package Manager will show you how. For other platforms, just follow the instructions on the Go download page.

In a terminal, type:

1
brew install go

In your home directory, create the directory go. The Go language will store all downloaded libraries there. Add to your .bashrc file and/or .zshrc file this line:

1
export GOPATH="/Users/<your user name>/go"

If you're using fish, add this to your config.fish file:

1
set -xg GOPATH "/Users/<your user name>/go"

Next, you need to install the libraries. The goWeb library supplies the web server framework, the amber library gives the Jade equivalent HTML preprocessor, and BlackFriday translates Markdown to proper HTML. Also, I use the Handlebars library for templating. To install these libraries, you need to type the following in the project directory:

1
go get github.com/hoisie/web
2
go get github.com/eknkc/amber
3
go get github.com/russross/blackfriday
4
go get github.com/murz/go-handlebars/handlebars

Now that Go and the needed libraries are on your system, the next step is to start coding. The goPress library has five files, while the main program is one file to call the library functions.

goPress.go Library File

I wanted the server to be as fast as possible. To achieve this, anything that is reusable is in memory. Therefore, a global variable keeps all of the assets and home page. You could design the server with all assets in memory, but this would cause memory bloating on large sites. Since the home page should be the most frequently loaded page, it is also kept in memory.

Using the file structure set up in the last tutorial, create a new directory in the src directory called goPress. This will be where all goPress library files are placed. The first file is goPress.go. Create the file and start to put in the following code.

1
package goPress
2
3
//
4
// Package:          goPress
5
//
6
// Description:        This package is for the goPress CMS
7
//                     written in the go programming
8
//                    language made by Google. This package
9
//                     defines everything for a full
10
//                    CMS.
11
//
12
13
//
14
// Import the libraries we use for this program.
15
//
16
import (
17
    "encoding/json"
18
    "github.com/hoisie/web"
19
    "io/ioutil"
20
    "log"
21
    "os"
22
    "strings"
23
)
24
25
//
26
// Define a structure to contain all the information
27
// important to the CMS. Capticalized
28
// variables within the structure is imported 
29
// and exported.
30
//
31
type goPressData struct {
32
    CurrentLayout  string
33
    CurrentStyling string
34
    ServerAddress  string
35
    SiteTitle      string
36
    Sitebase       string
37
    TemplatBase    string
38
    CapatchaWidth  int
39
    CapatchaHeight int
40
    Cache          bool
41
    MainBase       string
42
    content        map[string]string
43
    layoutBase     string
44
    mainpg         string
45
    postbase       string
46
    scripts        string
47
    stylesheet     string
48
    stylingBase    string
49
    template       string
50
}
51
52
var SiteData = new(goPressData)
53
var ServerParamFile string = "server.json"

The package statement at the top tells the compiler that this file is a part of a package library and gives the name of the library. Every file in this directory has to have this at the top to be a part of the file.

Next, you import all libraries referenced in this file. If you list a library and do not use it, the compiler will complain. This helps keep your code clean and tidy.

After loading the libraries, I define the different data structures and global variables that the CMS will use. These globals are library global. Outside the library, variables that begin with a capital letter can be referenced if the library name scopes it.

Next, add this function to the same file:

1
//
2
// Function:        GetGlobals
3
//
4
// Description:        This function is used to create the
5
//                     global variables initialize the
6
//                    global variables.
7
//
8
// Inputs:
9
//
10
func GetGlobals() {
11
    //
12
    // Load the Server Parameters from a file.
13
    //
14
    LoadServerParameters()
15
16
    //
17
    // Setup the basic paths to everything.
18
    //
19
    SiteData.layoutBase = SiteData.TemplatBase + "layouts/"
20
    SiteData.stylingBase = SiteData.TemplatBase + "styling/"
21
    SiteData.postbase = SiteData.Sitebase + "posts/"
22
23
    //
24
    // Create the content array that will hold the site 
25
    // fragments. Set the title now.
26
    //
27
    SiteData.content = make(map[string]string)
28
    SiteData.content["title"] = SiteData.SiteTitle
29
30
    //
31
    // Log that the data is being loaded.
32
    //
33
    log.Println("Loading data for site: " + SiteData.SiteTitle)
34
35
    //
36
    // Get all the basic information that is generic and 
37
    // in the styles and layout directories.
38
    // These will then be over written if a new default 
39
    // in the site area is found. This gives
40
    // the flexibility to load defaults from a directory 
41
    // without having to make sure that all
42
    // the necessary ones are loaded.
43
    //
44
45
    //
46
    // Get the 404 page contents
47
    //
48
    SiteData.content["404"] = GetPageContents(SiteData.stylingBase + SiteData.CurrentStyling + "/404")
49
50
    //
51
    // Get the sidebar contents
52
    //
53
    SiteData.content["sidebar"] = GetPageContents(SiteData.stylingBase + SiteData.CurrentStyling + "/sidebar")
54
55
    //
56
    // Get the footer contents
57
    //
58
    SiteData.content["footer"] = GetPageContents(SiteData.stylingBase + SiteData.CurrentStyling + "/footer")
59
60
    //
61
    // Get the template contents
62
    //
63
    SiteData.template = GetPageContents(SiteData.layoutBase + SiteData.CurrentLayout + "/template")
64
65
    //
66
    // Get the header contents
67
    //
68
    SiteData.content["header"] = GetPageContents(SiteData.stylingBase + SiteData.CurrentStyling + "/header")
69
70
    //
71
    // Get the main page contents
72
    //
73
    SiteData.mainpg = GetPageContents(SiteData.Sitebase + "pages/" + "main")
74
75
    //
76
    // The following will load page parts from the 
77
    // "parts" directory for the site. These might
78
    // overload those already defined or add new stuff 
79
    // that the users site templates
80
    // will need.
81
    //
82
    partsdir := SiteData.Sitebase + "parts/"
83
84
    //
85
    // Read the directory.
86
    //
87
    fileList, err := ioutil.ReadDir(partsdir)
88
    if err != nil {
89
        //
90
        // Error reading the directory.
91
        //
92
        log.Printf("Error reading directory: %s\n", partsdir)
93
    } else {
94
        //
95
        // Get the number of items in the directory list.
96
        //
97
        count := len(fileList)
98
99
        //
100
        // Loop through each directory element.
101
        //
102
        for i := 0; i < count; i++ {
103
            if !fileList[i].IsDir() {
104
                //
105
                // It is a file. Read it and add to the 
106
                // scripts variable.
107
                //
108
                filename := fileList[i].Name()
109
                parts := strings.Split(filename, ".")
110
                if filename != ".DS_Store" {
111
                    SiteData.content[parts[0]] = LoadFile(partsdir + filename)
112
                }
113
            }
114
        }
115
    }
116
117
    //
118
    // Clear out the global variables not set.
119
    //
120
    SiteData.scripts = ""
121
    SiteData.stylesheet = ""
122
}

The GetGlobals function loads all the globally stored information for the site. A hash map based on file name (without extension) stores the data from the server file, the layouts directory, and the styles directory. Then, everything in the site/parts directory is put into the same structure. This way, if the site just wants the defaults given in the theme, the user doesn't have to put a file for it in the site/parts directory.

In the same file, add these functions:

1
//
2
// Function:        SaveServerParameters
3
//
4
// Description:        This function is for saving the 
5
//                     authorization secret for DropBox.
6
//
7
// Inputs:
8
//
9
func SaveServerParameters() {
10
    if wfile, err := os.Create(ServerParamFile); err == nil {
11
        enc := json.NewEncoder(wfile)
12
        enc.Encode(&SiteData)
13
        wfile.Close()
14
    } else {
15
        log.Println("Writing Server file denied.")
16
    }
17
}
18
19
//
20
// Function:        LoadServerParameters
21
//
22
// Description:        This function is used to load the 
23
//                    parameters for this server.
24
//
25
// Inputs:
26
//
27
func LoadServerParameters() {
28
    if wfile, err := os.Open(ServerParamFile); err == nil {
29
        enc := json.NewDecoder(wfile)
30
        enc.Decode(&SiteData)
31
        wfile.Close()
32
        log.Println("Read the " + ServerParamFile + " server parameter file. Site Title is: " + SiteData.SiteTitle)
33
    } else {
34
        log.Println("No Server File found.")
35
    }
36
}

These are the helper functions SaveServerParameters() and LoadServerParameters(). These functions save and load the different server settings to the server.json file.

The next functions are for creating routes and our default routes. Add these functions to the same file:

1
//
2
// Function:        DefaultRoutes
3
//
4
// Description:        This function sets the default 
5
//                    routes for a CMS.
6
//
7
// Inputs:
8
//
9
func DefaultRoutes() {
10
    SetGetRoute("/", Mainpage)
11
    SetGetRoute("/sitemap.xml", SiteMap)
12
    SetGetRoute("/stylesheets.css", GetStylesheets)
13
    SetGetRoute("/scripts.js", GetScripts)
14
    SetGetRoute("/theme/images/(.*)", LoadThemeImage)
15
    SetGetRoute("/(favicon.ico)", ImagesLoad)
16
    SetGetRoute("/images/(.*)", ImagesLoad)
17
    SetGetRoute("/posts/([a-zA-Z0-9]*)/([a-zA-Z0-9]*)", PostIndex)
18
    SetGetRoute("/posts/([a-zA-Z0-9]*)/([a-zA-Z0-9]*)/(.*)", PostPages)
19
    SetGetRoute("/(.*)", TopPages)
20
}
21
22
//
23
// Function:        SetGetRoute
24
//
25
// Description:        This function gives an easy access 
26
//                    to the web variable setup in this 
27
//                    library.
28
//
29
// Inputs:
30
//         route         Route to setup
31
//        handler        Function to run that route.
32
//
33
func SetGetRoute(route string, handler interface{}) {
34
    web.Get(route, handler)
35
}
36
37
//
38
// Function:        StartServer
39
//
40
// Description:        This function is for starting the web 
41
//                    server using the SiteData 
42
//                    configuration.
43
//
44
// Inputs:
45
//
46
func StartServer(serverAddress string) {
47
    web.Run(serverAddress)
48
}

The DefaultRoutes() function creates the default routes to use in our CMS. The functions for these routes are in the other library files. The SetGetRoute() creates each route. This is simply a wrapper over the goWeb library function that takes a regular expression to define the format of the route and a function to execute when that expression is true. If you've ever used the Sinatra framework for Ruby or the Express framework for Node.js, then you will be familiar with this setup.

The creation order for the routes is important. If the first route contains a regular expression to match everything, then the rest of the routes aren't accessible. The first route would catch them all. Therefore, I defined the most specific routes first, and the more general routes last.

The StartServer() function starts the web server. It calls the goWeb function Run() that takes the address for the server.

Throughout the code, I make good use of the log.PrintLn() function. This prints to the console the message given with a date and time stamp. This is great for debugging, but is also used for traffic analysis.

PagesPosts.go Library File

Next, create the PagesPosts.go file in the same directory. This file will contain all the code for working with pages and post types. A page is simply a web page. A post is anything created over time: news posts, blog posts, tutorials, etc. In this file, add the following code:

1
package goPress
2
3
import (
4
    "bytes"
5
    "encoding/json"
6
    "github.com/eknkc/amber"
7
    "github.com/hoisie/web"
8
    "github.com/murz/go-handlebars/handlebars"
9
    "github.com/russross/blackfriday"
10
    "io/ioutil"
11
    "log"
12
    "os"
13
    "strings"
14
    "time"
15
)

As in the goPress.go file, it starts with the package declaration and the list of libraries to import. This file will make use of every library we downloaded for go.

1
//
2
// Function:         Mainpage
3
//
4
// Description:     This function is used to generate 
5
//                     and display the main page for the
6
//                     web site. This function will guide 
7
//                     the user to setup the DropBox
8
//                     account if this is the first time 
9
//                     being ran or the dropbox 
10
//                     authorization secret gets zeroed.
11
//
12
// Inputs:
13
//                    ctx     Contents from the request
14
//
15
func Mainpage(ctx *web.Context) string {
16
    //
17
    // Render the main page.
18
    //
19
    page := RenderPageContents(ctx, SiteData.mainpg, SiteData.Sitebase+"pages/main")
20
21
    return page
22
}

The Mainpage() function shows the front page of the site. It is simply a wrapper for the RenderPageContents() function specifying the main index page to be rendered. RenderPageContents() does all the real work.

1
//
2
// Function:         SiteMap
3
//
4
// Description:     This function is to give a site map 
5
//                    to requesters.
6
//
7
// Inputs:
8
//                    ctx     Contents from the request
9
//
10
func SiteMap(ctx *web.Context) string {
11
    var contents string
12
13
    wfile, err := os.Open(SiteData.Sitebase + "sitemap.xml")
14
    if err == nil {
15
        bcontents, _ := ioutil.ReadAll(wfile)
16
        contents = string(bcontents)
17
        wfile.Close()
18
    }
19
    return contents
20
}

The SiteMap() function gives the sitemap to the requester. It pulls the information from the sitemap.xml at the top of the site directory.

1
//
2
// Function:         PostPages
3
//
4
// Description:     This function generates the needed 
5
//                    post page.
6
//
7
// Inputs:
8
//                    ctx        What the browser sends
9
//                    posttype   The type of post
10
//                    postname   The name of the post 
11
//                                    type instance
12
//                    val        The name of the post 
13
//                                    page to display
14
//
15
func PostPages(ctx *web.Context, posttype string, postname string, val string) string {
16
    //
17
    // Get the page contents and process it.
18
    //
19
    pgloc := SiteData.postbase + posttype + "/" + postname + "/" + val
20
    return RenderPageContents(ctx, GetPageContents(pgloc), pgloc)
21
}

The PostPages() function displays the proper post requested. Once again, this just sets up the call to the RenderPageContents() function, which does all the main work.

1
//
2
// Function:          PostIndex
3
//
4
// Description:       This function generates the needed post index.
5
//
6
// Inputs:
7
//                    ctx       What the browser sends
8
//                    posttype  The type of post
9
//                    postname  The name of the post type instance
10
//
11
func PostIndex(ctx *web.Context, posttype string, postname string) string {
12
    //
13
    // Get the page contents and process it.
14
    //
15
    pgloc := SiteData.postbase + posttype + "/" + postname + "/index"
16
    return RenderPageContents(ctx, GetPageContents(pgloc), pgloc)
17
}

The PostIndex() function pulls together the information for a post index and gives it to the RenderPageContents() function.

1
//
2
// Function:         topPages
3
//
4
// Description:     This function will generate a 
5
//                    "static" top level page that is not
6
//                     a post page.
7
//
8
// Inputs:
9
//                    val         The name of the top level page
10
//
11
func TopPages(ctx *web.Context, val string) string {
12
    //
13
    // Look for the markdown of the page.
14
    //
15
    pgloc := SiteData.Sitebase + "pages/" + val
16
    return RenderPageContents(ctx, GetPageContents(pgloc), pgloc)
17
}

The topPages() function sets up the RenderPageContents() function for a standard page. All pages are in the pages/ directory.

1
//
2
// Function:         GetPageContents
3
//
4
// Description:     This function is used to retrieve 
5
//                    the page contents. It will first look 
6
//                    for a markdown page, then for a html 
7
//                    page, and then it looks for an amber
8
//                    page.
9
//
10
// Inputs:
11
//                    filename         The name of the file
12
//
13
func GetPageContents(filename string) string {
14
    //
15
    // Assume the page can not be found.
16
    //
17
    contents := SiteData.content["404"]
18
19
    //
20
    // Let's look for a markdown version first.
21
    //
22
    wfile, err := os.Open(filename + ".md")
23
    if err == nil {
24
        bcontents, _ := ioutil.ReadAll(wfile)
25
        wfile.Close()
26
        contents = string(blackfriday.MarkdownCommon(bcontents))
27
        //
28
        // Double quotes were turned into &ldquo; and
29
        // &rdquo;. Turn them back. Without this, and 
30
        // Handlebar macros will be broken.
31
        //
32
        contents = strings.Replace(contents, "&ldquo;", "\"", -1)
33
        contents = strings.Replace(contents, "&rdquo;", "\"", -1)
34
    } else {
35
        //
36
        // It must be an html. Look for that.
37
        //
38
        wfile, err = os.Open(filename + ".html")
39
        if err == nil {
40
            bcontents, _ := ioutil.ReadAll(wfile)
41
            contents = string(bcontents)
42
            wfile.Close()
43
        } else {
44
            //
45
            // It must be an amber. Look for that.
46
            //
47
            wfile, err = os.Open(filename + ".amber")
48
            if err == nil {
49
                wfile.Close()
50
                template, err2 := amber.CompileFile(filename+".amber", amber.Options{true, false})
51
                if err2 != nil {
52
                    //
53
                    // Bad amber file.
54
55
                    log.Println("Amber file bad: " + filename)
56
                } else {
57
                    //
58
                    // Put the default site info.
59
                    //
60
                    pgData := SiteData.content
61
62
                    //
63
                    // read in that pages specific data 
64
                    // to be added to the rest
65
                    // of the data. It is stored at the 
66
                    // same place, but in a json
67
                    // file.
68
                    //
69
                    if wfile, err := os.Open(filename + ".json"); err == nil {
70
                        //
71
                        // Load the json file of extra 
72
                        // data for this page. This could 
73
                        // override the standard data as 
74
                        // well.
75
                        //
76
                        enc := json.NewDecoder(wfile)
77
                        enc.Decode(&pgData)
78
                        wfile.Close()
79
                    } else {
80
                        log.Println("The page: " + filename + " did not have a json file.")
81
                    }
82
83
                    pgData["PageName"] = filename
84
85
                    //
86
                    // The amber source compiles okay. 
87
                    // Run the template and return
88
                    // the results.
89
                    //
90
                    var b bytes.Buffer
91
                    template.Execute(&b, pgData)
92
                    contents = b.String()
93
                }
94
            } else {
95
                //
96
                // A file could not be found.
97
                //
98
                log.Println("Could not find file:  " + filename)
99
            }
100
        }
101
    }
102
103
    //
104
    // Return the file contains obtained.
105
    //
106
    return contents
107
}

The GetPageContents() function loads the contents of all of the pages/posts. It first loads the 404 not found page contents from the global data structure. The function then looks for a Markdown file first, then a HTML file, and then an Amber file. Next, the routine converts all Markdown and Amber content to HTML. The Amber file might have associated data in a JSON file. That data file is also loaded for the processing of the Amber file.

The Markdown processing in Blackfriday has a consequence for the Handlebars processor. The Blackfriday markdown to HTML processor changes all double quotes to its HTML escaped equivalent (&ldquo; and &rdquo;). Since that is not 100% necessary for rendering, I reversed the change afterwards. This keeps all Handlebars macros that use double quotes functional.

If you want more file format types, just add them here. This routine loads every content type.

1
//
2
// Function:         RenderPageContents
3
//
4
// Description:     This function is used to process 
5
//                    and render the contents of a page.
6
//                     It can be the main page, or a post 
7
//                    page, or any page. It accepts the
8
//                     input as the contents for the page 
9
//                    template, run the page template
10
//                     with it, process all shortcodes and 
11
//                    embedded codes, and return the
12
//                     results.
13
//
14
// Inputs:
15
//                ctx                The calling context
16
//                 contents         The pages main contents.
17
//                filename        The name of the file the 
18
//                                contents was taken from.
19
//
20
func RenderPageContents(ctx *web.Context, contents string, filename string) string {
21
    //
22
    // Set the header information
23
    //
24
    SetStandardHeader(ctx)
25
26
    //
27
    // Put the default site info.
28
    //
29
    pgData := SiteData.content
30
31
    //
32
    // Add data specific to this page.
33
    //
34
    pgData["content"] = contents
35
36
    //
37
    // read in that pages specific data to be added to 
38
    // the rest of the data. It is stored at the same 
39
    // place, but in a json file.
40
    //
41
    if wfile, err := os.Open(filename + ".json"); err == nil {
42
        //
43
        // Load the json file of extra data for this 
44
        // page. This could override the standard data as 
45
        // well.
46
        //
47
        enc := json.NewDecoder(wfile)
48
        enc.Decode(&pgData)
49
        wfile.Close()
50
    } else {
51
        log.Println("The page: " + filename + " did not have a json file.")
52
    }
53
54
    //
55
    // Set the Page Name data field.
56
    //
57
    pgData["PageName"] = filename
58
59
    //
60
    // Register the helpers.
61
    //
62
    // NOTICE:    All helpers can not have spaces in the 
63
    //            parameter. Therefore, all of these 
64
    //            helpers assume a "-" is a space. It gets 
65
    //            translated to a space before using.
66
    //
67
    // Helper:             save
68
    //
69
    // Description:     This helper allows you do define 
70
    //                    macros for expanding inside the 
71
    //                    template. You give it a name, 
72
    //                    "|", and text to expand into. 
73
    //                    Currently, all spaces have to be 
74
    //                    "-".
75
    //
76
    handlebars.RegisterHelper("save", func(params ...interface{}) string {
77
        if text, ok := params[0].(string); ok {
78
            parts := strings.Split(text, "|")
79
            content := strings.Replace(parts[1], "-", " ", -1)
80
            pgData[parts[0]] = content
81
            return content
82
        }
83
        return ""
84
    })
85
86
    //
87
    //  The format has to use these sets of constants:
88
    //            Stdlongmonth = "January"
89
    //            Stdmonth = "Jan"
90
    //            Stdnummonth = "1"
91
    //            Stdzeromonth = "01"
92
    //            Stdlongweekday = "Monday"
93
    //            Stdweekday = "Mon"
94
    //            Stdday = "2"
95
    //            Stdunderday = "_2"
96
    //            Stdzeroday = "02"
97
    //            Stdhour = "15"
98
    //            stdHour12 = "3"
99
    //            stdZeroHour12 = "03"
100
    //            Stdminute = "4"
101
    //            Stdzerominute = "04"
102
    //            Stdsecond = "5"
103
    //            Stdzerosecond = "05"
104
    //            Stdlongyear = "2006"
105
    //            Stdyear = "06"
106
    //            Stdpm = "Pm"
107
    //            Stdpm = "Pm"
108
    //            Stdtz = "Mst"
109
    //
110
    //  Helper:            date
111
    //
112
    //  Description:     This helper prints the current 
113
    //                    date/time in the format
114
    //                     given. Please refer to the above 
115
    //                    chart for proper format codes. 
116
    //                    EX:  07/20/2015 is "01/02/2006"
117
    //
118
    handlebars.RegisterHelper("date", func(params ...interface{}) string {
119
        if format, ok := params[0].(string); ok {
120
            format = strings.Replace(format, "-", " ", -1)
121
            tnow := time.Now()
122
            return tnow.Format(format)
123
        }
124
        return ""
125
    })
126
127
    //
128
    // Render the current for the first pass.
129
    //
130
    page := handlebars.Render(SiteData.template, pgData)
131
132
    //
133
    // Process any shortcodes on the page.
134
    //
135
    page1 := ProcessShortCodes(page)
136
137
    //
138
    // Render new content from Short Code and filters.
139
    //
140
    page2 := handlebars.Render(page1, pgData)
141
142
    //
143
    // Return the results.
144
    //
145
    return page2}

RenderPageContents() is the main function used to create a web page. After it sets the standard header for the reply, this routine creates a data structure and fills it with the default contents, page contents, and an associated JSON file for the page. The Handlebars templater uses the data structure to render the entire page.

Next, the routine defines all of the Handlebars helper functions. Currently, there are two: save helper and date helper. If you would like more helper functions, this is where you would add them to your project.

The save helper takes two parameters: a name separated from contents by a |. Since the Handlebars helper parameters cannot contain spaces, the parameters use a - instead of a space. This allows you to create per page template variables inside the context of the page. For example, the {{save site|Custom-Computer-Tools}} macro will place Custom Computer Tools at the point of definition and anywhere else on the page that has {{site}}.

The date helper takes a format string and creates the proper date according to that format string. For example, the {{date January-2,-2006}} macro produces October 13, 2015 on that day.

The Handlebars templater processes the page twice: before shortcodes are rendered in case any shortcodes are in the template expansion, and after running the shortcodes in case a shortcode adds any Handlebars template actions. By the end, the function returns the full HTML contents for the requested page.

1
//
2
// Function:         SetStandardHeader
3
//
4
// Description:     This function is used as a one place 
5
//                    for setting the standard
6
//                     header information.
7
//
8
// Inputs:
9
//
10
func SetStandardHeader(ctx *web.Context) {
11
    //
12
    // Set caching for the item
13
    //
14
    ctx.SetHeader("Cache-Control", "public", false)
15
16
    //
17
    // Set the maximum age to one month (30 days)
18
    //
19
    ctx.SetHeader("Cache-Control", "max-age=2592000", false)
20
21
    //
22
    // Set the name to gpPress for the server type.
23
    //
24
    ctx.SetHeader("Server", "goPress - a CMS written in go from Custom Computer Tools: http://customct.com.", true)
25
}

The SetStandardHeader() function sets any custom header items into the reply. This is where you set the server information and any caching controls.

Images.go Library File

The next file to work on is the Images.go file and all the functions needed to send an image to the browser. Since this will be a full web server, it has to deal with the binary data of sending an image. Create the file Images.go and put this code in it:

1
package goPress
2
3
import (
4
    "github.com/hoisie/web"
5
    "io"
6
    "io/ioutil"
7
    "log"
8
    "math/big"
9
    "os"
10
    "path/filepath"
11
)
12
13
//
14
// Function:          ImagesLoad
15
//
16
// Description:       This function is called to upload an image for the
17
//                    images directory.
18
//
19
// Inputs:
20
//                    val        Name of the image with relative path
21
//
22
func ImagesLoad(ctx *web.Context, val string) {
23
    LoadImage(ctx, SiteData.Sitebase+"images/"+val)
24
}
25
26
//
27
// Function:         LoadThemeImage
28
//
29
// Description:         This function loads images from the theme's directory.
30
//
31
// Inputs
32
//                     image        Name of the image file to load
33
//
34
func LoadThemeImage(ctx *web.Context, image string) {
35
    LoadImage(ctx, SiteData.stylingBase+SiteData.CurrentStyling+"/images/"+image)
36
}

This library file starts just like the others: a package declaration and the library declarations. The ImagesLoad() function and the LoadThemeImage() function set up a call to the LoadImage() function to do the actual work. These functions allow for the loading of images from the site directory or from the current theme directory.

1
//
2
// Function:         LoadImage
3
//
4
// Description:     This function does the work of 
5
//                    loading an image file and passing it 
6
//                    on.
7
//
8
// Inputs:
9
//
10
func LoadImage(ctx *web.Context, val string) {
11
    //
12
    // Get the file extension.
13
    //
14
    fileExt := filepath.Ext(val)
15
16
    //
17
    // Set the http header based on the file type.
18
    //
19
    SetStandardHeader(ctx)
20
    ctx.ContentType(fileExt)
21
    if fileExt == ".svg" {
22
        //
23
        // This is a text based file. Read it and send to the browser.
24
        //
25
        wfile, err := os.Open(val)
26
        if err == nil {
27
            bcontents, _ := ioutil.ReadAll(wfile)
28
            wfile.Close()
29
            ctx.WriteString(string(bcontents))
30
        }
31
    } else {
32
        //
33
        // This is a binary based file. Read it and sent the contents to the browser.
34
        //
35
        fi, err := os.Open(val)
36
37
        //
38
        // Set the size of the binary coming down the pipe. Chrome has to have this value
39
        // one larger than real.
40
        //
41
        finfo, _ := os.Stat(val)
42
        i := big.NewInt(finfo.Size())
43
        ctx.SetHeader("Accept-Ranges", "bytes", true)
44
        ctx.SetHeader("Content-Length", i.String(), true)
45
46
        if err != nil {
47
            log.Println(err)
48
            return
49
        }
50
        defer fi.Close()
51
52
        //
53
        // Create a buffer to contain the image data. Binary images usually get
54
        // very big.
55
        //
56
        buf := make([]byte, 1024)
57
58
        //
59
        // Go through the binary file 1K at a time and send to the browser.
60
        //
61
        for {
62
            //
63
            // Read a buffer full.
64
            //
65
            n, err := fi.Read(buf)
66
            if err != nil && err != io.EOF {
67
                log.Println(err)
68
                break
69
            }
70
71
            //
72
            // If nothing was read, then exit.
73
            //
74
            if n == 0 {
75
                break
76
            }
77
78
            //
79
            // Write the binary buffer to the browser.
80
            //
81
            n2, err := ctx.Write(buf[:n])
82
            if err != nil {
83
                log.Println(err)
84
                break
85
            } else if n2 != n {
86
                log.Println("Error in sending " + val + " to the browser. Amount read does not equal the amount sent.")
87
                break
88
            }
89
        }
90
    }
91
}

The LoadImage() function checks the image type. If it is a svg file, then it loads as plain text. Assuming all other file types are binary files, the routine loads them more carefully. It will upload binary files in 1K blocks.

StyleSheetScripts.go Library File

The next file is for loading CSS and JavaScript. Since our build script compiles all the CSS and JavaScript to a single file each, these functions are really simple. Create the file StyleSheetScripts.go and add these lines:

1
package goPress
2
3
import (
4
    "github.com/hoisie/web"
5
    "io/ioutil"
6
    "log"
7
    "os"
8
)
9
10
//
11
// Function:         GetStylesheets
12
//
13
// Description:     This function is used to produce 
14
//                    the stylesheet to the user.
15
//
16
// Inputs:
17
//
18
func GetStylesheets(ctx *web.Context) string {
19
    //
20
    // See if we have already loaded them or not. If so, 
21
    // just return the pre-loaded stylesheet.
22
    //
23
    ctx.SetHeader("Content-Type", "text/css", true)
24
    SetStandardHeader(ctx)
25
    tmp := ""
26
    if SiteData.stylesheet == "" {
27
        tmp = LoadFile(SiteData.Sitebase + "css/final/final.css")
28
        //
29
        // If we are testing, we do not want the server 
30
        // to cache the stylesheets. Therefore, if 
31
        // cache is true, cache them. Otherwise,
32
        // do not.
33
        //
34
        if SiteData.Cache == true {
35
            SiteData.stylesheet = tmp
36
        }
37
    } else {
38
        //
39
        // We have a cached style sheet. Send it to the 
40
        // browser.
41
        //
42
        tmp = SiteData.stylesheet
43
    }
44
45
    //
46
    // Return the stylesheet.
47
    //
48
    return tmp
49
}
50
51
//
52
// Function:         GetScripts
53
//
54
// Description:     This function is to load JavaScripts 
55
//                    to the browser. This will
56
//                     actually load all the JavaScript 
57
//                    files into one compressed file
58
//                     for uploading to the browser.
59
//
60
// Inputs:
61
//
62
func GetScripts(ctx *web.Context) string {
63
    //
64
    // See if we have already loaded them or not. If so, 
65
    // just return the pre-loaded scripts.
66
    //
67
    ctx.SetHeader("Content-Type", "text/javascript", true)
68
    SetStandardHeader(ctx)
69
    tmp := ""
70
    if SiteData.scripts == "" {
71
        tmp = LoadFile(SiteData.Sitebase + "js/final/final.js")
72
73
        //
74
        // If we are testing, we do not want the server 
75
        // to cache the scripts. Therefore, if cache is 
76
        // true, cache them. Otherwise, do not.
77
        //
78
        if SiteData.Cache == true {
79
            SiteData.scripts = tmp
80
        }
81
    } else {
82
        //
83
        // We have a cached style sheet. Send it to the 
84
        // browser.
85
        //
86
        tmp = SiteData.scripts
87
    }
88
89
    //
90
    // Return the resulting compiled stylesheet.
91
    //
92
    return tmp
93
}
94
95
//
96
// Function:         LoadFile
97
//
98
// Description:     This function if for loading 
99
//                    individual file contents.
100
//
101
// Inputs
102
//                     file         name of the file to be 
103
//                                loaded
104
//
105
func LoadFile(file string) string {
106
    ret := ""
107
    log.Println("Loading file: " + file)
108
    wfile, err := os.Open(file)
109
    if err == nil {
110
        bcontents, err := ioutil.ReadAll(wfile)
111
        err = err
112
        ret = string(bcontents)
113
        wfile.Close()
114
    } else {
115
        //
116
        // Could not read the file.
117
        //
118
        log.Println("Could not read: " + file)
119
    }
120
    return ret
121
}

This file has three functions. The GetStylesheets() function loads the compiled CSS file. The GetScripts() function loads the compiled JavaScript file. With the cache flag set, both of these functions will cache the contents. I turn off the Cache flag while testing. The LoadFile() function is a simple file loading function for getting the file contents.

Shortcodes.go Library File

While I wanted a fast server, I also want a lot of flexibility. To achieve the flexibility, there are two different types of macro expansions: direct Handlebar expansion, and shortcode expansion.

The difference is that the Handlebars expansion is a simple, low-logic expansion, while the shortcode expansion is anything that you can program into the system: downloading information from an external site, processing information with an external program, or just about anything.

Create the Shortcodes.go file and place this into it:

1
package goPress
2
3
//
4
// Library:         Shortcodes
5
//
6
// Description:     This library gives the functionality 
7
//                    of shortcodes in the CMS. A shortcode 
8
//                    runs a function with specified 
9
//                    argument and a surrounded contents. 
10
//                    The function should process the 
11
//                    contents according to the arguments 
12
//                    and return a string for placement 
13
//                    into the page. Shortcodes are 
14
//                    recursively processed, therefore you 
15
//                    can have different shortcodes inside 
16
//                    of other shortcodes.  You can not 
17
//                    have the same shortcode inside of 
18
//                    itself.
19
//
20
import (
21
    "bytes"
22
    "log"
23
    "regexp"
24
)
25
26
//
27
// Type:             ShortcodeFunction
28
//
29
// Description:     This type defines a function that 
30
//                    implements a shortcode. The function
31
//                     should receive two strings and return 
32
//                    a string.
33
//
34
type ShortcodeFunction func(string, string) string
35
36
//
37
// Library Variables:
38
//
39
//             shortcodeStack         This array of functions 
40
//                                holds all of the 
41
//                                shortcodes usable by the 
42
//                                CMS. You add shortcodes 
43
//                                using the AddShortCode 
44
//                                function.
45
//
46
var (
47
    shortcodeStack map[string]ShortcodeFunction
48
)
49
50
//
51
// Library Function:
52
//
53
//                init         This function is called upon 
54
//                            library use to initialize any 
55
//                            variables used for the 
56
//                            library before anyone can 
57
//                            make a call to a library 
58
//                            function.
59
//
60
61
func init() {
62
    shortcodeStack = make(map[string]ShortcodeFunction)
63
}

This file starts like all the rest with a declaration of the package and libraries used. But it quickly is different with the defining of a special ShortcodeFunction variable type, a library variable, and an init() function. Library variables are only seen by a function of the library. This library variable, shortcodeStack, is a mapping of strings to a function.

The library init() functions allows you to run code before any other calls to the library. Here, I initialize the shortcodeStack data structure for holding the list of shortcodes.

1
//
2
// Function:         AddShortCode
3
//
4
// Description:     This function adds a new shortcode to 
5
//                    be used.
6
//
7
// Inputs
8
//         name           Name of the shortcode
9
//         funct          function to process the shortcode
10
//
11
func AddShortCode(name string, funct ShortcodeFunction) {
12
    shortcodeStack[name] = funct
13
}

The AddShortCode() function allows you to load a function for processing a shortcode into the library variable for all shortcodes.

1
//
2
// Function:        ProcessShortCodes
3
//
4
// Description:     This function takes in a string, 
5
//                    searches for shortcodes, process the 
6
//                    shortcode, put the results into the 
7
//                    string, and then return the fully 
8
//                    processed string.
9
//
10
// Inputs
11
//                page     String with possible shortcodes
12
//
13
func ProcessShortCodes(page string) string {
14
    //
15
    // Create a work buffer.
16
    //
17
    var buff bytes.Buffer
18
19
    //
20
    // Search for shortcodes. We first capture a 
21
    // shortcode.
22
    //
23
    r, err := regexp.Compile(`\-\[([^\]]*)\]\-`)
24
    if err == nil {
25
        match := r.FindString(page)
26
        if match != "" {
27
            //
28
            // Get the indexes to the matching area.
29
            //
30
            index := r.FindStringIndex(page)
31
32
            //
33
            // Save all the text before the shortcode 
34
            // into the buffer.
35
            //
36
            buff.WriteString(page[0:index[0]])
37
38
            //
39
            // Get everything that is left after the 
40
            // shortcode.
41
            //
42
            remander := page[index[1]:]
43
44
            //
45
            // Separate the strings out and setup 
46
            // variables for their contents.
47
            //
48
            submatch := r.FindStringSubmatch(match)
49
            name := ""
50
            contents := ""
51
            args := ""
52
53
            //
54
            // Get the name of the shortcode and separate 
55
            // the extra arguments.
56
            //
57
            r3, err3 := regexp.Compile(`(\w+)(.*)`)
58
            if err3 == nil {
59
                submatch2 := r3.FindStringSubmatch(submatch[1])
60
61
                //
62
                // The first submatch is the name of the 
63
                // shortcode.
64
                //
65
                name = submatch2[1]
66
67
                //
68
                // The second submatch, if any, are the 
69
                // arguments for the shortcode.
70
                //
71
                args = submatch2[2]
72
            } else {
73
                //
74
                // Something happened to the internal 
75
                // matching.
76
                //
77
                name = submatch[1]
78
                args = ""
79
            }
80
81
            //
82
            // Find the end of the shortcode.
83
            //
84
            final := "\\-\\[\\/" + name + "\\]\\-"
85
            r2, err2 := regexp.Compile(final)
86
            if err2 == nil {
87
                index2 := r2.FindStringIndex(remander)
88
                if index2 != nil {
89
                    //
90
                    // Get the contents and what is left 
91
                    // over after the closing of the 
92
                    // shortcode.
93
                    //
94
                    contents = remander[:index2[0]]
95
                    remander2 := remander[index2[1]:]
96
97
                    //
98
                    // If it is a real shortcode, then 
99
                    // run it!
100
                    //
101
                    if shortcodeStack[name] != nil {
102
                        //
103
                        // See if there is any shortcodes 
104
                        // inside the contents area.
105
                        //
106
                        contents = ProcessShortCodes(contents)
107
108
                        //
109
                        // Run the shortcode and add it's 
110
                        // result to the buffer.
111
                        //
112
                        buff.WriteString(shortcodeStack[name](args, contents))
113
                    }
114
115
                    //
116
                    // Process any remaining shortcodes.
117
                    //
118
                    buff.WriteString(ProcessShortCodes(remander2))
119
120
                } else {
121
                    //
122
                    // We have a bad shortcode 
123
                    // definition.  All shortcodes have 
124
                    // to be closed. Therefore,
125
                    // simply do not process anything and 
126
                    // tell the logs.
127
                    //
128
                    log.Println("Bad Shortcode definition. It was not closed.  Name:  " + name)
129
                    buff.WriteString(page[index[0]:index[1]])
130
                    buff.WriteString(ProcessShortCodes(remander))
131
                }
132
            } else {
133
                //
134
                // There was an error in the regular 
135
                // expression for closing the shortcode.
136
                //
137
                log.Println("The closing shortcode's regexp did not work!")
138
            }
139
        } else {
140
            //
141
            // No shortcodes, just copy the page to the 
142
            // buffer.
143
            //
144
            buff.WriteString(page)
145
        }
146
    } else {
147
        //
148
        // If the Regular Expression is invalid, tell the 
149
        // world!
150
        //
151
        log.Println("RegEx: Invalid expression.")
152
    }
153
154
    //
155
    // Return the resulting buffer.
156
    //
157
    return buff.String()
158
}

The ProcessShortCodes() function takes a string that is the web page contents and searches for all shortcodes in it. Therefore, if you have a shortcode called box, you would insert it in your webpage with this format:

1
-[box args="some items"]-
2
<p>This should be inside the box.</p>
3
-[/box]-

Everything after the space in the shortcode opener is the arguments for the shortcode to process. The formatting of the arguments is up to the shortcode function to process.

All short codes have to have a closing shortcode. What's inside the opening and closing shortcode is the process for shortcodes as well before sending to the shortcode processing function. I use the -[]- to define a shortcode so that inline JavaScript indexing doesn't get confused as a shortcode.

1
//
2
// Function:         ShortcodeBox
3
//
4
// Description:     This shortcode is used to put the 
5
//                    surrounded HTML in a box div.
6
//
7
// Inputs:
8
//            parms         The parameters used by the 
9
//                        shortcode
10
//            context        The HTML enclosed by the opening 
11
//                        and closing shortcodes.
12
//
13
func ShortcodeBox(parms string, context string) string {
14
    return ("<div class='box'>" + context + "</div>")
15
}
16
17
//
18
// Function:         ShortcodeColumn1
19
//
20
// Description:     This shortcode is used to put the 
21
//                    surrounded HTML in the first column.
22
//
23
// Inputs:
24
//            parms         The parameters used by the 
25
//                        shortcode
26
//            context        The HTML enclosed by the opening 
27
//                        and closing shortcodes.
28
//
29
func ShortcodeColumn1(parms string, context string) string {
30
    return ("<div class='col1'>" + context + "</div>")
31
}
32
33
//
34
// Function:         ShortcodeColumn2
35
//
36
// Description:     This shortcode is used to put the 
37
//                    surrounded HTML in the second column.
38
//
39
// Inputs:
40
//            parms         The parameters used by the 
41
//                        shortcode
42
//            context        The HTML enclosed by the opening 
43
//                        and closing shortcodes.
44
//
45
func ShortcodeColumn2(parms string, context string) string {
46
    return ("<div class='col2'>" + context + "</div>")
47
}
48
49
//
50
// Function:        ShortcodePHP
51
//
52
// Description:        This shortcode is for surrounding a 
53
//                    code block and formatting it's look 
54
//                    and feel properly. This one is for a 
55
//                    PHP code block.
56
//
57
// Inputs:
58
//            parms         The parameters used by the 
59
//                        shortcode
60
//            context        The HTML enclosed by the opening 
61
//                        and closing shortcodes.
62
//
63
func ShortcodePHP(params string, context string) string {
64
    return ("<div class='showcode'><pre type='syntaxhighlighter' class='brush: php'>" + context + "</pre></div>")
65
}
66
67
//
68
// Function:        ShortcodeJS
69
//
70
// Description:        This shortcode is for surrounding a 
71
//                    code block and formatting it's look 
72
//                    and feel properly. This one is for a 
73
//                    JavaScript code block.
74
//
75
// Inputs:
76
//            parms         The parameters used by the 
77
//                        shortcode
78
//            context        The HTML enclosed by the opening 
79
//                        and closing shortcodes.
80
//
81
func ShortcodeJS(params string, context string) string {
82
    return ("<div class='showcode'><pre type='syntaxhighlighter' class='brush: javascript'>" + context + "</pre></div>")
83
}
84
85
//
86
// Function:        ShortcodeHTML
87
//
88
// Description:        This shortcode is for surrounding a 
89
//                    code block and formatting it's look 
90
//                    and feel properly. This one is for a 
91
//                    HTML code block.
92
//
93
// Inputs:
94
//            parms         The parameters used by the 
95
//                        shortcode
96
//            context        The HTML enclosed by the opening 
97
//                        and closing shortcodes.
98
//
99
func ShortcodeHTML(params string, context string) string {
100
    return ("<div class='showcode'><pre type='syntaxhighlighter' class='brush: html'>" + context + "</pre></div>")
101
}
102
103
//
104
// Function:        ShortcodeCSS
105
//
106
// Description:        This shortcode is for surrounding a 
107
//                    code block and formatting it's look 
108
//                    and feel properly. This one is for a 
109
//                    CSS code block.
110
//
111
// Inputs:
112
//            parms         The parameters used by the 
113
//                        shortcode
114
//            context        The HTML enclosed by the opening 
115
//                        and closing shortcodes.
116
//
117
func ShortcodeCSS(params string, context string) string {
118
    return ("<div class='showcode'><pre type='syntaxhighlighter' class='brush: css'>" + context + "</pre></div>")
119
}
120
121
//
122
// Function:         LoadDefaultShortcodes
123
//
124
// Description:     This function is used to load in all 
125
//                    the default shortcodes.
126
//
127
// Inputs:
128
//
129
func LoadDefaultShortcodes() {
130
    AddShortCode("Box", ShortcodeBox)
131
    AddShortCode("Column1", ShortcodeColumn1)
132
    AddShortCode("Column2", ShortcodeColumn2)
133
    AddShortCode("php", ShortcodePHP)
134
    AddShortCode("javascript", ShortcodeJS)
135
    AddShortCode("html", ShortcodeHTML)
136
    AddShortCode("css", ShortcodeCSS)
137
}

The last section of code defines seven simple shortcodes and adds them to the shortcode array using the LoadDefaultShortcodes() function. If you want a different functionality, you just have to change this code and it will update it everywhere in your web site.

goPressServer.go Main Program File

The last file to create is the main program file. In the top of the development directory, create the file goPressServer.go and place this information:

1
package main
2
3
import (
4
    "./src/goPress"
5
)
6
7
//
8
// Function:         main
9
//
10
// Description:     This is the main function that is 
11
//                    called whenever the program is
12
//                     executed. It will load the globals, 
13
//                    set the different routes, and
14
//                     start the server.
15
//
16
// Inputs:
17
//
18
func main() {
19
    //
20
    // Load the default Shortcodes.
21
    //
22
    goPress.LoadDefaultShortcodes()
23
24
    //
25
    // Load all global variables.
26
    //
27
    goPress.GetGlobals()
28
29
    //
30
    // Setup the Default routes.
31
    //
32
    goPress.DefaultRoutes()
33
34
    //
35
    // Run the web server
36
    //
37
    goPress.StartServer(goPress.SiteData.ServerAddress)
38
}

The main() function is the routine called when the program runs. It will first set up the shortcodes, load in global variables, set the default routes, and then start the server.

Compiling and Running

To compile the whole program, move to the top directory where the goPressServer.go file is and type:

1
go build goPressServer.go

If all the files are in place, it should compile to goPressServer on the Mac and Linux systems, and goPressServer.exe on Windows.

Running goPressServer in the TerminalRunning goPressServer in the TerminalRunning goPressServer in the Terminal

When you execute the program in a terminal, you will see its log statements with the date and time as above.

The Front Page From the ServerThe Front Page From the ServerThe Front Page From the Server

If you open your browser to the server's address, you will get the front page. You will see the example shortcode and the two different Handlebars helper functions used. You now have your own web server!

As you can tell, I changed the front page and added three more pages to the original site design given in the tutorial Building a CMS: Structure. I also added the JavaScript library Syntax Highlighter in the site/js/ directory for displaying the code on the web page using the shortcode.

All of these changes are to show off the Handlebars and shortcode processing. But, due to Syntax Highlighter not working well with compression, I removed the JavaScript compression from the Gulp file. All of the changes are in this tutorial's download file.

There is a new course out, Go Fundamentals for Building Web Servers, that gives a great introduction to the Go language and how to program with it.

Conclusion

Now that you know how to build a simple yet powerful webserver using the go language, it's time for you to experiment. Create new pages, posts, embeddable parts, and shortcodes. This simple platform is far faster than using WordPress, and it is totally in your control. Tell me about your server in the comments below.