This article explains how to generate blogs using the Astro static site generator (SSG). Astro accepts Astro files as input and produces HTML as output. To code an Astro file, you'll need a solid understanding of HTML and JavaScript/TypeScript.
For my upcoming blog, I decided that I didn't want to use a traditional CMS platform like Wordpress. I'm a programmer, and I'd rather use a tool that generates the blog from source code. These tools are called static site generators, or SSGs, and the big names are Hugo, Jekyll, Gatsby, and Astro. I chose Astro because of its support for TypeScript, and though it took some time to learn, I'm satisfied with its flexibility and performance.
The goal of this article is to explain how to build a full-featured blogging site with Astro. The first part walks through the process of creating an example blog project and then generating blog files from the project. Then we'll look at the format of Astro files and the files that make up an Astro project. The last parts of the article cover routing and customization.
1. Getting Started
Astro's mission is to convert a project containing Astro files (*.astro) into a folder containing web site files (mostly *.html). Rather than start from scratch, it's easier to create Astro's default blogging project and customize it as needed. This section explains how to create the default project, convert it into a blogging site, and then view the site in a browser.
1.1 Creating the Project
Access to Astro is provided through the Node package manager, or npm. Node refers to Node.js, a runtime environment that provides access to over two million software packages. The vast majority of these packages are free, so if you’re building a web application, you’ll probably find several packages that will help.
Before you can use npm to create an Astro project, you'll need to install Node.js. The download page is https://nodejs.org, and you can download the installer by clicking the link for your operating system and processor type (probably 64-bit). This discussion assumes you’re running version 22 of Node or higher.
After you’ve installed Node, you can run npm from a command line. Every command starts with npm cmd_name
where cmd_name
identifies the command. To create a new Astro project, execute the following:
npm create astro@latest
Before creating the new project, Astro will ask six questions:
- Where should we create this project? - Enter the desired location of the project folder, either as an absolute path (C:\newblog) or as a path relative to the current directory (newblog).
- How would you like to start your project? - You can choose between Empty, Use blog template, and Include sample files. This article focuses on blogs, so select Use blog template.
- Do you plan to write TypeScript? - You can choose to write code in TypeScript (Yes) or JavaScript (No). For this article, select Yes.
- How strict should TypeScript be? - You can choose between Relaxed, Strict, and Strictest. For this article, Strict will be fine.
- Install dependencies? Choose Yes to have Node install the required packages needed to create an Astro project in TypeScript.
- Initialize a new Git repository? Choose Yes to have Node create a git repository to enable version control for the project.
After these questions are answered, the package manager will download the Astro package and its dependencies, and then create a project for a simple blogging site. Table 1 lists the files and folders in the project's top-level directory.
Table 1: Files and Folders in an Astro Project File/Folder Name | Description |
.git | The Git repository folder |
.vscode | Settings for the Visual Studio Code IDE |
node_modules | Node dependencies required by Astro |
public | Static assets to deploy to the web site |
src | Source files of the Astro project |
.gitignore | Names of files not to be included in the Git repository |
astro.config.mjs | Astro configuration settings |
package.json | Project configuration settings |
package-lock.json | Lists dependencies and their dependencies |
README.md | Describes the project |
tsconfig.json | Contains settings for the TypeScript compiler |
You can leave most of these files alone, but the src folder deserves attention. This contains the source files of the Astro project, and a large part of this article is concerned with the content of this directory. But before we delve into the details, it's a good idea to create and view the blog.
1.2 Building and Viewing the Blog
After you've created an Astro project, it's easy to compile all the *.astro files into *.html files. Open a command prompt in the project's top-level directory and execute the following command:
npm run build
As the build proceeds, the utility will display the project files and the HTML files generated from them. The following text provides an example:
▶ src/pages/about.astro
└─ /about/index.html (+12ms)
▶ src/pages/blog/index.astro
└─ /blog/index.html (+13ms)
▶ src/pages/index.astro
└─ /index.html (+3ms)
Once the build completes, the generated files will be stored in a new top-level directory named dist. You can view the HTML files by opening them in a browser. You can also launch a local server by executing npm run dev
. Then visit http://localhost:4321 in a browser to see the blog. Figure 1 presents a condensed illustration of the blog's main page.
Each page in the default blog has a header and a footer. The content between them is determined by the template of one or more Astro files. The next section explains what templates are.
2. The Astro File Format
If you look through the project's src folder, you'll see that most of the files are Astro (*.astro) files. Before we explore the project, it's important to understand the content of these files and how the content is structured. Every Astro file has (at most) two sections:
- frontmatter - TypeScript that imports components, defines variables, and performs other operations
- template - Markup to be rendered into HTML during the build, can access components and variables from frontmatter
Frontmatter is optional, but if it's present, it must be enclosed within lines made up of three hyphens. These are called code fences, and the following text shows how code fences separate the frontmatter from the template:
---
frontmatter written in TypeScript
---
template written in markup
This discussion looks at both of these sections. If you have a solid grasp of TypeScript and HTML, you'll have no trouble working with Astro files.
2.1 Frontmatter
The frontmatter of an Astro file contains code that executes just before the template is rendered. The most common frontmatter operations involve importing data structures and declaring variables and types.
Structures can be imported with the familiar import
statement. These statements can access JavaScript files (*.js), TypeScript files (*.ts), and other Astro files (*.astro). For example, if a data structure named XYZ
is defined in ABC.astro, it can be imported with the following code:
import XYZ from 'ABC.astro';
Declaring variables and types is just as easy. Variables in a let
statement can be updated, variables in a const
statement can't be updated, and the type
statement declares a custom type. Here are some typical declarations:
const today = new Date();
type Props = CollectionEntry<'blog'>['data'];
The frontmatter of an Astro file isn't limited to imports and declarations, but these statements make up the majority of the code I've encountered.
2.2 Template
Every Astro file has a template that will be converted to HTML during the build process. These sections are written in a format that resembles JavaScript XML (JSX), which resembles HTML. There are two points to know about templates:
- A template can use regular HTML tags like
<div>
, <a>
, <head>
, and <body>
. It can also create HTML tags from imported components, and a later section will explain what components are. - The template can include TypeScript variables and expressions surrounded by curly braces. For example, if x is a variable declared in the frontmatter, its value can be inserted in the template with
{x}
.
To demonstrate these points, suppose the frontmatter of an Astro file imports a component named MyComp
and declares a variable named compName
. In the template, the following tag creates an instance of MyComp
and sets its name attribute to compName
:
<MyComp name={compName} />
When the project is built, MyComp
's template will be inserted and its name
attribute will be set to compName
.
Astro's template syntax resembles JSX in many respects, but there's one major difference: the template of an Astro file doesn't change after the build. That is, the code in the frontmatter will only be executed during the build, so the inserted variables can only take one value.
3. Exploring the Project
At this point, you should be comfortable with Astro and Astro files. Now we'll dive deeper and look through the project's src directory. If you open this directory, you'll find the folders and files listed in Table 2.
Table 2: Folders and Files in the Project's Source (src) Directory Folder/File Name | Description |
pages | Astro files (*.astro) that represent pages to be generated |
components | Astro files (*.astro) that define components |
content | Markdown files (*.md or *.mdx) that provide the blog's posts |
layouts | Astro files (*.astro) that define a re-usable structure for pages |
styles | CSS files (*.css) that define style settings for the web site |
consts.ts | Global constants that can be accessed throughout the project |
env.d.ts | Tells the TypeScript compiler where to find Astro's type declarations |
The goal of this section is to explore these folders and explain what their files accomplish. The better you understand the project structure, the easier it will be to customize the project for your blog.
3.1 The Pages Folder
If you look through the src/pages folder, you'll find index.astro, about.astro, and a folder named blog, which contains another index.astro and an odd but important file named [...slug].astro. During the build, each file in src/pages will be converted into one or more HTML files.
If you look through these files, you'll see that their frontmatter contains import
statements like the following:
import Header from '../components/Header.astro';
import Layout from '../layouts/BlogPost.astro';
Header
is imported from a file in the components folder, so it's safe to assume that it's a component. Layout
is imported from a file in the layouts folder, so it's a special type of component called a layout. A proper introduction to components and layouts will be provided shortly.
If you compare index.astro and about.astro, you'll see that their templates are quite different. index.astro has <head>
and <body>
tags like a regular HTML file, but about.astro doesn't contain any of those tags. This is because the page structure of about.astro is defined using a layout, and I'll explain what layouts are shortly.
3.2 The Components Folder
Like many web frameworks, Astro is a component-centric toolset. Astro files can access components with import
statements and insert them into a template as though they were regular HTML elements.
Each component is defined in a separate Astro file with frontmatter and template sections. A component's template can access two important capabilities:
- props - global properties available to every component
- template directives - special attributes that control how a component/element behaves
This discussion looks at each of these capabilities.
3.2.1 Accessing Props
Let's say that a component named MyComp
has two attributes named attr1
and attr2
. And let's say that a layout component imports MyComp
and inserts it into its template in the following way:
<code><MyComp attr1="abc" attr2="xyz" /></code>
In its frontmatter, the MyComp
component can access the attribute values through the global Astro.props
value (which doesn't need to be imported). The following code gives an idea of how this works:
const { attr1, attr2 } = Astro.props;
This code declares the variables attr1
and attr2
and sets their values equal to the values of the component's attributes ("abc"
and "xyz"
in this example). These variables can be used throughout the template by surrounding them in curly braces: {attr1}
and {attr2}
.
To enable type-checking, many components create an interface that assigns types to the incoming attributes. In the following code, the interface contains attr1
and attr2
and assigns them both to the string
type:
interface Props {
attr1: string;
attr2?: string;
}
If either attribute isn't a string, Astro will produce a warning/error during the build. The question mark following attr2
tells Astro that the attribute is optional.
3.2.2 Template Directives
Template directives are special attributes that can be inserted into components or other HTML elements in an Astro template. Every directive name consists of two words separated by a colon, and there are three general directives:
class:list
- converts an array of elements into a string set:html
- injects a string into an element (similar to innerHTML
) set:text
- injects text into an element (similar to innerText
)
A template can contain JavaScript, but by default, components aren't hydrated in the client. To control when a component is hydrated, you need to insert a client directive into the component. Astro provides six client directives:
client:load
- hydrate the component immediately when the page is loaded client:idle
- hydrate the component after the page is loaded and the requestIdleCallback
event has fired timeout
- maximum time to wait before hydrating the component client:visible
- hydrate the component when it enters the user's viewport client:media
- hydrate the component when a CSS media query is met client:only
- skips server rendering, and renders only on the client
If you look at the HeaderLink.astro file in the src/components folder, you'll see that it uses the class:list
directive to set an anchor's text. Unfortunately, none of the component templates demonstrate how client directives are used.
3.3 The Layouts Folder
Astro files in the src/layouts directory define special components called layouts. A layout's goal is to specify a page structure that can be used throughout the blog. Astro files in the src/pages directory can import layouts to define the page's fundamental elements, such as <html>
, <head>
, <body>
, <style>
, and so on.
For example, if you look at src/pages/about.astro, you'll see that its frontmatter and template have the following structure:
---
import Layout from '../layouts/BlogPost.astro';
---
<Layout ...>
...
</Layout>
The frontmatter imports the Layout component, which serves as the top-level element in the template. This provides the page's structure, and if you look at BlogPost.astro, you'll see that its template has the following content:
<html ...>
<head>
<BaseHead ... />
<style>
...
</style>
</head>
<body>
<Header />
<article>
<div class="hero-image">
...
</div>
<div class="prose">
<div class="title">
...
</div>
<slot />
</div>
</article>
<Footer />
</body>
</html>
As shown, the layout contains components like BaseHead
, Header
, and Footer
. The template of BaseHead
consists of <meta>
and <link>
elements, and during the build process, these elements will be inserted into the <head>
of every page that uses this layout.
In the <div>
element of class prose
, there's an important element named <slot />
. This is where the content inside the layout component will be placed. Returning to the template in about.astro, the markup between <Layout>
and </Layout>
will be placed in the <slot />
element in the template of the Layout
component.
Most blogs display one post per page. For Astro blogs, the structure of these pages is determined by a single layout component. This makes it important to write the layout component correctly.
3.4 The Content Folder
In essence, the purpose of an Astro project is to package the blog's content for deployment. If you look in src/content, you'll find a folder named blog and a file named config.ts. The files in the blog folder contain the blog's posts, which are are written using Markdown (*.md) and Markdown eXtended (*.mdx). The config.ts file exports a collection that enables components to access the blog's posts.
3.4.1 Content Files
Thanks to Github, the Markdown language has exploded in popularity for formatting text. If a content file has the suffix *.md, its text can be formatted using Markdown's syntax rules. Table 3 lists nine of the basic rules and provides a description of each.
Table 3: Basic Markdown Syntax Formatting Rule | Example | Description |
Heading | # Heading 1
## Heading 2 | The more pound signs,
the smaller the heading |
Italics | * italicized text * | One asterisk prints text in italics |
Boldface | ** boldface text** | Two asterisks print text in boldface |
Unordered List | - unordered
- unordered | Dashes create items in an unordered list |
Ordered List | 1. Item 1
2. Item 2 | Number-dot-space creates items in
an ordered list |
Code | `code font` | Back ticks print text in code font |
Horizontal Rule | --- | Three hyphens create a horizontal line |
Image | ![alt](img.png) | Alternative text in square brackets,
then the image file in parentheses |
Hyperlink | [text](https://www.xyz.com) | Printed text in square brackets,
then the URL in parentheses |
In addition to these rules, Markdown supports its own frontmatter, which is usually used to provide metadata. As in an Astro file, frontmatter in a Markdown file is surrounded by code fences. Unlike an Astro file, Markdown frontmatter is written in YAML, which means names and values are separated by colons. The following text gives an idea of what this looks like.
---
title: "Title"
author: "Tom Smith"
---
# This is an important heading!
If a file has the suffix *.mdx, it's written in Markdown eXtended (MDX), which combines the simplicity of Markup formatting and with the flexibility of Astro files. MDX is a superset of Markdown, so fit supports regular formatting rules and frontmatter. In addition, there are four points to know:
- TypeScript
import
/export
statements can be inserted throughout the text. - Once imported, components can be inserted into text using markup similar to Astro templates.
- Names in frontmatter can be accessed as properties of a
frontmatter
class. For example, if title
is assigned a value in the frontmatter, it can be accessed in text as {frontmatter.value}
. - MDX can be used for files in src/pages as well as files in src/content.
If you look in the project's src/content/blog folder, you'll see a file named using-mdx.mdx. This demonstrates how MDX can be used in a blog post.
3.4.2 Content Collections and config.ts
Any file in src/content is called a content entry and any top-level folder in src/content is called a content collection. Each collection contains a specific type of content, so if your site contains blog posts and source code files, they should be stored in separate collections.
In code, content collections are created by calling the defineCollection
function exported by astro:content
. In the example project, this is called by the config.ts file in src/content, and its full code is given as follows:
import { defineCollection, z } from 'astro:content';
const blog = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
heroImage: z.string().optional(),
}),
});
export const collections = { blog };
In this code, the schema
property accesses the frontmatter of each content entry. By default, the title
, description
, and pubDate
fields are required, and the updatedData
and heroImage
fields are optional. The export
statement makes the content collection available throughout the project. Later in the article, I'll explain how components access this content and insert it into a page.
3.5 Styles
If you open the src/styles directory, you'll find a file named global.css. The rules defined in this stylesheet apply throughout the project, and don't need to be imported.
You can change the blog's font by changing the font-face
rules. The settings for the body
rule are particularly important, as they apply to every component in the body of a web page.
4. Routing
If you build the default project by running npm run build
, the generated blog files will be stored in the project's dist folder. The HTML files in this folder were generated from the files in the project's src/pages and src/content folders.
Table 3 clarifies the relationships between six of the project's source files and generated files. The first column lists the paths of the source files, the second column lists the paths of the generated files, and the third column lists the URLs of the generated files. The URLs are given relative to www.example.com because this is the default site given in astro.config.mjs.
Table 4: Generated Files and URLs Source File | Generated File | URL |
src/pages/index.astro | dist/index.html | www.example.com |
src/pages/about.astro | dist/about.html | www.example.com/about |
src/pages/blog/index.astro | dist/blog/index.html | www.example.com/blog |
src/content/blog/first-post.md | dist/blog/first-post/index.html | www.example.com/blog/first-post |
src/content/blog/second-post.md | dist/blog/second-post/index.html | www.example.com/blog/second-post |
src/content/blog/third-post.md | dist/blog/third-post/index.html | www.example.com/blog/third-post |
As shown, Astro creates a directory for each Markdown file (*.md) in src/content/blog, and each directory has an index.html file. As discussed earlier, Astro refers to the src/content/blog folder as a content collection and each file in the folder is a content entry.
This table is misleading because Astro doesn't directly convert content entries into HTML files. To understand how this conversion is performed, you need to be familiar with two topics:
- Astro's
CollectionEntry
type - The [...slug].astro file in the src/content/blog folder
Once you understand these topics, you'll have a solid grasp of Astro's dynamic routing, which automatically generates HTML files for blog posts.
4.1 The CollectionEntry Type
As mentioned earlier, the src/content/config.ts file calls the defineCollection
function with an object with properties named type
and schema
. The type
property is set to content
and schema
contains fields of the blog post's frontmatter. The function's return value is a CollectionConfig
.
Astro's getCollection
function accepts a CollectionConfig
and returns a CollectionEntry
. This important object has five properties:
id
- for content entries, this is the name of the markdown file (*.md or *.mdx) - collection - the name of the content collection containing the entry (
blog
) - data - contains fields from the
schema
property of the CollectionConfig
- slug - text to serve as the last segment of the entry's URL
- body - the text in the content file
By default, Astro sets the slug
property to the id
property after removing the file suffix, setting each character to lowercase, and replacing each space and underscore with a hyphen. You can set a custom slug for an entry by adding a slug
field to the frontmatter of the blog post file.
In addition to these properties, each CollectionEntry
has a method named render
. This compiles the entry's content and returns an Astro component that displays the compiled result.
4.2 The [...slug].astro File
In the /src/pages/blog folder, the [...slug].astro file is responsible for creating a page for each content entry in /src/content/blog. Its content is given as follows:
---
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>
The code in the frontmatter forms a list of content entries and tells Astro to create a page for each element in the list. There are four points to be aware of:
- If the name of an Astro file is enclosed in square brackets, its frontmatter can access its name as a variable. The frontmatter of [...slug].astro accesses a variable named
slug
. - The frontmatter calls
getCollection
to access CollectionEntry
objects in the collection named blog
. - The frontmatter iterates through the entries and creates an array of elements. Each element of this array has a property named
params
, and each params
has a property named slug
whose value equals the slug
property of the CollectionEntry
. - The frontmatter exports a function named
getStaticPaths
that returns the array constructed in Step 4. When Astro invokes this function, it creates a directory for each element of the array and set its name to params.slug
. This explains why Astro creates dist/blog/first-post, dist/blog/second-post, and so on.
To set the content of these generated pages, [...slug].astro performs four operations:
- The frontmatter calls the render method of each
CollectionEntry
. The resulting component is assigned the name Content
. - The template contains a
BlogPost
component that defines the page's layout. - The template passes the
CollectionEntry
's data
field to the BlogPost
, which it can access through Astro.props
. - The template inserts the
Content
component inside the BlogPost
component. As a result, the CollectionEntry
content will replace the <slot />
element in the template of the layout component.
Astro refers to this page-generation process as static mode because the pages are generated during the build process. Astro also supports a server mode, in which pages are rendered by the server. This doesn't use getStaticPaths
, and you can read more about server mode here.
5. Customizing the Blog
At this point, you should have a solid understanding of how to add posts to an Astro project and convert the project to HTML. This section presents a handful of customization tasks that blog authors may want to perform.
5.1 Project Configuration
If you're creating a new blog, the first file to change is astro.config.mjs in the project's top-level directory. This is the project's central configuration file, and by default, its content is given as follows:
import { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx';
import sitemap from '@astrojs/sitemap';
export default defineConfig({
site: 'https://example.com',
integrations: [mdx(), sitemap()],
});
At minimum, you should change the site
property to the URL of your site. If you don't want to include MDX, you can remove the second import statement and delete the mdx()
element from the integrations array.
Astro supports many other configuration options in astro.config.mjs. You can read the full list of them here.
5.2 The Header and Header Links
The default blog has a title in the upper left (Astro Blog), three links in the upper center (Home, Blog, and About), and social links in the upper right (Mastodon, Twitter, and Github). The banner across the top is defined in src/components/Header.astro with the following template markup:
<h2><a href="/">{SITE_TITLE}</a></h2>
<div class="internal-links">
<HeaderLink href="/">Home</HeaderLink>
<HeaderLink href="/blog">Blog</HeaderLink>
<HeaderLink href="/about">About</HeaderLink>
</div>
<div class="social-links">
...
</div>
The first line of this markup sets the blog's title in the upper left, and the text is imported from consts.ts in the project's src directory. The default content of this file is given as:
export const SITE_TITLE = 'Astro Blog';
export const SITE_DESCRIPTION = 'Welcome to my website!';
You'll probably want to change these constants. In addition, if there are any strings that you'd like to use throughout the site (such as a copyright notice), it's a good idea to add them to src/consts.ts.
In the Header
component's template, the division with class internal-links
creates the Home, Blog, and About links in the upper center. These links are implemented as instances of the HeaderLink
component, which is defined in src/components/HeaderLink.astro.
5.3 The BaseHead Component
If search engine optimization (SEO) is a concern, you can add keywords and other items to the BaseHead
component defined in src/components/BaseHead.astro. The BlogPost
layout inserts a BaseHead
into the <head>
element of every page that uses the layout.
The Basehead
also sets the site's favicon, and the following markup sets the default image:
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
You'll probably want to change the canonical URL, which sets a page's preferred URL if it can be accessed through multiple URLs. The default canonical URL is set in the following way:
<link rel="canonical" href={canonicalURL} />
The BaseHead
receives title
and description
attributes from the BlogPost
component, which sets them to the SITE_TITLE
and SITE_DESCRIPTION
constants in src/consts.ts. In the BaseHead
template, these attributes are used to set the primary meta elements of the page:
<title>{title}</title>
<meta name="title" content={title} />
<meta name="description" content={description} />
If you'd like your blogging site to be shared on social media, you can add metadata defined by the Open Graph protocol. For this end, the BaseHead contains the following lines:
<meta property="og:type" content="website" />
<meta property="og:url" content={Astro.url} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={new URL(image, Astro.url)} />
In these tags, the variable Astro.url
is always set to the URL of the page in which these tags are inserted.
5.4 The Footer Component
At the bottom of the blog, the footer displays the copyright notice and social media links. The abbreviated structure of the Footer
component is given as:
<footer>
© {today.getFullYear()} Your name here. All rights reserved.
<div class="social-links">
...
</div>
</footer>
<style>
...
</style>
Naturally, you'll want to add your name to the footer. You can set the footer's background color and padding by setting attributes in the <style>
...</style>
tags.
5.5 Really Simple Syndication (RSS)
RSS allows users to access the blog's content without having to manually visit the site. To support RSS, a blog needs to provide a file formatted according to the RSS 2.0 standard. This format is based on XML, and uses <rss>
and </rss>
as the root elements.
If you look in the dist folder, you'll see that the Astro build generated a file named rss.xml. The content of this file is determined by the rss.xml.js file in the project's src/pages folder. This defines a function named GET
, which is defined in the following way:
export async function GET(context) {
const posts = await getCollection('blog');
return rss({
title: SITE_TITLE,
description: SITE_DESCRIPTION,
site: context.site,
items: posts.map((post) => ({
...post.data,
link: `/blog/${post.slug}/`,
})),
});
}
In the RSS feed, the <title>
element is set to SITE_TITLE
and the <description>
element is set to SITE_DESCRIPTION
. The feed will also have an <item>
element for each blog post. If you'd like to customize the RSS feed with different elements, rss.xml.js is the file to modify.
History
This article was initially submitted on October 9, 2024.