As we know it, making a personal site takes time. To even start, I gave myself a goal - create the most basic blog and start writing. Pretty simple, huh? Not really.
I feel that every person has a huge pool of things he or she can talk or write about. We experience life, with our own twists and turns. After discovering something neat, we rarely share it. It is 2022, and it is well known - learning in public is the way to go. For myself, writing gives me time to ponder, clears up my thinking, makes me more precise and calm. There is also this feeling of completeness when you sum up a topic and polish what you know. This got me to work.
The initial goal of keeping things simple was pragmatic - I did not have much
success with starting a personal website in the past, so making it mostly
effortless prognosed some chance of success. I wanted to start with next or
remix, but the classic npm install
got me thinking - how basic could the tech
stack really be for a blog? (by the way, this brought back
a memory of a wonderful talk by Ryan Dahl)
And I did not answer that, as you may guess. Time went by, I started diving deep and got irritated, the editor was left empty. But what was left was a really fun question - how do I deem a stack simple? Or event better, going straight downhill, how to define "simple" in the context of programming (I know, this may have already been too philosophical).
I probably could rant a long time about calling something "simple". Even more so when I'm currently going through "The Mythical Man Month". And you as a reader probably do not want to make this an hour-long read. So stopping myself right in the tracks again, it went like this - I searched for an answer far and wide, considered multiple options, read far too many resources, until finally stopping to do the right thing as I should have from the start - to define the game I wanted to play, its rules, and a strategy to execute.
The first part was mostly done, I wanted to start writing as soon as possible. What lingered was the yearning for it to really be mine. I did not let that go away, so the final goal was to develop a personalised blog/personal website as fast as possible.
This gave me a sense of direction and context that created the requirements for the project:
Using my own guidelines the stack emerged by itself. I have chosen deno as a runtime, deno deploy as the deployment target, react as the templating engine (no client side code though, just rendering on the server), pure css for styling, and mdx with plugins to write articles quickly.
With deno, I did not have to set up typescript myself or through frameworks, the standard library removed the need for most external libraries, linter and other goodies were built in.
Deno is a clear upgrade over node, the only thing stopping it being not so rich
ecosystem. I did not have to learn any new frameworks, the project logic was
written in a whooping 3 files, when I have to change some styles, I simply open
styles.css
and change what I have to. React gives templating goodies, and does
not introduce any bundle size or magic as it is simply used for rendering html.
Mdx is a clear upgrade over writing html, is infinitely tweakable with plugins
and I can migrate my content to other system whenever I want.
It took a day to glue everything together. I encountered some issues while working with mdx and rendering react on the server but that was basically it. I won't deny that the task would be much harder if I was just starting my programming journey - when you work with raw requests and responses, knowing a thing or two about things like cache headers is invaluable and there is a lot of thinking about details. This is not pleasurable when you do not know what really happens. Modern frameworks abstract away that complexity, but this is exactly what made the stack worthwhile for me. I had an occasion to see the state of modern web APIs, to refresh my knowledge and see what can be achieved without additional abstractions. There is no build step and deployment takes ten seconds at most. If I have to add something like analytics I will add it to the request flow.
Of course there were concerns too. For example, raw pixel values fly left and right in the css for the time being, there is no consistency there, also, I may have used next and have mdx configured right away and the list goes on. But should I care about it right now? Nah.
I will say what most of us probably want to hear from time to time. Don't bullshit yourself and see things as they are. Does the solution solve the problem? Yes. Will you have issues with maintaining it? I certainly won't, year later I will just read the code and catch up, there is no hidden magic. Nobody else will work with me on it, so I don't have to care about that too.
So it is really simple, you may ask? Can you do what has to be done without pulling your hair out? Yes? Great! Does changing it introduce obstacles that will hinder development in the future? No? Wonderful. Is it quickly deployable? Probably? Good enough. You probably have something simple on your hands, all things considered.
This blog fulfils the criteria, so let me consider it simple. It came to be without much time but with a lot of care. Hopefully it will serve its purpose well.
The main code serving this website is shown below. External utils and components do not exceed 100 lines, and are mostly static html or some glue code for existing libraries. Taking in mind that there is no framework underneath, it is pretty crazy how much can be done in a modern way with the standard libraries and a bit of code.
If you are curious, the complete code is available publicly on github.
import { renderToStaticMarkup } from "react-dom/server";
import { serve } from "std/http/server.ts";
import { extname, resolve } from "std/path/mod.ts";
import { contentType } from "std/media_types/mod.ts";
import { router } from "rutt";
import Home from "./components/Home.tsx";
import Post from "./components/Post.tsx";
import { evaluate } from "./mdx.ts";
import { redirect } from "./utils.ts";
async function asset(path: string) {
let content: Uint8Array;
const realPath = resolve("./public", path);
try {
content = await Deno.readFile(realPath);
} catch (error) {
if (error instanceof Deno.errors.NotFound) {
return new Response("Not found", { status: 404 });
}
console.error(error);
return new Response("Server error", { status: 500 });
}
const mime = contentType(extname(realPath)) ?? "binary/octet-stream";
const isLongCacheable = mime?.startsWith("font") ||
mime?.startsWith("image") || mime.startsWith("text/css");
return new Response(content, {
headers: {
"content-type": mime,
...(isLongCacheable &&
{ "cache-control": "public, max-age=15552000, immutable" }),
},
});
}
async function post(slug: string) {
try {
return view(
Post({ post: await evaluate(`./posts/${slug}.mdx`) }),
);
} catch (error) {
if (error instanceof Deno.errors.NotFound) {
return new Response("Not found", { status: 404 });
}
console.error(error);
return new Response("Server error", { status: 500 });
}
}
function view(Component: JSX.Element) {
return new Response(
`<!DOCTYPE html>${renderToStaticMarkup(Component)}`,
{
headers: { "content-type": "text/html" },
}
);
}
serve(
router({
"/": () => view(<Home />),
"/blog": (req) => redirect(req, ""),
"/blog/:slug": async (_req, _ctx, match) => await post(match.slug),
"/:asset*": (_req, _ctx, match) => asset(match.asset),
}),
);