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