Step 2 - MPA site "Hello Gopher" Offline

Step 6 - Hello Gopher in browser

This tutorial shows how to build a simple two-page MPA that runs locally:

- How to structure a Go web server project
- Go embeds static files directly into the binary using the //go:embed directive
- DRY Components: Reusable HTML components to avoid repetition
- Static Asset Serving: Efficient delivery of CSS, JS, and images
- Vanilla Go Routing - Simple, custom routing without frameworks

By the end, you'll have a working site running at http://127.0.0.1:1234

1. Folder Structure

Folder Structure

This structure separates concerns:
- assets/ — All frontend files (HTML templates, CSS, JS, images)
- assets/tmpl/components/ — Reusable HTML snippets (DRY principle)
- assets/tmpl/pages/ — Complete page templates
- main.go — Server code and routing logic
- go.mod — Go module definition

The //go:embed directive in Go will bundle all assets into the compiled binary.

└── go/
    └── hello2/
        └── main/
            ├── assets/
            │   ├── css/
            │   │   ├── base.css
            │   ├── img/
            │   │   ├── gopher.jpg
            │   │   └── gvp.jpg
            │   ├── js/
            │   │   └── init.js                   
            │   └── tmpl/              
            │       ├── components/ 
            │       │   ├── httphead.html   
            │       │   └── httpend.html    
            │       └── pages/              
            │           ├── home.html 
            │           └── gvp.html
            ├── go.mod
            ├── main
            ├── main.go  
            └── init.go 

2. assets - Add CSS files

CSS

CSS files are in the assets/css folder. The base.css file contains the main styles.

html, body {
    box-sizing: border-box;
    padding: 0;
    margin: 0;
    text-decoration: none;
    font-family: 'Source Sans Pro', sans-serif;
    height: auto;
    font-size: 16px;
}

body {
    display: flex;
    justify-content: center;
    min-height: 100vh;
    margin: 0;
}

section {
    padding: 15px;
}

img {
    width: 100%;
    max-width: 800px;
    height: auto;
}

.button {
    display: inline-block;
    background-color: hsl(270, 50%, 50%);
    border: none;
    color: hsl(270, 50%, 98%) !important;
    padding: 0px 20px 0px 20px;
    height: 42px;
    line-height: 42px;
    text-align: center;
    text-decoration: none;
    font-size: 1rem;
    border-radius: 8px;
    margin-bottom: 15px;
    margin-right: 15px;
}

3. assets - Add Images

assets/img

Images can be in different formats. webp is amongst the smallest and most efficient formats. But not supported by all browsers yet. But good old jpg and png are still widely used.

gopher.jpg
gvp.jpg

4. assets - Add JavaScript files

Javascript

The init.js handles URL normalization. When someone visits the root URL (`/`), it redirects to "/home".

- Clean URLs (no empty paths)
- Consistent routing behavior
- The home page always loads by default

// initial load or load by refresh
document.addEventListener("DOMContentLoaded", function () {
    if (location.pathname === "/") {
        location.replace("/home");
    }
});

5. assets - Add HTML Reusable Components

Components makes the MPA more DRY

Instead of repeating the <head> content on every page, we define it once in a component "httphead". When you need to add a new CSS file or meta tag, you change it in one place, not on every page.

{{define "httphead"}}
<!-- Character encoding -->
<meta charset="UTF-8">

<!-- Viewport for responsive design -->
<meta name="viewport" content="width=device-width, initial-scale=1">

<!-- CSS -->
<link rel="stylesheet" href="/css/base.css">

{{end}}

{{define "httpend"}}
<script src="/js/init.js" defer></script>
{{end}}
6. assets - Add Go HTML templates

Go HTML templates

The micro site uses Go HTML templates to render the HTML pages (home.html and gvp.html). Both pages use two sub templates (httphead and httpend components).



<!DOCTYPE html>
<html lang="en">

<head>
    {{template "httphead" .}}
    <title>"Go Vanilla..."</title>
    <meta name="description" content="Go Vanilla everything!">
</head>

<body>
    <main>
        <div class="mainbox">
            <section>
                <h1>Hello Gopher!</h1>
                <img src="/img/gopher.jpg" alt="Hello Go"><br><br>
                <a href="/gvp" class="button">Step 2 of Go Web Programming with Vanilla Go</a>
            </section>
        </div>
    </main>
    {{template "httpend"}}
</body>

</html>

{{$title := "Go Vanilla!"}}
{{$desc := "Go + Vanilla everything!"}}

<!DOCTYPE html>
<html lang="en">

<head>
    {{template "httphead" .}}
    <title>{{$title}}</title>
    <meta name="description" content="{{$desc}}">
</head>

<body>
    <main>
        <div class="mainbox">
            <section>
                <h2>Go Vanilla!</h2>
                <img src="/img/gvp.jpg" alt="Go Vanilla">
                <br><br>
                <a href="/home" class="button">Go Home</a>
            </section>
        </div>
    </main>
    {{template "httpend"}}
</body>

</html>

7. Create the Go Server

Go

The entire Go server code is in main.go. The code uses the net/http package to create a simple web site.

package main

import (
	"embed"
	"fmt"
	"io/fs"
	"net/http"
	"strings"
	"text/template"
)

//go:embed assets/*
var assets embed.FS

var tpl *template.Template

func init() {
	// load templates
	tpl = template.Must(template.ParseFS(assets,
		"assets/tmpl/pages/*.html",
		"assets/tmpl/components/*.html",
	))

	// load assets
	static, _ := fs.Sub(assets, "assets")
	http.Handle("/css/", http.FileServer(http.FS(static)))
	http.Handle("/js/", http.FileServer(http.FS(static)))
	http.Handle("/img/", http.FileServer(http.FS(static)))
}

// entry point
func main() {
	http.HandleFunc("/", router)
	http.ListenAndServe(":1234", nil)
}

// serve pages dynamically
func router(w http.ResponseWriter, r *http.Request) {

	path := strings.Trim(r.URL.Path, "/")
	if path == "" || strings.HasPrefix(path, ".") {
		path = "home"
	} else if path == "favicon.ico" {
		return
	}

	err := tpl.ExecuteTemplate(w, path+".html", nil)
	if err != nil {
		log.Println("Something is wrong ", err.Error())
		http.Redirect(w, r, "/home", http.StatusSeeOther)
		return
	}
}

In Step 3, we'll deploy this to a VPS and make it accessible online with a real domain name.