Building Blog with Astro

Building Blog with Astro

Takahiro Iwasa
Takahiro Iwasa
10 min read
Astro Tailwind CSS

I recently rebuilt my blog using Astro, which has been trending a bit lately. Let me introduce an overview of Astro.

Astro Overview

It is a content-focused Static Site Generator (SSG). The official website introduces it as follows:

Astro is an all-in-one web framework for building fast, content-focused websites.

Tools like Hugo and Jekyll are well-known as similar options, but I found the following to be the key features of Astro, especially the second point, Zero JS, by default, which may become more widely adopted in other frameworks in the future. For more details, please refer to an official page.

  • Astro Islands: Divides areas on a page into Islands, allowing use of HTML, React, Vue, etc., for each.
  • Zero JS, by default: Converts as much as possible to HTML during build, rendering fast by not generating JavaScript.

Play with Astro

I will introduce setup using an official blog template. An official tutorial is also available, so please refer to it for more details.

Setting Up

Please run npm create astro@latest. After following prompts, setting up will be completed.

You can start a development server with npm run dev or astro dev. By default, it listens for requests at http://localhost:4321/.

npm create astro@latest

 astro   Launch sequence initiated.

   dir   Where should we create your new project?
         ./

  tmpl   How would you like to start your new project?
         Use blog template
 ██████  Template copying...

  deps   Install dependencies?
         Yes
 ██████  Installing dependencies with npm...

    ts   Do you plan to write TypeScript?
         Yes

   use   How strict should TypeScript be?
         Strict
 ██████  TypeScript customizing...

   git   Initialize a new git repository?
         Yes
 ██████  Git initializing...

  next   Liftoff confirmed. Explore your project!
         Run npm run dev to start the dev server. CTRL+C to stop.
         Add frameworks like react or tailwind using astro add.

         Stuck? Join us at https://astro.build/chat

╭─────╮  Houston:
│ ◠ ◡ ◠  Good luck out there, astronaut! 🚀
╰─────╯

Directory Structure

Here is directory structure immediately after installation. If you have experience with frontend development, you may understand roles of each directory by looking at the structure. For more details, please refer to an official page.

tree --dirsfirst -I dist -I node_modules

.
├── public
│   ├── fonts
│   │   ├── atkinson-bold.woff
│   │   └── atkinson-regular.woff
│   ├── blog-placeholder-1.jpg
│   ├── blog-placeholder-2.jpg
│   ├── blog-placeholder-3.jpg
│   ├── blog-placeholder-4.jpg
│   ├── blog-placeholder-5.jpg
│   ├── blog-placeholder-about.jpg
│   └── favicon.svg
├── src
│   ├── components
│   │   ├── BaseHead.astro
│   │   ├── Footer.astro
│   │   ├── FormattedDate.astro
│   │   ├── Header.astro
│   │   └── HeaderLink.astro
│   ├── content
│   │   ├── blog
│   │   │   ├── first-post.md
│   │   │   ├── markdown-style-guide.md
│   │   │   ├── second-post.md
│   │   │   ├── third-post.md
│   │   │   └── using-mdx.mdx
│   │   └── config.ts
│   ├── layouts
│   │   └── BlogPost.astro
│   ├── pages
│   │   ├── blog
│   │   │   ├── [...slug].astro
│   │   │   └── index.astro
│   │   ├── about.astro
│   │   ├── index.astro
│   │   └── rss.xml.js
│   ├── styles
│   │   └── global.css
│   ├── consts.ts
│   └── env.d.ts
├── README.md
├── astro.config.mjs
├── package-lock.json
├── package.json
└── tsconfig.json

Content Collections

Overview

Contents such as blog posts are managed as collections. For details, please refer to an official page.

Create any directory under src/content/ and place Markdown files inside that directory. MDX is also available. Below is a part of blog/first-post.md.

---
title: 'First post'
description: 'Lorem ipsum dolor sit amet'
pubDate: 'Jul 08 2022'
heroImage: '/blog-placeholder-3.jpg'
---

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
...

Collection Definition

Users can define metadata (frontmatter) and types for contents within src/content/config.ts using Zod. Although it is possible to use without defining collections, it is strongly recommended to define collections as mentioned in an official page to avoid spoiling benefits.

The src/content/config.ts file is optional. However, choosing not to define your collections will disable some of their best features like frontmatter schema validation or automatic TypeScript typings.

The following is config.ts just after the setup.

import { defineCollection, z } from 'astro:content';

const blog = defineCollection({
  // Type-check frontmatter using a schema
  schema: z.object({
    title: z.string(),
    description: z.string(),
    // Transform string to Date object
    pubDate: z.coerce.date(),
    updatedDate: z.coerce.date().optional(),
    heroImage: z.string().optional(),
  }),
});

export const collections = { blog };
KeyTypeMandatory
titlestringY
descriptionstringY
pubDateDateY
updatedDateDateN
heroImagestringN

Using Collections

You can access collections using functions like getCollection and getEntry.

Frontmatter is stored within data object. For example, you can access it with code like the following:

---
import { getEntry } from 'astro:content';

const blogPost = await getEntry('blog', 'welcome');
---

<h1>{blogPost.data.title}</h1>
<p>{blogPost.data.description}</p>

Pages

Routing

It is file-based. Files placed under pages become routings as they are. In an example just after the setup, you can access them as follows:

FileURL
pages/index.astrohttps://<YOUR_DOMAIN>/
pages/about.astrohttps://<YOUR_DOMAIN>/about/
pages/blog/index.astrohttps://<YOUR_DOMAIN>/blog/

Dynamic routing is also possible. Use [...NAME] as a directory name or file name. In an example just after the setup, blog/[...slug].astro is using dynamic routing.

The [...slug] part is defined as a return value of getStaticPaths function within the file. In blog/[...slug].astro, it sets slug: post.slug on line 8.

---
import { type CollectionEntry, getCollection } from 'astro:content';
import BlogPost from '../../layouts/BlogPost.astro';

export async function getStaticPaths() {
  const posts = await getCollection('blog');
  return posts.map((post) => ({
    params: { slug: post.slug },
    props: post,
  }));
}
type Props = CollectionEntry<'blog'>;

const post = Astro.props;
const { Content } = await post.render();
---

<BlogPost {...post.data}>
  <Content />
</BlogPost>

Page Structure

Pages, layouts, and components have a structure similar to typical frontend setups. Of course, you can also place components directly within pages.

Astro Components

If you have experience with React JSX/TSX, you should be able to handle it seamlessly. Here is a part of pages/index.astro. For more details, please refer to an official page.

---
import BaseHead from "../components/BaseHead.astro";
import Header from "../components/Header.astro";
import Footer from "../components/Footer.astro";
import { SITE_TITLE, SITE_DESCRIPTION } from "../consts";

// This will cause an error.
// const main = document.querySelector('main');
---

<!doctype html>
<html lang="en">
  <head>
    <BaseHead title={SITE_TITLE} description={SITE_DESCRIPTION} />
  </head>
  <body>
    <Header title={SITE_TITLE} />
    <main>
      <h1>🧑‍🚀 Hello, Astronaut!</h1>
      <!-- ... -->
    </main>
    <Footer />

    <script>
      // Use document, windoww, etc. here.
      const main = document.querySelector('main');
    </script>
  </body>
</html>

An important point to note is that the content within --- is processed by Node.js. This is related to mentioned above “Converts as much as possible to HTML during build, rendering fast by not generating JavaScript.” Therefore, using objects like document, window, etc., will result in errors. If you want to use them, place them within <script> tags.

 error   document is not defined
  Hint:
    Browser APIs are not available on the server.

    Move your code to a <script> tag outside of the frontmatter, so the code runs on the client.

    See https://docs.astro.build/en/guides/troubleshooting/#document-or-window-is-not-defined for more information.

Rendering

Content component is available.

You can get Content from CollectionEntry. Here is a part of pages/blog/[...slug].astro. It gets rendered content at line 15 and places Content at an arbitrary location at line 19.

---
import { type CollectionEntry, getCollection } from 'astro:content';
import BlogPost from '../../layouts/BlogPost.astro';

export async function getStaticPaths() {
	const posts = await getCollection('blog');
	return posts.map((post) => ({
		params: { slug: post.slug },
		props: post,
	}));
}
type Props = CollectionEntry<'blog'>;

const post = Astro.props;
const { Content } = await post.render();
---

<BlogPost {...post.data}>
	<Content />
</BlogPost>

If you want to directly import Markdown files, you can code like below.

---
import {Content as PromoBanner} from '../components/promoBanner.md';
---

<h2>Today's promo</h2>
<PromoBanner />

Building

Running npm run build or astro build will generate build artifacts in the dist/ directory. The following is an example of artifacts generated after building right after the setup. As you can see, no JavaScript is generated.

tree dist/ --dirsfirst

dist/
├── about
│   └── index.html
├── blog
│   ├── first-post
│   │   └── index.html
│   ├── markdown-style-guide
│   │   └── index.html
│   ├── second-post
│   │   └── index.html
│   ├── third-post
│   │   └── index.html
│   ├── using-mdx
│   │   └── index.html
│   └── index.html
├── fonts
│   ├── atkinson-bold.woff
│   └── atkinson-regular.woff
├── blog-placeholder-1.jpg
├── blog-placeholder-2.jpg
├── blog-placeholder-3.jpg
├── blog-placeholder-4.jpg
├── blog-placeholder-5.jpg
├── blog-placeholder-about.jpg
├── favicon.svg
├── index.html
├── rss.xml
├── sitemap-0.xml
└── sitemap-index.xml

Other Topics

Image Component

From Astro, Image component is provided. When you use the Image component with images placed under src/, it optimizes them (converts to webp) during the build. Images placed under public/ are not optimized, so it is recommended to place them under src/.

We recommend that local images are kept in src/ when possible so that Astro can transform, optimize and bundle them.

Tailwind CSS

Astro supports Tailwind CSS. By running npx astro add tailwind, install the integration.

npx astro add tailwind

✔ Resolving packages...

  Astro will run the following command:
  If you skip this step, you can always run it yourself later

 ╭────────────────────────────────────────────────────╮
 │ npm install @astrojs/tailwind tailwindcss@^3.0.24  │
 ╰────────────────────────────────────────────────────╯

✔ Continue? … yes
✔ Installing dependencies...

  Astro will generate a minimal ./tailwind.config.mjs file.

✔ Continue? … yes

  Astro will make the following changes to your config file:

 ╭ astro.config.mjs ───────────────────────────────╮
 │ import { defineConfig } from 'astro/config';    │
 │ import mdx from '@astrojs/mdx';                 │
 │ import sitemap from '@astrojs/sitemap';         │
 │                                                 │
 │ import tailwind from "@astrojs/tailwind";       │
 │                                                 │
 │ // https://astro.build/config                   │
 │ export default defineConfig({                   │
 │   site: 'https://example.com',                  │
 │   integrations: [mdx(), sitemap(), tailwind()]  │
 │ });                                             │
 ╰─────────────────────────────────────────────────╯

✔ Continue? … yes

   success  Added the following integration to your project:
  - @astrojs/tailwind

astro add tailwind executes the following:

  1. npm install @astrojs/tailwind [email protected]
  2. Generating tailwind.config.mjs (Tailwind CSS configuration)
  3. Adding tailwind() to integrations in astro.config.mjs

Autoprefixer will be installed together.

Then, you can write classes provided by Tailwind at class like the following.

<body>
  <Header />
  <main class="ml-2">
    ...
  </main>
  <Footer />
</body>

Integrations

You can extend functionality through a mechanism called Integration. The above-mentioned Tailwind CSS support is one such Integration. For details, please refer to an official page.

IDE Support

IDEProvided ByStability
VS CodeAstroStable
JetBrainsJetBrainsAs of Nov. 2023, not stable

Conclusion

Astro Islands and Zero JS, by default would become big trend for coming SSGs.

I hope you will find this post useful.

Takahiro Iwasa

Takahiro Iwasa

Software Developer at KAKEHASHI Inc.
Involved in the requirements definition, design, and development of cloud-native applications using AWS. Now, building a new prescription data collection platform at KAKEHASHI Inc. Japan AWS Top Engineers 2020-2023.