DEV Community

Cover image for Building a Blog with Go, Tailwind CSS, Cosmic & Deploy to Heroku with GitHub Actions
Tony Spiro
Tony Spiro

Posted on • Originally published at cosmicjs.com

Building a Blog with Go, Tailwind CSS, Cosmic & Deploy to Heroku with GitHub Actions

In this walkthrough, I'm going to show you how I built a blog in Go with content powered by the Cosmic headless CMS. I'll also talk about styling it using Tailwind CSS and lastly, I'll show you how you can utilize GitHub Actions to deploy your Go-based applications to Heroku. Let's get started.

TL;DR

View Demo
View the full source code on GitHub
Install the App

Fundamentals

Every blog consists of at least two components:

  • List Page (Blog roll)
  • Individual Blog Post Page

Go is an excellent back-end language to build an optimized and highly concurrent application. In order to show the potential of Go to our Cosmic Community, I'll be building a simple blog application. To get started, we'll need to populate content in Cosmic.

Blog Data

In order to provide the blog post data, I installed the Simple Blog app on my Cosmic Account Bucket and voila my blog CMS and API service was ready within seconds.

But if you don't want to install an App and want to try it on your own, you can follow these steps.

  1. Log in and Create a new Bucket in Cosmic
  2. Create an Object Type Posts in that bucket.
  3. Add following Metafields:
    • Hero -> Image/File Metafield
    • Description -> Textarea Metafield

Go App Code

This Go Blog app has three major components

  1. Domain
  2. Routes
  3. Templates Now, I will go through each of them separately.

Domain

Go is a statically typed language, meaning, in order to work with Go, we need to create structs to process the data from the Cosmic API. In our blog application, I will be working with one data model Post. Our Post struct consists of Title, Slug, Content, CreatedAt and Metadata.

// Post is a representation of post object
type Post struct {
  Title     string
  Slug      string
  Content   template.HTML
  CreatedAt time.Time `json:"created_at"`
  Metadata  Metadata
}
Enter fullscreen mode Exit fullscreen mode

As you see, I added json:"created_at" to our CreatedAt attribute, which I'll explain later on why it is required. Also, you see Metadata is a linked struct which contains hero and description Metafields from Cosmic CMS.

// Metadata is a representation of metadata object
type Metadata struct {
  Hero        Image
  Description string
}

// Image is a object of URL & ImgixURL
type Image struct {
  URL      string
  ImgixURL string `json:"imgix_url"`
}
Enter fullscreen mode Exit fullscreen mode

As this point, our Domain is completed. But there are few more structs needed as our Cosmic API returns different JSON responses from the multiple Objects and single Object endpoints. Below is the full code for domain.go file which I put in src/domain directory for clean code structure.

package domain

import (
  "html/template"
  "time"
)

// ObjectsData is a array of objects from Cosmic API
type ObjectsData struct {
  Objects []Post
}

// ObjectData is a object from Cosmic API
type ObjectData struct {
  Object Post
}

// Post is a representation of post object
type Post struct {
  Title     string
  Slug      string
  Content   template.HTML
  CreatedAt time.Time `json:"created_at"`
  Metadata  Metadata
}

// Metadata is a representation of metadata object
type Metadata struct {
  Hero        Image
  Description string
}

// Image is a object of URL & ImgixURL
type Image struct {
  URL      string
  ImgixURL string `json:"imgix_url"`
}
Enter fullscreen mode Exit fullscreen mode

As mentioned before, I added JSON field name from API response in front of some attributes i.e json:"created_at". It is because we will be using JSON Unmarshal in Go to parse and map the JSON response to our domain models. As it doesn't work with fields having underscore in it. So, we need to explicitly tell the name of the fields.

Now, I'll move to building routes and controllers for our application.

Routes (Controllers)

In every blog application, there are two major pages. One that will show the list of all blog posts and another which will show the single blog post. Before I'll implement controller actions, I'll show you how I set up the simple Go web server and routes using net/http and gorilla/mux modules. Below is the code for app.go file which is responsible for handling our routes and HTTP server.

package main

import (
  "fmt"
  "go-cosmic-blog/src/handlers"
  "go-cosmic-blog/src/utils"
  "log"
  "net/http"

  "github.com/gorilla/mux"
  "github.com/joho/godotenv"
)

func main() {
r := mux.NewRouter()

// Choose the folder to serve
staticDir := "/static/"

// Create the route
r.PathPrefix(staticDir).Handler(http.StripPrefix(staticDir, http.FileServer(http.Dir("."+staticDir))))

r.HandleFunc("/", handlers.Index).Methods("GET")
r.HandleFunc("/{slug}", handlers.Single).Methods("GET")

if err := godotenv.Load(); err != nil {
  log.Println("Error loading .env file")
}

port := utils.GetPortEnv()

fmt.Println("Starting server at port", port)
  if err := http.ListenAndServe(port, r); err != nil {
    log.Fatal(err)
  }
}
Enter fullscreen mode Exit fullscreen mode

As you see I also included joho/godotenv and two local modules go-cosmic-blog/src/handlers and go-cosmic-blog/src/utils. joho/godotenv will handle the environment variables from .env file and go-cosmic-blog/src/utils have helper functions for our application.

You can look at .env.example file here and src/utils/helpers.go here.

Now, I'll show you the creation of Index Handler. To keep everything structured, I created index.go file in src/handlers/ directory. This index handler will fetch the blog posts from Cosmic CMS, parse and map it to our domain models and then send that data to HTML templates. So, our skeleton for Index handler is as bellow:

package handlers // our local package name

// Index handler to serve / route
func Index(w http.ResponseWriter, r *http.Request) {

    // Fetch list of objects from Cosmic CMS

    // Parse and map it to Domain Models

    // send that data to our templates
}
Enter fullscreen mode Exit fullscreen mode

In order to fetch the post objects, I called the Cosmic CMS API endpoint. As this endpoint requires basic parameters like BUCKET_SLUG and READ_KEY. I put them in the .env so that we don't need to hard code them. Below code is getting these parameters from .env and then fetching post Objects from Cosmic CMS API.

if ok := utils.CheckIfEnvExists("BUCKET_SLUG"); !ok {
    http.Error(w, "BUCKET_SLUG is not present in the .env", http.StatusInternalServerError)
    return
}

var readKey string
if ok := utils.CheckIfEnvExists("READ_KEY"); ok {
    readKey = "&read_key=" + os.Getenv("READ_KEY")
}

bucketSlug := os.Getenv("BUCKET_SLUG")

url := utils.APIURL + bucketSlug + "/objects?&hide_metafields=true&type=posts&props=slug,title,content,metadata,created_at" + readKey

res, err := http.Get(url)
Then I used `json.Unmarshal` function to process the response and map it to our `ObjectsData` domain model.

var data domain.ObjectsData

if err != nil {
  log.Println(err)
} else {
  body, err := ioutil.ReadAll(res.Body)
  if err != nil {
    log.Println(err)
  } else {
    json.Unmarshal(body, &data)
  }
}
Enter fullscreen mode Exit fullscreen mode

Finally, I passed the processed data to the template to render it. Below is the complete code for src/handlers/index.go file.

package handlers

import (
  "encoding/json"
  "go-cosmic-blog/src/domain"
  "go-cosmic-blog/src/utils"
  "io/ioutil"
  "log"
  "net/http"
  "os"
  "text/template"
)

// Index handler to serve / route
func Index(w http.ResponseWriter, r *http.Request) {

if ok := utils.CheckIfEnvExists("BUCKET_SLUG"); !ok {
  http.Error(w, "BUCKET_SLUG is not present in the .env", http.StatusInternalServerError)
  return
}

var readKey string
if ok := utils.CheckIfEnvExists("READ_KEY"); ok {
  readKey = "&read_key=" + os.Getenv("READ_KEY")
}

bucketSlug := os.Getenv("BUCKET_SLUG")

url := utils.APIURL + bucketSlug + "/objects?&hide_metafields=true&type=posts&props=slug,title,content,metadata,created_at" + readKey

res, err := http.Get(url)
var data domain.ObjectsData

if err != nil {
  log.Println(err)
} else {
  body, err := ioutil.ReadAll(res.Body)
  if err != nil {
    log.Println(err)
  } else {
    json.Unmarshal(body, &data)
  }
}

t, _ := template.ParseFiles(
  "src/templates/index.html",
  "src/templates/head.html",
  "src/templates/header.html",
  "src/templates/footer.html",
)

t.Execute(w, data)
}
Enter fullscreen mode Exit fullscreen mode

Similarly, I created the Single Handler in src/handlers/single.go. You can copy code from here.

Now, our routes and controller part is complete. Let's move to render templates.

Templates

For rendering our templates, I used text/template which is easy and quite powerful as well. Furthermore, you can look at this Go Template Cheat Sheet for your understanding.

For better code structure, I divide the template into head, header and footer. Then index template is for index route and single template is for single post page. Below is the code of src/templates/index.html which is self-explanatory.

{{template "head"}}
{{template "header"}}
  <section class="container mx-auto px-4 py-12 max-w-5xl">
    {{if .Objects}}
      <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 gap-y-12">
        {{range .Objects}}
        <article>
          <a href="/{{.Slug}}">
            <div class="h-64 sm:h-56 lg:h-48 rounded-sm bg-gray-500 bg-cover" style="background-image: url({{.Metadata.Hero.ImgixURL}}?w=400)">
            </div>
          </a>
          <h2 class="text-black text-lg font-semibold mt-2">
            <a href="/{{.Slug}}">{{ .Title }}</a>
          </h2>
          <p class="mt-2 text-gray-600">{{.Metadata.Description}}</p>
          <div class="mt-1"><time class="text-gray-400 font-semibold">{{.CreatedAt.Format "02 Jan 2006"}}</time></div>
        </article>
        {{end}}
      </div>
    {{else}}
      <h2>No post found</h2>
    {{end}}
  </section>
{{template "footer"}}
Enter fullscreen mode Exit fullscreen mode

The rest of the templates you can check out here.

Styling

As you may already notice, I used classes in HTML templates for styling purposes. And these classes are from Tailwind CSS which is a modern CSS framework for building custom designs. In order to use Tailwind CSS in this application, I installed npm packages using this command

npm i -D tailwindcss cssnano postcss-cli
Enter fullscreen mode Exit fullscreen mode

and then I created static/styles/tailwind.css file with the following content.

Note: you need to have node and npm installed for this

@tailwind base;
@tailwind components;
@tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

For markdown support, I copied Github Markdown Styles for tailwind into that file as well.

Together with postcss you can build Tailwind CSS and also remove all unused classes in production for the smaller size. You can look at postcss.config.js, tailwind.config.js and npm scripts in package.json in the code repository.

Finally, for development purposes I installed reflex which is a tool to detect code changes and run our go run app.go command (Note: reflex only works on macOS and Linux). You can install that using this command:

go get github.com/cespare/reflex
Enter fullscreen mode Exit fullscreen mode

Voila, our blog application is ready. You can run npm start and go to http://localhost:8000 to access the application. It should look something like the screenshot below:
Cosmic Go Blog

Deployment

At this point, the blog application is fully functional locally. But I wanted to deploy this to Heroku as it is very easy for deployments and it supports Go applications. As this repository is on GitHub, I thought to give GitHub Actions a try.

I looked around and found a very easy GitHub action for Heroku deployments here.

To set up it properly, we need to make sure our Go application has a go.mod file. Also, I need to add secrets in our GitHub repository settings i.e BUCKET_SLUG, HEROKU_API_KEY, HEROKU_APP_NAME, HEROKU_EMAIL, READ_KEY.

After that, I just needed to use actions/checkout@v2 to check out our repository and then trigger Heroku deploy. Below is the full code for our Github Action file .github/workflows/production.yml.

name: Production Deployment

on:
  push:
    branches: master

jobs:
  build:
    name: Build
    runs-on: ubuntu-latest
    steps:
    - name: Check out code
      uses: actions/checkout@v2
    - name: Deploy to Heroku
      uses: akhileshns/heroku-deploy@v3.4.6 # This is the action
      with:
        heroku_api_key: ${{secrets.HEROKU_API_KEY}}
        heroku_app_name: ${{secrets.HEROKU_APP_NAME}}
        heroku_email: ${{secrets.HEROKU_EMAIL}}
        branch: "master"
      env:
        HD_BUCKET_SLUG: ${{secrets.BUCKET_SLUG}}
        HD_READ_KEY: ${{secrets.READ_KEY}}
Enter fullscreen mode Exit fullscreen mode

This will trigger the deployment on very commit in the master branch.

Conclusion

After this whole journey, I can confidently say that Go is a super simple and robust language to build your backend application. In addition, these are my takeaways:

Tailwind is the best CSS framework for building custom designs. It is very customizable.
It is very simple to add CI/CD to our application using GitHub Actions and it has very generous developer-friendly free tier.
Application deployments are very simple and easy with Heroku.

Hope you liked this article. If you have any comments or questions about building websites and apps with Cosmic, reach out to us on Twitter and join the conversation on Slack. You can also follow another step-by-step Go Guide in the Cosmic Docs.

Top comments (2)

Collapse
 
csgeek profile image
csgeek

One thing I don't fully follow, is Cosmic something you can self hosted to use the headless CMS or do you need to rely on Cosmic's server for your data being delivered to your app?

It seems like without the bucketslug and readkey this isn't doing much.

Collapse
 
tonyspiro profile image
Tony Spiro

Cosmic is a hosted headless CMS, so yes you do need a Cosmic account to populate content and to get your Bucket slug and read key.