Home

Site Devlog #1

Published , last updated

This is the first development log entry for this site. Mainly about how I built the site, how I structured it and why I chose this approach. This whole ordeal to reach this stage took several months, so get ready for a long write-up on this first stage.

Idea

My idea was to create my own personal portfolio site to better showcase my projects, alongside any articles including development logs and guides for people to read. I also want to showcase my photos from my camera in a gallery.

In terms of rendering, I didn't want to use client-side rendering for a whole portfolio site like this, mainly so that I can get a good SEO score in the first place. This left me with two main categories, server-side rendering (SSR) and static-site generation (SSG). Both have their pros and cons, and the latter especially is popular for personal blog sites, but I opted for SSR instead. I originally used Hugo which is a SSG framework popular for building personal sites, but due to the shortcomings when it comes to interactive elements or server-based resources, I didn't waste time any further and migrated to the SSR approach.

And yes, I don't want to rely on pre-existing solutions like WordPress, partly as a challenge, but mostly so that I can manage everything that's on my site.

Project setup

For this site, I'll be using Solid Start, a relatively young fullstack meta-framework amongst the plethora of JavaScript frameworks out there. Its core, Solid JS, utilizes fine-grained reactivity, which allows Solid to update only what has changed, thus improving performance and load times. I personally have read numerous articles from the framework's author, which definitely convinced me to try Solid JS when it's still in develoment. And Solid Start just hit v1.0 a few months ago, so this might be the perfect time to make use of it.

Setting up a new Solid Start project is simple enough according to the docs. I'll be using pnpm in this case, and for the project I will be using TypeScript and start with the "with-mdx" template.

$ pnpm create solid
.../share/pnpm/store/v3/tmp/dlx-29323    |   +1 +
.../share/pnpm/store/v3/tmp/dlx-29323    | Progress: resolved 1, reused 0, downloaded 1, added 1, done
┌
 Create-Solid v0.5.12
│
◇  Project Name
│  portfolio
│
◇  Is this a Solid-Start project?
│  Yes
│
◇  Which template would you like to use?
│  with-mdx
│
◇  Use Typescript?
│  Yes
│
◇  Project successfully created! 🎉
│
◇  To get started, run: ─╮
│                        │
│  cd portfolio          │
│  pnpm install          │
│  pnpm dev              │
│                        │
├────────────────────────╯
$ pnpm create solid

Writing content

Using MDX is definitely a huge benefit. For those who don't know, MDX is basically Markdown but with JSX, allowing for placing custom components without relying on custom syntax. In addition, I can achieve much cleaner, readable source code for each page.

I tried using JSX, but since the content has to be wrapped in a fragment, which also should be returned by a function, plus an element's contents has to be wrapped with the element's tags, I had to deal with at least 3 levels of indentation, which does not look great. In addition, the nature of pre-formatted code meant that I could not just inline code directly in a code block, so that has to be extracted to a separate variable, which breaks the reading flow.

The huge downside to using MDX is losing control over exactly how these elements are rendered. You could customize what component to render for each element, but extending the syntax to attach certain metadata, like a title for a code block, is a bit tricky in its vanilla configuration. That's where plugins come in.

Plugins allow syntax extensions which opens up a lot of things such as custom properties and elements. They could also transform existing parsed elements to, for example, add syntax highlighting to a code block according to its language. In my case, I had to rely on a lot of plugins, as you can see in the snippet of my app.config.ts file below. Both remark and rehype are used for MDX, so that's why you can see two separate lists for plugins.

app.config.ts
mdx.withImports({})({
define: {
"globalThis._importMeta_.env": `'globalThis._importMeta_.env'`,
},
jsx: true,
jsxImportSource: "solid-js",
providerImportSource: "solid-mdx",
rehypePlugins: [
// Adds heading text slug as id attribute
rehypeSlug,
// Extracts metadata from code blocks
rehypeCodeMetadata,
// Syntax highlighting for code blocks
[rehypeHighlight, highlightOptions],
[rehypeHighlightCodeLines, highlightLinesOptions],
],
remarkPlugins: [
// Parses markdown according to Github-flavored markdown
remarkGfm,
// Adds additional syntax for "hint" paragraphs
remarkHint,
// Unwraps images from paragraphs
remarkUnwrapImages,
],
}),

I also wrote a rehype plugin myself, rehypeCodeMetadata, which just stores the code's raw text and title in attributes, which can be referenced by my custom components.

Speaking of which, defining custom components for MDX is easy enough. The MDXProvider component accepts a components object which is basically a mapping of HTML element names to custom components. All props of the original element would be passed to its corresponding component.

mdx-components/index.ts
import CodeFigure from "./code-figure/CodeFigure";
import { H1, H2, H3, H4, H5, H6 } from "./heading/Heading";
import ImageFigure from "./image-figure/ImageFigure";
const MDXComponents = {
h1: H1,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
h6: H6,
pre: CodeFigure,
img: ImageFigure,
};
export default MDXComponents;

Metadata

Another thing I want to implement is page metadata. Basically, each page should have information about itself, like the name of the page, the summary and publish date of an article, which could later be aggregated for a number of uses such as a list of articles under a certain path. Solid Start does come equipped with file-based routing, where the directory structure (under the routes directory) dictates the paths of the pages, but it doesn't do any other fancy stuff, so I had to implement the metadata system from scratch.

Defining data

After I gave it some thought, I came up with a tree structure of some sort. The Basic node is just a placeholder for any page that doesn't have any templating, or children for that matter. The Index node is a node that has possible subpaths, and lastly the Article node holds metadata about the corresponding article. I can't seem to come up with a better name for Basic, believe me, I tried.

routes/data.ts
export interface BasicMetadataNode {
type: "BASIC";
name: string;
}
export interface IndexMetadataNode {
type: "INDEX";
name: string;
children: Record<string, MetadataNode>;
}
export interface ArticleMetadataNode {
type: "ARTICLE";
name: string;
publishedAt: Date;
lastUpdatedAt?: Date;
summary: string;
tags?: TagId[];
}
export type MetadataNode =
| BasicMetadataNode
| IndexMetadataNode
| ArticleMetadataNode;
export const metadataTree: IndexMetadataNode = {
type: "INDEX",
name: "Home",
children: {
articles: articlesMetadata,
gallery: galleryMetadata,
about: {
type: "BASIC",
name: "About",
},
},
};

Each node would live next to its page as a data.ts file. These would then be referenced by their parent nodes, over and over until you end up with a single root node, referred to as the metadata tree.

routes/data.ts
export const metadataTree: IndexMetadataNode = {
type: "INDEX",
name: "Home",
children: {
articles: articlesMetadata,
gallery: galleryMetadata,
about: {
type: "BASIC",
name: "About",
},
},
};

Getting data

The metadata tree definitely won't live client-side, it would only be referenced by server functions to process and provide the necessary data. If this whole tree is client-side, then that means loading any page loads metadata for every single page on the site, so that would unnecessarily add to load times. Speaking of which, we also need to make sure that we don't return the whole node, rather the metadata itself, so there should be separate data structures for the "client-facing" side.

server/metadata/index.ts
export interface Link {
name: string;
path: string;
}
export interface BasicPage {
type: "BASIC";
name: string;
}
export interface Index {
type: "INDEX";
name: string;
breadcrumbs: Link[];
}
export interface Article {
type: "ARTICLE";
name: string;
path: string;
breadcrumbs: Link[];
publishedAt: Date;
lastUpdatedAt?: Date;
summary: string;
tags?: Link[];
}

I placed all server functions under the server directory, although the data itself resides under the routes directory so that we can avoid two separate directory structures and keeping them both in sync. I won't go into detail into the implementation, though you can imagine the usual tree traversal using path fragments of a given pathname. Using these endpoints in a Solid component is as simple as calling the function in a createResource call.

Header.tsx
export default function Header() {
const [isNavOpen, setIsNavOpen] = createSignal(false);
const [menuEntries] = createResource(() => getMenuEntries());
return (
<>
<header class={classes.header}>
{/* ... */}
<Show when={menuEntries()}>
{(entries) => (
<Nav isOpen={isNavOpen()} entries={entries()} />
)}
</Show>
</header>
</>
);
}

Styling

I didn't want to rely on any styling frameworks like Bootstrap or Tailwind. I can see the appeal for rapid prototyping, but I don't want to pollute the markup with a ton of cryptic class names, most of which are just a one-to-one mapping to a CSS rule. So for now, the most I can do is rely on SCSS, specifically SCSS modules. I also used Font Awesome for icons since I don't want to think too much about providing my own icon set.

Ultimately, most of the time is spent on figuring out how I want my site to look like. I actually had a mock-up design in Penpot (something like Figma but it's free and open-source), but then I realized that design iterations using the web browser are much faster. Plus, this means I can figure out how to implement responsive design, which requires a lot of trial and error.

Next steps

At the time of writing, my site is quite functional. I can easily add a page as an .mdx file, then add the corresponding metadata next to it, then link it in the metadata tree. The page should then show up as an article entry. But the site itself still needs a lot of work, including:

  • page for showing articles with a specific "tag".
  • "Gallery" and "Projects" sections.
  • site logo and favicon.
  • image processing for handling multiple image resolutions using srcset.
  • caching server functions.
  • contact links and many more.

Until the next one!

EDIT: I had to update this log since there were some errors or missing content.

© 2024 Ahmad Zhafir Bin Yahaya