DEV Community

agj
agj

Posted on • Originally published at blog.agj.cl

Writing a static website in OCaml

I've had an images blog called “Piclog” for a long time, apparently since 2008, while I was still in university studying graphic design. As I moved away from the discipline I stopped paying much attention to the site, only occasionally throwing a graphic or two in whenever I'd made something relatively interesting.

Piclog screenshot

It started as an installation of an old PHP software called Pixelpost, which got abandoned a good while ago. I made a custom template for it, and that was the site for many years. At some point in 2014 I decided to drop Pixelpost and replace it with a little bit of custom PHP, but no database and no comments feature, while keeping the same design.

Now I've gone and done a similar thing. I'd rather not depend on a PHP script for a website so simple (or rather, at all,) so I decided to turn it into a static site. In order to make the process worth my while, I wanted to also experiment with some new technology.

I first gave Rust a go, since I've learned a bit but haven't used it for anything but simple exercises. Frankly there's a lot of concepts I can't quite grasp yet, so working on a real project with it would help me put things into place in my head.

I reached for some sort of library or framework for making static websites. After looking at my options, I decided to try Dioxus. Although I was aiming to make something completely static, I went through a tutorial that also teaches state management. It didn't leave a good taste in my mouth. It brought back memories of the JavaScript UI layer frameworks I escaped from, like Vue or React. In those, managing state is a chore—a lot of complexity and maintainability issues arise from it.

I looked for other alternatives, but found that they were all based on the same paradigm. After so many years of coding UI using the Elm architecture/MVU pattern, I can't go back to spaghetti state. Even if I wasn't going to use the state, I lost faith in the Rust web ecosystem more in general. And thus, perhaps unwarrantedly, ended up dropping Rust for this project.

I decided to work on it using OCaml. It's another language I've been learning and which I haven't had a good project to try it on. By the way, I've worked through part of the Cornell OCaml Programming: Correct + Efficient + Beautiful book, which, explains things very clearly but suffers from being more-or-less a manual for the language, explaining language features one by one and largely lacking concrete context. It's worked more or less as reference material for me.

I did look for static site generation tools in OCaml, but decided against it and ended up doing it “by hand.” In contrast to Rust, OCaml is a language much less alien to me; I'm not so scared of going head-on without training wheels. So in the spirit of learning core concepts and the standard library, I went for a pretty low-dependency approach. Although I didn't reinvent the wheel when it comes to, for instance, parsing markdown and YAML, I did implement a few interesting things.

One is a little library to generate HTML and XML, inspired by Elm's elm/html. It's a very basic thing, just functions that generate nested values of Element.t and Attribute.t types, with a to_string function that does all the formatting and escaping.

Below is a single function that returns a bit of such code. It looks pretty much the exact same as Elm code.

let view_picture_thumbnail (picture : Picture.t) : Element.t =
  El.a
    ~a:[ At.href (Picture.local_url picture.id) ]
    [ El.img
        ~a:
          [ At.class_ "thumbnails"
          ; At.src (Picture.thumbnail_local_url picture)
          ; At.alt picture.title
          ; At.title picture.title
          ; At.width "150"
          ; At.height "150"
          ]
    ]
;;
Enter fullscreen mode Exit fullscreen mode

Forgive the archaic HTML straight from the 2000s (width and height attributes for an <img> element!;) it's a straight conversion of the original template HTML. But as you may appreciate, it's just functions and lists. The use of the optional ~a labelled argument in element functions to pass a list of attributes is something I borrowed from the vdom OCaml library, which I took a look at after I'd already written all the views.

I learned from Elm to love this kind of simple DSL made out of plain functions over more overwrought solutions, such as templating languages that end up reinventing conditionals and iteration (Mustache,) or language extensions that force complexity into the tooling (JSX.) The Rust frameworks all insist on using macros to have special syntax for this kind of view building, and I get the feeling that it's largely just because people are used to having that sort of syntax that mimicks HTML. At least macros don't require extra tooling, but they do add cognitive load.

Another thing I learned a bit from was my implementation of YAML decoders, also modelled after Elm's elm/json decoders. Since that's a core library that's mostly “kernel code” (JavaScript) rather than Elm code, I had to take a peek at Simon Lydell's reimplementation using pure Elm to figure out a few things. I created the decoders because the parsing library I used does not offer a very ergonomic way of accessing the parsed data.

Here's an example use decoding some picture data using applicative style (I think! I still need to properly learn category theory/Haskell):

let picture_decoder ~(id : string) ~(description : string) ~(published : Time.datetime)
  : t Ydec.t
  =
  Ydec.succeed
    (fun legacy_id title file has_large categories tags original_date related ->
       let id =
         match legacy_id with
         | Some legacy_id -> Id_with_legacy_id (id, legacy_id)
         | None -> Id id
       in
       { id
       ; title
       ; description
       ; file
       ; has_large
       ; categories
       ; tags
       ; published
       ; original_date
       ; related
       })
  |> Ydec.apply (Ydec.optional_field "legacyId" Ydec.int)
  |> Ydec.apply (Ydec.field "title" Ydec.string)
  |> Ydec.apply (Ydec.field "file" Ydec.string)
  |> Ydec.apply (Ydec.field "hasLarge" Ydec.bool)
  |> Ydec.apply (Ydec.field "categories" Category.list_decoder)
  |> Ydec.apply (Ydec.field "tags" (Ydec.list Tag.decoder))
  |> Ydec.apply (Ydec.field "originalDate" original_date_decoder)
  |> Ydec.apply (Ydec.field "related" (Ydec.list related_id_decoder))
;;
Enter fullscreen mode Exit fullscreen mode

So yeah, it was fun figuring this stuff out!

Now, at this point the new site is just a copy of the old one but converted into plain HTML files. Next I want to rework the design, massage the data so that it's easier to add new pictures, and refactor some stuff I think could be implemented better. Maybe I'll write again about what I learned after all that!

Top comments (0)