Table of Contents
Introduction
A nearly zero dependency blog, created from scratch and built for the Replit Template Jam. Pretty much everything is built with vanilla JavaScript APIs on the client (the Shadow DOM and fetch
) or NodeJS APIs on the server (http
and fs
essentially power the whole operation)
Even though I ended up not winning, it was still an incredibly enlightening experience to build a project with as few dependencies as possible. As a look back on this project, although I realize that while well-maintained (and often large) libraries are a necessity for the web, I also recognize our responsibility as developers to be more responsible with client-side JavaScript.
Features
You can use Zero Blog without any setup or friction - just click run on Replit and it'll automatically prompt you for all the information it needs. On a slight tangent, I don't have the resources to deploy all of my side projects, so you'll have to be satisfied with these looong screenshots (unless you can deploy them yourself).
Zero Blog setup is hassle free!
Posts and metadata are controlled and written with Markdown, as many other blogs are. Once you've written a few posts, they will show up on the home page. For simplicity, you can set an "emoji" splash screen rather than configuring a thumbnail - just like Zenn!
Zero Blog home page with a few default posts
Overall, the blog functions as most would expect a blog to function. Replit users can log in and comment on posts, most Markdown is supported (including highlighted code blocks), and all posts have a neat table of metadata and a sticky table of contents (this was really hard to implement because of the Shadow DOM).
Zero Blog post, zoomed-out to show the table of contents
Design
The design of this website is directly inspired by Zenn. I'm not entirely sure how I even found this website in the first place, considering I can't the posts. I think they're written in Japanese?
Zero Blog's design is informed by zenn.dev
Regardless, the design is both modern and approachable, making it a good fit for a beginner blog template. Zero Blog uses some principles of Zenn's design language and layout, with a few important distinctions. I changed the palette into something more orange and bronze, generally included more spacing, and replaced borders with light shadows. Hopefully, these changes emphasize the friendly nature of the blog, rather than the professional moods associated with Zenn's blue color palette and more "well defined" components (conveyed through darker borders and greater contrast, for example).
Tech Stack
Zero Blog does not use any dependencies, instead implementing everything from scratch with Vanilla JavaScript (and NodeJS in the case for the server); I call these internal libraries "Zero" libraries to emphasize their lightweight nature. Zero Blog is essentially several packages built into one application:
-
markdown renderer
-
templating engine
-
custom server
-
a reactive framework based on the shadow DOM
- a client side router
- reusable components, like syntax highlighting
I would never condemn myself to creating websites without reactivity, even if upholding the lightweight theme required building an entirely new framework. Granted, the client-side framework is not very good in terms of performance and developer experience.
In terms of performance, the diffing algorithm for reactivity is quite simple and primarily involves traversing DOM nodes and simply updating properties (replacing entire DOM nodes if something major changed). I don't know enough about these kinds of algorithms to apply any futher optimizations, and some patterns used in the components are explicitly inefficient. For example, all styles are essentially inlined because I didn't have a bundler or something cool like a CSS-in-JS library to automatically generate class names. Like a traditional SPA, all of the pages and routes are loaded regardless of what the user requested, which negatively impacts loading times for the user.
However, Using Web Components and the Shadow DOM does make for a very intuitive route structure to debug, which is strangely reminiscent of what Remix and React Router might generate. I suppose I was ahead of my time (it also looks cool I guess).
Zero Router containing two routes: the "Home" route and dynamic "Posts" route
In terms of developer experience, all components are built with legacy React-esque classes. Initially this seems OK, but it gets worse when you realize that no bundler or processor means JSX is completely unsupported (necessitating pure JavaScript functions). Here's what the header component look like:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import Zero, { ZeroUtils } from "/lib/Zero";
Zero.define(
"z-header",
class ZHeader extends Zero {
render() {
return h.header(
// yup, styles are inlined (or added as a style tag in the shadow DOM)
{ style: globalStyles.bgWrapper },
h.div(
{ style: styles.headerDiv },
h.a(
{},
h.zLink(
{ href: "/" },
h.h1({ style: styles.headerLink }, "📦 Zero Blog"),
),
),
h.zSearch(),
),
);
}
},
);
Technically, Zero Blog is NOT a "zero dependency" blog completely because I use Replit Auth and their KV Database to authenticate and store comments. I could have interfaced with the API directly, but I figured using some Replit products and packages was a necessity to create a Replit-themed template. Interestingly, Replit Auth is "technically" not a dependency because Replit automaticallys adds unique headers for logged-in users, so it is not necessarily an installed package. In any case, Zero Blog's implementation necessitates deploying on Replit because of its tight integation with Replit products.
I will admit that not everything should be built from scratch - public libraries are often far better maintained, more feature complete, and battle-tested compared to what a single developer could make in a week - but building Zero Blog was certainly a fun experience. Furthermore, I realize that lightweight Vanilla Javascript libraries does not really benefit the server-end (unless the concern is having more features may impact performance or that rare case were storage actually matters), because the client is not consuming that complexity anyways.
Challenges Faced
The most significant challenge is simply managing complexity. Every new feature requires real consideration because a self-imposed restriction of as few dependencies as possible most likely means you'll have to build that feature by yourself. For example, I ultimately decided on a Markdown blog because I didn't want to implement a feature-complete rich text editor and did not really know where to start - a simple Markdown parser seemed almost trivial in comparison.
The challenge I actually remember was fanagling around with the Shadow DOM. Now that's a real headache. For starters, it is nearly impossible to create shared styles among components because the Shadow DOM is sort of like an isolated iframe container. For example, creating a reusable "button" class in a stylesheet for the "normal" DOM will have absolutely no effect on a Shadow DOM web component. This was a recurring (and annoying) issue: making anything that depended on other components in even the most trivial of ways required using JavaScript. Positioning a "sticky" table of contents requires JavaScript, even though all major browsers already support this in CSS. Furthermore, clicking on the links on the table of contents to jump to the appropriate heading also requires JavaScript, even though you can do this in the "normal" DOM just by adding IDs to the header (you can even try this now - scroll back up and click on "Challenges Faced"). Overall, it took me far too long to realize why nothing was working as expected.
Future Enhancements
Rewrite with TypeScript. Seriously, types are great. It'll still be "zero dependency", especially if I use another runtime like Deno. If there's enough interest, I'll probably create a separate branch that can be deployed independently from Replit. Currently, Zero Blog will deploy successfully, but authentication and commenting will not work without Replit.
Beyond that, there's also a lot of kinks that need to be worked out - I just fixed serving local images (in 2024!) - and most of the libraries are completely untested. Once those libraries are better tested and rewritten in TypeScript, I will probably bundle and package them under vanilla-libraries
as a lightweight alternative to the more well-established frameworks out there.