Private areas in Jekyll with Apache
Jekyll-built static sites are public by default. However, have you ever wanted to create a private area where you can upload articles for review and keep them from the public eye until they’re ready? That was my use case recently. Here’s how I solved this particular puzzle.

Image credits: Jekyll logo, Apache feather logo, Entry forbidden sign
My blog is a static site built with Jekyll and the Minimal Mistakes theme. The site is hosted in a Hetzner webspace, which means that the pages are served via Apache.1 These are the assumptions I made when writing up this post. So, if you use a provider which doesn’t use Apache, or the files are different because of a different Jekyll theme, then this solution likely won’t work for you. Nevertheless, I hope it gives someone a helping hand when trying to create their own private area of a Jekyll-built site!
The two main ingredients we need are Jekyll collections and Apache Authentication and Authorization. Let’s see how we combine them to achieve our goal.
Create a “subscribers” area
The first step is to create a new path within which to put articles for
reviewers or subscribers. If you’re using Jekyll in its standard
configuration, posts will always have their path based at the root domain.
For example, if you host your blog on
https://example.com,2 and /:title/ as your permalink
setting (as I do), articles will appear under their own path. Hopefully, a
concrete example will clear what I mean. Imagine you have an article called
“Reading books for fun and profit”. Jekyll will make the article available
under the URL https://example.com/reading-books-for-fun-and-profit/. The
path I’m talking about is the reading-books-for-fun-and-profit/ bit.
Jekyll has lots of flexibility in how to set up permalinks for your posts.
If you’ve used the default option, then you’ll see a URL with category, date
and post title. Continuing with the example just mentioned, you might see
something like this:
https://example.com/books/2028/02/29/reading-books-for-fun-and-profit.html.
In this case, the path will be books/2028/02/29/.
Anyway, I hope you get my point about the URL paths that Jekyll creates for
blog posts. What we want to do here is to create the path subscribers/ at
the domain root. Then, our “subscribers” can access preview articles under
https://example.com/subscribers/, instead of whatever structure Jekyll
uses for posts. To achieve this goal, we’ll use Jekyll
collections.
Collections are a great way to group related content like members of a team or talks at a conference.
That’s basically what we want to do; the only difference being we don’t want our collection to be publicly available. URL path protection is a separate step, so we’ll get to that later.
To create a collection, we use the collections setting in the
_config.yml file:
collections:
subscribers:
output: true
With this configuration in place, Jekyll will know to look for Markdown
files within a _subscribers/ directory located in the base directory (i.e.
where _config.yml and friends live). Note that the leading underscore in
the directory name is important: the configuration value is subscribers,
but the directory name is called _subscribers/.
The output: true option tells Jekyll to process all files in the
_subscribers/ directory and produce a rendered page for each file. Since
each file we want to process in this directory is going to be an article for
our “subscribers”, this is the behaviour we want.
Now, to start creating some content. Create the _subscribers/ directory in
the base project directory:
mkdir _subscribers/
Then, create an article within that directory by opening a file called
_subscribers/reading-books-for-fun-and-profit.md in your favourite editor.
Fill it with this text:
---
title: Reading books for fun and profit
categories:
- Books
---
Books rock! You should read more of them. Did you know that libraries are
quite simply *packed* with them? Mind = Blown. :exploding_head:
Building your site in development mode, you should now see your article
appear under your new, shiny subscribers/ URL path:
http://localhost:4000/subscribers/reading-books-for-fun-and-profit/
That’s cool! But it could be better. You’ll likely find that there isn’t
any styling, so you’ll see only plain text at the top of the page. This is
not something you’d like to show off to your mates! To get the styling
right, we need to extend the defaults: setting in the Jekyll config. Open
your _config.yml file and append the following code to your defaults:
configuration:
# Defaults
defaults:
<snip>
# subscribers' area page styling
- scope:
path: ""
type: subscribers
values:
layout: single
author_profile: true
read_time: true
Now, if you rebuild your site, you should see the article at
http://localhost:4000/subscribers/reading-books-for-fun-and-profit/ styled
nicely with a better layout and styling. This setup also shows your author
bio information (author_profile: true) and how long it will take to read
the article (read_time: true). How much those settings are only related
to the Minimal Mistakes theme, I don’t know, so YMMV. In my case, this made
the “subscribers” area consistent with the rest of my site.
Creating a “subscribers” landing page
With the subscribers/ path created and an article in it, it’d also be nice
for “subscribers” to see an overview of the available articles in the
“Subscribers’ Area”. I.e. if someone navigates to
https://example.com/subscribers/, then they won’t be presented with a 404
page or similar; they’ll see a list of articles to read at their
leisure.3
To make such a landing page, we need to create a file not in the
_subscribers/ directory, but in the base project directory. This was
something that surprised me, so hopefully you’ll now be spared that
confusion. I’d expected everything associated with the new collection to be
kept within that directory, but alas, I was wrong. Oh well.
Jekyll expects this file to consist of the collection name followed by a
.html extension, i.e. in our case that is, subscribers.html. This
file’s content is a mixture of Jekyll frontmatter, Markdown and HTML, which
is a bit weird, but that’s the way this particular cookie crumbles, I guess.
Open your favourite editor and create a file called subscribers.html with
this content:
---
layout: archive
title: Subscribers' Area
author_profile: true
---
{% assign entries_layout = page.entries_layout | default: 'list' %}
<div class="entries-{{ entries_layout }}">
{% for post in site.subscribers %}
{% include archive-single.html type=entries_layout %}
{% endfor %}
</div>
I gleaned this code from perusing the Minimal Mistakes theme code, so
hopefully this also works for you. ![]()
I’m fairly sure I understand what this code is doing, so here’s my explanation.
We base our layout on the archive layout as shown in the frontmatter at
the beginning of the file. This seems to be sensible as it is what’s used
for the main “Recent Posts” page as well. Why it’s called “archive”, I
don’t know, but it works, so I’m not complaining. The title key provides
the page’s title, and the author_profile key puts the blog author’s bio
information on the same page to keep the same look and feel as the rest of
the site. Note that the styling configuration change we made to the
defaults: setting above doesn’t apply to this page. This is because it’s
not a file within the _subscribers/ directory, hence we have to mention
author_profile in its frontmatter.
The assign statement sets the entries_layout variable either to the
value of page.entries_layout or to list if page.entries_layout isn’t
set or doesn’t exist. We pass this variable as the type argument to the
archive-single template used later within the include statement. It
also helps with the styling of the entries appearing within the <div>
block, via the <div>’s class attribute. Within the <div>, we loop
over all posts in the list of files that Jekyll found in the _subscribers/
directory. In other words, we iterate over all elements in the
site.subscribers list. Note that it’s necessary to use the variable name
post as the loop variable here because the article-single template uses
this variable name internally. I probably could have written something
myself which used a more appropriate variable name, but it would have
duplicated a lot of code for little reason.
The loop creates an overview of the available articles, showing their titles as well as the “teaser” text for that article.4
Let’s add another article to see what the list can look like with more than one article.
Open a new file called _subscribers/books-in-modern-society.md and fill it
with this content:
---
title: Books in modern society
categories:
- Books
---
Are parchment scrolls a thing of the past? Are books the future or just a
fad? We asked Pliny the Elder his opinion: "I think there is a world market
for maybe five books". Others have chimed in on this new trend with "I love
the smell of books in the morning". It seems that expert opinion is divided
on this contentious issue.
Rebuilding the development site and navigating to
http://localhost:4000/subscribers/, you should see something like this:

We’ve thus created an area for a certain group of privileged people to read our articles. Great! The only problem is: anyone can read it. I.e. it’s public and not private. How do we fix that? By getting Apache to control access for us.
Make the “subscribers” area private
Now that we have a directory to protect, let’s protect it.
The Hetzner directory protection
docs
describe how to do this if you’re a Hetzner user. However, once you’ve gone
through that process, you realise that this is just an automated way to
create .htaccess and .htpasswd files in the directory you want to
protect. It’s easier to create these files ourselves, rather than clicking
through a GUI to achieve the same goal.
The way to do this is to create two files, a .htaccess file and a
.htpasswd file, and upload them to the subscribers/ directory where our
blog is hosted. The .htaccess file controls the access configuration, and
the .htpasswd file contains the usernames and passwords of our
“subscribers” so that only they can access this directory.
The .htaccess file looks like this:
AuthUserFile "/usr/www/users/<username>/subscribers/.htpasswd"
AuthName "Subscribers area"
AuthType Basic
require valid-user
which is a standard Apache .htaccess
configuration
allowing basic HTTP
authentication.
The AuthUserFile option specifies the password file to use for
authentication. In this case, the path is specific to Hetzner; thus, you
will likely need to change the /usr/www/users component to some other
value for your setup. The main point is that we specify the path to the
.htpasswd file within the subscribers/ directory on our hosting
provider’s infrastructure. We’ll copy the file there when deploying our
Jekyll site to our hosting provider.
The
AuthName
value
sets the name of the authorization realm for a directory.
This value is just a name, so it’s easiest here to use the same name as the title we gave our subscribers’ area landing page.
As mentioned above, we use basic HTTP authentication; hence, the config
option AuthType is set to Basic.
And of course, we require a valid user for login, hence we set require
valid-user. We don’t want any old riff-raff looking at our nice preview
articles!
With the configuration all set up, we now need to create a .htpasswd file.
This file contains colon-separated usernames and hashed passwords for those
who are allowed access to our “subscribers” area. The password hashing uses
the bcrypt encryption option of the Apache htpasswd
command. But
first, what password should we use?
In my use case, I thought it would be best if I chose a long, random password and informed each user individually what their username and password are. It’s clear that this doesn’t scale to hundreds of users. But then, I only want to show some articles to friends for review, so this solution is ok for me. Also, if someone finds out the password for a user, it’s not that bad. I mean, whoever happens to nab this information only gets read-only access to a couple of articles. Not a major security incident. Of course, if you have hundreds of subscribers, you’ll need to come up with a much better solution!
To create a good random password, my tool of preference is
pwgen. If you’re on Debian, you can
install it in the usual manner:
$ sudo apt install pwgen
To create a single 20-character password, call the program like so:
$ pwgen 20 1
We then feed this output into the htpasswd program (which is part of the
apache2-utils Debian
package):
$ htpasswd -nbB alice password-generated-from-pwgen
alice:$2y$05$iFwBDdjNbE3sef7siS2/dOu.DqAkVhiw8zVcMLiPfE.y9tw.i8ypO
where the -n option displays the output on the terminal, -b uses the
password specified on the command line, and -B forces bcrypt
encryption. The two arguments to
htpasswd are the username (in this case, alice) and the password
generated by pwgen.
You then paste the output from htpasswd into the .htpasswd file which is
located in the _subscribers/ directory. If you don’t want to do that by
hand, then you can get htpasswd to update the file directly by removing
the -n option and specifying the .htpasswd file explicitly.
First, create the file with touch (htpasswd requires the password file
to exist beforehand):
$ touch _subscribers/.htpasswd
and set the password:
$ htpasswd -bB _subscribers/.htpasswd alice password-generated-from-pwgen
Adding password for user alice
You can add further users in the same manner, e.g. for the user bob:
$ htpasswd -bB _subscribers/.htpasswd bob password-generated-from-pwgen
Adding password for user bob
You don’t want everyone in the world to be able to read this file, so set its file permissions to remove group and world read permissions:
$ chmod 0600 _subscribers/.htpasswd
The last step in this process is to upload the .htaccess and .htpasswd
files to the subscribers/ subdirectory where your blog is hosted. This
process depends upon your hosting provider, so I’m not going to describe it
here. I’m guessing that there’s either an FTP or rsync connection that
you can use to copy your blog’s _site contents to the appropriate location
on your hosting provider’s servers. You’ll have to work that bit out on
your own, sorry. :-/
One thing I will note: if you want to get Jekyll to copy your .htaccess
and .htpasswd files into the _site/subscribers directory when building
your site, you’ll need to tell Jekyll to include them. You do this by
adding these filenames to the include: directive in your _config.yml
file:
include:
<snip>
- .htaccess
- .htpasswd
Now, when you copy your site to your hosting provider, the .htaccess and
.htpasswd files will be copied to the correct location automatically.
Wrapping up
And that’s it! Now you have most of the pieces to the puzzle of how to password-protect a subdirectory of your Jekyll-built site. Although I can’t flesh out a complete solution,5 I hope you’ve been able to see enough of the shape of the solution to be able to fill in the gaps on your own.
I hope that helps, and I wish you luck!
-
I deduced this detail from the Hetzner docs, where configuration of web server extensions uses
mod_http2and redirecting a domain to a subdomain requires usingmod_rewriteand setting up a.htaccessfile. I.e. classic Apache config. ↩ -
Look, I know it’s not hosted there, but we’ll make the assumption for now and use that as a placeholder for the actual domain. ↩
-
That is, of course if they’re not already reading a book, which is always difficult to avoid. ↩
-
This is because I don’t know what theme you might be using and how that impacts things nor what hosting provider you have. If you want help with this, give me a yell! ↩
Support
If you liked this post and want to see more, please buy me a coffee!