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

F# / Giraffe Markdown Blog - System Design

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:

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

Post List Page (/posts) - Renders the list of available posts (newest first) with links to their Post Page and Tag Lists

Post Page

Post Page (/posts/POST_ID) - Renders the markdown file as a blog post

Tag Page

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 a Post
  • 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

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 from PostService and builds the HTML tree with renderPostLayout 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

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 Page

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.