Intro

One phrase
— In this modern video era, play it in hardcore mode without the video platform.

It took me 4 months or so to complete the journey through building my personal website up to a minimum decent level. Here, you'll join a quick tour of the whole development experience.

If you're interested in the code behind my website, see my Github repo. Code is worth a thousand words.

Reader personas

  • Want to build a website with Astro but have no idea how the whole project structure could be or what kind of features to be added
  • Want to see some examples of the whole project built with Astro

My proficiency level

As of the day when I'm writing this article, my proficiency level in this field is as follows:

  • Have a three-year experience as a software engineer in total (Not in a row)
  • Understand the basics of JavaScript, TypeScript, and Astro
  • Have built several websites using Next.js or Astro

Main

Astro

Basically, I managed to develop it within the scope of what Astro offers. Other than that, I used a JS framework, SolidJS. It goes without saying that all written in TypeScript (and TSX).

Languages: TypeScript, TSX, YAML
Data format: JSON
Meta framework: Astro
Framework: SolidJS
Styling: Vanilla CSS, Astro's Scoped CSS, Vanilla Extract
DB: Cloudflare D1(Sqlite)
ORM: Drizzle ORM
Dev: Dev Containers, Docker
CI/CD: Github Actions, DangerJS
Commit management: Git-cz
Deployment: Cloudflare Pages
CMS: Front Matter CMS
Bot management: Cloudflare Turnstile
Package manager: Bun
Dependency management: Renovate
Linter & formatter: Biome
Proofreading: Textlint
Git hooks manager: Lefthook
Email sender: Resend
Demo: How the search functionality works
Demo: How the search functionality works

I used Pagefind, a Rust based static search library. It's lightweight, lightning fast, and easy to use.
It's also self-contained on the browser without needing to interact with server. The mechanism is simple; Pagefind creates index files in advance referring to static files that are generated at build time.

Along the way, I got hung up on some key points:

  • Callable via a shortcut key
    • Press Ctrl + K if you're on PC
  • Custom keyword highlighting
  • Accessible from everywhere
    • The search button is placed in the navigation bar
  • Automatic input cursor focus
    • Once you open the search modal, the cursor is automatically focused on the window

Internationalization (i18n)

It's all directory-based manual translation built on top of the Astro Content Collections API.

I was able to achieve it thanks to the official doc. First, I followed the steps and created locale directories (en and ja, in my case.) You need to create as many files as the number of languages for the same content.

The Content Collections API covers YAML and JSON formats as well as those of Markdown and MDX. I chose YAML for storing translation data because of its brevity.

content directory tree
content directory tree

OG Image (Open Graph)

When it comes to individual entries of blog and news, their OG Images are dynamically generated via the Astro API endpoints.
It's dynamic in the sense that every path of OG Images is generated dynamically referring to the Astro dynamic routes, but static in the sense that they are generated at build time.

Along the way, the satori library generates an svg image based on the HTML & CSS input, and then the Sharp library optimizes and formats it to a PNG image.

The path example
https://younagi.dev/api/og/blog/astro-website.png
 OG Image example of this page
OG Image example of this page

Remark/Rehype Plugins

In my website, all articles are written in the Markdown/MDX format, but the basic syntax is limited to an absolute minimum.

That's why I decided to use those off-the-shelf Remark and Rehype plugins or make them on my own to extend it to the extent I feel comfortable writing stuff. The feel of writing means everything to bloggers.

They belong to the ecosystem called Unified.
Remark: Process and handle Markdown in the form of AST(mdast)
Rehype: Process and handle HTML in the form of AST(hast)
AST is short for Abstract Syntax Tree. For more details, consult other sources.
KaTex

KaTeX\KaTeX beautifully styles mathematical expressions on a web page. To render the magic spells into mathematical expressions, I added the remark-math and rehype-katex plugins.

Input

$$
x = {-b \pm \sqrt{b^2-4ac} \over 2a}
$$
$$
( \sum_{k=1}^{n} a_k b_k )^2 \leq ( \sum_{k=1}^{n} {a_k}^2 ) ( \sum_{k=1}^{n} {b_k}^2 )
$$
$$
\int_{0}^{1} f(x) \ dx
= \lim_{n \to \infty} \dfrac{1}{n} \sum_{k=0}^{n-1} f \left (\dfrac{k}{n} \right)
$$

Output

x=b±b24ac2ax = {-b \pm \sqrt{b^2-4ac} \over 2a} (k=1nakbk)2(k=1nak2)(k=1nbk2)( \sum_{k=1}^{n} a_k b_k )^2 \leq ( \sum_{k=1}^{n} {a_k}^2 ) ( \sum_{k=1}^{n} {b_k}^2 ) 01f(x) dx=limn1nk=0n1f(kn)\int_{0}^{1} f(x) \ dx = \lim_{n \to \infty} \dfrac{1}{n} \sum_{k=0}^{n-1} f \left (\dfrac{k}{n} \right)
Code blocks

As a developer, sharing some code examples is inevitable destiny. The rehype-pretty-code did all the heavy lifting in adding code syntax highlighting for me. Incidentally, the syntax highlighter is Shiki.

example.astro
---
type Props = {
  title: string
}

const { title } = Astro.props
---

<div>{`This is an example page titled ${title}.`}</div>
Callouts

I've been using Obsidian for years as my knowledge base and love the callout component. I wanted to have it here too so embarked on developing it on my own.

Along the way, I got hung up on some key points:

  • Expandable/foldable
    • If there are either "+" or "-" mark beside the callout title, it's regarded as expandable/foldable
    • "+" is "Expanded by default" and "-" is "Folded by default"
    • Nested callouts are also expanded or folded when the outer callout toggles
  • Various types and colors

Below are some examples.

Info callout
> [!info]+Info
> Info callout example. (Expanded by default)
Info callout example. (Expanded by default)

Caution callout
> [!warning]-Warning
> Warning callout example. (Folded by default)
Warning callout example. (Folded by default)

Check callout
> [!check] Check
> Check callout example. (No expansion/fold)
Check
Check callout example. (No expansion/fold)

Nested callout
> [!note]+
> Callout
>
> > [!info]+
> > Nested callout
> >
> > > [!warning]+
> > > further nested callout

> [!question]+
> Question!
>
> > [!failure]+
> > Failure!
> >
> > > [!check]+
> > > Check!
> > >
> > > > [!quote]+
> > > > Quote!
Callout
Nested callout
further nested callout
Question!
Failure!
Check!
Quote!
OEmbed

URLs pasted directly morphs into embedded frames if they are compatible with embedding media.

I created a Remark plugin and added the integrations with Canva, YouTube, and Google Slides on my own. Other than them, URLs are transformed in accordance with the OEmbed format.
Incidentally, the unfurl library fetches the media metadata necessary for the format behind the scenes.

Canva
https://www.canva.com/design/DAGKC41Tjws/zSEw1hvi9r30o5KiF96AGA/view
 Canva embed image
Canva embed image
YouTube
https://www.youtube.com/watch?v=dsTXcSeAZq8
 YouTube embed image
YouTube embed image
Spotify
https://open.spotify.com/intl-ja/track/04z1fwsw1gXI8HWQpoETa9?si=862c02d3e52a49b0

Google Slides
https://docs.google.com/presentation/d/1CbeSiVYta0VTuENQ-25OLcIV5vK8pkcBJ-8DfKqlE2I/edit?usp=sharing
 Google Slides embed image
Google Slides embed image
Embeds often affect page loading and SEO badly due to their original sources' formats: a large amount of JS, unoptimized image assets, etc..
In reality, The PageSpeed Insights score of this page was miserable.
That's why I replaced them with their screenshots. I'm not going to use it that frequently though I put in a considerable amount of time creating it...

When it comes to other bare links than OEmbed compatible ones, they are transformed into a link card as a fallback. I made up a Remark plugin for this coupled with that for the OEmbed.

the unfurl library does great work under the hood here again. It fetches the OG image and other site metadata via the URL.

https://younagi.dev

Contact Form

Demo: How the contact form works
Demo: How the contact form works

What I spent a considerable amount of time racking my brain was the contact form. Having gone through several major changes, it's ended up with SolidJS and TSX.

That said, Turning the whole contact form into a client-side loaded component with the Astro client directive causes a serious CLS(Cumulative Layout Shifts) problem1 that could affect SEO badly. With that in mind, I made a detour and ended up putting it in a modal component.2

To make it simple and concise, I utilized the Modular Forms for form controls, Kobalte for UI, and Valibot for validation and other useful libraries.
Here are some points:

  • The client side validation
    • The Modular Forms x Valibot x Cloudflare Turnstile
  • The server side validation
    • The Valibot x Cloudflare Turnstile
  • The submit button label changes when submitting
    • For users to get to know easily what's going on
    • This feature can easily be topped thanks to the form control of the Modular Forms
  • After the form successfully submitted, it redirects to a thanks page

For bot management, I got hung up on Cloudflare Turnstile compared to notorious Google reCAPTCHA. (I never forget the wasted time solving the car puzzles...) It's free and fully compatible with other Cloudflare services, so I had no other choice.

Cloudflare

My website heavily relies on the Cloudflare's ecosystem.

Cloudflare Pages

When I was into Next.js, my go-to hosting service was Vercel, but it felt like I needed to revisit it along with the transition to Astro. I came to a conclusion: Cloudflare Pages.

Why Cloudflare Pages?

I decided to use Cloudflare Pages to host my website for the following reasons:

  • The generous free plan
    • The unlimited maximum bandwidth per month is awesome (As of May 9, 2024)
  • Lightning fast deployment
  • Custom email addresses for your domain available
    • I'm using it for the contact form
    • The messages are sent to my private email address via the custom one so it doesn't have to be open to the public (Not to mention you can set the reply-from to the custom one)
  • Own domains available (paid)
    • https://younagi.dev

I suppose the free plan suffices for a small project like my website. That's the most appealing factor to me.

Cloudflare D1

Regarding the backend, I'm using Cloudflare D1 as database. It also offers a generous free plan, though I'm not going to dig deeper into it here.

Likes Button
Demo: How the likes button works
Demo: How the likes button works

With the combination of D1 database and the Drizzle ORM(Object-relational Mapping), I created the Likes button as you can see at the bottom of this article.

Long story short, the mechanism is as follows:

  • The total likes count is displayed on the button
    • In the event of the page loaded, it forwards a GET request to the likes API endpoint to get the total likes count and that of the current user based on the session ID from cookies
    • The session ids are stored in the database coupled with the page info
  • When clicked, it counts up one when the user hasn't done it and vice versa
    • It forwards a POST request to the likes API endpoint in which the request data is inserted into the database table

Front Matter CMS

CMS(Content Management System) selection is also time-consuming. I fell in love with Front Matter CMS for the following reasons:

  • Writing and storing articles in local
  • Markdown/MDX file format

It's unlike other headless CMS in that it's a VS Code extension and works in local, in your code editor! This centralizes all the editing work: code tweaks, blog writing, and deployment. I believe it'd especially be a huge advantage to developers.

When it comes to the setup, consult the Front Matter official doc. During the procedure, it automatically detects Astro's content folder and creates each content's fields' definitions in the frontmatter.json accordingly.

Customization of Front Matter CMS

Demo: Create a new article
Demo: Create a new article

In the frontmatter.json, there are some notable customizations:

  • Compatible with directory-based internationalization(i18n)
    • You can specify locale directories (in my case, en and ja as of now) inside the content directory
    • By doing so, you can select in which language you're creating a post via the VSCode command and it does create the file under the designated directory for you
  • Compatible with the Astro's Content Collections (with both the content and data types)
    • You can use the Data functionality for Astro's data collections, not to mention the Contents for Astro's content collections
    • Contents like blog and news are treated as "contents" while data like translations, site metadata, and blog categories & tags as "data"

Outro

Over the course of my development journey, I consulted a bunch of information sources. I wouldn't be able to complete it if it hadn't been for them. Here are some of them that I visited very often. Thank you so much for your helpful information and code!

Overall:

Full-text search

Contact form:

DB setup:

Front Matter CMS setup:

Styling:


  1. When you use other JS frameworks that contain JavaScript (in my case, SolidJS) inside an Astro component or page, you need to explicitly tell the Astro compiler what kind of framework. This is because they are supposed to be loaded on the client side by the Astro's specifications once they contain JS; therefore the compiler has no clue who they are at build time or on the server side. Since the contact form is all about user interactions and requires JavaScript as long as you seek a lot more features, it must be a client-side loaded component.

  2. Files with an extension .astro are rendered on the server side. If you put a client-side rendered component on them, it looks like it's suddenly pouring down on the page right after being loaded. For those small components, this is not that big deal, but for a large one like the contact form causing a tremendous layout shift.


Caffeine helps me create more

◀ Back to Top Scroll to Top ▲