Step 3 - Make the "Hello Gopher" Online

Now is it time to raise the bar and make your Go MPA site (Step 2) available online. These steps will guide you through deploying your Go MPA site to a VPS and make it available online. In this part, we will enhance the folder structure, add fonts, add favicons and improve the SEO of the site.

Hello3 Live Step 3 - Hello Gopher in Online
1. Expanded Folder Structure

Why this structure?

With this structure are basically all assets within the assets folder. Regardless if the normally should be served in the root. Go serve these files as they were in the root. Just simpler to manage.

── go/
    └── hello3/
        └── main/
            ├── assets/
            │   ├── css/
            │   │   ├── base.css
            │   │   └── fonts.css 
            │   ├── favicons/
            │   │   ├── apple-touch-icon.png
            │   │   ├── favicon-16x16.png
            │   │   ├── favicon-32x32.png
            │   │   ├── favicon-96x96.png 
            │   │   ├── favicon-192x192.png     
            │   │   ├── favicon.ico
            │   │   └── favicon.svg          
            │   ├── fonts/
            │   │   ├── source-sans-pro-v14-latin-300.woff
            │   │   ├── source-sans-pro-v14-latin-300.woff2
            │   │   ├── source-sans-pro-v14-latin-600.woff
            │   │   ├── source-sans-pro-v14-latin-600.woff2
            │   │   ├── source-sans-pro-v14-latin-regular.woff
            │   │   └── source-sans-pro-v14-latin-regular.woff2
            │   ├── img/
            │   │   ├── gopher.jpg
            │   │   └── gvp.jpg
            │   ├── seo/
            │   │   ├── robots.txt
            │   │   └── sitemap.xml                                   
            │   └── tmpl/              
            │       ├── components/ 
            │       │   ├── httphead.html   
            │       │   └── httpend.html    
            │       └── pages/              
            │           ├── home.html 
            │           └── gvp.html
            ├── go.mod
            ├── main
            ├── main.go  
            └── init.go 
2. assets - Add Base CSS and Font Styles

assets/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;
}

/* source-sans-pro-300 - latin */
@font-face {
  font-family: 'Source Sans Pro';
  font-style: normal;
  font-weight: 300;
  font-display: block;
  src:
    local('Source Sans Pro Light'), local('SourceSansPro-Light'),
    url('../fonts/source-sans-pro-v14-latin-300.woff2') format('woff2'),
    url('../fonts/source-sans-pro-v14-latin-300.woff') format('woff');
}

/* source-sans-pro-regular - latin */
@font-face {
  font-family: 'Source Sans Pro';
  font-style: normal;
  font-weight: 400;
  font-display: block;
  src:
    local('Source Sans Pro Regular'), local('SourceSansPro-Regular'),
    url('../fonts/source-sans-pro-v14-latin-regular.woff2') format('woff2'),
    url('../fonts/source-sans-pro-v14-latin-regular.woff') format('woff');
}

/* source-sans-pro-600 - latin */
@font-face {
  font-family: 'Source Sans Pro';
  font-style: normal;
  font-weight: 600;
  font-display: block;
  src:
    local('Source Sans Pro SemiBold'), local('SourceSansPro-SemiBold'),
    url('../fonts/source-sans-pro-v14-latin-600.woff2') format('woff2'),
    url('../fonts/source-sans-pro-v14-latin-600.woff') format('woff');
}



h1 {
  font-size: 3rem;
  line-height: 2rem;
  font-weight: 300;
}

h2 {
  font-size: 3rem;
  line-height: 3rem;
  font-weight: 300;
  margin-bottom: 15px;
}

h3 {
  font-size: 1.5rem;
  line-height: 2.5rem;
}

p {
  font-size: 1rem;
  line-height: 1.5rem;
  margin-bottom: 15px;
  max-width: 800px;
}

/* scales with root font size */
#header h1 {
  font-size: clamp(24px, 3vw, 36px);  
}
#header h2 {
  font-size: clamp(18px, 3vw, 24px);
}

3. assets - Add Favicon files

assets/favicons

Favicon files are in the assets/favicons folder. These are used to display the site's icon in the browser tab and bookmarks.

Favicons created using Realfavicongenerator

apple-touch-icon.png
favicon-16x16.png
favicon-32x32.png
favicon-96x96.png 
favicon-1
92x192.png     
favicon.ico
favicon.svg 

4. assets - Add Fonts

assets/fonts

To avoid downloading fonts on every page load, we store them in the assets/fonts folder for faster page load. You can download fonts from Google Fonts or other font providers.

Fonts dowloaded from Google Fonts

source-sans-pro-v14-latin-300.woff
source-sans-pro-v14-latin-300.woff2
source-sans-pro-v14-latin-600.woff
source-sans-pro-v14-latin-600.woff2
source-sans-pro-v14-latin-regular.woff
source-sans-pro-v14-latin-regular.woff2
5. 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

6. assets - Add SEO files

assets/seo

SEO files include robots.txt and sitemap.xml. These files help search engines understand and index your site.

Created by Sitemap Generator

Sitemap: https://hello3.go4webdev.org/seo/sitemap.xml
User-agent: *
Allow: /

<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
    <!--  created with Free Online Sitemap Generator www.xml-sitemaps.com  -->
    <url>
        <loc>https://mpa.go4webdev.org/gopher</loc>
        <lastmod>2025-11-28T16:27:13+00:00</lastmod>
        <priority>1.00</priority>
    </url>
    <url>
        <loc>https://mpa.go4webdev.org/gvp</loc>
        <lastmod>2025-11-28T16:27:13+00:00</lastmod>
        <priority>0.80</priority>
    </url>
</urlset>

7. assets - Add Reusable HTML Components

assets/html/components

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">

<!-- favicons & icons -->
<link rel="icon" href="/favicons/favicon.svg" type="image/svg+xml">
<link rel="icon" href="/favicons/favicon.ico" sizes="any">
<link rel="icon" type="image/png" sizes="16x16" href="/favicons/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="/favicons/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="192x192" href="/favicons/favicon-192x192.png">
<link rel="apple-touch-icon" href="/favicons/apple-touch-icon.png">

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

<!-- Fonts -->
<link rel="preload" href="fonts/source-sans-pro-v14-latin-300.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="fonts/source-sans-pro-v14-latin-regular.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="fonts/source-sans-pro-v14-latin-600.woff2" as="font" type="font/woff2" crossorigin>

<!-- Social sharing -->
<link rel="canonical" href="{{.url}}">
<meta property="og:type" content="website">
<meta property="og:site_name" content="go4webdev">
<meta property="og:url" content="{{.url}}">
<meta property="og:image" content="https://go4webdev.org/img/{{.page}}.jpg">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:image" content="https://go4webdev.org/img/{{.page}}.jpg">
<!--Convention: Each page must have a corresponding hero image named after its page ID-->
{{end}}

{{define "httpend"}}
<!-- javascript -->
{{end}}
8. assets - Add HTML templates

assets/html/pages

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).

{{$title := "Hello Gopher!"}}
{{$desc := "A simple web page built with Go Vanilla."}}

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

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

<body>
    <main>
        <div class="mainbox">
            <section>
                <h1>Hello Gopher!</h1>
                <img src="/img/gopher.jpg" alt="Go Vanilla"><br>
                <p>Gopher is the small, friendly mascot of the Go programming language has become an iconic symbol of
                    Go's simplicity, efficiency, and playful spirit. This micro Hello3 site — built with Go — uses the
                    Gopher image to illustrate its small footprint.
                </p>
                <a href="/gvp" class="button">Go Vanilla...</a>
            </section>
        </div>
    </main>
    {{template "httpend"}}
</body>

</html>

{{$title := "Go Vanilla stack..."}}
{{$desc := "GSV - (Go + Vanilla + Postgresql)"}}

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

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

<body>
    <main>
        <div class="mainbox">
            <section>
                <h1>Go Vanilla!</h1>
                <img src="/img/gvp.jpg" alt="Go Vanilla">
                <p>
                    With Vanilla Go as the foundation and Vanilla HTML/CSS/JS as the frontend, this stack stays
                    minimal. No frameworks, no build tools, no bundlers — just the core technologies working together in
                    their simplest forms. The Go Vanilla stack relies on LTS (Long Term Support) versions to reduce
                    maintenance burden, shrink the footprint, and keep the overall system as simple as possible.
                </p>
                <a href="/home" class="button">Go Home</a>
            </section>
        </div>
    </main>
    {{template "httpend"}}
</body>

</html>

9. Create the Go Server

Go

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

package main

import (
	"embed"
	"fmt"
	"html/template"
	"net/http"
	"strings"
)

// embed all assets into the binary
var (
	//go:embed assets/*
	assets embed.FS
	tpl    *template.Template
)

// initialize templates
func init() {
	init_html()
	init_assets()
}

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

// serve pages dynamically
func router(w http.ResponseWriter, r *http.Request) {
	path := strings.Trim(r.URL.Path, "/")
	if path == "" {
		path = "mpa"
	}

	if strings.HasPrefix(path, ".") {
		return
	}

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

// generate canonical URL for each page
func geturl(path string) map[string]string {
	return map[string]string{
		"page": path,
		"url":  "https://go4webdev.org/" + path,
	}
}

// set security headers
func setheader(w http.ResponseWriter) {
	w.Header().Set("Vary", "Accept-Encoding")
	w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
	w.Header().Set("X-Frame-Options", "SAMEORIGIN")
	w.Header().Set("X-Content-Type-Options", "nosniff")
	w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
	csp := "default-src 'self';" +
		"script-src 'self' *.cloudflareinsights.com;" +
		"style-src 'self';" +
		"connect-src 'self' *.cloudflare.com;" +
		"img-src 'self' https://*.go4webdev.org;" +
		"frame-ancestors 'none';" +
		"base-uri 'self';" +
		"form-action 'self';" +
		"upgrade-insecure-requests"

	w.Header().Set("Content-Security-Policy", csp)

	permissions := "geolocation=(), microphone=(), camera=(), payment=(), usb=(), accelerometer=(), gyroscope=()"
	w.Header().Set("Permissions-Policy", permissions)
	w.Header().Set("Cache-Control", "max-age=3600")
}


package main

import (
	"fmt"
	"html/template"
	"io/fs"
	"log"
	"net/http"
	"path"
)

// Initialize HTML templates
func init_html() {
	var err error
	// Load templates from the correct path
	tpl_files, err := fs.Sub(assets, "assets/tmpl")
	if err != nil {
		log.Fatal("loading templates: ", err)
	}

	// Parse all HTML files recursively in the template structure
	tpl, err = template.New("").ParseFS(tpl_files, "**/*.html")
	if err != nil {
		log.Fatal("parsing templates: ", err)
	}
}

// Serve assets files
func init_assets() {
	dirs := []string{"img", "css", "js", "fonts", "favicons", "pwa", "seo"}
	for _, dir := range dirs {
		static, err := fs.Sub(assets, path.Join("assets", dir))
		if err != nil {
			log.Printf("Skipping directory %s: %v", dir, err)
			continue
		}
		http.Handle("/"+dir+"/", http.StripPrefix("/"+dir+"/", http.FileServer(http.FS(static))))

		// "move" robots.txt,sitemap.xml etc to the root
		if dir == "seo" {
			http.Handle("/robots.txt", http.FileServer(http.FS(static)))
			http.Handle("/sitemap.xml", http.FileServer(http.FS(static)))
		}

		if dir == "favicons" {
			http.Handle("/favicon.ico", http.FileServer(http.FS(static)))
			http.Handle("/favicon.svg", http.FileServer(http.FS(static)))
			http.Handle("/apple-touch-icon.png", http.FileServer(http.FS(static)))
			http.Handle("/favicon-16x16.png", http.FileServer(http.FS(static)))
			http.Handle("/favicon-32x32.png", http.FileServer(http.FS(static)))
			http.Handle("/favicon-96x96.png", http.FileServer(http.FS(static)))
			http.Handle("/favicon-192x192.png", http.FileServer(http.FS(static)))
		}
	}
}

10. Test and Deploy to VPS

Test Offline first

Compile and start the server in VS Code terminal. Run these commands:

go build
./main

To stop the server: Press "Ctrl+C" in the terminal

Open a browser and visit: http://127.0.0.1:1234

You should see the "Hello Gopher" page with the gopher image and a button linking to the second page.

Troubleshooting:
- Port already in use → change port :1234 in main.go
- Template error? Check file paths match exactly (case-sensitive on Linux/Mac)
- Hard refresh (cache) → Windows: Ctrl+F5, Mac: Cmd+Shift+R
- Browser caching → try incognito or restart terminal
- Assets not loading? Verify the //go:embed directive includes assets/**
- Caching issues. Clear browser cache, use incognito mode, Kill Terminal.
- Page not found? Ensure template filename matches the URL (e.g., "/gvp" needs "gvp.html")

Deploy Online

Deploying your Go executable to a VPS involves some additional steps such as:

Deployment Checklist (Simplified)
1. Test locally in browser (as above)
2. Compile Go binary for Debian: GOOS=linux GOARCH=amd64 go build
3. Set up a VPS and transfer the binary.
4. Configure Nginx as a reverse proxy.
5. Set up DNS (e.g., Cloudflare) and SSL.
6. Use Systemd for automatic restarts.

Full VPS setup is documented here:

Hosting Go executables

Once you have followed the hosting guide, your Go MPA site will be live on the internet. Congratulations — you have deployed your first Go MPA site!