Jekyll and hosted on Github Pages.

" /> Jekyll and hosted on Github Pages.

" />

How to password-protect content in Jekyll / GithubPages

01 Oct 2019

This blog is written using Jekyll and hosted on Github Pages.

In order to provide additional value to my sponsors, I wanted to publish some exclusive content for that people. I thought about how to do this, without requiring a separate server or special plugins, and in this article I’m going to show you how I did it.

First of all,


Prerequisites

Create a private repository at github

For this to work, you have to make the content of your blog private so nobody can read the code and find the urls or read the content. This is totally free at GitHub and most of other GIT hosting solutions.

Being able to publish the static content of your blog

Since the code is private, if you want to use GitHub, you will need to be Pro.

Free option

Alternatively you can deploy it yourself. You can also use the free Github Azure Pipelines that work on private repositories and is free. to deploy it to your own server, or any place where you can store static content.

Basic structure

_includes/catalog_hidden.json

{
  { %- for post in site.posts % }
  { %- if post.hidden % }
  "{ { post.permalink_free | absolute_url } }": "{ { post.url | absolute_url } }",
  { %- endif % }
  { %- endfor % }
  "": ""
}

This will be used to access the JSON from JavaScript, and this lists all the hidden pages of the site.

password.json

For each user, you have to create a .json file with a front matter like this one:

---
permalink: /user-c8390c0891fb878614be01d5e042f423d34b53fd9403333c86e2a5c6e8bb0952.json
hidden: true
---
{ % include catalog_hidden.json % }

The permalink should contain in the url the username and the password of the user hashed in SHA256. You can use online tools to generate SHA256 easily for a string.

Posting

In my case I wanted to provide articles that have one free part, and one part requiring login. In my case I’m creating two posts per entry. For example:

_posts/2019/2019-10-02-github-pages-jekyll-password-protect.md

---
layout: post
permalink: /github-pages-jekyll-password-protect/
title: "How to password-protect content in Jekyll / GithubPages"
feature_image: /i/sponsor/sponsor-password-protect.jpg
tags: [blog, github, jekyll, sponsors]
hidden: false
sponsor: true
---

This blog is written using [Jekyll](https://jekyllrb.com/)
and hosted on [Github Pages](https://pages.github.com/).
...

_posts/2019/2019-2019-10-02-github-pages-jekyll-password-protect-sponsor.md

The permalink must have something random in the url, so anyone can know its address. I’m using: https://www.uuidgenerator.net/ to generate UUIDs easily.

---
layout: post
permalink_free: /github-pages-jekyll-password-protect/
permalink: /github-pages-jekyll-password-protect/b8ffc8d5-f0a5-4f3b-b1c5-ccdb437cba65/
hidden: true
---

## Prerequisites
...

The magic

The hidden key in the frontmatter, allows the jekyll-paginate plugin to ignore the specified pages. In other cases you should use { %- unless post.hidden % }{ % endunless % } to filter the hidden posts from listing like RSS, sitemaps or catalogs.

_layouts/post.html

...
<div class="content" id="post-content">
    <div id="actual-post-content">
        {{page.content}}
    </div>

    <div id="sponsor-content">
        { % if page.sponsor % }
        { % include sponsor_article.html % }
        { % endif % }

        { % if page.hidden % }
        { % include sponsor_thanks.html % }
        { % endif % }
    </div>
</div>

<script type="text/javascript">
    registerSponsor();
</script>
...

_include/sponsor_article.html

<div style="width: 100%;height: 4em;position:relative;margin-top:-4em;background:linear-gradient(rgba(0,0,0,0) 0%, var(--color5) 75%);">
</div>
<div class="sponsor_base sponsor_down">
    <h3>The full article is exclusive for my github sponsors that help me to continue with my projects</h3>
    <p>To continue reading it, please, log-in.</p>
    <form action="javascript:formSponsorLogin()">
        <input id="sponsor-user" type="text" placeholder="User"/>
        <input id="sponsor-pass" type="password" placeholder="Password"/>
        <input type="submit" value="Login"/>
    </form>
    <p>You can become my sponsor here to get your login information: <a href="https://github.com/sponsors/soywiz" target="_blank">https://github.com/sponsors/soywiz</a>.</p>
</div>

script.js

You can find the small sha256 function here: https://geraintluff.github.io/sha256/

function normalizeUrl(url) {
    if (url && url.match && url.match(/^https?:\/\//)) {
        return new URL(url).pathname
    } else {
        return url
    }
}

async function sponsorDecode(user, pass) {
    if (user !== "" && pass !== "") {
        const url = `/${user}-${sha256(pass)}.json`;
        const file = await fetch(url);
        const json = await file.json();
        const normalizedMap = {};

        for (const [key, value] of Object.entries(json)) {
            normalizedMap[normalizeUrl(key)] = normalizeUrl(value);
        }
        window.sponsorMap = normalizedMap;
        dispatchEvent(new Event("SPONSOR_INIT"))
    }
    //console.warn("SPONSOR!", window.sponsorMap)
}

async function sponsorInit() {
    await sponsorDecode(localStorage.getItem("sponsorUser") || "", localStorage.getItem("sponsorPass") || "");
}

async function sponsorLogin(user, pass) {
    try {
        localStorage.setItem("sponsorUser", user);
        localStorage.setItem("sponsorPass", pass);
        await sponsorInit();
        return true;
    } catch (e) {
        alert("Password error");
        return false;
    }
}

async function formSponsorLogin() {
    const userNode = document.querySelector('#sponsor-user');
    const passNode = document.querySelector('#sponsor-pass');
    if (!await sponsorLogin(userNode.value, passNode.value)) {
        userNode.value = "";
        passNode.value = "";
        userNode.focus();
    }
}

async function sponsorLogout() {
    localStorage.removeItem("sponsorUser");
    localStorage.removeItem("sponsorPass");
    document.location.reload();
}

window.sponsorMap = {};

let setSponsoredContent = false;

async function updateSponsor() {
    if (!setSponsoredContent) {
        const newUrl = window.sponsorMap[document.location.pathname];
        if (newUrl) {
            setSponsoredContent = true;
            const url = await fetch(newUrl);
            const html = await url.text();
            const parser = new DOMParser();
            const node = parser.parseFromString(html, "text/html");
            document.querySelector("#actual-post-content").appendChild(node.querySelector("#actual-post-content"));
            document.querySelector("#sponsor-content").innerHTML = node.querySelector("#sponsor-content").innerHTML;
            //postContentThis.innerHTML = postContent.innerHTML;
            //console.warn(newUrl);
            //console.warn(postContent)
        }
    }
}

function registerSponsor() {
    addEventListener("SPONSOR_INIT", () => {
        //console.warn("SPONSOR_INIT");
        updateSponsor();
    });

    updateSponsor();
}

sponsorInit();

Final words

So the idea is:

  • There is an unknown url that link normal urls to urls with the full content
  • To find that unknown url with the catalog you use the user and the hashed password
  • If the user/password pair is right, the url will lead to a file with the catalog, otherwise that would produce a 404
  • We store the credentials via localStorage, so the user don’t need to login each time.
  • We fetch the right content via javascript, and replace the relevants parts of the page as soon as possible

And that’s it. Hope you found it useful!