How to build a modern web app using (mostly) old school tech - part 1
Yesterday, I relaunched the Lean Web Club, a growing collection of focused tutorials and projects on how to build things for the web using HTML, CSS, and vanilla JS.
It’s a subscription membership platform, and I built it from scratch in a little over a week using the same principles I teach. The whole app is mostly HTML and CSS, with a little JS on the front end and a little PHP on the backend.
It’s incredibly fast, easy-to-maintain, and even works offline if your internet connection drops.
Over the next few days, I wanted to share how I actually built my modern web app with mostly old school tech. If you want to see it in action for yourself, you can join for free over at LeanWebClub.com.
Alright, let’s dig in!
Mostly static HTML
The heart of the Lean Web Club is mostly static HTML.
I use Hugo, a static site generator (or SSG), to take markdown files, mash them into some HTML template files, and spit out ready-to-serve HTML. Today, 11ty is a great alternative, but I’m deeply invested in Hugo already.
SSGs combine that best things about dynamic templating platforms like WordPress with the best parts of plain old HTML.
Templating with an SSG is much, much easier than using WordPress, but you also don’t have to manually code hundreds of HTML files. You can learn more about my process here.
Most of the Lean Web Club site is a bunch of markdown files that get rendered into flat HTML files. Hugo can do really cool things like loop through all of the projects, find any that reference the current tutorial, and spit out a list of “related projects.”
WordPress can do that, too, but because Hugo does it once ahead of time, users get responses back from the server much, much faster.
At the top of every markdown file is something called front matter. This is where you tuck any variables or custom details about the current page. In WordPress, this would be called “custom metadata” and require multiple functions to add. In an SSG, you just add a property.
---
title: document.querySelector()
description: How to find an element in the DOM.
sourceCode: https://path-to-a-git-repo.com
---Then, in your template, you can reference those properties (and some other generic ones) to create your layout.
<h1>{{ .Title }}</h1>
<p class="text-intro">{{ Params.description }}</p>
{{ .Content }}
<p><a href="{{ .Params.sourceCode }}">Source Code</a></p>It is, frankly, a joy to work with.
A little PHP
One of my favorite features of Hugo is custom output formats.
Instead of just HTML files, you can use Hugo to generate other file formats, and even create your own custom output formats.
For the Lean Web Club, I created a custom PHP output format. I also created a second HTML file format, subscribe, that outputs as a subscribe.html file instead of index.html.
outputFormats:
subscribe:
name: subscribe
mediaType: text/html
baseName: subscribe
isHTML: true
permalinkable: false
php:
name: php
mediaType: application/x-httpd-php
isPlainText: true
mediaTypes:
application/x-httpd-php:
suffixes:
- phpThen, I specified that these two custom outputs should be the default for most files.
outputs:
home: ["php", "subscribe"]
section: ["php", "subscribe"]
page: ["php", "subscribe"]For pages that should be public, I add a public front matter parameter.
---
title: Join the Lean Web Club
public: true
---Inside the template for my PHP file, I grab that parameter and use it to define an $is_public PHP variable.
<?php
// Hugo Variables
{{- $url := urls.Parse (trim .URL "/") -}}
{{- $paths := split $url.Path "/" -}}
$root_dir = dirname(__FILE__, {{ len $paths }});
$root_url = getenv('ROOT_URL');
$is_public = {{ if isset .Params "public" }}true{{ else }}false{{ end }};
Then, I do some checks to see if the user is logged in or not, and if the page is public or not.
If it’s public and they’re logged in, I redirect them to the dashboard. If it’s not public and they’re logged out, I redirect them to the sign in page.
If neither of those things is true, I grab the subscribe.html file and show its contents.
<?php
// Hugo Variables
{{- $url := urls.Parse (trim .URL "/") -}}
{{- $paths := split $url.Path "/" -}}
$root_dir = dirname(__FILE__, {{ len $paths }});
$root_url = getenv('ROOT_URL');
$is_public = {{ if isset .Params "public" }}true{{ else }}false{{ end }};
// Get user status
require_once($root_dir . '/api/helpers-token.php');
$is_logged_in = get_user();
$is_active = has_active_subscription();
// If the user is logged out and page is private
if (!$is_logged_in && !$is_public) {
header('Location: ' . $root_url . '/login/');
exit();
}
// If the user is logged in and page is public
if ($is_logged_in && $is_public) {
header('Location: ' . $root_url . '/dashboard/');
exit();
}
// Otherwise, show content
include('subscribe.html');
Now, I’m able to use my static site generator to create pre-rendered HTML files, and a tiny index.php file to handle routing behavior.
It all works automatically, and I don’t need to setup a complex JavaScript-based router with weird not-quite-links custom elements and all that. The user clicks real links, the page loads, and content either shows for them or they get routed somewhere else.
Server config
To make this work, I added a couple of items to my Apache server’s .htaccess file.
First, I tell the server to look for an index.php file as the main file in a directory, then index.html if you can’t find one. I also define my ROOT_URL environment variable.
And finally, I tell the server to deny direct access to my subscribe.html files. Only my PHP files can read and return those. This keeps the private member-only content private.
DirectoryIndex index.php index.html
SetEnv ROOT_URL https://leanwebclub.com
# Block access to subscribe.html files
<FilesMatch "subscribe.html">
Require all denied
</FilesMatch>To be continued…
Over the next few days, I’ll share how I handle dynamic content like logging in, user authentication, and so on. I’ll also share how I integrated billing with Stripe.
In the meantime, you can dig into lean web approaches like this over at LeanWebClub.com.