Blog

This blog post is a continuation of Building a Markdown Website with Caddy and concludes the two-part series. In the previous post, I discussed some of the benefits of building an entire website out of Markdown documents, and then I showed how Caddy can render markdown out-of-the-box. This post elaborates on some of these concepts to show how to build a very basic blog (like the one you're reading now)!

Features

Our blog will provide some pretty basic functionality:

For the sake of performance and simplicity, we won't enable the user to search for a particular blog post. After all, they can just use Google. 🫠

Directory Structure

Recall the directory structure from the prior post:

Last time, we focused mostly on the Caddyfile configuration and /index.html. In this post, we will talk about /markdown/blog/index.md, which is a Markdown file with some special Go template syntax. Below I have included most of index.md file, and as before, we'll discuss it one line at a time:

---
title: Blog
---

<!--
{{- $blogPath := .OriginalReq.URL.Path | trimPrefix "/blog/" }}
{{- $fileListing := listFiles "/markdown/blog" }}
{{- $blogFiles := list }}
{{- range $file := $fileListing }}
	{{- if ext $file | eq ".md" | and (eq "index.md" $file | not) }}
		{{- $file = replace (ext $file) "" $file }}
		{{- $blogFiles = append $blogFiles $file }}
	{{- end }}
{{- end}}
{{- $blogFiles = $blogFiles | sortAlpha | reverse }}
{{- $currBlogIndex := -1 }}
{{- if eq $blogPath "" }}
	{{- $currBlogIndex = 0 }}
{{- else }}
	{{- range $index, $file := $blogFiles }}
		{{- if kebabcase $file | eq $blogPath }}
			{{- $currBlogIndex = $index }}
		{{- end }}
	{{- end }}
{{- end }}
{{- if lt $currBlogIndex 0 }}
	{{- httpError 404 }}
{{- end }}

{{- $currBlogFile := index $blogFiles $currBlogIndex -}}
-->

# Blog

<nav>
<ul>
{{ if len $blogFiles | lt (add $currBlogIndex 1) }}
<li class="prev">
&larr; Previous: [{{ add $currBlogIndex 1 | index $blogFiles
}}]({{ add $currBlogIndex 1 | index $blogFiles | kebabcase | printf "./%s" }})
</li>
{{- end }}
{{- if gt $currBlogIndex 0 }}
<li class="next">
[{{ sub $currBlogIndex 1 | index $blogFiles
}}]({{ sub $currBlogIndex 1 | index $blogFiles | kebabcase | printf "./%s" }}): Next
&rarr;
</li>
{{- end }}
</ul>
</nav>

{{- if printf "/markdown/blog/%s.md" $currBlogFile | fileExists }}
{{- $markdownFile := printf "/markdown/blog/%s.md" $currBlogFile | readFile | splitFrontMatter }}
{{- $title := default (substr 10 -1 $currBlogFile) $markdownFile.Meta.title }}
{{- $date := default (substr 0 10 $currBlogFile) $markdownFile.Meta.date }}

## {{ $title }} _{{ $date }}_

{{ $markdownFile.Body }} {{- else }} No blog entries exist yet! {{- end }}

{{- if $blogFiles | len | lt 1 }}

---

# Blog Post Index

    {{- range $file := $blogFiles }}
    	{{- if eq $file $currBlogFile }}
    		{{- printf "\n- %s" $file }}
    	{{- else }}
    		{{- printf "\n- [%s](/blog/%s)" $file (kebabcase $file) }}
    	{{- end }}
    {{- end }}

{{- end }}

The first part of our blog index.md file includes the front matter meta data, and as mentioned in our prior post, this tells index.html how to render the <title> of the page.

---
title: Blog
---

The next section is an HTML comment containing some Go template syntax. Here's how it works step-by-step:

  1. Set $blogPath to the original URL path before rewriting to index.html. Then, we remove the /blog/ prefix from the path. See trimPrefix.

    {{- $blogPath := .OriginalReq.URL.Path | trimPrefix "/blog/" }}
    
  2. Set $fileListing to a list of filenames read from the /markdown/blog directory. See listFiles.

    {{- $fileListing := listFiles "/markdown/blog" }}
    
  3. Iterate through our filenames in $fileListing to populate a new list of $blogFiles. These files are only files that have the .md file extension. In addition, we exclude the current file (i.e. index.md). Finally, before adding the filename to the $blogFiles list, we remove the file extension.

    {{- $blogFiles := list }}
    {{- range $file := $fileListing }}
    	{{- if ext $file | eq ".md" | and (eq "index.md" $file | not) }}
    		{{- $file = replace (ext $file) "" $file }}
    		{{- $blogFiles = append $blogFiles $file }}
    	{{- end }}
    {{- end}}
    
  4. Sort $blogFiles in descending order. Since I have chosen filenames to begin with the ISO 8601 date format of YYYY-MM-DD, $blogFiles is now a list of all blog entry filenames with the *.md file extension removed, sorted chronologically with the newest blog entries appearing first. See sortAlpha and reverse.

    {{- $blogFiles = $blogFiles | sortAlpha | reverse }}
    
  5. Determine the current blog post based on the $blogPath we computed in step 1. We assume that the URL path is just the kebab case version of the Markdown filename. For example, the HTTP request /blog/2024-09-06-new-website should render the Markdown file at /markdown/blog/2024-09-06 New Website.md. If the $blogPath is the empty string, it must be that we are requesting /blog/, so we just show the first (most recent) blog post at index 0. If the blog post was not found, we issue a 404 Not Found.

    {{- $currBlogIndex := -1 }}
    {{- if eq $blogPath "" }}
    	{{- $currBlogIndex = 0 }}
    {{- else }}
    	{{- range $index, $file := $blogFiles }}
    		{{- if kebabcase $file | eq $blogPath }}
    			{{- $currBlogIndex = $index }}
    		{{- end }}
    	{{- end }}
    {{- end }}
    {{- if lt $currBlogIndex 0 }}
    	{{- httpError 404 }}
    {{- end }}
    
    {{- $currBlogFile := index $blogFiles $currBlogIndex -}}
    

Rendering the Blog Post

At this point, we are now ready to render the current blog post:

{{- if printf "/markdown/blog/%s.md" $currBlogFile | fileExists }}
{{- $markdownFile := printf "/markdown/blog/%s.md" $currBlogFile | readFile | splitFrontMatter }}
{{- $title := default (substr 10 -1 $currBlogFile) $markdownFile.Meta.title }}
{{- $date := default (substr 0 10 $currBlogFile) $markdownFile.Meta.date }}

## {{ $title }} _{{ $date }}_

{{ $markdownFile.Body }} {{- else }} No blog entries exist yet! {{- end }}

This time, instead of using the include function as we did in index.html, we are using readFile, which does not evaluate any Go template syntax in the blog posts. We also use the YYYY-MM-DD date in the filename to determine the date of the blog post, although the front matter meta data can supercede this.

Before we render the blog post, we show the user some basic "Previous" and "Next" links to the previous and next blog post:

<nav>
	<ul>
		{{ if len $blogFiles | lt (add $currBlogIndex 1) }}
		<li class="prev">
			&larr; Previous: [{{ add $currBlogIndex 1 | index $blogFiles }}]({{ add
			$currBlogIndex 1 | index $blogFiles | kebabcase | printf "./%s" }})
		</li>
		{{- end }} {{- if gt $currBlogIndex 0 }}
		<li class="next">
			[{{ sub $currBlogIndex 1 | index $blogFiles }}]({{ sub $currBlogIndex 1 |
			index $blogFiles | kebabcase | printf "./%s" }}): Next &rarr;
		</li>
		{{- end }}
	</ul>
</nav>

Blog Index

And finally, at the end of our blog post entry, we show a table of all blog posts:

{{- if $blogFiles | len | lt 1 }}

---

# Blog Post Index

    {{- range $file := $blogFiles }}
    	{{- if eq $file $currBlogFile }}
    		{{- printf "\n- %s" $file }}
    	{{- else }}
    		{{- printf "\n- [%s](/blog/%s)" $file (kebabcase $file) }}
    	{{- end }}
    {{- end }}

{{- end }}

Conclusion

Again, it's quite amazing that this blog literally consists of a collection of Markdown files all rendered by Caddy under the hood with minimal configuration. No other software is needed! I hope you enjoyed this little mini-series. Now go forth and compose some Markdown documents!


Blog Post Index