Author’s Note: I’m currently in the process of migrating old blog posts to this new system. That may mean some links, syntax highlighting, and other details are broken or missing temporarily. Sorry for the inconvenience! needs a landing page. At the time of writing, all that is has is an automatically-generated directory listing. But I like that directory listing. I like that you can find all the files that exist on this site.

The directory listing, as it currently exists, is provided by nginx through the following magical code:

location / {
  autoindex on;

That simple directive instructs nginx to build an automatic listing of every file, if you visit a URL that corresponds to a folder on the drive. Let’s look at an example of what it generates:

<head><title>Index of /</title></head>
<body bgcolor="white">
<h1>Index of /</h1><hr><pre><a href="../">../</a>
<a href="comments/">comments/</a>                                          30-Jan-2017 04:34                   -
<a href="drafts/">drafts/</a>                                            30-Jan-2017 04:34                   -
<!-- etc -->
<a href="being-with-anger.html">being-with-anger.html</a>                              30-Jan-2017 04:04                7327
<a href=""></a>                                30-Jan-2017 04:04                3538
<!-- etc -->

I’d like to preserve this type of thing if I can, but I couldn’t find a way to get nginx to respond with a directory listing on-demand. So it’s time to roll our own!

ls and nginx

ls is the command to list all the files in the current directory on Unix systems. So let’s go with that. should show all the files in the root directory of the website. should show all the files in the drafts/ subfolder, et cetera, et cetera.

Now, if we have a file with no extension, nginx won’t tell the browser it’s an HTML document, and so many browsers may end up downloading the contents rather than trying to display them. So let’s update nginx to simply assume that any files without extensions are HTML files. Also, while we’re at it, let’s tell nginx to try to serve “/foo.html” if the url is “/foo”, so we can start sharing article URLs without the file extension:

server {
  ... etc
  default_type text/html;

  location / {
    try_file $uri $uri.html $uri/ =404;

One final tweak, while we’re here: let’s let nginx know about Markdown files. It’s frustrating to have the browser always assume they’re intended for download. We’ll edit /etc/nginx/mime.types and add the following:

types {
  text/markdown   md;

Cool. So now, if we can just write an ls file in each directory, nginx will serve it up as HTML.


It’s always fun to write fake code before real code:

for every folder
  create an ls file in that folder
  add a link to the parent folder in the ls file
  for every subfolder in the folder
    add a link to the subfolder in the ls file
  for every file in the folder
    add a link to the file in the ls file

We already have a deploy script that runs whenever a file is saved, so we have a natural place to insert this directory generation. But we do run a small risk with the pseudocode as-is: writing to the “ls” file would trigger a re-update. Which would write to the “ls” file. Which would trigger a re-update. Which…

if filename isn't "ls"
  ... all the stuff we said before

A few other small things that nginx provides: modified-at timestamps and filesizes. That’s easy enough for us to generate as well.

MODIFIED=$(date +'%a %b %d %T %Y' -r $FILE)
SIZE=$(cat $FILE | wc -c)

Also, note that we want to list all the subfolders first, then all the files. That’s going to inform our final code.

The final code

So we’ve got a fair bith of bash scripting here, but it matches our pseudocode. We use the find command to find all directories, then again with -maxdepth 1 to find all immediate directories and files. Beyond that, it’s just a matter of manipulating the strings, and we’ve got ourselves our directory listings!


if [ "${FILENAME##*/}" != "ls" ]; then
  find . -type d | while read DIR; do
    echo "
<!DOCTYPE html>
<html lang='en'>
<meta charset='utf-8'>
<tr><td><a href='../ls'>../</a></td></tr>
    " > $DIR/ls
    find $DIR -maxdepth 1 -type d | sort | while read SUBDIR; do
      if [ "$DIR" != "$SUBDIR" ]; then
        MODIFIED=$(date +'%a %b %d %T %Y' -r $SUBDIR)
        echo "<tr><td><a href='${RELPATH:1}/ls'>${RELPATH:1}/</a></td><td>$MODIFIED</td></td></tr>" >> $DIR/ls
    find $DIR -maxdepth 1 -type f | sort | while read FILE; do
      MODIFIED=$(date +'%a %b %d %T %Y' -r $FILE)
      SIZE=$(cat $FILE | wc -c)
      echo "<tr><td><a href='${RELPATH:1}'>${RELPATH:1}</a></td><td>$MODIFIED</td><td>$SIZE</td></tr>" >> $DIR/ls
    echo "
"      >> $DIR/ls

In our case, instead of using <pre> tags, we’re using basic HTML tables. This should let things size a little bit nicer on mobile devices, so how about that! We’ve made a minor improvement on nginx.

Back where we started

We’re reinventing the wheel a bit here, but I personally think it’s important to be able to navigate the site as a directory structure – too often I find myself stymied by a site’s particular mechanism for organizing content, when what I’d really like is just “show me all the things, and let me worry about how to get there”.

More importantly, this frees us up to start looking at changing what shows up at the root of any particular folder. We’re finally cleared to start building a homepage!

←Previous Post | Next Post→