Build a Membership Blog with Magic and 11ty


In this Guide, we’ll learn how to build a simple membership blog that uses 11ty as the static site generator and Magic as the auth solution.

Here’s a demo of the app!

Quick Start

  1. git clone
  2. cd magic-11ty
  3. npm install
  4. Grab your Test Publishable Key from your Magic Dashboard and paste it into header.njk.
  5. npm start

Note: When moving to production, we recommend that you whitelist your domain for your Publishable API Keys on the Magic Dashboard so that your API keys only work on your domain.

What's 11ty?

11ty is a simpler static site generator. It was built for those wanting to build sites that’ll require only some static landing pages and a few lines of JS, and maybe a blog to generate. It uses the timeless idea of templating for component libraries. What’s great about 11ty is that we can choose the HTML templating language of our choice and it handles all of the complicated routing and page generation for us!

What's Magic?

Magic is a plug and play SDK that enables passwordless authentication using magic links. If you want to learn how to use Magic to authenticate users into an Eleventy-powered blog, read on!

Magic x 11ty

Set up our pages with 11ty

Shout out to Stephanie Eckles and her tutorial, Build an Eleventy Site from Scratch. It’s a simple and straightforward tutorial that teaches you how to build a blog with 11ty. Feel free to follow her tutorial to get started with 11ty. With a few tweaks to the sample app provided by Stephanie, we can customize the blog to fit our use case. Here are main pages of our site:

  1. Login
  2. Profile
  3. Home

Let's explore each of them.


This is the login page. It’s where the user will be authenticated using the Magic Client SDK for Web.

magic-11ty-blog > src >

title: "Login"
layout: "base.njk"
templateEngineOverride: njk,md

<!-- 0. HTML -->
<form id="form">
   <input id="input" type="email" />
   <button type="submit">Login</button>
<div id="result">

<!-- 1. Use loginWithMagicLink to authenticate user -->
const form = document.querySelector("#form");
const input = document.querySelector("#input");
const result = document.querySelector("#result");
form.onsubmit = async (e) => {
 const email = input.value;
 const didToken = await magic.auth.loginWithMagicLink({
 result.innerText = didToken; // Display the user’s DID token
 if (didToken) {

Here we’re using loginWithMagicLink to authenticate the user with their email address. It returns a promise that is resolved when the user clicks the Magic link that’s emailed to them. The resolved value is a Decentralized ID token named didToken which has a default 15-minute lifespan. Receiving the didToken confirms that the user has been authenticated by Magic.


This is the user profile page, it’s where the user is routed to once they’ve logged in.

magic-11ty-blog > src >

title: "Profile"
layout: "base.njk"
templateEngineOverride: njk,md

<!-- 0. HTML -->
<h3>Welcome 👇🏼</h3>
<h3 id="email"></h3>
<h3>Public Address 👇🏼</h3>
<h3 id="publicAddress"></h3>

<!-- 1. Ensure the user’s info is displayed -->
// Assumes the user is already logged in
const displayUserInfo = async () => {
 let emailElement = document.getElementById("email");
 let publicAddressElement = document.getElementById("publicAddress");
 try {
   const { email, publicAddress } = await magic.user.getMetadata();
   emailElement.innerHTML = email;
   publicAddressElement.innerHTML = publicAddress;
 } catch {
   // Handle errors if required!
   emailElement.innerHTML = "DNE";
   publicAddressElement.innerHTML = "DNE";

The email address and public address is retrieved using Magic’s getMetadata method. The public address can be thought of simply as the user’s unique ID provided by Magic.


This is our home page. It’ll have a list of all the blogs we’re fetching from a mock headless CMS. If you want to learn about how we’re sourcing this data, watch Stephanie’s quick tutorial.

magic-11ty-blog > src >

title: "My Cool Blog"
layout: "base.njk"
templateEngineOverride: njk,md

<!-- 0. HTML -->
## Articles
<div id="content">

<!-- 1. Only share articles with logged in users -->
// Assumes user is already logged in
let contentElement = document.getElementById("content");
const articles = `<ul>
{% for article in collections.articles %}
<li><a href="{{ article.url }}">{{ }}</li>
{% endfor %}
const loginMessage = `Please log in to see my blog!`;

try {
 contentElement.innerHTML = articles;
} catch {
 // Handle errors if required!
 contentElement.innerHTML = loginMessage;

Because we only want logged in users to have access to this page, we’re using Magic’s isLoggedIn method to check whether or not they’re logged in. This method returns true if they’ve been authenticated by Magic, otherwise it will return false.

Set up header.njk

Get and Init Magic SDK

You’re probably wondering how we were able to access the Magic SDK across our .md files. The answer is in our magic-11ty-blog > src > _includes > header.njk file! It’s a pretty big Nunjucks file, so let’s go through it one code block at a time.


💡 Nunjucks: A rich and powerful templating language for JavaScript. It offers a complete feature set that makes templating a breeze.

First of all, this is where we’re getting and initializing the Magic SDK with the Publishable Test Key we obtained from the Magic dashboard.

<!-- 1. Get Magic SDK -->
<script src=""></script>
<!-- 2. Initialize Magic -->
 const magic = new Magic("YOUR_PUBLIC_KEY");

Next, you’ll see that I’ve added a loader element to help create a better UX while we wait for the Magic SDK to load.

<!-- 3. Add loader to wait for Magic SDK -->
<div id="loader" class="loader">
 <img src="" class="loaderImage"/>

Add Navigation Links

Here are our navigation links! We’ll need to display each one of them depending on whether or not the user is logged in, which is why they each have a unique id.

<!-- 4. Create our Navigation links -->
<a id="home" href="/">Home</a>
<a id="profile" href="/profile">Profile</a>
<a id="login" href="/login">Login</a>
<button id="logout" onclick="logout()">Logout</button>

As you can see, we’ve specified which pages are private and which are public. We’ve also added references to each of the Navigation links so that we can hide them accordingly.

<!-- 5. Update visible pages depending on whether or not the user is logged in -->
let privatePages = ["/profile/", "/"];
let publicPages = ["/login/"];

const loginElement = document.querySelector("#login");
const profileElement = document.querySelector("#profile");
const logoutElement = document.querySelector("#logout");
const homeElement = document.querySelector("#home");
const loaderElement = document.querySelector("#loader");

Write getUser()

Here we write a function called getUser() which uses Magic’s isLoggedIn method to check if a user is currently logged in to the Magic SDK. Depending on the outcome, we will display certain navigation links.

// Get the user & check whether or not they are logged in.
// Show or hide pages depending on the outcome.
const getUser = async () => {
 // Gets the page user is currently on
 const currentPath = window.location.pathname;

 // Checks if user is currently logged into the Magic SDK
 const isLoggedIn = await magic.user.isLoggedIn();

 // If the user is logged in...
 if (isLoggedIn) {
   // ...prevent them from accessing public pages.
   if (publicPages.includes(currentPath)) {
   } else {
     // Or hide links they don’t need to see = "none";
     // Get rid of the loader once the user reaches the correct page = "none";
 } else {
   // If a logged out user tries to to access a private page, send them back to the login page
   if (privatePages.includes(currentPath)) {
   } else {
     // Hide links logged out users shouldn't be able to see = "none"; = "none"; = "none";
     // Get rid of the loader once the user reaches the correct page = "none";


Write logout()

The last important piece of code to see in header.njk is our logout() function! We use Magic’s logout() method to log out the currently authenticated Magic user. Once the user has been logged out, they’ll be taken back to the login page.

<!-- 6. Log out the currently authenticated Magic user -->
 const logout = async () => {
   try {
     await magic
   } catch (error) {
     // Handle errors if required!
     console.log('Ran into this error while logging out: ', error);
   console.log('WHOO! User has logged out of all Magic SDK sessions.');


Alright, that’s it for today. Thank you for reading! If you have any questions, please ask them in the comments section below, or in our Discourse Forum. Until next time!

Let's make some magic!