In this post I will talk a little about my workflow for publishing content. I find it quite simple and therefore decided to share it with you.
The first part is to know the infrastructure that I built for my server. My blog and homepage resides on my Raspberry Pi Zero W which is connected to my router with a small USB cable to receive power. And the router is connected to a UPS. This part was done this way because I wanted to venture into the world of hosting. And I liked the results, but it is much easier to pay for a VM in any cloud.
Router configuration
This part is not difficult, the goal is to route traffic to my server, for that I entered the Arris configuration and created a virtual host, I put a static IP for the RPi and added a port forwarder from port 443 to 4433. This way I can upload a service without needing root privileges.Some optional things that I decided to have, and with great difficulty, were:
- Business internet plan: to have ports 25, 80 and 443 unlocked. The block is illegal in Brazil, but anyway. Without this unlock, the only way is to use VPN.
- Static IP: together with the business plan it was the most difficult, at least 3 months talking to the operators, horrible service, unprepared staff.
The server
Now we come to the actual programming. The server is written in Go and listens on port 4433, so I run it as an unprivileged user, which gives me more security and simplifies the process a lot, since my user has all the permissions for the publishing flow. The code can be found on Git, also hosted on my RPI: repository.In the server, I decided to use templates, Go has a very good native package. That's why I have a template of the blog's homepage, with the article listing; and a template of the article. But if I wanted to, I could have used static pages too, which would be even simpler.
The article listing code is simple:
postDir, err := os.ReadDir(path.Join(root, "b")) if err != nil { println("read posts error:", err.Error()) } for _, p := range postDir { if l := len(p.Name()); p.Name()[l-5:] != ".html" { continue } name := strings.ReplaceAll(p.Name(), "-", " ") name = name[3 : len(name)-5] info, _ := p.Info() data = append( data, post{ Name: name, Link: p.Name(), CTime: info.ModTime(), }, ) } sort.Slice(data, func(i, j int) bool {return data[i].Link > data[j].Link}) if err := temp.ExecuteTemplate(w, "blog.html", data); err != nil { println("execute error:", err.Error()) }
I could have done better and used what was defined within the template to show a more interesting name. I did it this way in another project of mine, and it turned out quite interesting. But here I used this simpler way.
For the articles, I used a method so that I wouldn't have to restart the server when there was any change in the pages because when using templates, the parsing of the templates is usually done at the beginning of the program. See:
temp, err := posts.Clone() if err != nil { println("clone template", err.Error()) http.Error(w, err.Error(), http.StatusInternalServerError) return } _, err = temp.ParseFiles(path.Join(root, r.URL.Path)) if err != nil { println("parse template", err.Error()) http.Error(w, err.Error(), http.StatusInternalServerError) return } i := strings.LastIndex(r.URL.Path, "/") + 1 if err := temp.ExecuteTemplate(w, "template.html", r.URL.Path[i:]); err != nil { println("execute error:", err.Error()) }
This way, the base template (posts) is preloaded, and only the requested article is read with each execution. Therefore, I don't need to restart the server for changes in the pages, only when there are changes in the server code.
For these cases, the RPi has a cron job that restarts the server every 1 hour, simple and functional, by the way, the cron runs without privileges too. The file update is done using the good old scp , which I use through the command make to reduce the risk of errors:
deploy-blog: $(wildcard www/b/*.*) cd www && rssgen -a blmayer.dev -c "b" -d "blog by b" -t "feed" -l "en_US" b/*.html > b/feed.xml scp $^ zero:blmayer.dev/www/b/
Here I also generate the RSS using a simple shell script that I made. It's a classic to make your own RSS; most blogs have their own. A Google search will find some.
The templates
This part is the face of the blog and can change completely from one project to another simply by changing the CSS styles and HTML. However, the functionalities are basically the same: on the home page, we have something about the blog and the list of publications. Using the html/template package from Go is very simple:<!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>posts</title> <link rel=icon href=data:,> <style> {{`{{template "style.html"}}`}} </style> </head> <body> // ← <a href=/>back</a> <h1>Blog!</h1> {{`{{range .}}`}} // <a href=/b/{{`{{.Link}}`}}>{{`{{.Name}}`}}</a><br> {{`{{end}}`}} <h2>what is this?</h2> this is a small space on the internet that i post topics like programming, linux, mathematics and some random stuff. we are a happy member of the <a href="//250kb.club">250Kb</a> and <a href="//512kb.club">512Kb</a> clubs. <hr> <center> <a href=/dp>←</a> derelict garden webring <a href=/dn>→</a> </center> <br> This work is licensed under <a href=//creativecommons.org/licenses/by/4.0/>CC BY 4.0</a>. </body> </html>
This is the home page of the blog, the size could be further reduced, but I am already satisfied. Note the code snippet {{`{{range .}}`}} that creates the list of articles with the data that was passed in the ExecuteTemplate command. This way, the links are populated.
The article template is a bit different because I use the definitions when the article is requested:
<!DOCTYPE html> <html lang={{`"{{template "lang"}}"`}}> <head> <title>{{`{{template "title"}}`}}</title> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="description" content={{`"{{template "desc"}}"`}}> <meta name="revised" content={{`"{{template "revised"}}"`}}> <style> {{`{{template "style.html"}}`}} </style> </head> <body> // ← <a href=/b>back to posts</a> <h1>{{`{{template "title"}}`}}</h1> {{`{{template "content"}}`}} hr> <h3>responses</h3> {{`{{range (responses .)}}`}} <small> <b>From:</b> {{`{{(index .From 0).Address}}`}} <i style="float:right">{{`{{.Date.Format "Mon, 02 Jan 2006 15:04:05 MST"}}`}}</i> </small> <blockquote>{{`{{.TextBody}}`}}</blockquote> {{`{{end}}`}} <p><a href="mailto:blog@mail.blmayer.dev?subject={{`{{.}}`}}">+</a></p> </body> </html>
Basically the same thing, but here I use {{`{{template "content"}}`}}, which replaces itself with the indicated content. This method is interesting because it is dynamic, and the values are filled using ParseFiles(), which I showed previously.
Lastly, the content itself:{{` {{define "lang"}}en{{end}} {{define "title"}}title{{end}} {{define "desc"}}description{{end}} {{define "revised"}}2021-11-02{{end}} {{define "content"}}The {{`{{define ...}}`}} precisely define the value of the template that will be used above. This way, it is very simple to write a new article: just copy this template, rename the file, and fill in the fields.The content of the post.
{{end}}`}}
Another optional feature here, I added an email integration to be able to receive user feedback. Here, I use Dovel, a rudimentary email server that has a go interface to be used with templates.
About Git
If it were just the blog on the server, the code would be basically what is shown above. However, I added Git access and visualization using a Go library. The configuration is simply a configuration file. But that's a subject for another article.Adding content
Now that all the configuration and development work is done, creating and deploying content is simple:- Copy the template.
- Rename it
- Write the content.
- Run make deploy-blog.
Conclusion
It wasn't an easy process overall, and my impression is that we are technologically behind, as the worst part was the internet plan. If there weren't so many complications with blocked ports and network configuration by the operators, the project would have ended in a weekend (at least the functional part). Of course, styling and content development can take an indefinite amount of time. As I wanted to integrate with Git and email, the project became more complex.The code part is not complicated and can be even easier when using ready-made projects or pre-configured Docker images. Here I wanted to do everything from scratch for two reasons: 1. to learn how the many parts work internally, and 2. to create a lighter version than current projects.
It's this second point that I'm most proud of, everything is very light and efficient: the blog's homepage has 2700 bytes and loads in 80ms, it's valid and simple HTML, my portfolio, the page above the blog, has 575 bytes; this allows the project to be served from my Raspberry Pi Zero W, which only needs 5V to operate. In addition, it still loads other projects like my Git and email server.
These are the difficulties you may encounter if you decide to venture down this path, at least here in Brazil. I hope I've helped in some way. I say it's worth it if you value extreme simplicity, like to do things your way, and want to get away from the dependence of the infamous big techs, libraries or frameworks, and above all, learn a lot.
Future plans
I still want to change some things in the project, out of pure curiosity and zeal:- Use a solar panel and battery to save energy.
- Separate Git from the project.
- Migrate the Git code to my own implementation.