"Life is all about sharing. If we are good at something, pass it on." - Mary Berry

How do I build this blog?

2019-10-11

Categories: Programming

Raspberry Pi 4

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:

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:

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}}">&laquo;</a></li>
 7    {{else}}
 8        <li class="disabled"><a>First</a></li>
 9        <li class="disabled"><a>&laquo;</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}}">&raquo;</a></li>
18        <li><a href="{{.paginator.PageLinkLast}}">Last</a></li>
19    {{else}}
20        <li class="disabled"><a>&raquo;</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:

Tried both commento and remark42 and I choosed remark42 for some reasons:

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

Edit on GitHub

Related Posts: