Home

Site Devlog #2

Published

This is the second development log entry for this site. After a few months off due to other things going on in my life, I finally got to updating this site, even though the changes are not that much on the surface level. In short, there is now a working gallery section (full of test images as of now of course), an page showing articles filtered by a specific tag, and finally the site logo.

The gallery section basically showcases a set of pictures grouped as albums. Clicking on a picture reveals a preview showing the larger version, and within this preview you can also navigate to the next or previous image in that album.

Image preview feature
Image preview feature shown after clicking a thumbnail

For each picture, I'd need two image files - one for the thumbnail and one for the full preview image. After grouping them accordingly as albums and attaching extra fields, the data should be ready to render, so this sounds simple enough. However, the challenge for me was figuring out the best way to update this dataset later on.

Backend approach

My first idea was to build a backend for these images instead of building them statically. I tried this out with a separate CLI application connected to an SQLite database, and this database would also be used by the main application. However, there were several glaring problems I encountered as I went and implemented this solution:

  • Updating the gallery will be a huge pain to do, since I'd have to SSH into the server, scp the image files, and run the correct command.
  • This approach is 100% imperative than declarative - I can't exactly see what the album would look like after I run an "update" command to rearrange the pictures.
    • As a result, commands should have no unwanted side effects when there are errors along the way (e.g. a database transaction should not be committed until everything is correct).
  • The processed image files should be saved in a directory that is also accessible by the main application. This means adding an extra - somewhat implicit - requirement for the main application to run.

The only benefit to this approach is that I don't have to update the main application to update the gallery, which now is definitely not worth considering the above downsides.

Integrated approach

The current solution is building the dataset directly in the main application, then rendering that instead. To handle image processing, I relied on the vite-imagetools plugin, which allows me to import image files with import directives which basically are parameters for the image processor.

For this feature, I only need to resize the images to a certain maximum width and height, and if possible, set the quality level to make the files lightweight. In addition, the plugin also allows importing additional metadata such as an image's width and height, using the as=metadata directive.

Using directives, I can define how the thumbnail and full images should be processed:

import pic1Thumb from "./1.jpg?w=600&h=600&fit=inside&quality=80&as=metadata";
import pic1Full from "./1.jpg?w=1200&h=1200&fit=inside&quality=80&as=metadata";

However, this would introduce a lot of repetition of the directives. Luckily, using defaultDirectives in the config, I can at least resolve this repetition by introducing a single source of truth:

imagetools.config.ts
const imageToolsConfig: Partial<VitePluginOptions> = {
defaultDirectives: (url) => {
if (url.searchParams.has("gallery-thumbnail")) {
return new URLSearchParams({
fit: "inside",
w: "600",
h: "600",
format: "jpeg",
quality: "80",
as: "metadata",
});
}
if (url.searchParams.has("gallery-full")) {
return new URLSearchParams({
fit: "inside",
w: "1200",
h: "1200",
format: "jpeg",
quality: "80",
as: "metadata",
});
}
return new URLSearchParams();
},
};

Then the above import statements can be written as such:

import pic1Thumb from "./1.jpg?gallery-thumbnail";
import pic1Full from "./1.jpg?gallery-full";

Tag filtering

This page simply shows all articles tagged with a specific tag ID in the path param (here the id param from /articles/tag/[id]). You can try it out by clicking on one of the tags on the footer of this page.

My existing codebase already had support for tagging articles. I already defined a constant object which maps tag IDs to tag names. The only thing that needed to be done was to create a server function which returns the filtered articles.

I realized that pagination might be necessary in the near future, so I implemented it so that it can be used for this feature as well as the normal article listings under an "index" path.

server/metadata/articles.ts
const paginate = (articles: Article[], currentPage: number) => {
const rangeStart = (currentPage - 1) * PAGE_SIZE;
const rangeEnd = rangeStart + PAGE_SIZE;
const pagedArticles = articles.slice(rangeStart, rangeEnd);
const maxPage = Math.ceil(articles.length / PAGE_SIZE);
return {
articles: pagedArticles,
maxPage,
} satisfies PagedArticles;
};
export const getArticles = (path: string, page: number) => {
const pathSegments = parsePath(path);
const node = getNodeAt(pathSegments);
if (!node || node.type !== "INDEX") return undefined;
const articles = getArticlesUnderIndex(node, pathSegments)
.map(([, article]) => article)
.toSorted((a, b) => compareDesc(a.publishedAt, b.publishedAt));
return paginate(articles, page);
};
export const getArticlesWithTag = (tagId: TagId, page: number) => {
const articles = getArticlesUnderIndex(metadataTree, [])
.map(([, article]) => article)
.filter(
(article) =>
article.tags && article.tags.some((tag) => tag.id === tagId),
)
.toSorted((a, b) => compareDesc(a.publishedAt, b.publishedAt));
return paginate(articles, page);
};

I also finally added a site logo!

Site logo
Site logo

Admittedly it's just a simple logo, might change in the future when I get my grips on logo design. But it's good enough as a way to distinguish my site for now. With the help of favicon generator, it was simple enough to generate the favicon and the corresponding logos for mobile devices from a given SVG file.

© 2024 Ahmad Zhafir Bin Yahaya