Author
connor mccutcheon

Welcome to Gitpost

Welcome to Gitpost, our home for code and company. This blog is an essential component of our vision. We aim to share ideas internally and externally about the process of building Gitpost—the business, the product, and most importantly, the company. This app will be a foundational pillar of our company’s culture, so we need to build a strong foundation.

Getting Serving

First and foremost, we need to get our file server running. This server will initially serve articles uploaded into the source code, and we will add a database later. To get started, let's use a basic directory structure with a main package in our root directory, a public directory for static files, and a templates directory for HTML files. This should be familiar to anyone who has used the Go standard library. We are also going to add an articles directory for our markdown articles.

package main

import (
	"embed"
	"html/template"
	"log"
	"net/http"
)

var (
	//go:embed all:articles
	articlesFS embed.FS

	//go:embed all:public
	publicFS embed.FS

	//go:embed all:templates
	templatesFS embed.FS
	
	templates = template.Must(template.ParseFS(templatesFS, "templates/*.html"))
)

func main() {
	log.Println("Running server @ http://localhost:5000")
	
	http.Handle("GET /{$}", serve("frontpage.html"))
	http.Handle("/public/", http.FileServer(http.FS(publicFS)))

	if err := http.ListenAndServe("0.0.0.0:5000", nil); err != nil {
		log.Fatal(err)
	}
}

func serve(tmpl string) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if err := templates.ExecuteTemplate(w, tmpl, nil); err != nil {
			fmt.Fprint(w, err.Error())
		}
	}
}

One thing that might stand out is the serve function at the bottom of the file. This is a common function I will use during the prototyping phase. When I am using mock data and am focus more on the user experience flows and mobile responsiveness. Here is a basic html example of what we might be developing for the homepage. For more information about this project check out the repository.

{{template "head-include"}}

<body>
  <main>
	<article>
		<h2>First Blog Post</h2>
		<p>This is a short description...</p>
	</article>
	<article>
		<h2>Second Blog Post</h2>
		<p>This blog contains images...</p>
	</article>
  </main>
</body>

We could add a function to the signature of the serve function, but I think that would messy the implementation and ultimately be a worse clutter the main function which I would like to avoid. Instead, lets take an object oriented approach by switching out the anonymous function and closured variables for a data structure with the ServeHTTP method, and change our original serve function into a factory.

func serve(tmpl string) Server {
	return Server{Template: tmpl}
}

type Server struct {
	Template string
	Request  *http.Request
	Article  *Article
}

func (app Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	app.Request = r
	if err := templates.ExecuteTemplate(w, tmpl, &app); err != nil {
		fmt.Fprint(w, err.Error())
	}
}

Importantly here we are not using a pointer to the newly constructed Server object. This in tern will create a new instance of the Server object on each request. This gives us a nice isolation and actor model, with very minor overhead. Public methods defined on a pointer to the Server struct will be available to the template via the change to the ExecuteTemplate call. Let's implement a method and update our html template.

func (s *Server) Articles() (articles []Article) {
	dirs, err := articlesFS.ReadDir("articles")
	if err != nil {
		return
	}
	for _, dir := range dirs {
		if !dir.IsDir() {
			continue
		}
		files, err := articlesFS.ReadDir("articles/" + dir.Name())
		if err != nil {
			continue
		}
		for _, f := range files {
			name := f.Name()
			articles = append(articles, Article{dir.Name(), name[:len(name)-3]})
		}
	}
	return articles
}
{{template "head-include"}}

<body>
  <main>
	{{range .Articles}}
	<article>
		<h2>{{.Title}}</h2>
		<p>{{.Summary}}</p>
	</article>
	{{end}}
  </main>
</body>

You might at this point be wondering what an Article is, so here is that file, though a more up to date version can be found in the repository. We are using Goldmark here, so congratulations on being our only Go dependency so far.

package main

import (...)

type Article struct {
	Root string
	Name string
}
  
func (a *Article) Author() string { return strings.Replace(a.Root, "-", " ", -1) }
func (a *Article) Title() string { return strings.Replace(a.Name, "-", " ", -1) }
func (a *Article) Content() template.HTML {
	path := fmt.Sprintf("articles/%s/%s.md", a.Root, a.Name)
	file, err := articlesFS.ReadFile(path)
	if err != nil {
		return template.HTML(err.Error())
	}
	var buf bytes.Buffer
	goldmark.Convert(file, &buf)
	return template.HTML(buf.String())
}

Gotta' Go Fast

No one wants to use a slow website, and any disruption to the flow of ideas must be eliminated. We could solve this in many ways, but we chose HTMX with a simple improvement to our links to make pages load asynchronously and noticeably improves the experience. Congratulations on being our only Javascript dependency.

 <main hx-boost="true">
	 {{range .Articles}}
		<a href="/article/{{.Root}}/{{.Name}}>
		...

Putting Lipstick on the Pig

Lastly we need to make it look passible before can start driving traffic. I like DaisyUI because of how easy it is to use with HTML and JSX. This allows us to not be tightly coupled to any choices in Javascript land.

<main class="grid grid-cols-3 gap-4 py-8 mx-auto max-w-screen-xl w-full">
	{{range .Articles}}
	<a class="col-span-2 card max-w-lg  bg-base-100">
	  <div class="card-body">
		  <h2 class="card-title font-bold mb-2 capitalize">{{.Title}}</h2>
		  <div class="flex items-center gap-2">
		    <div class="avatar">
			  <div class="w-6 rounded-full bg-base-300">
			    <img src="https://robohash.org/{{.Root}}">
			  </div>
		    </div>
		    <span class="font-bold opacity-80">{{.Author}}</span>
		  </div>
	  </div>
  </a>
  {{end}}
</div>