Build a Personal Website with Astro, Cloudflare Pages, D1, and Front Matter CMS

Table of Content

Intro

While I thought to myself "I gotta learn Rust real quick...", I built my personal website using Astro against my will on the pretext of investigation into frontend and ended up spending about 4 months. I'd be lying if I said I never regret.

You can see the source code published below. 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 using Astro
  • Have no idea what sort of features to add to a personal website

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(For me)
  • Have built several websites using Next.js or Astro

Main

Astro

What's Astro in the first place? In a nutshell, it's "a JavaScript(Hereafter JS) based metaframework" that makes it easier to built a lightning fast website.

It's called a "metaframework" in that you can leverage other JS frameworks as you like in your project. (e.g. React, Vue, SolidJS, etc.)

For more details, see the official homepage.

It's geared toward service like corpolate websites or blog where user interactions are unilateral rather than bilateral (e.g. EC, SNS)

In my case, I mainly use what Astro offers and partly rely on SolidJS. Also, almost all code is written in TypeScript and TSX. I couldn't have helped shaking like a leaf without them.

Languages: TypeScript, TSX, YAML
Data format: JSON
Meta framework: Astro
Framework: SolidJS
Styling: Tailwind CSS, Astro's Scoped CSS
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

Search functionality plays an integral part of users' comfortable browsing lives.

A lot of personal websites are going all out to add the ancient access counter while dragging their feet on a search window pining for the good old days, which I think is very cool. But it's a different story here. 1

Developing a decent search functionality by myself is laborious, so I use Pagefind, a Rust based static search library. It's lightweight, lightning fast, and easy to use.

It's also self-contained on the client side without needing to interact with server. The mechanism is simple; Pagefind creates index files beforehand based on those generated-at-build-time static files and responds to searching by keyword(s) referring to them.

Along the way, I stick to these key features:

  • 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)

Astro thankfully takes responsibility for content management: the Content Collections API. For more details, see the official doc.

Applying that feature, I add multiligual support by separating language directories... which means, I have to go through a rough patch of manual translation.
"I'm in luck I can learn languages at the same time! It kills two birds with one stone!" I'm talking to myself to stay sane.

Speaking of the content formats, it covers YAML and JSON formats as well as Markdown/MDX. I use YAML for storing translation data because of its brevity.

content directory tree
content directory tree

OG Image (Open Graph)

Just in case, I add OG images to every page dreaming of some heavy readers frequently visiting this website and singing its praises on SNS.

For the homepage and other fixed pages, static images are applied but when it comes to individual blog and news pages, their OG Images are dynamically generated via the Astro dynamic routes.
Thanks to the feature, I don't have to rack my brain around them every time a new article is posted. Thank you, Astro.

Incidentally, the process of image generation is done like a bucket brigade from satori to sharp.

  1. satori's job: generates a SVG image based on the HTML & CSS input
  2. sharp's job: returns a PNG image with optimization from the SVG image passed onto
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

I mentioned earlier on there being a plenty of options for content formats in Astro.
In my website, all articles are written in the Markdown/MDX format.

You can manage content with the standard syntax but it feels not enough somehow. Readers having good taste must be looking for some rich content like code blocks, mathematical expressions, media frames, and the likes...!

Driven by that belief, I extended the default syntax using the Remark and Rehype plugins. Most are off-the-shelf and some are developed by my own. 2

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

Despite that I'm neither well versed in maths nor going to do maths here, I gave in to the temptation... in preparation for the world line where I'm a master of maths.

KaTeX\KaTeX converts specific magic spells into beautifully styled mathematical expressions for us. All you need to do are add the remark-math and rehype-katex plugins and tweak the styles a little bit.

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)

At the end of the day, I just wanted to do this.

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. 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 was missing the Obsidian callout dialect and succumbed to the temptation again.

Along the way, I stick to 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 callout example. (Expanded by default)
Info callout example. (Expanded by default)

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

Check callout
> [!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

I confidently steered in the direction of personal blog in the modern era of video but my resolve is already wavering. Are we meant to dance in the palm of those major tech companies after all...?

I've sort of convinced myself that we cannot run away from YouTube and inevitably added a remark plugin for media frames.

In design, bare URLs will be transformed into media frames if they are compatible with the OEmbed format. Integrations with Canva, YouTube, and Google Slides are also supported.

"This is merely a safety net. I will never resort to such a feature as long as it can be expressed in the blog...!", I tell myself repeatedly.

Incidentally, unfurl works diligently behind the scenes fetching the media metadata necessary for the format.

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 can't stop feeling a pang of regret now that it's obvious this feature seems destined to be a white elephant despite that I spent a considerable amount of time. But at the same time, I sort of feel relieved that it's unexpectedly become harder to rely on other media.

Other bare links than OEmbed compatible ones become link cards. I create a Remark plugin for this coupled with the aforementioned OEmbed one.

Again, unfurl does great work here. It fetches the OG image and other site metadata via the URL.

https://younagi.dev

Contact Form

Demo: How the contact form works

Remark/Rehype plugin things are extremely tiring, but the contact form is just as vicious.
Having gone through a lot of twists and turns, it's ended up landing on the SolidJS and TSX ground.

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) problem. 3 It could affect SEO badly. So I made an emergency landing and squeezed it all in a modal component. 4

To make it simple and concise, I use 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 strongly stick to Cloudflare Turnstile compared to the notorious Google reCAPTCHA. 5 It's free and fully compatible with other Cloudflare services, so it's no brainer for me.

Cloudflare

Cloudflare Pages

Cloudflare is, so to speak, a jack of all trades for internet service. it offers CDN, cybersecurity tools, hosting, etc. This time, I deploy my website using Cloudflare Pages.

Why Cloudflare Pages?

The reasons why I use Cloudflare pages are:

  • 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

The free plan seems to suffice for a small project like my website.

Cloudflare D1

Regarding the backend, I use 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

Integrating the D1 database with the Drizzle ORM(Object-relational Mapping), I create the Likes button as you can see at the bottom of every blog entry.
While I am the one who broke off with major social media, I've unconsciously added the good button, which is an avator of them. Is this inevitable fate?

In 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

"I've built my blog. Now, how and where do I manage the content?" There are many options: CMS(Content Management System), in local, or...
My choice is Front Matter CMS. The reasons are:

  • 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. Woundn't it especially be a huge advantage to developers?

When it comes to the setup, consult the official doc.
Along the way, it automatically detects Astro's content folder and creates each content's fields' definitions in the frontmatter.json accordingly. Basically, you're supposed to tweak the settings in this file at the root of your project.

Customization of Front Matter CMS

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 "content" while data like translations, site metadata, and blog categories & tags as "data"

Outro

It's been a long way...

Long before this, I did a lot of turorials for various frameworks to settle into and was working hard once zeroing in on Next.js x Vercel.

But most of the challenges ended in failure. When I almost couldn't resist the temptation of saying "I accomplished... NOTHING!!", right on cue, Astro showed up on my doorstep.

Since then, I've come this far consulting rich info sources the internet ocean proudly offers. I would be a seaweed at sea now if it hadn't been for them. With gratitude, here are some I visited very often.

Overall:

Full-text search

Contact form:

DB setup:

Front Matter CMS setup:

Styling:


  1. They can be seen when wandering around the internet ocean, so I highly recommend you giving it a try like a tuna.

  2. I spent about a month creating and being focused on the plugins.

  3. 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 will be loaded on the client by 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 loaded on the client side.

  4. 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.

  5. I remember being plagued by the reCAPTCHA as a user as if it were just yesterday. There's no way I'd be the questioner of that puzzle quizzes.


Thanks👏

◀ Back to Top Scroll to Top ▲