Blog
Building a Markdown Blog with Caddy 2024-10-07
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:
- Display previous / next links that link to the previous and next blog entry
- Provide an index of all blog posts
/blog/
should render the most recent blog post/blog/2024-09-06-new-website
should render the blog post with the filename/markdown/blog/2024-09-06 New Website.md
.
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:
- /var/www/blakesite.com – website's root folder
- index.html – file with the secret sauce 😉 to load and render Markdown documents
- … other assets like JS, CSS, and images
- markdown
- index.md – the homepage (i.e.
/
) - .. other pages
- blog –
/blog/
- 2024-09-06 New Website.md –
/blog/2024-09-06-new-website
, for example. The URL is just the kebabcase version of the filename. - 2024-09-11 Why Databases.md
- ... any additional blog entries as Markdown documents
- index.md – has some secret sauce 😉 to run the blog
- 2024-09-06 New Website.md –
- index.md – the homepage (i.e.
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">
← 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
→
</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:
-
Set
$blogPath
to the original URL path before rewriting toindex.html
. Then, we remove the/blog/
prefix from the path. SeetrimPrefix
.{{- $blogPath := .OriginalReq.URL.Path | trimPrefix "/blog/" }}
-
Set
$fileListing
to a list of filenames read from the/markdown/blog
directory. SeelistFiles
.{{- $fileListing := listFiles "/markdown/blog" }}
-
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}}
-
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. SeesortAlpha
andreverse
.{{- $blogFiles = $blogFiles | sortAlpha | reverse }}
-
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.
Navigation
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">
← 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 →
</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
- 2024-12-14 Typora – A Brief Review of the Best Markdown Editor
- 2024-11-06 nushell – A Shell Using Structured Data
- 2024-10-07 Building a Markdown Blog with Caddy
- 2024-10-06 Surround Sound on a Raspberry Pi 3
- 2024-10-03 Building a Markdown Website with Caddy
- 2024-09-25 My Git Configuration
- 2024-09-11 Why Databases
- 2024-09-06 New Website