Chris Hannah

Text Case Is Coming to the Command Line #

If you didn’t already know, I make a text transformation app for iOS, iPadOS, and macOS, called Text Case. It’s grown quite a bit over it’s lifetime, supporting now well over 60 different formats, custom flows, and many more features. And I’ve now decided, the next place I want to bring it to, is the command line

And not just to be an additional benefit of purchasing the existing apps. This is an open-source project, which eventually I want to distribute via Homebrew.

That may sound weird, seeing as you need to pay to have it on other platforms. Why would I release it for free?

Basically, I’m a fan of free and open-source software. And while I don’t think it’s crazy to ask for £2.99 for an app that I’ve put quite a number of hours into making, I do also want to give something back. So, I plan on building a version of Text Case that will allow people to quickly transform text, using the command line.

At the same time, I’m bound to improve my own programming skills, seeing as my code will now be in the public eye. And maybe I’ll learn more about building and distributing open-source software.

However, I do have to say that not all of Text Case will be coming to the command line. I only plan on adding support for the core formats. At least, that’s the plan for now.

Right now, it supports just 17 different formats, and I’ll be working on bringing the rest over as time goes on. And because it’s open-source, you can keep up to date with the current state by checking it out on GitHub. (You can even use it right now, if you’re comfortable with building from source.)

Here’s a quick screenshot of the current version in action:

textcase cli

I hope this news sounds good to at least some of you. In the mean time, I’m going to get back to adding more format options.

Configuring Nginx to Work With Hugo’s “Ugly” URLs #

I recently migrated my blog from running on Ghost to Hugo. With that, came a few changes to how the pages were built, how the URLs were formed, and also the rules around showing 404s and redirecting where possible.

In a default Hugo installation, individual blog posts are each written as index.html files inside a directory named after the given slug. So you would end up with something like so:

├─ hello-world/
│  ├─ index.html
├─ about/
│  ├─ index.html
├─ my-first-blog-post/
│  ├─ index.html

That means when someone visits either /hello-world or /hello-world/, they’ll be taken to the content (Which is actually stored as /hello-world/index.html).

Just to make things difficult, I didn’t want my file structure to look like that. It felt wrong. Instead I wanted each blog post to be generated as it’s own html file, with the slug being the filename.

Fortunately, Hugo has an option called uglyURLs, which does exactly that. So the same structure above would instead look like this:

├─ about.html
├─ my-first-blog-post.html
├─ hello-world.html

However, while this may look good to me. It introduced a few issues that I had to deal with. Primarily being that the .html extension had to be given for a page to load. There was no more /about being redirected to /about.html. And definitely no /about/ being supported.

That obviously would break a lot of past links to my blog posts. So I had to find a way to deal with this.

So I set about finding a way in nginx, to essentially remove the need to have the extension in the URL. But I then realised, that it wasn’t that simple.

Because, let’s say I have an about page with the filename about.html. Ideally, I want /about.html, /about, and /about/ to redirect to this page. But at the same time, I have a mini-site for my app, Text Shot, that sites at /text-shot/index.html, and ideally I want /text-shot/index.html, /text-shot/, and /text-shot to all redirect to this site.

That led me to a lot of Googling, regular expressions, and quite a bit of time spent trying various options in my nginx configuration. (If you were super unlucky, you may have hit a 404 while I was testing).

By default, my nginx configuration handled the requests with the try_files function:

try_files $uri $uri.html $uri/ $uri =404;

Basically, if request was /about, it would try to fetch a file from these locations (in order): /about, /about.html, /about/, and then if all of those fail, produce a 404.

The code to deal with removing the need to include .html was relatively simple. All it does is check if the request includes the extension, and if so, remove it and perform a 302 redirect to the plain URI.

if ($request_uri ~ ^/(.*)\.html(\?|$)) {
    return 302 /$1;
try_files $uri $uri.html $uri/ $uri =404;

However, that still meant that if you visited /about/, it would think you were looking for /about/index.html, rather than the /about.html page.

So, I had to then add another redirect. So if the url ended in a trailing slash (but wasn’t just /), it would also perform a 302 redirect to a url without the trailing slash. I ended up with this:

location / {
    if ($request_uri ~ ^/(.*)\.html(\?|$)) {
        return 302 /$1;
    if ($request_uri ~ ^/([-a-zA-Z0-9@:%_\+.~?&//=]*)\/$) {
        return 302 /$1;
    try_files $uri $uri.html $uri/index.html $uri/ $uri =404;

For reference, $uri is the full request URL, and $request_uri is anything after the host.

There’s bound to be a way for this logic to be improved. But as far as I can see, it works how I expect, and it gives me the exact behaviour I was looking for.

Now for some examples. First off, the ways in which you would get to a file located at /about.html:

(Note: all redirects are stated, the rest is the priority order of the try_files function.)

  1. /about.html: 302 to /about -> try /about -> try about.html.
  2. /about/: 302 to /about -> try /about -> try about.html.
  3. /about: try /about -> try /about.html.

And for the page located at /text-shot/index.html:

  1. /text-shot: try /text-shot -> try /text-shot.html -> try /text-shot/index.html.
  2. /text-shot/: 302 to /text-shot -> try /text-shot -> try /text-shot.html -> try /text-shot/index.html.
  3. /text-shot/index: try /text-shot/index -> try /text-shot/index.html.
  4. /text-shot/index.html: 302 to /text-shot/index -> try /text-shot/index -> try /text-shot/index.html.
  5. /text-shot/index/: 302 to /text-shot/index -> try /text-shot/index -> try /text-shot/index.html.

Note: nginx will search for an index file when try_files checks $uri/, so there’s no need to handle that. However, there were circular redirects when /text-shot/ would be redirected to /text-shot, and then eventually back to /text-shot/, so I added an explicit attempt to $uri/index.html to avoid this.

Admittedly, a few of those examples are a bit odd, and likely will never happen. But I had to cover all bases.

As you can see, it’s not perfect. There are some requests that go through a 302 just to end up at the exact page that was requested. Like #2 above.

In an ideal world, I’ll never touch this configuration again. And there’s also a low chance that anyone reading this would ever need to do anything similar. But, I can’t say I found many resources online for my specific scenario, so I thought I’d write my own.

I guess I brought this all on myself when I decided I wanted to both install my own custom Hugo blog on a linux VM, and to also want static files with the full filename, not just convenient “pretty” URLs that Hugo generates by default. Oh well, at least I learned a bit more about how to configure nginx I guess.

Blog Update #

As of tonight, I’ve now completed the final stage of transitioning my blog from Ghost to Hugo.

As you may have known, my blog has been powered by Ghost for a few years. It’s definitely served me well. However, a while ago I started to like the idea of static sites. Seeing as my blog is essentially a list of static posts, it felt a bit weird that the pages were being served dynamically. So, I started on the journey of moving to Hugo.

It took a bit of work to adapt my existing theme to work with Hugo’s template system. But I managed to get it pretty similar, while also adding a few improvements at the same time.1

Of course, being a static site, pages now load super fast, and they’re extremely lightweight. But that’s not the only good thing about using Hugo. It’s also super easy to host other static pages alongside the source Markdown post files. This means I can build mini-sites for my apps, and keep them managed within my blog. I can control the structure of the site better, either by using categories/tags or by structuring the source files in the way I want them to be generated. And another great one, I can now create custom pages and templates. For example, I built a custom archive page for all of the posts on the blog, and a few extras that as a bit different for essays and travel updates.

In the early version of this new Hugo blog, I had it being deployed to a Digital Ocean app via a GitHub action that was triggered after pushing new posts. But, that didn’t allow me full control of the VM it was running from, so I decided to switch to a droplet (VM), and have built my own minimal installation.

Obviously, Hugo is installed, but apart from that the only other things I had to install was nginx and certbot. So very minimal. I was wondering how I’d manage the deployment, because I still wanted to have the site automatically rebuild after pushing my changes. Luckily, I found a guide by Josh Hausotter that shows you how to configure a remote repository on your Digital Ocean droplet and a “post receive” action that runs a script whenever changes are received to generate the static files and move them to the correct directory. I honestly never thought about using git this way.

As for how I write and publish my posts from my own machine. I do that using Neovim on my Mac, and then just pushing to the remote repository. Neovim might not be the most trendy tool for writers, but I find it works for me, and I also use it for writing code, so I’m pretty comfortable with the keybinds.

You may be wondering, what does this mean for you? Do you need to change anything? Well, in theory, no. The posts are now stored as static html files, however I have configured nginx for these to work without the extension, and the filenames/slugs haven’t changed. Technically, the RSS feed is now different. Hugo generates the RSS feed in an index.xml file at the base of the site, which you can find here. However, I have set up directs for /feed and /rss, so you shouldn’t need to do anything.

Hopefully, this change will go by mostly unnoticed. But if you do notice something odd, you know where to find me. (Links are at the bottom of the page)

Written: while watching Oblivion with my cat.

  1. The theme is named Hurley. (Lost reference) ↩︎

Recent Posts