Converting a Static Site to Buffalo
Overview
I recently had a friend come to me and mention that their website I created for them years ago needed some updating. After looking at it, it was clear it was time for an update. This article will walk through the design decisions and technical challenges that I needed to address in bringing their site up to some modern standards.
I recently had a friend come to me and mention that their website I created for them years ago needed some updating. After looking at it, it was clear it was time for an update. This article will walk through the design decisions and technical challenges that I needed to address in bringing their site up to some modern standards.
Related Content
Before getting too far, if you already watched the video and simply want to browse the code, here is the repository:
github.com/gopherguides/convert-static-site-to-buffaloThe Problem
The old site had many problems. First, the layout was no longer mobile friendly.
Because the site was no longer mobile friendly, using a device such as a tablet or phone, users would no longer find their business when searching for them with popular search engines.
The next problem was the image gallery. I could simply create static pages for all those images, but I wanted a better solution than that. Additionally, I wanted some modern navigation, etc.
To summarize, I had to accomplish the following:
- Update the layout to be mobile friendly
- Create a dynamic image gallery
- Create dynamic navigation
- Allow for future updates that may need to be
dynamic
in nature
Picking A Technology
There are endless options to choose from for modern web development. However, I wanted something that could satisfy the following:
- Easy deployment
- Fun to work with
- Modern asset pipline (scss, asset signing, etc.)
- Live Reload
- Full Web Stack
- Quick to install and get running
As you can imagine, that quickly narrows down both the technology and the frameworks available. We all know that Ruby on Rails can do all that (with varying degrees on deployment depending on your hosting solution). However, I love Go, so naturally, I was biased towards a solution writtin in go.
Enter Buffalo
Buffalo satisfied all of my criteria (and much more). I didn't need a database for this project, but knowing that I can easily add support for that in the future is also a big win for this project.
Now that I've decided on a technology, it was time to get started. The first thing I had to do was download the latest Buffalo binary to generate my project.
Once you have downloaded the latest version, you can verify you have the correct version by running the following command:
$ buffalo version
INFO[0001] Buffalo version is: v0.15.3
Generating The Project
The first thing we need to do is now create the initial project. This will do things like:
- Create an asset pipeline
- Create a starting route and template to work from
- Create some basic configuration
To create the project, I ran the following command:
buffalo new craigscustomsound --skip-pop
The --skip-pop
flag tells the project not to generate any database dependencies.
You will see something like the following output as the project creates all of the initial files needed (the output is abbreviated for purposes of this blog):
DEBU[2020-01-24T12:31:31-06:00] Step: e9ef182f
DEBU[2020-01-24T12:31:31-06:00] Chdir: /Users/corylanou/tmp/craigscustomsound
DEBU[2020-01-24T12:31:31-06:00] File: /Users/corylanou/tmp/craigscustomsound/.codeclimate.yml
DEBU[2020-01-24T12:31:36-06:00] File: /Users/corylanou/tmp/craigscustomsound/webpack.config.js
.
.
.
DEBU[2020-01-24T12:31:36-06:00] LookPath: yarnpkg
DEBU[2020-01-24T12:31:36-06:00] Exec: yarnpkg install --no-progress --save
DEBU[2020-01-24T12:31:37-06:00] yarn install v1.15.2
DEBU[2020-01-24T12:31:37-06:00] info No lockfile found.
DEBU[2020-01-24T12:31:37-06:00] [1/4] Resolving packages...
DEBU[2020-01-24T12:31:37-06:00] warning popper.js@1.16.1: Popper changed home, find its new releases at @popperjs/core
DEBU[2020-01-24T12:31:51-06:00] [2/4] Fetching packages...
DEBU[2020-01-24T12:31:54-06:00] [3/4] Linking dependencies...
DEBU[2020-01-24T12:31:57-06:00] [4/4] Building fresh packages...
DEBU[2020-01-24T12:32:04-06:00] success Saved lockfile.
DEBU[2020-01-24T12:32:04-06:00] Done in 27.45s.
DEBU[2020-01-24T12:32:04-06:00] Step: e19e0b2c
DEBU[2020-01-24T12:32:04-06:00] Chdir: /Users/corylanou/tmp/craigscustomsound
DEBU[2020-01-24T12:32:05-06:00] Step: 5d306080
DEBU[2020-01-24T12:32:05-06:00] Chdir: /Users/corylanou/tmp/craigscustomsound
DEBU[2020-01-24T12:32:05-06:00] File: /Users/corylanou/tmp/craigscustomsound/.gitignore
DEBU[2020-01-24T12:32:05-06:00] Exec: git init
Initialized empty Git repository in /Users/corylanou/tmp/craigscustomsound/.git/
DEBU[2020-01-24T12:32:05-06:00] Exec: git add .
DEBU[2020-01-24T12:32:05-06:00] Exec: git commit -q -m Initial Commit
INFO[2020-01-24T12:32:05-06:00] Congratulations! Your application, craigscustomsound, has been successfully built!
INFO[2020-01-24T12:32:05-06:00] You can find your new application at: /Users/corylanou/tmp/craigscustomsound
INFO[2020-01-24T12:32:05-06:00] Please read the README.md file in your new application for next steps on running your application.
The longest part of this task is actually creating the asset pipeline and downloading all the node
dependencies.
Once that is done, you will have a directory called craigscustomsound
(or whatever you used for your project name). That directory will have the following structure:
.
├── Dockerfile
├── README.md
├── actions
├── assets
├── config
├── fixtures
├── go.mod
├── go.sum
├── grifts
├── inflections.json
├── locales
├── main.go
├── node_modules
├── package.json
├── public
├── templates
├── webpack.config.js
└── yarn.lock
Running The Server
Now that the initial project is generated, I can start up the web server and actually see the new site that was generated.
First, change directories to the project you created:
cd craigscustomsound
Now, start the server with the following command:
buffalo dev
This will start up all services needed to browse the new site. It starts up on localhost:3000.
You will now see the initial page that was created for the project:
How it Works
There are several files that you will need to know about to start creating and working with your new project. First, we'll start with the go
files that contain the code for the handlers
.
There are two primary files to be aware of. They are app.go
and home.go
, and reside in the actions
folder:
actions/
├── app.go
└── home.go
In the app.go
file, the initial routes
, middleware
, and more are wired into the system:
func App() *buffalo.App {
if app == nil {
app = buffalo.New(buffalo.Options{
Env: ENV,
SessionName: "_craigscustomsound_session",
})
// Automatically redirect to SSL
app.Use(forceSSL())
// Log request parameters (filters apply).
app.Use(paramlogger.ParameterLogger)
// Protect against CSRF attacks. https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)
// Remove to disable this.
app.Use(csrf.New)
// Setup and use translations:
app.Use(translations())
app.GET("/", HomeHandler)
app.ServeFiles("/", assetsBox) // serve files from the public directory
}
return app
}
Most notably, what we care about, is the home
handler that is being wired up to serve the /
default route:
app.GET("/", HomeHandler)
The next file, home.go
, is the code that will actually serve the /
route, as well as decide which template to use.
package actions
import "github.com/gobuffalo/buffalo"
// HomeHandler is a default handler to serve up
// a home page.
func HomeHandler(c buffalo.Context) error {
return c.Render(200, r.HTML("index.html"))
}
The above code is the boilerplate for simply stating to load the index.html
template, and serve it with a status code of 200
(ok).
Next, let's take a look at the templates for the site.
Templates
The templates for the site reside in the templates
directory. There are three initial files:
templates/
├── _flash.plush.html
├── application.plush.html
└── index.plush.html
The file application.plush.html
is your site
template, and where you would add any global
content, such as Google Analitics tracking, and things of that nature.
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="utf-8">
<title>Buffalo - Craigscustomsound</title>
<%= stylesheetTag("application.css") %>
<meta name="csrf-param" content="authenticity_token" />
<meta name="csrf-token" content="<%= authenticity_token %>" />
<link rel="icon" href="<%= assetPath("images/favicon.ico") %>">
</head>
<body>
<div class="container">
<%= partial("flash.html") %>
<%= yield %>
</div>
<%= javascriptTag("application.js") %>
</body>
</html>
Notice it is using a partial
file for showing flash
messages. This is very much like how Ruby on Rails does partials as well. Also notice that the partial
file of _flash.plush.html
starts with an underscore (_
). This convention is used for all partials when creating template files.
Next is the index.plush.html
file. This is a fairly large file to start, but remember that it rendered the default page that showed all of your routes and initial site info. You can feel free to replace all of the content in this page with your content for the site you are building.
Creating the New Site
Now that we have a quick tour of the files we need to work with, we will want to add all of our pages. The pages we are creating are as follows:
- Home Page
- About Us Page
- Contact Us Page
- Products & Services Page
- Image Gallery Page
To do that, I'll create the following new files in the templates
directory:
about.plush.html
application.plush.html
contact.plush.html
gallery.plush.html
products.plush.html
And remember we already have this file to re-purpose:
index.plush.html
Next, I need to create routes for each of these files, handlers, and serve each of the new templates.
For the handlers, I'll create the following files in the actions
directory:
about.go
contact.go
gallery.go
products.go
And remember we already have the home.go
file that is already serving our index page.
Each of these files will have the same basic handler and serve the correct template for each one. For example, this is the content of about.go
:
package actions
import "github.com/gobuffalo/buffalo"
func AboutHandler(c buffalo.Context) error {
return c.Render(200, r.HTML("about.html"))
}
Finally, we need to wire the routes up. We do this in app.go
app.GET("/", HomeHandler)
app.GET("/about", AboutHandler)
app.GET("/products", ProductsHandler)
app.GET("/contact", ContactHandler)
app.GET("/gallery", GalleryHandler)
This is now the initial set of files needed to start building out the entire site.
Routing
To ensure that we have the correct routes, we can use the buffalo
cli (command line interface) tool to list the actual routes. This can be done with the following command:
$ buffalo routes
METHOD | PATH | ALIASES | NAME | HANDLER
------ | ---- | ------- | ---- | -------
GET | / | | rootPath | craigscustomsound/actions.HomeHandler
GET | /about/ | | aboutPath | craigscustomsound/actions.AboutHandler
GET | /contact/ | | contactPath | craigscustomsound/actions.ContactHandler
GET | /gallery/ | | galleryPath | craigscustomsound/actions.GalleryHandler
GET | /products/ | | productsPath | craigscustomsound/actions.ProductsHandler
You can see that it lists out the Method
, Path
, Alias
, Name
, and associated Handler
for each route.
Helpers
Because I want navigation to track
and highlight
the page it is on, I will need to create a helper. The site is using bootstrap, and for navigation, the have an active class that will automatically highlight the page.
The first thing needed is to create a partial
for the navigation html template. I'll create the following file:
templates\_navbar.plush.html
I can then include that in my application.plush.html
file with the following tag:
<%= partial("navbar.html") %>
Now I only need to edit one file for all of the sites navigation.
For the helper, I effectively want to be able to specify the route
that the navigation should match. It should write out the active
class for the navigation element if it matches that route. So first, I'll create a helper.
We typicaly add our global helpers in actions/render.go
.
This is the helper I created to look at the current route, and if it matches what was passed in via the template, it will return out the string active
:
func activeClass(n string, help plush.HelperContext) string {
if p, ok := help.Value("current_route").(buffalo.RouteInfo); ok {
if p.PathName == n {
return "active"
}
}
return ""
}
Next, we need to update templates/_navbar.plush.html
to use the helper:
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<a class="navbar-brand" href="/" id="brand">Custom Sound & Video</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav mr-auto">
<li class="nav-item <%=activeClass("rootPath")%>">
<a class="nav-link" href="/">Home</a>
</li>
<li class="nav-item <%=activeClass("aboutPath")%>">
<a class="nav-link" href="/about">About Us</a>
</li>
<li class="nav-item <%=activeClass("productsPath")%>">
<a class="nav-link" href="/products">Products & Services</a>
</li>
<li class="nav-item <%=activeClass("galleryPath")%>">
<a class="nav-link" href="/gallery">Gallery</a>
</li>
<li class="nav-item <%=activeClass("contactPath")%>">
<a class="nav-link" href="/contact">Contact Us</a>
</li>
</ul>
<span class="navbar-text navbar-right">
<a href="tel:+17155777945">(715) 577-7945</a>
</span>
</div>
</nav>
We needed to put the helper in every navigation element and specify the corresponding route.
The Static Content
Now that I have dynamic navigation working, I was able to finish out all the static pages by simply creating the content from the previous site. This brings me to the following layout:
Image Gallery
The image gallery presents a little bit of code. I wasn't concerned with needing any type of interface for this specific client to post new photos. Since they don't update their images very often, I just needed something that could read the current directory of images, and then list them out as thumbnails, with the ability to view the larger image.
The first thing we need to understand is that Buffalo packages
all static assets. This means that it looks at files such as css
, images
, etc. and compiles them into a binary representation. It then takes that compiled representation and builds it into the final binary that you ship to production. To make this work, it creates as assetBox
in the actions
package:
By default, it packages up the public
directory:
var assetsBox = packr.New("app:assets", ".public")
And it serves that directory from app.go
with the following command:
app.ServeFiles("/", assetsBox) // serve files from the public directory
Now that we know how assets are stored, I can write my handler for the gallery page.
I have three directories I want to server images from:
- commercial
- theater
- prewire
I'll do the same pattern for all of these assets, but we'll show the example of how I got a list of images from the commercial
directory:
commercial := []string{}
assetsBox.Walk(func(nm string, f packd.File) error {
if strings.HasPrefix(nm, "assets/images/gallery/commercial/medium/") {
commercial = append(commercial, strings.TrimPrefix(nm, "assets/"))
}
return nil
})
Effectively, I'm walking the assetsBox
that we created when we fired the site up. I match any file that has the desired path, and store it in a slice. Then I can add that slice to my pages context with the following command:
c.Set("commercial", commercial)
This now allows me to access this list of information from my site template.
Here is the entire handler for the gallery with all three directories being processed:
package actions
import (
"strings"
"github.com/gobuffalo/buffalo"
"github.com/gobuffalo/packd"
)
// HomeHandler is a default handler to serve up
// a home page.
func GalleryHandler(c buffalo.Context) error {
commercial := []string{}
assetsBox.Walk(func(nm string, f packd.File) error {
if strings.HasPrefix(nm, "assets/images/gallery/commercial/medium/") {
commercial = append(commercial, strings.TrimPrefix(nm, "assets/"))
}
return nil
})
c.Set("commercial", commercial)
theater := []string{}
assetsBox.Walk(func(nm string, f packd.File) error {
if strings.HasPrefix(nm, "assets/images/gallery/theater/medium/") {
theater = append(theater, strings.TrimPrefix(nm, "assets/"))
}
return nil
})
c.Set("theater", theater)
prewire := []string{}
assetsBox.Walk(func(nm string, f packd.File) error {
if strings.HasPrefix(nm, "assets/images/gallery/prewire/medium/") {
prewire = append(prewire, strings.TrimPrefix(nm, "assets/"))
}
return nil
})
c.Set("prewire", prewire)
return c.Render(200, r.HTML("gallery.html"))
}
Next, we have to create the html layouts. I'm using bootstraps pre-defined classes for both the images, and modal popups. To see how the final layout was accomplished, you can refer to the video at the top of this page as well as the repo.
By using Bootstrap and Buffalo, I was able to create a dynamic image gallery in about an hour that satisfied all of the criteria for this project.
Building the Binary
Now that the entire site is complete, we want to build and deploy the binary. I'm hosting on a simple droplet at Digital Ocean. There are a number of ways to deploy a Buffalo app and they are well documented, so I will only be showing how to build the binary itself.
You can use the following command to build
a single binary for your target platform to deploy your project:
buffalo build
This will place a built binary called craigscustomsound
and place it in the bin
directory.
It is important to keep in mind that by default this binary is built for the operating system and architecture of the computer I am on. Since I am deploying this on a linux based machine, I need to specify the target operating system:
GOOS=linux buffalo build
You can specify any target architecture or operating system for Buffalo the same way the Go build tools work. For more information on what options are available, you can read the Building Go Applications for Different Operating Systems and Architectures article.
Now I have a single static binary that I can place on my droplet and run without any additional files.
Production Configuration
The last thing I need to do is configure the service to run on a different port. By default, the buffalo dev
command starts on port 3000
. However, for my environment, that won't work.
I'm using a reverse proxy on my server, so I need it to run on port 8001
. I could accomplish this by hard coding the port in my program, setting an environment variable on my host, or by using a configuration file. For this project, I opted to use the configuration file.
By default, all Buffalo projects will look for a .env
file in the root of the project. You can find information for more configuration options here.
This is the config for this project in production:
# This .env file was generated by buffalo, add here the env variables you need
# buffalo to load into the ENV on application startup so your application works correctly.
# To add variables use KEY=VALUE format, you can later retrieve this in your application
# by using os.Getenv("KEY").
#
# Example:
# DATABASE_PASSWORD=XXXXXXXXX
# SESSION_SECRET=XXXXXXXXX
# SMTP_SERVER=XXXXXXXXX
PORT=8001
Summary
In this project we went from an outdated, static website, to a modern dynamic website. By using the Buffalo project, I was able to quickly scaffold my pages and use bootstrap to lay them out. This project barely touches any of the features that the Buffalo project provides, but also shows that you can customize Buffalo for just the needs at hand.
Overall, the project took about 2 hours from start to finish. As a reference, this blog article and video production took three days. I hope you enjoyed this article. Feel free to contact me with suggestions and feedback on this article on twitter.
More Articles
Hype Quick Start Guide
Overview
This article covers the basics of quickly writing a technical article using Hype.
Writing Technical Articles using Hype
Overview
Creating technical articles can be painful when they include code samples and output from running programs. Hype makes this easy to not only create those articles, but ensure that all included content for the code, etc stays up to date. In this article, we will show how to set up Hype locally, create hooks for live reloading and compiling of your documents, as well as show how to dynamically include code and output directly to your documents.
Go (golang) Slog Package
Overview
In Go (golang) release 1.21, the slog package will be added to the standard library. It includes many useful features such as structured logging as well as level logging. In this article, we will talk about the history of logging in Go, the challenges faced, and how the new slog
package will help address those challenges.