How do I build this blog?
2019-10-11
Categories: Programming
Recently, I decided to find a new job as a Golang developer. So, I updated my resume, sent to my friends to ask for review. Then I submitted it enclosed herewith a cover letter to recruiters. Some didn’t reply, and the other replied with a message like this “You are so good, but I’m so sorry…”.
What is the reason?
As you can see in my resume, I started my career as a .NET developer, then my passionate on Linux and open source lead me to a different direction: system administrator. I dedicated myself to this role for a significant period before transitioning back to work as a Golang developer 2 years ago.
I was looking around to see how can I write a software engineering resume and here’s what I found:
- https://www.freecodecamp.org/news/heres-the-resume-i-used-to-get-a-job-at-google-as-a-software-engineer-26516526f29a/
- https://www.freecodecamp.org/news/writing-a-killer-software-engineering-resume-b11c91ef699d/
An important section that I was missing is: Software Projects. So I decided to write my own blog to boost up my resume. Moreover, writing things down also give me an opportunity to truly understand what I’ve learned.
The main parts that I’m going to do:
- blog posts are wrote in markdown (metadata use YAML format)
- pagination
- create a master page by adding header, footer
- add commenting system
- Medium layout
- make it mobile friendly
Development
Parsing metadata and markdown
An example of metadata:
1title: Resume 2date: Mon Sep 30 20:43:10 +07 2019 3description: My resume 4tags: 5 - devops 6 - golang 7 - resume
A blog post struct:
1type Post struct { 2 Title string 3 Date publishDate 4 Description string 5 Content template.HTML 6 Tags []string 7 File string 8}
It can be parsed like this:
1 fileread, err := ioutil.ReadFile(f) 2 if err != nil { 3 return nil, errors.Wrap(err, "ioutil.ReadFile") 4 } 5 6 lines := strings.Split(string(fileread), "\n") 7 var closingMetadataLine int 8 for i := 1; i < len(lines); i++ { 9 if lines[i] == yamlDelim { 10 closingMetadataLine = i 11 } 12 } 13 metadata := strings.Join(lines[1:closingMetadataLine], "\n") 14 15 p := Post{} 16 if err := yaml.Unmarshal([]byte(metadata), &p); err != nil { 17 return nil, errors.Wrap(err, "yaml.Unmarshal") 18 }
and the content can be processed by using Blackfriday:
1 content := strings.Join(lines[closingMetadataLine+1:], "\n") 2 p.Content = template.HTML(bf.Run( 3 []byte(content), 4 bf.WithRenderer( 5 bfchroma.NewRenderer( 6 bfchroma.WithoutAutodetect(), 7 bfchroma.ChromaOptions(html.WithLineNumbers()), 8 bfchroma.ChromaStyle(styles.SolarizedDark), 9 ), 10 ), 11 ))
Pagination
Based on current page, a paginator
template can be created as follows:
1{{ define "paginator" }} 2{{if .paginator.HasPages}} 3<ul class="pagination pagination"> 4 {{if .paginator.HasPrev}} 5 <li><a href="{{.paginator.PageLinkFirst}}">First</a></li> 6 <li><a href="{{.paginator.PageLinkPrev}}">«</a></li> 7 {{else}} 8 <li class="disabled"><a>First</a></li> 9 <li class="disabled"><a>«</a></li> 10 {{end}} 11 {{range $_, $page := .paginator.Pages}} 12 <li{{if $.paginator.IsActive .}} class="active"{{end}}> 13 <a href="{{$.paginator.PageLink $page}}">{{$page}}</a> 14 </li> 15 {{end}} 16 {{if .paginator.HasNext}} 17 <li><a href="{{.paginator.PageLinkNext}}">»</a></li> 18 <li><a href="{{.paginator.PageLinkLast}}">Last</a></li> 19 {{else}} 20 <li class="disabled"><a>»</a></li> 21 <li class="disabled"><a>Last</a></li> 22 {{end}} 23</ul> 24{{end}} 25{{ end }}
then included in the home
template:
1{{ define "home" }} 2{{ template "header" . }} 3<h1>QuanTA's blog</h1> 4{{ range .posts }} 5 <a href="/posts/{{ .File }}"><h2>{{ .Title }}</h2></a> 6 <p>{{ .Date | formatDate }}</p> 7 <p>Tags: 8 {{ range $_, $tag := .Tags }} 9 <a href="/tags/{{ $tag }}">#{{ $tag }}</a> 10 {{ end }} 11 </p> 12 <p>{{ .Description }}</p> 13 <hr> 14{{ end }} 15{{ template "paginator" . }} 16{{ template "footer" . }} 17{{ end }}
When executing home
template, I need to pass both paginator
and posts
, so I used pongo2
context to do that:
1 nums := len(posts) 2 paginator := pagination.NewPaginator(r, postsPerPage, int64(nums)) 3 offset := paginator.Offset() 4 endPos := offset + postsPerPage 5 if endPos > nums { 6 endPos = nums 7 } 8 data := pongo2.Context{"paginator": paginator, "posts": posts[offset:endPos]} 9 if err := templates.ExecuteTemplate(w, "home", data); err != nil { 10 log.Fatal(err) 11 }
Adding header, footer
Every pages should have the same header and footer. So, I added it follow this
Commenting system
Since I don’t have much experience with frontend, I decided to embed a commenting system instead of writing my own. Looking around and I found:
- https://fedidat.com/530-blog-comments/
- https://techroads.org/comparing-blog-comment-systems-with-privacy-features/
Tried both commento and remark42 and I choosed remark42
for some reasons:
- Login with only one click (instead of two in
commento
: click on Login then chose Google, GitHub, …) - The editor has some basic icons: bold, italic, quote, code, link, image, …
I built my own docker image to run on Raspberry Pi 4:
1 remark42: 2 build: . 3 image: quantonganh/remark42:arm32 4 container_name: "remark42" 5 hostname: "remark42" 6 restart: always 7 environment: 8 - REMARK_URL=https://remark42.dynu.net 9 - SECRET=xx 10 - STORE_BOLT_PATH=/srv/var/db 11 - BACKUP_PATH=/srv/var/backup 12 - AUTH_GITHUB_CID=<client_id> 13 - AUTH_GITHUB_CSEC=<client_secret> 14 volumes: 15 - ./var:/srv/var 16 labels: 17 - traefik.enable=true 18 - traefik.http.routers.remark42.rule=Host(`remark42.dynu.net`) 19 - traefik.http.routers.remark42.entrypoints=https 20 - traefik.http.routers.remark42.tls.certresolver=le 21 - traefik.http.services.remark42.loadbalancer.server.port=8080
In the frontend side, I just need to something like this at the end of post
template:
1<script> 2 var remark_config = { 3 host: "https://remark42.dynu.net", 4 site_id: 'remark', 5 components: ['embed'], 6 url: 'https://quanta.dynu.net/posts/{{ .File }}',
Make sure that host
is matched with REMARK_URL
environment variable and url
is the URL of a specifi blog post.
Medium layout
https://www.freecodecamp.org/news/how-to-recreate-mediums-article-layout-with-css-grid-b4608792bad1/
Mobile friendly
https://www.thesitewizard.com/css/mobile-friendly-responsive-design.shtml
Continuous Integration
1 gitea: 2 image: quantonganh/gitea:1.9.4-arm32v6 3 container_name: gitea 4 restart: always 5 volumes: 6 - gitea:/data 7 ports: 8 - "222:22" 9 environment: 10 - DISABLE_REGISTRATION=true 11 labels: 12 - traefik.enable=true 13 - traefik.http.services.gitea.loadbalancer.server.port=3000 14 - traefik.http.routers.gitea.rule=Host(`git-tea.dynu.net`) 15 - traefik.http.routers.gitea.entrypoints=http 16 - traefik.http.routers.gitea.middlewares=https-redirect@file 17 - traefik.http.routers.gitea-secured.rule=Host(`git-tea.dynu.net`) 18 - traefik.http.routers.gitea-secured.entrypoints=https 19 - traefik.http.routers.gitea-secured.tls=true 20 - traefik.http.routers.gitea-secured.tls.certresolver=le 21 22 drone: 23 image: drone/drone:1-linux-arm 24 container_name: drone 25 restart: always 26 volumes: 27 - drone:/data 28 environment: 29 - DRONE_AGENTS_ENABLED=true 30 - DRONE_GITEA_SERVER=https://git-tea.dynu.net 31 - DRONE_GITEA_CLIENT_ID=<client_id> 32 - DRONE_GITEA_CLIENT_SECRET=<client_secret> 33 - DRONE_RPC_SECRET=<rpc_secret> 34 - DRONE_SERVER_PROTO=https 35 - DRONE_SERVER_HOST=drone-ci.dynu.net 36 - DRONE_USER_CREATE=username:quanta,admin:true 37 labels: 38 - traefik.enable=true 39 - traefik.http.services.drone.loadbalancer.server.port=80 40 - traefik.http.routers.drone.rule=Host(`drone-ci.dynu.net`) 41 - traefik.http.routers.drone.entrypoints=http 42 - traefik.http.routers.drone.middlewares=https-redirect@file 43 - traefik.http.routers.drone-secured.rule=Host(`drone-ci.dynu.net`) 44 - traefik.http.routers.drone-secured.entrypoints=https 45 - traefik.http.routers.drone-secured.tls=true 46 - traefik.http.routers.drone-secured.tls.certresolver=le
https-redirect
middleware is configured like this:
1http: 2 middlewares: 3 https-redirect: 4 redirectScheme: 5 scheme: https
Deploy
Dockerfile
is so simple:
1FROM arm32v7/alpine:3.10 2 3RUN mkdir /app 4 5COPY assets /app/assets 6 7COPY favicon.ico /app 8 9COPY posts /app/posts 10 11COPY templates/ /app/templates 12 13COPY blog /app 14 15WORKDIR /app 16 17ENTRYPOINT [ "/app/blog" ] 18 19EXPOSE 80
If you have a local docker registry, you can use docker plugin to push it to. Otherwise, you can build and make it avaiable to the host by something like this:
1 - name: build-docker 2 image: quantonganh/docker:19.03.3-rc1-armv7 3 commands: 4 - docker build -t quantonganh/blog:${DRONE_SOURCE_BRANCH} . 5 volumes: 6 - name: docker 7 path: /var/run/docker.sock
Tags: golang
Related Posts:
- How to create snippets in Helix?
- A terminal UI for Taskwarrior
- A simple terminal UI for ChatGPT
- Learning how to code
- gocloud - writing data to a bucket: 403