Building mah home
A custom, handcrafted personal site, just for me. 👉👈
This is a playground for myself, exploring ideas and technologies; nothing fancy.
- Deno runtime
- Deno Deploy
- How much can we stretch the Free tier?
- React on the server with Deno
- I have lots of experience with React, but with "regular" Single Page Applications (you know, a tiny HTML file that all it does is to load the main JS file that instantiates the entire thing, which implies that if JS is disabled user will only get a blank page).
- Here I want to start exploring:
- Using components to generate static HTML (no interactivity, no JS runs in the browser)
- Then start adding interactivity (as necessary) with Server Side Rendering + Rehydration
- Replace SSR + Rehydration with React Server components
- All of this while still using Deno
- How simple can a content management system can get
- Using Markdown documents as content source
- Last, but not least, have fun!
Non-goals
- Authentication
- Highly dynamic content
- Database-driven content
Journal
2025-07-04 - Javascript surprises with objects and classes
When we define and create a class like this:
class Thing {
readonly prop: string;
constructor(value: string) {
this.prop = value;
}
}
const thingy = new Thing('something');
then this is true:
expect(Object.values(thingy)).toStrictEqual(['something']);
And this is true in my tests, but when running the webserver, I was getting empty collections with Object.values()
/Object.keys()
/Object.entries()
. So properties were not being enumerated for some reason. But since accessing properties directly (like console.log(thingy.prop)
) does work fine, at first I thought it was some kind of engine/runtime optimization being performed in the code?
But no, actually. The difference in my case is how these instances were created. In my tests, the object instances are created as "normal", that is using the class' constructor (new Thingy(...)
). However, when the webserver is running this instances are not created using the constructor, but actually they are being "unpickled", with a static utility method in the class definition:
class Thing {
readonly prop: string;
constructor(value: string) {
this.prop = value;
}
static fromJSON(x: unknown): Thing {
// there's an extra step not shown here asserting that `x` has the proper structure
// in this case, `x` must be an object like `{ x: 'some string' }`
return Object.create(
Thing.prototype,
Object.entries(x).reduce((acc, [field, value]) => {
acc[field] = {
value,
writable: false,
};
return acc;
}, {} as Record<string, PropertyDescriptor>),
);
}
}
This works, but it's not ideal for a few reasons:
- It's on me to check that the shape/fields of the incoming object matches the fields of the class.
- There's very little typechecking here; that is, TS has no way to assert that the returned object is actually an instance of
Thing
. - The constructor's code is not run at all; so this might not be suitable in all cases; this works for now because the code in the real constructor does nothing more than lookup values to store in the fields.
Anyways... After a lot of experimenting and documentation reading, I realized the key concept with methods like Object.values()
"enumerating". Turns out when we do this.prop = value
in the constructor, the property being set on the instance is enumerable by default, but when the property is set via Object.create()
/Object.defineProperty()
they are not enumerable by default, we have to set enumerable: true
in the property descriptor to get those keys/values to show up.
So yeah, Fun Fact Friday!
2025-07-03 - Working on-and-off
I've been distracted by life; obviously I won't spend all my time working on this little "digital garden".
Another thing that's makes me slow down "progress" (whatever that means in this context) is that sometimes I swing between adding interesting features to "The Onion" and keeping it simple and add only what's necessary to make the site do what I want. I usually end up keeping it simple and scrapping all the experimental code I added while attempting to make something interesting work.
Something I'm adding is a feed reader, very much like in matklad's site. However (at least for now) the feeds from my site will come from feed URLs found in my links list. Also the contents of the feeds will also be part of the search index I plan to add to the site.
2025-06-26 - Many things!
I'm having fun with The Onion! Like, looking for possible deduplication, but just enough of it. Also playing with generics and combinators (can I call them combinators?) The idea is that small functions combined together, one wrapping the other could accomodate a lot of use cases.
Testing in Deno feels OK: It's fast, and so far @std/expect
is more than enough. Also, code coverage works out of the box; the HTML report is a bit to read regarding missed branches in complex lines, but it's not incorrect.
Turns out ESLint and typescript-eslint
run just fine under Deno. And the configuration looks exactly as I would expect it to look like in a Node project (except for the import
lines).
I changed the styling a bit to allow for better reading while in mobile.
2025-06-21 - Native CSS nesting is here? WOOT!?!?!?
Turns out all browsers support it now? I was aware it was being implemented... and now it just works? Since 2023?
Anyway, it does work! Compared to SCSS (which is what I'm familiar with) It does have some limitations, but it's not a big deal, for now at least.
2025-06-20 - Middleware and Testing
Rolling my own middleware
Turns out cooking up an onion-style "middleware" mechanism is rather easy? It's modeled like Django's: Each middleware item is a function of this type:
type Middleware = (
request: Request,
getResponse: (request: Request) => Response | Promise<Response>,
) => Response | Promise<Response | undefined> | undefined;
For example:
function renderMySection(request, getResponse) {
if (new URL(request.url).pathname === '/my-section') {
return getResponseForMySection(request);
}
}
But we can also pre-process a request for the next middleware:
function stripAllHeaders(request, getResponse) {
/**
* Because `Request` objects are immutable and therefore we can't change
* their fields, we create a new one using the fields from the original.
*/
const newReq = new Request({ ...request, headers: new Headers() });
return getResponse(newReq);
}
And we can also post-process a response:
async function addCachingHeaders(request, getResponse) {
const response = await getResponse(request);
/**
* Because `Response` objects are immutable and therefore we can't change
* their fields, we create a new one using the fields from the original.
*/
return new Response(response.body, response);
}
Then the server's main request handler is just a function the goes through
Testing with Deno
Deno includes a test runner, the API is rather simple (not necessarily a bad thing) and there's the @std/expect
which provides a Jest-like assertion API (which is the style I'm familiar with). Currently, what's awkward about these APIs is writing tests off test cases as tuples. For example, in Jest I would write something like:
it.each([
[1, 1, 2],
[4, 5, 9],
])('addition', (left, right, expected) => {
expect(left + right).toBe(expected);
});
What I like about that is that both the test case tuples and the test block is all one single statement. However there's no equivalent in Deno (turns our test steps are not to be used for that). There's also a separate @std
library that provides describe
/it
API, but it also doesn't have each
, so I'm holding off on installing it for now (I might change my mind).
For now I ended up doing something like this:
((testCases: [left: number, right: number, expected: number][]) => {
Deno.test({
name: `addition ${left} + ${right} = ${expected}`,
fn() {
expect(left + right).toBe(expected);
}
})
})([
[1, 1, 2],
[4, 5, 9],
]);
Looks weird, but note that this is actually a "IIFE" (Immediately Invoked Function Expression), which was actually a very common thing to find in early web development. It's basically two things happening in the same statement: the definition of the function to be invoked (the ((...params) => {/* body */})
), and the actual invocation (the ([params])
next to it). In terms of testing, we define (with the function expression) the test body and the type/shape of the test cases it accepts, and then we pass the actual test cases as parameters to the IIFE.
While odd-looking, it fulfills the requirement of having all of it in one statement, and actually allows to define better the type of the test cases, because now we have a proper place for it, the argument position. With it.each()
there's no place inside the same statement to define the type; one can only use satisfies
(like it.each([/*...*/] satisfies SomeType[])('test name', () => {/*...*/})
) which only checks that the values are structurally compatible with the named type, but doesn't assign them the type; or otherwise use as
(like it.each([/*...*/] as SomeType[])('test name', () => {/*...*/})
) which is worse because using as
is like telling TS "trust me bro, I'm telling you what type this thing is" and any mistake will get ignored by the typechecker.
2025-06-19 - Basic setup
Deno.serve()
seems enough for now. We give it a handler function that takes a Request
and must return a Response
object. Very functional! The handler then does basic string matching on new Url(request.url).pathname
and decide what to do next. For now, the main request handler will try the content pages first (/links
and /pages/*
) then fallback to static files ("assets") and then fallback to a 404 page (also loaded from a markdown document).
Loading pages is very simple right now; translate url.pathname
to a file like data/pages/filename.mdx
.
Maybe this will change in the future... I don't know yet!