When the Nova 11 announcement appeared in one of my feeds, it was clear I wouldn’t be able to resist. I love, love, love truly polished native Mac apps and Nova completely nails it in that category. I played with previous versions of Nova before but I never had a real use case for it. I’m not a web developer and adapting it to the programming environments I use is somewhat challenging so I always slide back to specialized IDEs for those (Goland for Go, CLion for C++, Visual Studio Code for Haskell and TeXShop for LaTeX). But now I have this blog and this is all Markdown, CSS, Html, Javascript, in other words a great use case for Nova.

The plan is to use Nova to compose content for the blog. Slight wrinkle in the plan is that I don’t have much to say. It’s not a very active blog, so I’m going to resort to the usual trick of talking about the tools I use in general and also specifically for this blog. I’m introducing a new tag “tools” and will post about editors, IDEs, Notion API, Mastodon API, GPG and Keyoxide under this tag.

We’ll start the tooling series with this post about Nova and Hugo, the blog engine powering this blog. Honestly, there isn’t much to say. It works great. I created a project at the root directory of my blog. This gives me navigation and easy access to all the Markdown content files and also all the html, css and javascript from the template I use. I run a local hugo server with

 hugo server --buildDrafts

and have set up the Project Settings/Preview settings in Nova such that it uses the local Hugo web server instead of the built-in static web server:

settings

I also turned on Append relative paths for previewable documents and set the local root to content corresponding to the content folder of my Hugo blog. This way, when I click on a Markdown file I get its Preview as Hugo would render it.

But wait, an attentive reader might notice that I entered “localhost:8080”. Hugo usually runs on “:1313” if you don’t change it. Well, the setup I just described has a small snag. The “Append relative paths” checkbox makes Nova append the complete relative path, for example posts/foo/index.md. Hugo doesn’t like that. In Hugo the URL to the same post would be posts/foo. To fix this, I run a second web server which is basically a reverse proxy that does the necessary URL path manipulation to please Hugo. It’s a very simple Gin server that given a request, does the URL path manipulation and then turns around and hits the Hugo web server running at “localhost:1313”:

package main

import (
	"io"
	"log"
	"net/http"
	"os"
	"runtime"
	"strings"

	"github.com/gin-gonic/gin"
)

func main() {
	gin.SetMode(gin.ReleaseMode)

	nuCPU := runtime.NumCPU()
	runtime.GOMAXPROCS(nuCPU)

	router := gin.New()

	router.NoRoute(func(c *gin.Context) {
		path := c.Request.URL.Path
		if strings.HasSuffix(path, "index.md") {
			path = strings.TrimSuffix(path, "index.md")
		} else if strings.HasSuffix(path, ".md") {
			path = strings.TrimSuffix(path, ".md")
		}
		backendUrl := "http://localhost:1313" + path

		response, err := http.Get(backendUrl)
		if err != nil || response.StatusCode != http.StatusOK {
			c.Status(http.StatusServiceUnavailable)
			return
		}

		reader := response.Body
		defer func(reader io.ReadCloser) {
			err := reader.Close()
			if err != nil {
				log.Println("failed to close response body")
			}
		}(reader)
		contentLength := response.ContentLength
		contentType := response.Header.Get("Content-Type")

		extraHeaders := map[string]string{}

		c.DataFromReader(http.StatusOK, contentLength, contentType, reader, extraHeaders)
	})
	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}

	if err := router.Run(":" + port); err != nil {
		log.Panicf("error: %s", err)
	}
}

You can grab its source here.