Matthias Le Brun@bloodyowl

ReScript Pages

2020/11/26

Last year, I wrote about how this static single application was built. From that time, I had to build several static websites, and at some point had to make them simpler to use.

The main websites I've built with the previous solutions are quite different:

  • This website, which is fairly simple and just has a blog and a static page
  • Putain de code!, which has a bit more content
  • BeOp, our company website, which needs i18n, and requires more ease of use, as not only tech people work on it

BeOp

I recently rebuilt the latter, and wanted to share the resources between the three. I believe that in development, it is good to repeat yourself at first, observe what time does, then commonise what you can.

So I started creating a shared «framework» that'd work on the three websites: ReScript Pages.

Fundamentally, the principles from the previous post apply, except it's a bit more flexible and complete.

1. A user-friendly dev server

My previous solution made you run a command after updating some files so that you could preview. That was annoying in the long run. ReScript Pages includes a dev server with a live reload.

2. Same experience in dev

The dev server gives you the pre-rendered page: no surprises with hydration.

3. i18n native support

The configuration gives you variants for each language you have. The various paths and subdirectories are handled without needing to do anything else: all your links will work without needing to touch anything.

let default =
  Pages.make(
    make,
    {
      siteTitle: "Matthias Le Brun",
      siteDescription: "Front-end developer and designer. ReasonML, ReasonReact, ReactJS.",
      mode: SPA,
      distDirectory: "dist",
      baseUrl: "https://bloodyowl.io",
      staticsDirectory: Some("statics"),
      paginateBy: 20,
      variants: [|
        {
          subdirectory: None,
          localeFile: None,
          contentDirectory: "contents",
          getUrlsToPrerender: ({getAll}) =>
            Array.concatMany([|
              [|"/", "blog"|],
              getAll("blog")->Array.map(slug => "/blog/" ++ slug),
              [|"404.html"|],
            |]),
        },
      |],
    },
  );

4. Easy SEO & styling

I've embedded react-helmet and bs-css so that everything can be pre-rendered properly, with all the good parts in.

5. A damn simple API

Here's the actual component displaying this very page:

[@react.component]
let make = (~slug, ()) => {
  let post = Pages.useItem("blog", ~id=slug);
  <>
    {switch (post) {
     | NotAsked
     | Loading => <ActivityIndicator />
     | Done(Ok(post)) =>
       <>
         <Pages.Head> <title> post.title->React.string </title> </Pages.Head>
         <div className=Styles.container>
           <h1 className=Styles.title> post.title->ReasonReact.string </h1>
           <div className=Styles.date>
             {post.date
              ->Option.map(Js.Date.fromString)
              ->Option.map(Date.getFormattedString)
              ->Option.map(ReasonReact.string)
              ->Option.getOr(React.null)}
           </div>
           <div
             className=Styles.body
             dangerouslySetInnerHTML={"__html": post.body}
           />
           <BeOpWidget />
         </div>
       </>
     | Done(Error(_)) => <ErrorIndicator />
     }}
  </>;
};

React Hooks really have unlocked a way to make APIs that look and feel simple.

6. Some bonus features

  • RSS feed generation
  • Sitemap generation
  • Post scheduling support

Conclusion

Overall it's been a really fun project to build (and also a bit frustrating, let's be honest, tweaking complex path resolutions, utterly complex webpack configs and flexible data-stores isn't always a pleasure).

If you're curious, don't hesitate to check:

Hope you like it! 👋

Liked this article?
→ Share it on Bluesky
→ Sponsor me on GitHub