Skip to content

Instantly share code, notes, and snippets.

@robertfairley
Last active September 1, 2020 16:26
Show Gist options
  • Save robertfairley/f061df3a6a2fe15d429f824a5c38d089 to your computer and use it in GitHub Desktop.
Save robertfairley/f061df3a6a2fe15d429f824a5c38d089 to your computer and use it in GitHub Desktop.
quick markdown blog write-up

I'll have to update this description at another time, but in short these are the relevant files, configs, and scripts for deploying a very simple blog using Golang.

To get started, make a folder, add all of these files to a directory and create necessary directories to fit a structure like so (ignoring everything but the files in this Gist for now) :

.
├── LICENSE # (Github or MIT)
├── Makefile
├── README.md
├── posts
├── scripts
│   └── deploy.sh
├── site-config.json
├── src
│   └── main.go
├── static
│   ├── cv
│   ├── images
│   ├── js
│   └── themes
└── views

Setup

Modify the src/main.go file accordingly. You'll need to set up your routes/logic. What's provided is a virtual clone of my own blog so any routes are set up as such. If you want a 'hello world' piece, just strip the extra routes and consult the Go docs if it's a bit foggy what's happening.

The views directory should contain any static pages, templates, or includes. The static dir should contain any clientside scripts, stylesheets, images, and other assets.

The src directory is of course your backend code.

The scripts directory are any development tools or helpers you need. Included is a script that helps with deployment to a Digital Ocean server, for example. If you're using D.O. you may want to edit the desitination path constants.

Development

Edit the files to suit your desired environment/setup nad run make build to build a macOS-compatible local binary. The default port for running the ./bin/server is 5005. Every backend change needs the server to be recompiled. Clientside/static changes don't require a rebuild.

Running

After building just run make start or ./bin/server. You can of course also use Golang's run command: go run src/main.go.

Deployment

Run make build-deploy and then make deploy. The make build-deploy command must be run first so that an Ubuntu/Linux-compatible binary is compiled. This is necessary for my D.O. deployment environment. Your needs may be different.

#!/usr/bin/env sh
PROJECT_DIR=$(pwd)/[!.][!vendor][!src]*
PROJECT_DEST_DIR="/srv/go_blog"
BIN_FILE="/srv/go_blog/bin/server"
POSTS_DIR="/srv/go_blog/posts"
testForCredentials() {
[ -z "$DO_USER" ] \
&& { echo "SSH user environment variable not set."; exit 1; }
[ -z "$DO_BLOG_IP" ] \
&& { echo "SSH ip address environment variable not set."; exit 1; }
}
deploy() {
testForCredentials;
echo "Building...";
make build-deploy;
echo "Done.";
echo "Uploading to server...";
ssh $DO_USER@$DO_BLOG_IP "mkdir $PROJECT_DEST_DIR";
#ssh $DO_USER@$DO_BLOG_IP "[ -z $BIN_FILE ] && rm -f $BIN_FILE;";
scp -rp $PROJECT_DIR $DO_USER@$DO_BLOG_IP:/srv/go_blog;
ssh $DO_USER@$DO_BLOG_IP "mkdir $POSTS_DIR";
scp -rp ./posts/* $DO_USER@$DO_BLOG_IP:/srv/go_blog/posts;
echo "Done.";
}
deploy;
GOPATH=$(shell pwd)/vendor:$(shell pwd)
GOBIN=$(shell pwd)/bin
GOFILES=$(wildcard *.go)
GONAME="server"
DEPLOY_OS="linux"
run:
@go run src/main.go
start:
./bin/$(GONAME)
build:
@make clean
@GOPATH=$(GOPATH) GOBIN=$(GOBIN) go build -o bin/$(GONAME) ./src/$(GOFILES)
build-deploy:
@make clean
@GOPATH=$(GOPATH) GOBIN=$(GOBIN) GOOS=$(DEPLOY_OS) go build -o bin/$(GONAME) ./src/$(GOFILES)
test:
@echo "Running tests..."
@echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
@GOPATH=$(GOPATH) go test ./src && echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nPASSED"
get:
@GOPATH=$(GOPATH) GOBIN=$(GOBIN) go get ./src
clean:
@GOPATH=$(GOPATH) GOBIN=$(GOBIN) go clean
@rm -rf ./bin ./pkg
reset:
@rm -rf ./vendor ./bin ./pkg
docs:
@godoc -http=:5005
deploy:
@./scripts/deploy.sh
.PHONY: build build-deploy get run start clean docs deploy
package main
import (
"encoding/json"
"fmt"
"html/template"
"io/ioutil"
"log"
"math/rand"
"net/http"
"os"
"path"
"strconv"
"strings"
blackfriday "gopkg.in/russross/blackfriday.v2"
)
// SiteSettings - settings JSON data struct
type SiteSettings struct {
Title string
Slogan string
Notice string
Hostname string
Port string
PostExt string
PostsDir string
ViewsDir string
StaticDir string
}
// Page - site page data
type Page struct {
Title string
Date string
Greeting interface{}
Content interface{}
}
// DirInfo - directory info including the original path
type DirInfo struct {
Children []os.FileInfo
Info os.FileInfo
Path string
}
// PostInfo - ... tbd
type PostInfo struct {
Info []byte
Path string
Meta PostMeta
Data interface{}
}
// PostDate - structure for holding parsed dates
type PostDate struct {
Year string
Month string
Day string
}
// PostMeta - Basic post metadata
type PostMeta struct {
Title string
DateStr string
Date PostDate
Excerpt string
URL string
image string
}
// StringKeyValue - JSON key:value as string
type StringKeyValue map[string]string
// Globals
var cwd, _ = os.Getwd()
var settings = loadSettings()
var staticPath = cwd + settings.StaticDir
var viewsPath = cwd + settings.ViewsDir
var postsPath = cwd + settings.PostsDir
var postList, getPostListErr = getPostList()
// loadSettings - Load the site settins from a JSON to prevent
// the need to recompile if certain global settings change.
func loadSettings() SiteSettings {
settingsPath := cwd + "/site-config.json"
settingsFileOutput, _ := ioutil.ReadFile(settingsPath)
var result StringKeyValue
json.Unmarshal(settingsFileOutput, &result)
settingsOutput := SiteSettings{
Title: result["title"],
Slogan: result["slogan"],
Notice: result["notice"],
Hostname: result["hostname"],
Port: result["port"],
PostExt: result["postExt"],
PostsDir: result["postsDir"],
ViewsDir: result["viewsDir"],
StaticDir: result["staticDir"],
}
return settingsOutput
}
// getContents - Get the markdown contents of a post and
// convert it to renderable HTML
func getContents(path string) []byte {
postFilePath := postsPath + path
fullContents, err := ioutil.ReadFile(postFilePath)
// Remove the metadata header. Maybe extract this process later on.
contents := strings.Split(string(fullContents), "---")[1]
if err != nil {
log.Fatal(err)
}
output := blackfriday.Run([]byte(contents))
return output
}
// getPostName - Parse the provided string and return
// the full post filename.
func getPostName(filepath string) string {
filename := path.Base(filepath)
postName := strings.Split(filename, settings.PostExt)
return postName[0]
}
// getPostFilename - concatenate the standard blog post
// file extension and the provided path to the post.
func getPostFilename(postURL string) string {
return postURL + settings.PostExt
}
// getPostDate - format a date as a structure for convenient use
func getPostDate(dateStr string) PostDate {
dateSpl := strings.Split(dateStr, "-")
return PostDate{
Year: dateSpl[0],
Month: dateSpl[1],
Day: dateSpl[2],
}
}
// getPostURL - get a formatted post url string
func getPostURL(path string, date PostDate) string {
var postURL string
postName := getPostName(path)
postURL = "/posts/" + date.Year + "/" + date.Month + "/" + postName
return postURL
}
// getPostMeta - generate a struct containing basic post metadata
func getPostMeta(path string) PostMeta {
postFile, _ := ioutil.ReadFile(path)
dat := string(postFile)
headerDelim := "---"
infoDelim := ": "
postHeader := strings.Split(dat, headerDelim)[0]
postHeaderSpl := strings.Split(postHeader, "\n")
postTitle := strings.Split(postHeaderSpl[0], infoDelim)[1]
postDateStr := strings.Split(postHeaderSpl[1], infoDelim)[1]
postDate := getPostDate(postDateStr)
postExcerpt := strings.Split(postHeaderSpl[2], infoDelim)[1]
postURL := getPostURL(path, postDate)
postImage := strings.Split(postHeaderSpl[3], infoDelim)[1]
postMeta := PostMeta{
Title: postTitle,
Date: postDate,
DateStr: postDateStr,
Excerpt: postExcerpt,
URL: postURL,
image: postImage,
}
return postMeta
}
// getPostList - Get and return a list of absolute paths
// to every available blog post and as a second value return
// a list of every available post filename
func getPostList() ([]PostInfo, error) {
var yearsDirs []os.FileInfo
var years []DirInfo
var months []DirInfo
var posts []PostInfo
var err error
yearsDirs, err = ioutil.ReadDir(postsPath)
if err != nil {
return nil, err
}
for _, year := range yearsDirs {
thisYear := DirInfo{
Info: year,
Path: postsPath + year.Name() + "/",
}
thisYear.Children, err = ioutil.ReadDir(thisYear.Path)
if err != nil {
return nil, err
}
years = append(years, thisYear)
}
for i := 0; i < len(years); i++ {
for j := 0; j < len(years[i].Children); j++ {
thisMonthPath := years[i].Path + years[i].Children[j].Name() + "/"
thisMonth := DirInfo{
Info: years[i].Children[j],
Path: thisMonthPath,
}
thisMonth.Children, err = ioutil.ReadDir(thisMonth.Path)
if err != nil {
return nil, err
}
months = append(months, thisMonth)
}
}
for _, month := range months {
for i := 0; i < len(month.Children); i++ {
postName := month.Children[i].Name()
postPath := month.Path + postName
postData, err := ioutil.ReadFile(postPath)
if err != nil {
return nil, err
}
fmt.Println(postPath)
postMeta := getPostMeta(postPath)
thisPost := PostInfo{
Info: postData,
Path: postPath,
Meta: postMeta,
}
posts = append(posts, thisPost)
}
}
return posts, nil
}
// HomeRouteHandler - Response for the home page
func HomeRouteHandler(w http.ResponseWriter, r *http.Request) {
pageTemplate := template.Must(template.ParseFiles(viewsPath + "/home.html"))
postImageBasePath := "/static/images/"
var postLinks string
var noScriptPostLinks string
var postImage string
for i := 0; i < len(postList); i++ {
if postList[i].Meta.image != "" {
postImage = "url('" + postImageBasePath + postList[i].Meta.image + "')"
} else {
r := strconv.Itoa(rand.Intn(255))
g := strconv.Itoa(rand.Intn(255))
b := strconv.Itoa(rand.Intn(255))
postImage = "rgb(" + r + "," + g + "," + b + "); filter: saturate(0.5)"
}
postLinks += `<div class="column col-6 col-sm-12">
<div class="parallax post-card rounded" onClick="window.location.href = '` + postList[i].Meta.URL + `'">
<div class="parallax-top-left" tabindex="1"></div>
<div class="parallax-top-right" tabindex="2"></div>
<div class="parallax-bottom-left" tabindex="3"></div>
<div class="parallax-bottom-right" tabindex="4"></div>
<div class="parallax-content">
<div class="post-card__front parallax-front">
<div class="post-card__label title rounded">` + postList[i].Meta.Title + `</div>
<div class="post-card__label"><small>` + postList[i].Meta.DateStr + `</small></div>
<div class="post-card__label rounded">
<a href="` + postList[i].Meta.URL + `">Read More</a>
</div>
</div>
<div class="parallax-back" style="background: ` + postImage + `; width: 100%; height: 400px; clear: both;">
</div>
</div>
</div>
</div>`
noScriptPostLinks += `<div>
<h5><a href="` + postList[i].Meta.URL + `">` + postList[i].Meta.Title + `</a> - ` + postList[i].Meta.DateStr + `</h5>
</div>`
}
data := Page{
Title: settings.Title,
Content: template.HTML(`<div class="container">
<div class="columns" style="display:none;" id="post-cards">` + postLinks + `</div>
<noscript>
<nav>
<h2>Post</h2>
<hr />
` + noScriptPostLinks + `
</nav>
</noscript>
<script>
document.querySelector("#post-cards").style = null;
</script>
</div>`),
}
pageTemplate.Execute(w, data)
}
// AboutRouteHandler - Response for the about page
func AboutRouteHandler(w http.ResponseWriter, r *http.Request) {
pageTemplate := template.Must(template.ParseFiles(viewsPath + "/about.html"))
pageTemplate.Execute(w, nil)
}
// CvRouteHandler - Response for the CV page
func CvRouteHandler(w http.ResponseWriter, r *http.Request) {
pageTemplate := template.Must(template.ParseFiles(viewsPath + "/cv.html"))
pageTemplate.Execute(w, nil)
}
// PostRouteHandler - Response for any blog post. Parses the markdown file
// and injects it into the page based on the `year/month/post-name`
// path beneath `/posts/` sub-routes`
func PostRouteHandler(w http.ResponseWriter, r *http.Request) {
postPath := strings.TrimPrefix(r.URL.Path, settings.PostsDir)
pageTemplate := template.Must(template.ParseFiles(viewsPath + "/post.html"))
postContents := getContents(getPostFilename(postPath))
data := Page{
Content: template.HTML(postContents),
}
pageTemplate.Execute(w, data)
}
func main() {
if getPostListErr != nil {
fmt.Print("SERVER EXITED DUE TO ERROR READING/PARSING POST LIST:\n")
log.Fatal(getPostListErr)
}
http.HandleFunc("/", HomeRouteHandler)
http.HandleFunc("/about", AboutRouteHandler)
http.HandleFunc("/cv", CvRouteHandler)
http.HandleFunc(settings.PostsDir, PostRouteHandler)
fs := http.FileServer(http.Dir(staticPath))
http.Handle(settings.StaticDir, http.StripPrefix(settings.StaticDir, fs))
fmt.Printf("Listening at %s%s\n", settings.Hostname, settings.Port)
log.Fatal(http.ListenAndServe(settings.Port, nil))
getPostList()
}
{
"title": "Shite Write...up",
"slogan": "",
"notice": "",
"hostname": "http://example.com",
"port": ":5005",
"postExt": ".md",
"postsDir": "/posts/",
"viewsDir": "/views",
"staticDir": "/static/"
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment