Building mah home

A custom, handcrafted personal site, just for me. 👉⁠👈

This is a playground for myself, exploring ideas and technologies; nothing fancy.

Non-goals

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:

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!