Build a Simple Markdown Blog with F# / Giraffe
Date: 2023-12-03 | create | tech | fsharp | giraffe |
This post is part of the 2023 F# Advent Calendar.
So you want to build a blog with F# / Giraffe. Good choice. Great stack. Huge fan.
Previously we built an F# / Giraffe backend with type-safe server-side HTML rendering. So you could just use that approach to build / write your blog.
But writing in HTML is kinda bothersome. There's all those tags and you might need to provide styles and you'd have to escape certain characters like <
, &
, >
, etc.
Markdown is typically a more ergonomic approach. In this post we'll explore how to build a markdown blog with F# / Giraffe.
Q: How can we build a markdown blog with F# / Giraffe?
Answer
In this post we'll explore an example project that serves blog posts written in markdown as full, styled HTML pages.
The stack:
- App: F# / Giraffe
- Markdig - Markdown parsing
- Yamldotnet - Frontmatter parsing
- Hosting: Docker / Docker-compose for containerization and orchestration
The way we'll render our Markdown files as posts is:
- Request hits our backend -
/posts/POST_ID
- We look for a corresponding markdown file with that name (FILE_NAME == POST_ID)
- If found, we'll:
- Parse the file
- Pull out the FrontMatter
- Encode the Markdown as HTML
- Return a page using the Post's data / html body
As always, the example project source code is available:
- Key parts of source code: Here in this blog
- Full project source code: Available to HAMINIONs subscribers in my example project repo
Blog - Pages and Screenshots
The entire blog is driven off of the Markdown files in the project.
- Frontmatter: Title, Date, and Tags
- Body: The rest of the file
The example Markdown files we're using include an assortment of different Markdown features so you can get an idea of how it might look for a real post. Generally they look like this:
THREE_DASHES_HERE
title: "Post - 1"
date: 2023-11-01
tags: [
"odd"
]
THREE_DASHES_HERE
Example from: https://markdown-it.github.io/
# h1 Heading 8-)
## h2 Heading
### h3 Heading
#### h4 Heading
##### h5 Heading
###### h6 Heading
Note: THREE_DASHES_HERE would actually be 3 * -
but this is breaking my site currently (likely bad parsing on my part) so just imagine it's there.
Here's an overview of the pages in our blog and what they look like. The styles look a little goofy (Standard PicoCSS) but should still serve to show how you can import global css, mix with local styles you make yourself, and have that style the resulting Markdown file pages.
Post List Page (/posts
) - Renders the list of available posts (newest first) with links to their Post Page and Tag Lists
Post Page (/posts/POST_ID
) - Renders the markdown file as a blog post
Tag Page (/tags/TAG_ID
) - Renders the list of posts for a given tag
This is obviously a very simple blog but it covers most of what you'd want and should provide a good foundation to build whatever it is you're missing.
PostService - Markdown to HTML
The core of our work in this app actually isn't the HTML rendering (as we've covered that previously). The novel work here is trying to parse useful information out of our static markdown files so that we can use them dynamically (when we get a request).
To that end, I've created a class I've named PostService
that handles a lot of this work for us. Yes a class is not very "functional" but the beauty of F# is you can go more / less functional as you wish. Typically when we're doing something stateful - wrapping it in a class (or hiding it in a function) is the 3S solution.
On app startup, we're creating PostService
with a filePath
param to point it at the folder of Markdown files we want it to handle. It will then do some pre-processing to better understand the files it's dealing with, do some simple indexing for fast retrieval, and make them available via clean APIs.
On creation, PostService will:
- List all files in the given
filePath
- Create an index of dict<
POST_ID
,POST_PATH
> for fast retrieval - create a method
getPost
that can read a markdown file, parse its frontmatter, and convert it into aPost
- create
dateOrderedPostIds
to sort posts by date (but we only store the postId so minimal ongoing memory footprint) - create
tagToPostsIdsLookup
to have a quick index for tags <> posts (only storing tags, postIds) - Surfaces simple functions for accessing this data:
- GetPost - by postId
- GetOrderedPosts - Ordered posts with pagination
- GetPostsForTag - Posts by tag with pagination
Dumping the code here for you to peruse but you can trust me it does what I said:
[<CLIMutable>]
type PostFrontMatter =
{
title : string
date : DateOnly
tags : string array
}
type PostService(
filePath : string) =
let allPostsPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), filePath)
let allPostPaths = Directory.GetFiles(allPostsPath)
let allPostIdsToPathLookup =
allPostPaths
|> Array.map (
fun path -> Path.GetFileNameWithoutExtension(path), path
)
|> Map.ofArray
let getPostPath
(postId : string)
: string option
=
match (
allPostIdsToPathLookup
|> Map.containsKey postId
) with
| true -> Some allPostIdsToPathLookup[postId]
| false -> None
let getPostRawString
(postId : string)
: string option
=
option {
let! postFilePath = getPostPath postId
let rawText = File.ReadAllText(postFilePath)
return rawText
}
let yamlDeserializer =
(new DeserializerBuilder())
.WithNamingConvention(LowerCaseNamingConvention.Instance)
.Build()
let getPostFrontMatterFromMarkdownDocument
(markdownDocument : MarkdownDocument)
: {|
FrontMatter: PostFrontMatter
RemainingMarkdown: MarkdownDocument
|}
=
let yamlBlock =
markdownDocument
.Descendants<YamlFrontMatterBlock>()
.FirstOrDefault()
let yaml =
yamlBlock
.Lines
.ToString()
let frontMatter = yamlDeserializer.Deserialize<PostFrontMatter>(yaml)
markdownDocument.Remove(yamlBlock)
|> ignore
{|
FrontMatter = frontMatter
RemainingMarkdown = markdownDocument
|}
let markdownPipeline =
(new MarkdownPipelineBuilder())
.UseAdvancedExtensions()
.UseYamlFrontMatter()
.Build()
let getPost
(postId : string)
: Post option
=
let postString = getPostRawString postId
if Option.isNone postString
then None
else
let markdownDocument =
Markdown.Parse(postString.Value, markdownPipeline)
let parsedDocument =
getPostFrontMatterFromMarkdownDocument
markdownDocument
let metadata = parsedDocument.FrontMatter
Some
{
Id = postId
Title = metadata.title
Date = metadata.date
Tags =
metadata.tags
|> Array.toList
Body = parsedDocument.RemainingMarkdown.ToHtml()
}
let getAllPosts : unit -> Post list =
fun () ->
allPostIdsToPathLookup
|> Map.keys
|> Seq.map (
fun postId -> getPost postId
)
|> Seq.choose id
|> Seq.toList
let dateOrderedPostsIds : string list =
getAllPosts()
|> Seq.sortByDescending (fun p -> p.Date)
|> Seq.map (fun p -> p.Id)
|> Seq.toList
let tagToPostsIdsLookup : System.Collections.Generic.IDictionary<string, string list> =
getAllPosts()
|> List.map (
fun post ->
post.Tags
|> List.map (
fun tag ->
tag, post.Id
)
)
|> List.collect id
|> List.groupBy (fun tagToPostId -> fst tagToPostId)
// Flatten to (tag, postId list)
|> List.map (
fun (tag, tagToPostId) ->
(tag, (
tagToPostId
|> List.map (fun tuple -> snd tuple)
))
)
|> dict
let getPostsForPostIds
(postIds : string list)
(pageNumber : int)
(pageSize : int)
: Post list
=
let skip = pageNumber * pageSize
if skip >= postIds.Length
then []
else
let posts =
postIds
|> List.filter (
fun postId ->
allPostIdsToPathLookup
|> Map.containsKey postId
)
|> List.skip skip
|> List.truncate pageSize
|> List.map (fun id -> getPost id)
|> List.map (fun p -> p.Value)
posts
let getOrderedPosts
(pageNumber : int)
(pageSize : int)
: Post list
=
getPostsForPostIds
dateOrderedPostsIds
pageNumber
pageSize
let getPostsForTag
(postTag : string)
(pageNumber : int)
(pageSize : int)
: Post list
=
let tagExists =
tagToPostsIdsLookup.ContainsKey(postTag)
if not tagExists
then []
else
getPostsForPostIds
tagToPostsIdsLookup[postTag]
pageNumber
pageSize
member __.GetAllPostNames() =
allPostIdsToPathLookup
|> Map.keys
member __.GetPost
(postId : string)
: Post option
=
getPost postId
member __.GetOrderedPosts
(pageNumber : int)
(pageSize : int)
: Post list
=
getOrderedPosts pageNumber pageSize
member __.GetPostsForTag
(postTag : string)
(pageNumber : int)
(pageSize : int)
: Post list
=
getPostsForTag
postTag
pageNumber
pageSize
Note: The deserialization of the markdown file and its frontmatter was quite a lot of faff. But I've abstracted it out into two easy-ish functions so hopefully it's easy enough to follow
Rendering pages with server-side HTML
In my previous post, we templated our HTML with Scriban. Currently I don't think that step is useful / necessary so here we're going back to raw string templating.
Helper Utilities
Building raw HTML strings may seem like a faff and prone to errors. Usually I'd agree with you but doing so with F# actually allows us to rely on its excellent type system more than we'd be able to with the template intermediary. So here, as usual, static types win.
But doing this manually every time certainly would be a faff so I wanted to first share some small utilities I built that I found useful (and that we'll use in the other components):
// Super simple string encode
// This is what's used by Scriban - https://github.com/scriban/scriban/blob/master/src/Scriban/Functions/HtmlFunctions.cs#L65
let htmlEncode
(input : string)
: string
=
System.Net.WebUtility.HtmlEncode(input)
let htmlForEach<'a>
(items : 'a list)
(renderer : 'a -> string)
(joinString : string)
: string
=
items
|> List.map (
fun i ->
renderer i
)
|> String.concat joinString
let formatDate
(date : DateOnly)
: string
=
date.ToString("o", CultureInfo.InvariantCulture)
let renderPostLayout
(head : string)
(body : string)
(footer : string)
: string
=
$"""
<!DOCTYPE html">
<html lang="en" data-theme="light>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@1/css/pico.min.css">
<link rel="stylesheet" href="/css/global.css">
{ head }
</head>
<body>
{ body }
</body>
<footer>
{ footer }
</footer>
</html>
"""
- htmlEncode - Gives us an easy way to encode any raw strings so they're safe for HTML. This is what we were essentially using Scriban for previously but here we just use it when we need it.
- htmlForEach - Helper for iterations
- formatDate - This is the YYYY-MM-DD standard (and the only date format you should use)
- renderPostLayout - This gives us a layout for this entire area of our app. This is a glimpse at how we'll be building our components in the rest of the post.
Post Page
The PostPage is what renders our blog post page with all your writing.
- postPageHandler - This is the top-most function that is called when a url is hit. The serviceTree is how I do dependency injection (in this case for
PostService
) and we're passing the sanitized url params down - renderPostPageAsync - This is the beginning of our page, it fetches the
Post option
fromPostService
and builds the HTML tree withrenderPostLayout
we went over before - renderPostTypographyComponent - Actually does the rendering of the Post sections. Note that it also is dealing with the possibility that
Post
does not exist. This is a great example of how we can be very type-safe when we stay mostly in F#, a power we would lose (or at least need some null-ish mappings) if we tried to do the same with a C# templating intermediary. - renderTagLinks - renders the tags with their respective urls
type PostTypographyComponentProps =
{
Post : Post option
}
let renderTagLinks
(post : Post)
: string
=
PostPresentation.htmlForEach
post.Tags
(fun t ->
$"""
<a href="/tags/{t}">
{t}
</a>
"""
)
"<span> | </span>"
let renderPostTypographyComponent
(props : PostTypographyComponentProps)
: string
=
let postBody =
match props.Post with
| None ->
"No Post Found!"
| Some p ->
$"""
<article>
<div class="headings">
<h1>{ PostPresentation.htmlEncode p.Title }</h1>
<h3>{ PostPresentation.htmlEncode (PostPresentation.formatDate p.Date) }</h3>
<p>
{ renderTagLinks p }
</p>
</div>
</article>
<div>
{ p.Body }
</div>
"""
let template =
$"""
<div class="container">
{ postBody }
</div>
"""
template
type PostPageProps =
{
PostId : string
}
let renderPostPageAsync
(serviceTree : PostServiceTree)
(props : PostPageProps)
: Async<string>
=
async {
let post =
serviceTree
.PostService()
.GetPost
props.PostId
return
PostPresentation.renderPostLayout
"<title>Blog Example</title>"
(renderPostTypographyComponent { Post = post })
""
}
let postPageHandler
(serviceTree : PostServiceTree)
(props : PostPageProps)
=
fun (ctx : HttpContext) ->
async {
return!
renderPostPageAsync
serviceTree
props
}
Post List Page
The Post List Page is very similar to the Post Page and we even reuse some of those components!
- postListPageHandler - the entrypoint for this page, called by Giraffe
- renderPostListPageAsync - This is doing some work to grab our paginated ordered posts and fill in the layout. Note this does not need to be Async as there are no async ops happening but often the top-most compoent will be as a lot of IO is wrapped in it
- renderPostListComponent - This is how we're listing all of our posts. Again, this is just a component with props so is very reusable as we'll see in TagList
type PostListComponentProps =
{
Title : string
Posts : Post list
}
let renderPostListComponent
(props : PostListComponentProps)
: string
=
let postsHtml =
PostPresentation.htmlForEach<Post>
props.Posts
(fun post ->
$"""
<a href="/posts/{ post.Id }">
<article style="margin: 1rem; padding: 1rem;">
<div class="headings" style="margin-bottom: 0;">
<h3>{ PostPresentation.htmlEncode post.Title }</h2>
<h4>{ post.Date }</h3>
</div>
<div>
{ PostPageView.renderTagLinks post }
</div>
</article>
</a>
"""
)
""
let html =
$"""
<div class="container">
<h1>{ PostPresentation.htmlEncode props.Title }</h1>
{ postsHtml }
</div>
"""
html
type PostListViewProps =
{
PageNumber : int
PageSize : int
}
let renderPostListPageAsync
(serviceTree : PostServiceTree)
: Async<string>
=
async {
let props =
{
PageNumber = 0
PageSize = 10
}
let posts =
serviceTree
.PostService()
.GetOrderedPosts
props.PageNumber
props.PageSize
return
PostPresentation.renderPostLayout
"<title>Blog Example</title>"
(
renderPostListComponent
{
Title = "All Posts"
Posts = posts
}
)
""
}
let postListPageHandler
(serviceTree : PostServiceTree)
=
fun (ctx : HttpContext) ->
async {
return!
renderPostListPageAsync
serviceTree
}
Tag List Page
At this point, we've already written most of the reusable components we need for Tag List, so it's pretty short.
- tagPostListPageHandler - entrypoint
- renderTagPostListPageAsync - Page renderer
type TagPostListPageProps =
{
Tag : string
}
let renderTagPostListPageAsync
(serviceTree : PostServiceTree)
(props : TagPostListPageProps)
: Async<string>
=
async {
let posts =
serviceTree
.PostService()
.GetPostsForTag
props.Tag
0
10
return
PostPresentation.renderPostLayout
"<title>Blog Example</title>"
(
renderPostListComponent
{
Title = "Tag: " + props.Tag
Posts = posts
}
)
""
}
let tagPostListPageHandler
(serviceTree : PostServiceTree)
(tag : string)
=
fun (ctx : HttpContext) ->
async {
return!
renderTagPostListPageAsync
serviceTree
{
Tag = tag
}
}
Serving the App
Okay so we've gone over most of what the app does:
- Indexing and parsing Markdown -> HTML
- Rendering HTML for each page
The last thing is how all of this goes together so that we're actually serving these endpoints so people can hit them / get our blog posts.
There are a few parts of this:
- Domain Endpoints
- Spinning up the app
I'll explain as much as I can here but going into details about how F# / Giraffe works is beyond the scope of this post so if you're curious to learn more, you can check out:
Domain Endpoints
First we're going to define a function that organizes all the endpoints for our domain Posts
in one place so its easy to configure in our domain and have it connected with the rest of the app at the composition root.
let constructEndpoints (serviceTree : PostServiceTree) =
[
GET [
route "/posts" (
renderView (
PostListView.postListPageHandler serviceTree
)
)
routef "/posts/%s" (
fun postId ->
renderView (
PostPageView.postPageHandler
serviceTree
{ PostId = postId}
)
)
routef "/tags/%s" (
fun tag ->
renderView (
PostListView.tagPostListPageHandler
serviceTree
tag
)
)
]
]
Note that we're calling a custom middleware renderView
which is handling turning the returned strings into an html response the browser knows how to deal with. We talk ab this more in Type-safe Server-side HTML Rendering with F# / Giraffe.
let renderView
(handler : HttpContext -> Async<string>)
: HttpHandler
=
fun(next : HttpFunc) (ctx : HttpContext) ->
task {
let! result = handler ctx
return! htmlString result next ctx
}
Spinning up the App
Now we've got to combine all the Domains of our app into one thing that we can run. This is the Composition Root.
Here we build up our ServiceTree (all dependencies), then build our Routes with their dependencies.
let buildServiceTree
(appConfiguration: AppConfiguration)
: ServiceTree
=
let postService = PostService("Static/AllPosts")
let allPostNames = postService.GetAllPostNames()
{
Settings = {
AppConfiguration = appConfiguration
}
Posts =
{
PostService = fun () -> postService
}
}
let routes (configuration : AppConfiguration) =
let serviceTree = buildServiceTree configuration
List.concat [
[
subRoute "" [
GET [
route "/ping" (text "pong")
]
]
];
(PostEndpoints.constructEndpoints serviceTree.Posts)
]
Now that we have our list of routes with all their dependencies passed in we can compose all of these things in our core Program.fs. Again, going into details about how Giraffe works is beyond the scope of this post so if you're curious, checkout Build a simple F# web API with Giraffe
let configureApp (app : IApplicationBuilder) =
let environment_name =
match (Environment
.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")) with
| null -> ""
| x -> x.ToLower()
let configuration = fetchConfiguration environment_name
let endpointsList = routes configuration
app
.UseStaticFiles() // Important for wwwroot to be served!
.UseRouting()
.UseCors(
Action<_>
(fun (b: Infrastructure.CorsPolicyBuilder) ->
// Put real allowed origins in here
b.AllowAnyHeader() |> ignore
b.AllowAnyMethod() |> ignore
b.AllowAnyOrigin() |> ignore)
)
.UseEndpoints(
fun e ->
e.MapGiraffeEndpoints(endpointsList)
)
|> ignore
let configureServices (services : IServiceCollection) =
services.AddCors() |> ignore
// Add Giraffe dependencies
services.AddGiraffe() |> ignore
[<EntryPoint>]
let main _ =
Host.CreateDefaultBuilder()
.ConfigureWebHostDefaults(
fun webHostBuilder ->
webHostBuilder
.UseKestrel(fun c -> c.AddServerHeader <- false)
.Configure(configureApp)
.ConfigureServices(configureServices)
|> ignore)
.Build()
.Run()
0
Next
With that, you should be able to build a markdown blog running on F# / Giraffe! I know there's a lot of code in here but I figured I'd give you more code than less so you have more to reference.
If you'd like to get your hands on the full project source code, checkout my code examples repo available to HAMINIONs subscribers.
If you liked this post, you might also like:
Want more like this?
The best / easiest way to support my work is by subscribing for future updates and sharing with your network.