Reading Time: 6m33s
Embarking on this project to learn something new, I had to avoid falling into common pitfalls of doing things that I know works. A comfortable and trusted approach would have been to opt for a database (sql or nosql) to keep state and manage content. Although this is tried, tested and works well… it does mean:
All of this will work, but we are trying out new technologies and tools… so why not break the mould?
| Aim | Ideas |
|---|---|
| Break the mold | Instead of maintaining state with a database, explore using local yaml files and read content from files |
| Markdown HTML parser | Writing is dynamic. Ideally we want to maintain flexibility, prioritise change and have a historical versions for comparisons. We could generate these journal entries from markdown, it is a file format that is common in the tech space and there could be adopted libraries readily available |
It would have been a comfortable reach to store and manage content with some sort of database, heck even try a bit of sqllite (something I’ve been wanting to try out for a while now). However, that would have gone down a path which albeit familiar would have led to unnecessarily increasing the complexity of this small project.
As the famous quote by Ralph Waldo Emerson:
All life is an experiment. The more experiments you make, the better.
In breaking the mold, I did attempt to set some initial constraints:
go standard library forAfter much deliberation, I decided on using yaml files to store content. go has excellent support for encoding yaml which is very similar to using json tags inside a struct declaration.
go has quite an extensive suite of tools when it comes to reading and writing of bytes. After all we have the famous io.Reader and io.Writer interfaces. I enjoy how much control go gives you, you can read an entire file all at once (just be conscious of memory usage) or optimise for memory usage we can read a file in chunks.
This meant, I only really needed to think about modelling my type definitions in a logical way. ie, say I want to build a small avatar component that displays the icons of software tools or languages. I can define the model as below:
1type TechAvatar struct {
2 Icon string `yaml:"Icon"`
3 Caption string `yaml:"Caption"`
4}
Which is quite easy to understand at first glance, and the yaml file for this is as simple:
1- Icon: "/static/gopher.png"
2 Caption: "Golang icon"
3- Icon: "/static/nodejs.png"
4 Caption: "NodeJS icon"
This yaml file can easily be opened and read into a slice of TechAvatar. Which can be passed to a component that iterates over the slice and builds the html template required for rendering the tech avatar in a generic style. Furthermore, we can make use of generics in go to make reading from the yaml file generic for any model.
1func readFromFile[T any](f string) []T {
2 file, err := os.Open(f)
3 if err != nil {
4 // handle the error
5 }
6 defer file.Close()
7 // read from the file
8 var items []T
9 err = yaml.Unmarshal(out, &items)
10 if err != nil {
11 // handle marshalling error
12 }
13 return items
14}
This simplified content management significantly, there was no need to manage and maintain a database. Whilst fetching content all runs through one single, centralised and generic function all thanks to go and generics.
Pretty nifty!
A lot of static site generators, generate the respective pages during build time. This significantly reduces latency costs of generating content to be served during runtime. Static site generation also drastically reduces complexity, as a developer does not need to manage and maintain a database (monitor latency, upgrade versions, migrations etc.). An additional advantage is the ability to leverage version control of content, as most likely content updates can be tracked by a git history of some sort.
A different approach to static site generators can be a more traditional database-driven solution. Where content of pages are fetched, injected into templates which generate html that is sent back to the client. This approach is adopted by famous frameworks such as WordPress and Drupal.
The downside to static site generation is flexibility, part of WordPress and what makes it so powerful is the plethora of plugins available to extend and enhance certain areas of a website, like SEO or a Subscription mailer etc.
In this example and experiment, we tried to incorporate the best of both worlds. Meaning:
markdownThe end solution was in actual fact quite an interesting journey. I ended up using markdown as a mechanism to store content, which I parsed and extended during runtime by making use of Goldmark markdown parser library, which is written in go. This library had a wide variety of parsers and extensions to choose from, and even allowed me to inject custom styling to some of the html elements as seen in the below snippet:
1func (a *tableStyleTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
2 for c := node.FirstChild(); c != nil; c = c.NextSibling() {
3 for r := c.FirstChild(); r != nil; r = r.NextSibling() {
4 // some code to inject custom styling or add custom html attributes to html elements
5 }
6 }
7}
Pretty nifty I think!
| Aim | Outcomes |
|---|---|
| Break the mold | A fun exercise working with files in go, the standard library provides powerful yet easy to use tools to interact with files and is simple enough to make use of generics in go to have a single function for all files that contains some content which has been modelled |
| Markdown HTML parser | This was probably the best win of the lot, I could create dynamic journal entries (or blogs) writing them in the familiar markdown syntax which is widely used and convert these markdown files into custom styled html elements |