An Astronaut chilling in space while looking at the earth in the background and a rocket in the foreground

How I built this blog

If you have considered creating a blog, this article is for you. I will explain how I made this blog and the technologies I used.

This article will be a 10 min read.

This blog post was last updated on: 7/26/2023.

I wanted to create a full-fledged blog with good SEO and great DX, there were a lot of contestants for the job, but I decided to go with Astro.

The stack

I did my fair bit of research before “marrying” myself to Astro, I considered Next.js, Sveltekit, and even Gatsby, but I decided to go with Astro, and here is why. I wanted to use animations and things unavailable to a regular CMS blog. With Astro, you have several options when it comes to page rendering; you can do it “on-demand” (SSR) or ahead of time (Static Site Generation). I opted to do both the static content rendered beforehand and all the interactivity and private pages (like Home ).

I also use Firebase as my “backend” for the blog, I use it to store all of the data with Firestore , and I use Firebase Storage to store all the images. I also use Firebase Hosting to host the blog. It also helps me to preview the “new” blog posts before I publish them to the world.

When it comes to styling I use a combination of TailwindCSS , DaisyUI , and custom CSS with the style Tag of svelte. These options allow me to create as many custom components as I want and use them in my blog posts to theme them, depending on your theme of choice.

For animations, I mainly rely on svelte transitions and framer motion Both of which are extremely powerful and easy to use, they only require a tiny part of experimentation on your side.

As a good part I use svelte transitions if the animation is more complex or I want something different I would use Framer motion

Why Astro?

I did my fair bit of investigation and research before fully committing to Astro. I tried NextJS, SvelteKit, and NuxtJS, basically all the big boys I was confortable with. While I work daily with React and love it, I wanted to experiment with Astro and the stores that offer great connection with firebase. If I want to return to my roots, I can always write a React component.

For example for svelte and the magic connection to the stores I use the svelte stores to share code between components and pages. I use the following code to create a store that connects to firestore and updates the data whenever it changes.

docStore.ts
function docStore<T = never>(
path: string,
converter: FirestoreDataConverter<T>,
defaultValue: T | null = null,
) {
let unsubscribe: () => void
const docRef = doc(db, path).withConverter(converter)
const { subscribe } = writable<T | null>(null, (set) => {
unsubscribe = onSnapshot(docRef, (snapshot) => {
if (snapshot.exists()) set((snapshot.data() as T) ?? null)
else set(defaultValue)
})
return () => unsubscribe()
})
return {
subscribe,
ref: docRef,
id: docRef.id,
}
}

With the code above we can pull data from firestore super simple, with just a const blogLike = docStore('blog/BLOGID',blogConverter) and render it in the UI with the magic $ syntax

<script>
const blogLike = docStore('blog/<BLOGID>', blogConverter)
</script>
<section>
<span>Views: {$blogLike?.views}</span>
<span> Likes: {$blogLike?.likes}</span>
</section>

Now let’s say we would like to have the data from your user data as well as the user from auth this is really simple with the use of derived stores from svelte. Now whenever we want to use the userData, it’s going to mutate each and every time we update the current user that’s logged in and It’s goig to stream the changes to whoever is listening to that specific store.

const userData = derived(user, ($user, set) => {
if ($user)
return docStore<UserData>(
`users/${$user.uid}`,
userDataConverter,
).subscribe(set)
return set(null)
})

Why not Next.js?

I love it in my daily work with React, but I wanted to try something new that I don’t have much experience with and will be nice working with. That’s the reason I went with Astro. I also want to expand this blog to be a learning platform where you can take notes on the blog post and keep growing as a software engineer.

How does the code words

It’s super simple thanks to the Astro Routing system and the Content collections added in version 2.0.0 basically I just need to write some base code in my .mdx file and voala, I can start writting the new blog post without any issues. as well as to create Astro pages and have all of the pre-rendered working in our favor. to get the best performance ever.

What is the vscode config that I use?

Astro has some unique vscode extensions that I use to make my life easier. I use the following config and extensions to make my experience more manageable.

.vscode/settings.json
{
"cSpell.words": ["Firestore", "Mdsvex"],
"workbench.editor.labelFormat": "short",
"explorer.sortOrder": "filesFirst",
"explorer.compactFolders": false,
"material-icon-theme.folders.color": "#43a047",
"workbench.tree.indent": 16,
"testing.automaticallyOpenPeekView": "never"
}
.vscode/extensions.json
{
"recommendations": [
"astro-build.astro-vscode",
"bradlc.vscode-tailwindcss",
"esbenp.prettier-vscode",
"formulahendry.auto-rename-tag",
"moalamri.inline-fold",
"pkief.material-icon-theme",
"svelte.svelte-vscode",
"toba.vsfire",
"unifiedjs.vscode-mdx"
]
}

What the heck is mdx?

MDX is an extension of Markdown that allows you to import and use custom JS components (These can be built with React,Svelte,etc.).

Even if you’ve never written Markdown, you have probably seen it floating around the web or your repos with a README.md. Here is what a Markdown file looks like.

Hello This is a paragraph.
This is another paragraph, with some **Bold text**
# This is a title
This is an unordered shopping list:
- Apples
- carrots
- bananas

Whenever we use Markdown in a web app, there is a “compile” step; this step converts the Markdown to HTML so that the browser can render it correctly. Those asterisks become strong tag, the list goes to a ul and the paragraph get a p tag.

We can create our own set of primitives; because we are using the X of MDX we can use any component we want. I’m not limited by all of the default markdown components; we can use anything. <InfoBlock> Like this info block </InfoBlock>

Like this info block

What is frontmatter?

Think of frontmatter as the metadata of your blog post. It’s the data that you want to be available to your blog post but not available as writing, this is something like the title, description, updatedOn and publishedDate of your blog post. This is created as a YAML block at the start of the blog and this helps me get the basic info going for SEO and building the url.

any-blog-post.mdx
---
title: Title
description: Short description
tags:
- Tutorial
- Astro
mainTag: Astro
publishedDate: 2023-07-26T00:00:00.000Z
image: ./images/images/astronaut.webp
imageAlt: Image Alt for accessibility purposes.
published: true
updatedOn: 2023-08-08T23:29:51.712Z
---
import Components from '@/components/Blog'
export const components = Component
# Title

How do I deploy this?

I have a full CI/CD for this blog, it’s for github actions and it’s super simple to get started with it. I only had to

Build helpers

I’m using a lot of build helpers to get the ball rolling as fast and straightforward as possible. I’m lazy, and I wanted to leave my “updatedOn” to be automatically updated whenever I update the blog post. and committed the changes. I use a pre-commit hook to check the styling with prettier and eslint. Whenever I edit a .mdx file (an article, blogPost) or similar, I will use gray-matter to update the date inside each.mdx file that is in the commit.

I stumbled across the post from Adam Collier , by editing the .mdx file at commit-time with lint-staged. I can confirm it works great.

A little bit of love.

If you want to start on a blog as simple as possible, use Astro or a ready-to-go Gatsby starter, but if you want to hack around your solution and learn a lot of things in the process, I recommend starting a blog with Sveltekit. It has some hassle to get started but once you get the ball rolling it’s super simple to work with.

What are git hooks?

Git hooks are scripts that run automatically whenever a particular event occurs in a Git repository. They let you run a command before or after an event such as commit, push, and receive. Git hooks are a built-in feature no need to download anything. Git hooks are run locally. They are not transferred to the server when you push or pull.

There is a super simple way to get started with the use of git hooks and this are the ones that I use:

  • post-update
  • pre-commit
  • pre-push
  • prepare-commit-msg

Git hook: pre-commit

It’s super simple, you just need to install two dependencies and that’s it. It will take care of getting your git hooks inside your git repo. and lint-staged it’s going to run any command in any file changed. For example, instead of linting all the files in your project, it will lint only the files you changed. thus making the pre-commit step faster.

Terminal window
npm i -D husky lint-staged
npm pkg set scripts.prepare="husky install"
npx husky add .husky/pre-commit "npx lint-staged"

After you install both dependencies, they will be added to your package.json file, and you can start working with the lint-staged part of it. I like using the sort-package-json package to normalize it, for the files that handle logic, run prettier and eslint to ensure the code is formatted correctly. to the md and mdx files I run a script that updates the front-matter of the file with the current date, last but not least have prettier format all the files that It supports.

package.json
{
"lint-staged": {
"package.json": ["npx sort-package-json --no"],
"*.{js,jsx,ts,tsx,astro,svelte}": ["eslint --fix", "prettier --write"],
"*.{md,mdx}": ["prettier --write", "node ./.husky/updateFrontmatter.cjs"],
"*": ["prettier --ignore-unknown --write"]
}
}
.husky/updateFrontmatter.cjs
const exec = require('child_process').execSync
const fs = require('fs').promises
const matter = require('gray-matter')
async function updateFrontmatter() {
const [, , ...mdFilePaths] = process.argv
mdFilePaths.forEach(async (path) => {
const file = matter.read(path)
const { data: currentFrontmatter } = file
if (currentFrontmatter.published === true) {
const published = exec(`git diff --cached ${path}`)
.toString()
.includes('+published: true')
const now = new Date()
const updatedFrontmatter = {
...currentFrontmatter,
...(published
? {
publishedDate: now,
published,
}
: {}),
updatedOn: now,
}
file.data = updatedFrontmatter
const updatedFileContent = matter.stringify(file)
fs.writeFile(path, updatedFileContent)
}
})
}
updateFrontmatter()

Git hook: pre-push

I like using all of my testing and typecheck the whole repo before pushing it to the remote.

Terminal window
npx husky add .husky/pre-commit "npm run test:ci && npx tsc --noEmit"

Git hook: prepare-commit-msg

For the prepare-commit-msg I like a lot the commit lint and here is the reason.

To install the commit lint you need to add one more dependency to your project.

Terminal window
npm i -D @commitlint/cli @commitlint/config-conventional
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit ${1}'

And in your package.json modify it a little bit to look like this

package.json
{
"commitlint": {
"extends": ["@commitlint/config-conventional"]
}
}

And voila, it’s going to lint your commit message and make sure that it’s following the conventional commits. Your commit messages are going to look better because it will be easier to get the intent, scope and description of the commit.

Git hook: post-merge

This is the last one that I personally use, whenever we are working in a large team, it can be a little frustrating to have the dependencies out of sync. and they are throwing some errors because of it. I have spent a long time just to the error be solved with a npm install

.husky/post-merge
. "$(dirname -- "$0")/_/husky.sh"
changed_files=$(git diff-tree -r --name-only --no-commit-id ORIG_HEAD HEAD)
# if the package-lock.json file was changed, then run npm install in bash
if grep -q 'package-lock.json' $changed_files; then
npm install
fi
# if the yarn.lock file was changed, then run yarn install in bash
if grep -q 'yarn.lock' $changed_files; then
yarn install
fi
# if the pnpm-lock.yaml file was changed, then run pnpm install in bash
if grep -q 'package-lock.json' $changed_files; then
pnpm install
fi

This blog post was last updated on: 8/24/2023.