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:
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:
export GOPATH="/Users/<your user name>/go"
If you’re using fish, add this to your config.fish file:
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:
go get github.com/hoisie/web go get github.com/eknkc/amber go get github.com/russross/blackfriday 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.
package goPress // // Package: goPress // // Description: This package is for the goPress CMS // written in the go programming // language made by Google. This package // defines everything for a full // CMS. // // // Import the libraries we use for this program. // import ( "encoding/json" "github.com/hoisie/web" "io/ioutil" "log" "os" "strings" ) // // Define a structure to contain all the information // important to the CMS. Capticalized // variables within the structure is imported // and exported. // type goPressData struct { CurrentLayout string CurrentStyling string ServerAddress string SiteTitle string Sitebase string TemplatBase string CapatchaWidth int CapatchaHeight int Cache bool MainBase string content map[string]string layoutBase string mainpg string postbase string scripts string stylesheet string stylingBase string template string } var SiteData = new(goPressData) 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:
// // Function: GetGlobals // // Description: This function is used to create the // global variables initialize the // global variables. // // Inputs: // func GetGlobals() { // // Load the Server Parameters from a file. // LoadServerParameters() // // Setup the basic paths to everything. // SiteData.layoutBase = SiteData.TemplatBase + "layouts/" SiteData.stylingBase = SiteData.TemplatBase + "styling/" SiteData.postbase = SiteData.Sitebase + "posts/" // // Create the content array that will hold the site // fragments. Set the title now. // SiteData.content = make(map[string]string) SiteData.content["title"] = SiteData.SiteTitle // // Log that the data is being loaded. // log.Println("Loading data for site: " + SiteData.SiteTitle) // // Get all the basic information that is generic and // in the styles and layout directories. // These will then be over written if a new default // in the site area is found. This gives // the flexibility to load defaults from a directory // without having to make sure that all // the necessary ones are loaded. // // // Get the 404 page contents // SiteData.content["404"] = GetPageContents(SiteData.stylingBase + SiteData.CurrentStyling + "/404") // // Get the sidebar contents // SiteData.content["sidebar"] = GetPageContents(SiteData.stylingBase + SiteData.CurrentStyling + "/sidebar") // // Get the footer contents // SiteData.content["footer"] = GetPageContents(SiteData.stylingBase + SiteData.CurrentStyling + "/footer") // // Get the template contents // SiteData.template = GetPageContents(SiteData.layoutBase + SiteData.CurrentLayout + "/template") // // Get the header contents // SiteData.content["header"] = GetPageContents(SiteData.stylingBase + SiteData.CurrentStyling + "/header") // // Get the main page contents // SiteData.mainpg = GetPageContents(SiteData.Sitebase + "pages/" + "main") // // The following will load page parts from the // "parts" directory for the site. These might // overload those already defined or add new stuff // that the users site templates // will need. // partsdir := SiteData.Sitebase + "parts/" // // Read the directory. // fileList, err := ioutil.ReadDir(partsdir) if err != nil { // // Error reading the directory. // log.Printf("Error reading directory: %s\n", partsdir) } else { // // Get the number of items in the directory list. // count := len(fileList) // // Loop through each directory element. // for i := 0; i < count; i++ { if !fileList[i].IsDir() { // // It is a file. Read it and add to the // scripts variable. // filename := fileList[i].Name() parts := strings.Split(filename, ".") if filename != ".DS_Store" { SiteData.content[parts[0]] = LoadFile(partsdir + filename) } } } } // // Clear out the global variables not set. // SiteData.scripts = "" SiteData.stylesheet = "" }
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:
// // Function: SaveServerParameters // // Description: This function is for saving the // authorization secret for DropBox. // // Inputs: // func SaveServerParameters() { if wfile, err := os.Create(ServerParamFile); err == nil { enc := json.NewEncoder(wfile) enc.Encode(&SiteData) wfile.Close() } else { log.Println("Writing Server file denied.") } } // // Function: LoadServerParameters // // Description: This function is used to load the // parameters for this server. // // Inputs: // func LoadServerParameters() { if wfile, err := os.Open(ServerParamFile); err == nil { enc := json.NewDecoder(wfile) enc.Decode(&SiteData) wfile.Close() log.Println("Read the " + ServerParamFile + " server parameter file. Site Title is: " + SiteData.SiteTitle) } else { log.Println("No Server File found.") } }
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:
// // Function: DefaultRoutes // // Description: This function sets the default // routes for a CMS. // // Inputs: // func DefaultRoutes() { SetGetRoute("/", Mainpage) SetGetRoute("/sitemap.xml", SiteMap) SetGetRoute("/stylesheets.css", GetStylesheets) SetGetRoute("/scripts.js", GetScripts) SetGetRoute("/theme/images/(.*)", LoadThemeImage) SetGetRoute("/(favicon.ico)", ImagesLoad) SetGetRoute("/images/(.*)", ImagesLoad) SetGetRoute("/posts/([a-zA-Z0-9]*)/([a-zA-Z0-9]*)", PostIndex) SetGetRoute("/posts/([a-zA-Z0-9]*)/([a-zA-Z0-9]*)/(.*)", PostPages) SetGetRoute("/(.*)", TopPages) } // // Function: SetGetRoute // // Description: This function gives an easy access // to the web variable setup in this // library. // // Inputs: // route Route to setup // handler Function to run that route. // func SetGetRoute(route string, handler interface{}) { web.Get(route, handler) } // // Function: StartServer // // Description: This function is for starting the web // server using the SiteData // configuration. // // Inputs: // func StartServer(serverAddress string) { web.Run(serverAddress) }
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:
package goPress import ( "bytes" "encoding/json" "github.com/eknkc/amber" "github.com/hoisie/web" "github.com/murz/go-handlebars/handlebars" "github.com/russross/blackfriday" "io/ioutil" "log" "os" "strings" "time" )
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.
// // Function: Mainpage // // Description: This function is used to generate // and display the main page for the // web site. This function will guide // the user to setup the DropBox // account if this is the first time // being ran or the dropbox // authorization secret gets zeroed. // // Inputs: // ctx Contents from the request // func Mainpage(ctx *web.Context) string { // // Render the main page. // page := RenderPageContents(ctx, SiteData.mainpg, SiteData.Sitebase+"pages/main") return page }
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.
// // Function: SiteMap // // Description: This function is to give a site map // to requesters. // // Inputs: // ctx Contents from the request // func SiteMap(ctx *web.Context) string { var contents string wfile, err := os.Open(SiteData.Sitebase + "sitemap.xml") if err == nil { bcontents, _ := ioutil.ReadAll(wfile) contents = string(bcontents) wfile.Close() } return contents }
The SiteMap()
function gives the sitemap to the requester. It pulls the information from the sitemap.xml
at the top of the site directory.
// // Function: PostPages // // Description: This function generates the needed // post page. // // Inputs: // ctx What the browser sends // posttype The type of post // postname The name of the post // type instance // val The name of the post // page to display // func PostPages(ctx *web.Context, posttype string, postname string, val string) string { // // Get the page contents and process it. // pgloc := SiteData.postbase + posttype + "/" + postname + "/" + val return RenderPageContents(ctx, GetPageContents(pgloc), pgloc) }
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.
// // Function: PostIndex // // Description: This function generates the needed post index. // // Inputs: // ctx What the browser sends // posttype The type of post // postname The name of the post type instance // func PostIndex(ctx *web.Context, posttype string, postname string) string { // // Get the page contents and process it. // pgloc := SiteData.postbase + posttype + "/" + postname + "/index" return RenderPageContents(ctx, GetPageContents(pgloc), pgloc) }
The PostIndex()
function pulls together the information for a post index and gives it to the RenderPageContents()
function.
// // Function: topPages // // Description: This function will generate a // "static" top level page that is not // a post page. // // Inputs: // val The name of the top level page // func TopPages(ctx *web.Context, val string) string { // // Look for the markdown of the page. // pgloc := SiteData.Sitebase + "pages/" + val return RenderPageContents(ctx, GetPageContents(pgloc), pgloc) }
The topPages()
function sets up the RenderPageContents()
function for a standard page. All pages are in the pages/
directory.
// // Function: GetPageContents // // Description: This function is used to retrieve // the page contents. It will first look // for a markdown page, then for a html // page, and then it looks for an amber // page. // // Inputs: // filename The name of the file // func GetPageContents(filename string) string { // // Assume the page can not be found. // contents := SiteData.content["404"] // // Let's look for a markdown version first. // wfile, err := os.Open(filename + ".md") if err == nil { bcontents, _ := ioutil.ReadAll(wfile) wfile.Close() contents = string(blackfriday.MarkdownCommon(bcontents)) // // Double quotes were turned into “ and // ”. Turn them back. Without this, and // Handlebar macros will be broken. // contents = strings.Replace(contents, "“", "\"", -1) contents = strings.Replace(contents, "”", "\"", -1) } else { // // It must be an html. Look for that. // wfile, err = os.Open(filename + ".html") if err == nil { bcontents, _ := ioutil.ReadAll(wfile) contents = string(bcontents) wfile.Close() } else { // // It must be an amber. Look for that. // wfile, err = os.Open(filename + ".amber") if err == nil { wfile.Close() template, err2 := amber.CompileFile(filename+".amber", amber.Options{true, false}) if err2 != nil { // // Bad amber file. log.Println("Amber file bad: " + filename) } else { // // Put the default site info. // pgData := SiteData.content // // read in that pages specific data // to be added to the rest // of the data. It is stored at the // same place, but in a json // file. // if wfile, err := os.Open(filename + ".json"); err == nil { // // Load the json file of extra // data for this page. This could // override the standard data as // well. // enc := json.NewDecoder(wfile) enc.Decode(&pgData) wfile.Close() } else { log.Println("The page: " + filename + " did not have a json file.") } pgData["PageName"] = filename // // The amber source compiles okay. // Run the template and return // the results. // var b bytes.Buffer template.Execute(&b, pgData) contents = b.String() } } else { // // A file could not be found. // log.Println("Could not find file: " + filename) } } } // // Return the file contains obtained. // return contents }
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 (“
and ”
). 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.
// // Function: RenderPageContents // // Description: This function is used to process // and render the contents of a page. // It can be the main page, or a post // page, or any page. It accepts the // input as the contents for the page // template, run the page template // with it, process all shortcodes and // embedded codes, and return the // results. // // Inputs: // ctx The calling context // contents The pages main contents. // filename The name of the file the // contents was taken from. // func RenderPageContents(ctx *web.Context, contents string, filename string) string { // // Set the header information // SetStandardHeader(ctx) // // Put the default site info. // pgData := SiteData.content // // Add data specific to this page. // pgData["content"] = contents // // read in that pages specific data to be added to // the rest of the data. It is stored at the same // place, but in a json file. // if wfile, err := os.Open(filename + ".json"); err == nil { // // Load the json file of extra data for this // page. This could override the standard data as // well. // enc := json.NewDecoder(wfile) enc.Decode(&pgData) wfile.Close() } else { log.Println("The page: " + filename + " did not have a json file.") } // // Set the Page Name data field. // pgData["PageName"] = filename // // Register the helpers. // // NOTICE: All helpers can not have spaces in the // parameter. Therefore, all of these // helpers assume a "-" is a space. It gets // translated to a space before using. // // Helper: save // // Description: This helper allows you do define // macros for expanding inside the // template. You give it a name, // "|", and text to expand into. // Currently, all spaces have to be // "-". // handlebars.RegisterHelper("save", func(params ...interface{}) string { if text, ok := params[0].(string); ok { parts := strings.Split(text, "|") content := strings.Replace(parts[1], "-", " ", -1) pgData[parts[0]] = content return content } return "" }) // // The format has to use these sets of constants: // Stdlongmonth = "January" // Stdmonth = "Jan" // Stdnummonth = "1" // Stdzeromonth = "01" // Stdlongweekday = "Monday" // Stdweekday = "Mon" // Stdday = "2" // Stdunderday = "_2" // Stdzeroday = "02" // Stdhour = "15" // stdHour12 = "3" // stdZeroHour12 = "03" // Stdminute = "4" // Stdzerominute = "04" // Stdsecond = "5" // Stdzerosecond = "05" // Stdlongyear = "2006" // Stdyear = "06" // Stdpm = "Pm" // Stdpm = "Pm" // Stdtz = "Mst" // // Helper: date // // Description: This helper prints the current // date/time in the format // given. Please refer to the above // chart for proper format codes. // EX: 07/20/2015 is "01/02/2006" // handlebars.RegisterHelper("date", func(params ...interface{}) string { if format, ok := params[0].(string); ok { format = strings.Replace(format, "-", " ", -1) tnow := time.Now() return tnow.Format(format) } return "" }) // // Render the current for the first pass. // page := handlebars.Render(SiteData.template, pgData) // // Process any shortcodes on the page. // page1 := ProcessShortCodes(page) // // Render new content from Short Code and filters. // page2 := handlebars.Render(page1, pgData) // // Return the results. // 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.
// // Function: SetStandardHeader // // Description: This function is used as a one place // for setting the standard // header information. // // Inputs: // func SetStandardHeader(ctx *web.Context) { // // Set caching for the item // ctx.SetHeader("Cache-Control", "public", false) // // Set the maximum age to one month (30 days) // ctx.SetHeader("Cache-Control", "max-age=2592000", false) // // Set the name to gpPress for the server type. // ctx.SetHeader("Server", "goPress - a CMS written in go from Custom Computer Tools: http://customct.com.", true) }
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:
package goPress import ( "github.com/hoisie/web" "io" "io/ioutil" "log" "math/big" "os" "path/filepath" ) // // Function: ImagesLoad // // Description: This function is called to upload an image for the // images directory. // // Inputs: // val Name of the image with relative path // func ImagesLoad(ctx *web.Context, val string) { LoadImage(ctx, SiteData.Sitebase+"images/"+val) } // // Function: LoadThemeImage // // Description: This function loads images from the theme's directory. // // Inputs // image Name of the image file to load // func LoadThemeImage(ctx *web.Context, image string) { LoadImage(ctx, SiteData.stylingBase+SiteData.CurrentStyling+"/images/"+image) }
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.
// // Function: LoadImage // // Description: This function does the work of // loading an image file and passing it // on. // // Inputs: // func LoadImage(ctx *web.Context, val string) { // // Get the file extension. // fileExt := filepath.Ext(val) // // Set the http header based on the file type. // SetStandardHeader(ctx) ctx.ContentType(fileExt) if fileExt == ".svg" { // // This is a text based file. Read it and send to the browser. // wfile, err := os.Open(val) if err == nil { bcontents, _ := ioutil.ReadAll(wfile) wfile.Close() ctx.WriteString(string(bcontents)) } } else { // // This is a binary based file. Read it and sent the contents to the browser. // fi, err := os.Open(val) // // Set the size of the binary coming down the pipe. Chrome has to have this value // one larger than real. // finfo, _ := os.Stat(val) i := big.NewInt(finfo.Size()) ctx.SetHeader("Accept-Ranges", "bytes", true) ctx.SetHeader("Content-Length", i.String(), true) if err != nil { log.Println(err) return } defer fi.Close() // // Create a buffer to contain the image data. Binary images usually get // very big. // buf := make([]byte, 1024) // // Go through the binary file 1K at a time and send to the browser. // for { // // Read a buffer full. // n, err := fi.Read(buf) if err != nil && err != io.EOF { log.Println(err) break } // // If nothing was read, then exit. // if n == 0 { break } // // Write the binary buffer to the browser. // n2, err := ctx.Write(buf[:n]) if err != nil { log.Println(err) break } else if n2 != n { log.Println("Error in sending " + val + " to the browser. Amount read does not equal the amount sent.") break } } } }
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:
package goPress import ( "github.com/hoisie/web" "io/ioutil" "log" "os" ) // // Function: GetStylesheets // // Description: This function is used to produce // the stylesheet to the user. // // Inputs: // func GetStylesheets(ctx *web.Context) string { // // See if we have already loaded them or not. If so, // just return the pre-loaded stylesheet. // ctx.SetHeader("Content-Type", "text/css", true) SetStandardHeader(ctx) tmp := "" if SiteData.stylesheet == "" { tmp = LoadFile(SiteData.Sitebase + "css/final/final.css") // // If we are testing, we do not want the server // to cache the stylesheets. Therefore, if // cache is true, cache them. Otherwise, // do not. // if SiteData.Cache == true { SiteData.stylesheet = tmp } } else { // // We have a cached style sheet. Send it to the // browser. // tmp = SiteData.stylesheet } // // Return the stylesheet. // return tmp } // // Function: GetScripts // // Description: This function is to load JavaScripts // to the browser. This will // actually load all the JavaScript // files into one compressed file // for uploading to the browser. // // Inputs: // func GetScripts(ctx *web.Context) string { // // See if we have already loaded them or not. If so, // just return the pre-loaded scripts. // ctx.SetHeader("Content-Type", "text/javascript", true) SetStandardHeader(ctx) tmp := "" if SiteData.scripts == "" { tmp = LoadFile(SiteData.Sitebase + "js/final/final.js") // // If we are testing, we do not want the server // to cache the scripts. Therefore, if cache is // true, cache them. Otherwise, do not. // if SiteData.Cache == true { SiteData.scripts = tmp } } else { // // We have a cached style sheet. Send it to the // browser. // tmp = SiteData.scripts } // // Return the resulting compiled stylesheet. // return tmp } // // Function: LoadFile // // Description: This function if for loading // individual file contents. // // Inputs // file name of the file to be // loaded // func LoadFile(file string) string { ret := "" log.Println("Loading file: " + file) wfile, err := os.Open(file) if err == nil { bcontents, err := ioutil.ReadAll(wfile) err = err ret = string(bcontents) wfile.Close() } else { // // Could not read the file. // log.Println("Could not read: " + file) } return ret }
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:
package goPress // // Library: Shortcodes // // Description: This library gives the functionality // of shortcodes in the CMS. A shortcode // runs a function with specified // argument and a surrounded contents. // The function should process the // contents according to the arguments // and return a string for placement // into the page. Shortcodes are // recursively processed, therefore you // can have different shortcodes inside // of other shortcodes. You can not // have the same shortcode inside of // itself. // import ( "bytes" "log" "regexp" ) // // Type: ShortcodeFunction // // Description: This type defines a function that // implements a shortcode. The function // should receive two strings and return // a string. // type ShortcodeFunction func(string, string) string // // Library Variables: // // shortcodeStack This array of functions // holds all of the // shortcodes usable by the // CMS. You add shortcodes // using the AddShortCode // function. // var ( shortcodeStack map[string]ShortcodeFunction ) // // Library Function: // // init This function is called upon // library use to initialize any // variables used for the // library before anyone can // make a call to a library // function. // func init() { shortcodeStack = make(map[string]ShortcodeFunction) }
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.
// // Function: AddShortCode // // Description: This function adds a new shortcode to // be used. // // Inputs // name Name of the shortcode // funct function to process the shortcode // func AddShortCode(name string, funct ShortcodeFunction) { shortcodeStack[name] = funct }
The AddShortCode()
function allows you to load a function for processing a shortcode into the library variable for all shortcodes.
// // Function: ProcessShortCodes // // Description: This function takes in a string, // searches for shortcodes, process the // shortcode, put the results into the // string, and then return the fully // processed string. // // Inputs // page String with possible shortcodes // func ProcessShortCodes(page string) string { // // Create a work buffer. // var buff bytes.Buffer // // Search for shortcodes. We first capture a // shortcode. // r, err := regexp.Compile(`\-\[([^\]]*)\]\-`) if err == nil { match := r.FindString(page) if match != "" { // // Get the indexes to the matching area. // index := r.FindStringIndex(page) // // Save all the text before the shortcode // into the buffer. // buff.WriteString(page[0:index[0]]) // // Get everything that is left after the // shortcode. // remander := page[index[1]:] // // Separate the strings out and setup // variables for their contents. // submatch := r.FindStringSubmatch(match) name := "" contents := "" args := "" // // Get the name of the shortcode and separate // the extra arguments. // r3, err3 := regexp.Compile(`(\w+)(.*)`) if err3 == nil { submatch2 := r3.FindStringSubmatch(submatch[1]) // // The first submatch is the name of the // shortcode. // name = submatch2[1] // // The second submatch, if any, are the // arguments for the shortcode. // args = submatch2[2] } else { // // Something happened to the internal // matching. // name = submatch[1] args = "" } // // Find the end of the shortcode. // final := "\\-\\[\\/" + name + "\\]\\-" r2, err2 := regexp.Compile(final) if err2 == nil { index2 := r2.FindStringIndex(remander) if index2 != nil { // // Get the contents and what is left // over after the closing of the // shortcode. // contents = remander[:index2[0]] remander2 := remander[index2[1]:] // // If it is a real shortcode, then // run it! // if shortcodeStack[name] != nil { // // See if there is any shortcodes // inside the contents area. // contents = ProcessShortCodes(contents) // // Run the shortcode and add it's // result to the buffer. // buff.WriteString(shortcodeStack[name](args, contents)) } // // Process any remaining shortcodes. // buff.WriteString(ProcessShortCodes(remander2)) } else { // // We have a bad shortcode // definition. All shortcodes have // to be closed. Therefore, // simply do not process anything and // tell the logs. // log.Println("Bad Shortcode definition. It was not closed. Name: " + name) buff.WriteString(page[index[0]:index[1]]) buff.WriteString(ProcessShortCodes(remander)) } } else { // // There was an error in the regular // expression for closing the shortcode. // log.Println("The closing shortcode's regexp did not work!") } } else { // // No shortcodes, just copy the page to the // buffer. // buff.WriteString(page) } } else { // // If the Regular Expression is invalid, tell the // world! // log.Println("RegEx: Invalid expression.") } // // Return the resulting buffer. // return buff.String() }
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:
-[box args="some items"]- <p>This should be inside the box.</p> -[/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.
// // Function: ShortcodeBox // // Description: This shortcode is used to put the // surrounded HTML in a box div. // // Inputs: // parms The parameters used by the // shortcode // context The HTML enclosed by the opening // and closing shortcodes. // func ShortcodeBox(parms string, context string) string { return ("<div class='box'>" + context + "</div>") } // // Function: ShortcodeColumn1 // // Description: This shortcode is used to put the // surrounded HTML in the first column. // // Inputs: // parms The parameters used by the // shortcode // context The HTML enclosed by the opening // and closing shortcodes. // func ShortcodeColumn1(parms string, context string) string { return ("<div class='col1'>" + context + "</div>") } // // Function: ShortcodeColumn2 // // Description: This shortcode is used to put the // surrounded HTML in the second column. // // Inputs: // parms The parameters used by the // shortcode // context The HTML enclosed by the opening // and closing shortcodes. // func ShortcodeColumn2(parms string, context string) string { return ("<div class='col2'>" + context + "</div>") } // // Function: ShortcodePHP // // Description: This shortcode is for surrounding a // code block and formatting it's look // and feel properly. This one is for a // PHP code block. // // Inputs: // parms The parameters used by the // shortcode // context The HTML enclosed by the opening // and closing shortcodes. // func ShortcodePHP(params string, context string) string { return ("<div class='showcode'><pre type='syntaxhighlighter' class='brush: php'>" + context + "</pre></div>") } // // Function: ShortcodeJS // // Description: This shortcode is for surrounding a // code block and formatting it's look // and feel properly. This one is for a // JavaScript code block. // // Inputs: // parms The parameters used by the // shortcode // context The HTML enclosed by the opening // and closing shortcodes. // func ShortcodeJS(params string, context string) string { return ("<div class='showcode'><pre type='syntaxhighlighter' class='brush: javascript'>" + context + "</pre></div>") } // // Function: ShortcodeHTML // // Description: This shortcode is for surrounding a // code block and formatting it's look // and feel properly. This one is for a // HTML code block. // // Inputs: // parms The parameters used by the // shortcode // context The HTML enclosed by the opening // and closing shortcodes. // func ShortcodeHTML(params string, context string) string { return ("<div class='showcode'><pre type='syntaxhighlighter' class='brush: html'>" + context + "</pre></div>") } // // Function: ShortcodeCSS // // Description: This shortcode is for surrounding a // code block and formatting it's look // and feel properly. This one is for a // CSS code block. // // Inputs: // parms The parameters used by the // shortcode // context The HTML enclosed by the opening // and closing shortcodes. // func ShortcodeCSS(params string, context string) string { return ("<div class='showcode'><pre type='syntaxhighlighter' class='brush: css'>" + context + "</pre></div>") } // // Function: LoadDefaultShortcodes // // Description: This function is used to load in all // the default shortcodes. // // Inputs: // func LoadDefaultShortcodes() { AddShortCode("Box", ShortcodeBox) AddShortCode("Column1", ShortcodeColumn1) AddShortCode("Column2", ShortcodeColumn2) AddShortCode("php", ShortcodePHP) AddShortCode("javascript", ShortcodeJS) AddShortCode("html", ShortcodeHTML) AddShortCode("css", ShortcodeCSS) }
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:
package main import ( "./src/goPress" ) // // Function: main // // Description: This is the main function that is // called whenever the program is // executed. It will load the globals, // set the different routes, and // start the server. // // Inputs: // func main() { // // Load the default Shortcodes. // goPress.LoadDefaultShortcodes() // // Load all global variables. // goPress.GetGlobals() // // Setup the Default routes. // goPress.DefaultRoutes() // // Run the web server // goPress.StartServer(goPress.SiteData.ServerAddress) }
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:
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.
When you execute the program in a terminal, you will see its log statements with the date and time as above.
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.
Comments