2024-06-02

Readying a blog for revision histories and sprouts under unstatic


I've been developing support for my take on Chris Krycho' "sprouts" against this blog. Much of that support is now built into unstatic, my library for building static-site generators. But it does also require some support from within applicatons of that library, from the scala code and the untemplates of the individual site generators.

I'm going to upgrade my "drafts" blog to support revisions, diffs, and sprouts. I'll document what it takes to do that here.

Enable revision- and diff-generation in Scala code

In the object DraftsSite, the unstatic.ztapir.ZTSite that defines the site to be generated, inside the unstatic.ztapir.simple.SimpleBlog that defines the blog, add a RevisionBinder that can pull old revisions of pages and generate them into the website, and a DiffBinder that can generate diffs between current and new revisions:

 override val revisionBinder : Option[RevisionBinder] = Some( RevisionBinder.GitByCommit(DraftsSite, JPath.of("."), siteRooted => Rel("public/").embedRoot(siteRooted)) )
 override val diffBinder     : Option[DiffBinder]     = Some( DiffBinder.JavaDiffUtils(DraftsSite) )

By default, SimpleBlog sets these values to null. We override them.

The RevisionBinder we are using is RevisionBinder.GitByCommit. Its constructor accepts

  1. our ZTSite;
  2. a file path (java.nio.file.Path) to the git repository in which revisions are stored, just '.' for us because the git repository is the static-site generator's working directory;
  3. a function that converts a site-rooted path (unstatic.UrlPath.Rooted) into the associated path within the repository relative to its root (as unstatic.UrlPath.Rel);
  4. A RevisionBinder.RevisionPathFinder, a function which takes a document's site-rooted path and a "revision spec" (which for this revision binder is a full-size hex git commit) and determines the path the revision should take within the site.

We omit the fourth argument because we use a default, which coverts a path like /a/b/whatever.html to /a/b/whatever-oldcommit-c6e71f4d689f2b208c3eae19e647435322fa6d04.html

For a DiffBinder, we use DiffBinder.JavaDiffUtils, based on the java-diff-utils library. When we ask it to generate a diff for a path, we give it a reference to the RevisionBinder.RevisionPathFinder so it can know the filenames old versions get generated into. We also give it a DiffBinder.DiffPathFinder, which computes the pathnames of the generated diffs. Again, the DiffBinder.DiffPathFinder is omitted our code. We rely a default argument, which produces diff paths like /a/b/whatever-diff-72eaf9fdfebc9e627bff33bbe1102d4d250ad1d0-to-199e44561de3fd9e731a335d8b2a655f42d9bc04.html.

Now, if we ever provide update histories to any posts, copies of any old revisions referenced will be generated into the public directory of the site, as well as diffs between adjacent items in the update history.

Modify the site to generate update histories at the end of posts

It's a matter of taste, but we'll display update histories only on single-post permalink pages, not at the end of each post when concatenated together. And we won't include them as content in RSS. (Update histories do get included as additional metadata in RSS. That's built in.) SimpleBlog conveniently distinguishes between Single, Multiple, and Rss; we can just check our presentation and behave appropriately.

So... We'll

  1. Steal layout-update-history.html.untemplate from the tech blog, and bring it in as a layout of drafts. (I had to import com.interfluidity.drafts.DraftsSite.MainBlog, and modify the link in the note to point to the drafts got repository, rather than the tech rep.)
  2. Modify layout-entry.html.untemplate in drafts to bring in the new layout of update history. That turns out to be really easy, because we already have logic at the end of our entry layout to restrict addition of previous and next links to single page presentations. So all we have to do is add our update history layout just after the div for those links, but within the conditionally added region. It's literally just
    <( layout_update_history_html( input ) )>
    

    inserted just after that div, still within the conditional region.

Modify the main layout and CSS so that old revisions are visually distinct from, and link back to, current revisions

At the top of the body element of layout-main.html.untemplate, we add an empty div element called top-banner.

  </head>
  <body>
    <div id="top-banner"></div>

In current revisions, this will remain invisible and empty. But we'll add a bit of javascript to detect if we're in an old revision, and add some HTML with a link back to the current revision. If we are in an old revision, we'll also add a class called old-draft to the body element, so that we can do whatever we feel like in CSS to make the old revision visually distinct.

We use a javascript regular expression and our current location to decide if we are in an old revision.

    <script>
      document.addEventListener("DOMContentLoaded", function() {
          const regex = /(^.*)\-oldcommit\-[0-9A-Fa-f]+\.html/;
          const match = window.location.pathname.match(regex);
          if (match) {
              const b  = document.querySelector("body");
              const tb = document.getElementById("top-banner");
              b.classList.add("old-draft");
              tb.innerHTML = "You are looking at an old, superceded version of this page. For the current version, please <a href=\"" + match[1] + ".html\">click here</a>.";
          }
       });
    </script>

We adjust our main CSS to keep the top-banner div at the top of our document, when it's relevant:

body.old-draft #top-banner {
    position: fixed;
    top: 0;
    left: 0;
    width: 100vw;
    color: black;
    background-color: yellow;
    text-align: center;
    font-family: 'RobotoCondensed', 'Arial', 'Helvetica', sans-serif;
    font-variation-settings: "wght" 500;
    padding-top: 4px;
    padding-bottom: 4px;
    border-bottom: 2px solid black;
}

Also add CSS so that, when viewing old revisions, the documents look, well, old.

body.old-draft {
    padding-top: 1em;
    background-color: #F3F5DA;
    color: #6E7FD9;
    font-family: 'GabrieleD', 'Courier';
}

(These choices were inspired by the TT2020 image here, although ultimately I went for Gabriele, because the TT2020 file sizes were very large.)

Add a prologue to posts with revisions or that generate sprout RSS

When a post is a revision or a sprout, we want a prologue that indicated that it is, with links to the prior revision, the update history, and the sprout RSS.

I'm too lazy to describe what it took to add that in detail, but here's a nice, concise commit. Check out the diff.

Miscellaneous tweaks

I don't want to have to import UpdateRecord whenever I want to add update histories to entries, so I added them as an extra import to my untemplate customizer in my mill build file, build.sc:

  override def untemplateSelectCustomizer: untemplate.Customizer.Selector = { key =>
    var out = untemplate.Customizer.empty

    if (key.inferredPackage.indexOf("mainblog")>=0 && key.inferredFunctionName.startsWith("entry_")) {
      out = out.copy(extraImports=Seq("unstatic.*","com.interfluidity.drafts.DraftsSite.MainBlog","unstatic.ztapir.simple.UpdateRecord"))
    }

The "update history note" should be small, so I add to css:

.update-history-note {
    font-size: smaller;
    line-height: 100%;
}

Republish the site

Even though nothing visible should change, let's go ahead and republish the site, so that our javascript and css scaffolding for old-looking updates become available.

Test and tweak

Even though I don't have any actual new revisions to create, I added a fake revision history to the most recent post, played around in CSS with the look of the old revision until I liked it, then commented away the fake update history.