Create an Astro blog from scratch

BUDDING
Planted: 2024-06-03
#TUTORIAL
Last tended: 2024-06-03


Astro is a web framework for creating content-driven websites. It allows you to build extremely fast websites. It is perfect for building blogs and we are going to do exactly that. We are going to build an Astro blog from scratch.

You can find the complete code here: https://github.com/Ramkarthik/astro-blog-tutorial

This is the blog we are building from scratch: https://quick-astro-blog-tutorial.vercel.app/

We will be going over the steps below to build this blog:

  1. Create an Astro project
  2. Add some basic styling
  3. Create a layout
  4. Set up default website configurations
  5. Create an About page
  6. Create a Nav header
  7. Create a folder to add the blog content (the easy route)
  8. Use Content Collections for our blog
  9. Setting up the dynamic routes for our blog
  10. Getting Markdown content from collection entry
  11. Create a blog listing page
  12. Using our first Astro Integration to add SEO
  13. Create an RSS feed for the blog
  14. Add Sitemap and robots.txt file
  15. Preparing for deployment
  16. Next steps

Before we start, you want to make sure you have Node.js installed. Astro recommends using v18.17.1 or v20.3.0 or higher. ( v19 is not supported.)

0. Create an Astro project

Open Terminal and navigate to the folder where you want to create the blog and run this command to create an Astro project:

npm create astro@latest

It will ask you a couple of questions:

1. Where should we create your new project? ./name-of-your-project
2. How would you like to start your new project? Empty (You can use the blog template here but this is to learn how to set up one from scratch, so we will choose the Empty template)
3. Do you plan to use TypeScript? Yes (You can choose no if you don't want to use TypeScript)
4. How strict should TypeScript be? Strict
5. Install dependencies? Yes
6. Initialize a new git repository? Yes

This will create the Astro project. Navigate into the folder to run the project. Open the project in your favorite editor.

cd name-of-your-project
npm run dev

Go to https://localhost:4321 and you will see the page displaying Astro. If you chose the blog template in step 2, you may see a different page.

Before we get started, let’s modify the tsconfig.json file a little bit to make things easier for referencing different components.

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "extends": "astro/tsconfigs/strict"
}

Let’s get started.

1. Add some basic styling

We can write the CSS from scratch as well or use something like Tailwind. To keep things easier, we will use one of the many classless CSS. A classless CSS styles a page based on the HTML elements instead of using class names. We are using this to make it easier for us to style the HTML rendered using Markdown. The Astro Markdown render outputs a basic HTML and using a classless CSS, we don’t have to worry too much about styling each element. Classless CSS are also lightweight so our blog will be extremely fast.

There are many classless CSS options. For this project (and my blog), I’m going to be using Sakura. You can use any of those options.

  1. Download the CSS file
  2. Create a folder named css inside the public folder
  3. Paste the file and rename it to style.css

Now go to \src\index.astro and add the CSS file to the end of the <head> tag.

<link rel="stylesheet" href="/css/style.css" type="text/css" />

You should see the changes already. Without adding any classes, we can see that our page is already styled.

2. Create a layout

Layouts are basic templates that can be shared across different pages. We want the basic html, head, and footer elements to be available on each page. So let’s create a base layout that will store these as a template.

  1. Create a folder named layouts under the src folder (\src\layouts)
  2. Create a file inside the layouts folder named Base.astro (\src\layouts\Base.astro)

The page we are seeing now comes from the pages\index.astro page. This is the default page or the entry point. We will be creating both the static pages (ex: \about) and the dynamic pages (ex: \blog\my-first-post) by creating Astro pages inside the pages folder.

Now let’s move all the code inside the index.astro page to the Base.astro page we created.

Go to index.astro and add the Base.astro layout.

pages\index.astro
---
import Base from "@/layouts/Base.astro";
---
<Base />

You will notice a couple of things:

  1. In Astro, you write JavaScript code within the three dash separators.
  2. Similar to React, you can also write JavaScript alongside HTML.

If you refresh the page now, you should not see any difference because we only moved the content from index.astro to Base.astro and referenced Base.astro from index.astro.

This brings us to a new concept in Astro… Slots. Astro uses <slot/> to inject the child components.

Let’s go to Base.astro. Not every line of code there belongs in a template, mainly the <h1> tag. Replace that with <slot />.

Go to index.astro, and add the h1 tag within the <Base> tag.

pages\index.astro
---
import Base from "@/layouts/Base.astro";
---
<Base>
    <h1>Astro</h1>
</Base>

Go to the browser and you will not see any changes. What we did was move the template code from index.astro into Base.astro and use the layout.

3. Set up default website configurations

We need some basic information about our website that we need to display in many places. We don’t want to type them everywhere. We want to store these configurations in one place and refer to them wherever we need them so that if we want to make any changes, we only have to change the configuration.

  1. Create a folder named utils inside the src folder (src\utils)
  2. Create a file named AppConfig.ts inside the utils folder (AppConfig.js if you don’t want TypeScript)
utils\AppConfig.ts
export const AppConfig = {
    author: "Author Name",
    title: "My personal website",
    description: "This is my personal website",
    image: "/images/social.png", // this will be used as the default social preview image
    twitter: "@handle",
    site: "https://yourwebsite.com/" // this is your website URL
}

Our website currently displays Astro. Let’s change that to show our name from the config file we created. Let’s also bring in the description and display that.

pages\index.astro
---
import Base from "@/layouts/Base.astro";
import { AppConfig } from "@/utils/AppConfig";
---
<Base>
  <h1>{AppConfig.title}</h1>
  <p>{AppConfig.description}</p>
</Base>

4. Create an About page

Now that we have the home page, let’s see how we can create a new page - in this case, an About (https://localhost:4321/about) page.

Astro uses file-based routing. Let’s see some examples:

src/pages/index.astro -> mysite.com/
src/pages/about.astro -> mysite.com/about
src/pages/about/index.astro  -> mysite.com/about

As you can see, there are two ways to create an About page. We will use the first approach and create a new file named about.astro inside the pages folder.

pages\about.astro
---
import Base from "@/layouts/Base.astro";
---
<Base>
    <h1>About</h1>
</Base>

Go to http://localhost:4321/about and you should see the page we created.

5. Create a Nav header

We now have two pages, so we need a way to link to them from our home page as well as our other pages. We will do this by creating a nav header. Since we want this header to appear on all our pages, we will add it to the Base.astro layout page.

Instead of adding the code for the header directly to this file, we will create a separate component (Nav.astro). From the React docs: “Components let you split the UI into independent, reusable pieces, and think about each piece in isolation.”

We will store all the components inside a separate folder called components which we will create under the src folder (src\components).

  1. Create a folder named components inside the src folder (src\components)
  2. Create a file named Nav.astro

Let’s display our name on the left and the navigation links on the right.

src\components\Nav.astro
---
import { AppConfig } from "@/utils/AppConfig";
---
<nav role="navigation">
  <a href="/">{AppConfig.author}</a>
  <div>
    <a href="/about">About</a>
  </div>
</nav>

We will style this in a minute. We only have two pages, so this approach is fine. But we will likely create more nav links like blog, rss, etc and there’s a better way to manage that than adding a new line of code here with the name and the link.

Let’s go back to our AppConfig.ts and add our list of pages.

src\utils\AppConfig.ts
export const AppConfig = {
        author: "Author Name",
        title: "My personal website",
        description: "This is my personal website",
        image: "/images/social.png", // this will be used as the default social preview image
        twitter: "@handle",
        site: "https://yourwebsite.com/",// this is your website URL
        pages: [{
            name: "About",
            link: "/about"
        }]
    }

We can now modify the Nav.astro component to get the links from the pages array.

src\components\Nav.astro
---
import { AppConfig } from "@/utils/AppConfig";
---

<nav role="navigation" class="justify-between">
  <a href="/">{AppConfig.author}</a>
  <div>
    {
      AppConfig.pages.map((p, index) => {
        return (
          <span>
            <a href={p.link}>
              <small>{p.name}</small>
            </a>
            {index != AppConfig.pages.length - 1 && <small>|</small>}
          </span>
        );
      })
    }
  </div>
</nav>

You might be familiar with this syntax of mapping an array and returning JSX, if you’ve used React before. One thing you may notice is the missing key property. Astro doesn’t require a key property.

Also, notice the class="justify-between" added to the <nav> element. We will use this later to style the nav.

You won’t see any changes on the website yet because we haven’t added the Nav.astro component to our Base.astro layout. Let’s do that now. We will add the <Nav /> component just above the slot.

src\layouts\Base.astro
---
import Nav from "@/components/Nav.astro";
---

<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
    <meta name="viewport" content="width=device-width" />
    <meta name="generator" content={Astro.generator} />
    <title>Astro</title>
    <link rel="stylesheet" href="/css/style.css" type="text/css" />
  </head>
  <body>
    <Nav />
    <slot />
  </body>
</html>

If we go to http://localhost:4321/, we should now see the title and the navigation links. Let’s style this so that the title is on the left and the nav links are on the right. Remember the nav="class" that we added to the nav element before. We will use that to style the nav.

Add this to the end of the style.css file:

public\style.css
.justify-between {
  display: flex;
  justify-content: space-between;
}

Perfect, we have the header set up now. In case you don’t see the changes, press Ctrl+R on the webpage to hard reload (disabling the cache). Let’s move on to the blog (the interesting part).

6. Create a folder to add the blog content (the easy route)

We want to create a folder to store the contents of our blog. Each post will be a Markdown file. We will do the easy implementation first and then change that to match our needs.

  1. Create a folder named blog inside the pages folder (pages\blog)
  2. Create a sample Markdown file inside the blog folder

I’m going to create a file named my-first-post.md:

src\pages\blog\my-first-post.md
---
title: My first post
createdDate: "2024-05-18"
modifiedDate: "2024-05-18"
tags: ["first-tag"]
summary: "A summary of the post"
---

This is my first blog post written in Markdown.

The content within the three dashed separator --- is called frontmatter. We will later use the information from the frontmatter to display on our page.

Now go to the URL: http://localhost:4321/blog/my-first-post and you should see a very basic version of your blog post content. You will not see the title yet and that’s where frontmatter comes into play.

7. Use Content Collections for our blog

Currently, we have the blog content inside the pages folder. We want to keep the pages folder for code and move our blog content to a separate folder so that we have a separation of concerns and it is also easier to manage it this way.

Astro provides an API called Content Collections starting from [email protected]. To use this feature, we have to create a folder named content inside the src folder. This content folder is restricted for content collections and should not be used for anything else.

So let’s go ahead and move our blog folder from src\pages to src\content. If you followed the steps so far, your project folder should look like this:

.astro
node_modules
public
	css
		style.css
	favico.svg
src
	components
		Nav.astro
	content
		blog
			my-first-post.md
	layouts
		Base.astro
	pages
		about.astro
		index.astro
	utils
		AppConfig.ts
	env.t.ds
.gitignore
astro.config.mjs
package-lock.json
package.json
README.md
tsconfig.json

Now the URL http://localhost:4321/blog/my-first-post will not work because we have moved the blog folder within the content folder.

8. Setting up the dynamic routes for our blog

We want the URL http://localhost:4321/blog/my-first-post to work again. We can see that we have the \blog route which means we have to create a folder named blog inside the pages folder.

Once we create the folder, we need to set up a way to handle the dynamic part of the URL, called the slug. In our case, the slug is my-first-post. But for each post, this will change.

Let’s create a file named [slug].astro inside the blog folder (src\content\blog\[slug].astro).

For the dynamic paths to work, we need to let Astro know of the different possible paths. We do this by implementing the getStaticPaths function. For us to know the different paths available, we have to get each file inside the content folder and return the slug. We do this using the getCollection API provided by Astro via the astro:content module that is built-in.

The getCollection API works only with folders inside the content folder. We get the posts inside the blog folder, map through each item, and return the slug as a parameter and also the contents of each item as props. Astro has a nifty way to retrieve the props through Astro.props.

While we are on this file, let’s add some basic HTML as well. We will use the Base.astro layout we created.

src/pages/blog/[slug].astro
---
import { getCollection } from "astro:content";

export async function getStaticPaths() {
  const posts = await getCollection("blog");
  return posts.map((post) => ({
    params: { slug: post.slug },
    props: {
      post,
    },
  }));
}
---

<Base>
  <h1>Title</h1>
</Base>

Go to http://localhost:4321/blog/my-first-post and it should now work. We have successfully migrated to the Content Collections API.

But wait, where’s the content of the post we saw earlier? We have to bring those in from Astro.props.

Remember the frontmatter we added to our post? Let’s first define a schema so that we get type safety. We need it when we get the values from Astro.props.

Create a file named config.ts under the content folder (don’t add it inside the blog folder).

src\content\config.ts
import { z, defineCollection } from 'astro:content';

const blogCollection = defineCollection({
  type: 'content',
  schema: z.object({
    title: z.string(),
    summary: z.string(),
    tags: z.array(z.string()),
    createdDate: z.string(),
    modifiedDate: z.string(),
  }),
});

export const collections = {
  'blog': blogCollection,
};

Once we define the schema, we have to let Astro know to generate the types. We can do that either by stopping the server (Ctrl+C) or by running npm run astro sync.

Now let’s edit the [slug].astro file to display the blog post title. For this, we have to extract the title from Astro.props and add this to the existing <h1> tag.

src/pages/blog/[slug].astro
---
import { getCollection } from "astro:content";
import Base from "@/layouts/Base.astro";

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

const { post } = Astro.props;
const { title, summary, createdDate, tags } = post.data;
---
<Base>
  <h1>{title}</h1>
</Base>

Now when you go to http://localhost:4321/blog/my-first-post, you should see the title from the frontmatter of the post appear.

What about the content?

9. Getting Markdown content from collection entry

We are writing our posts in Markdown. We want Astro to generate HTML for the markdown and display that. We do this by first using the render() function Astro provides and then adding that to the HTML block.

src/pages/blog/[slug].astro
---
import { getCollection } from "astro:content";
import Base from "@/layouts/Base.astro";

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

const { post } = Astro.props;
const { title, summary, createdDate, tags } = post.data;
const { Content } = await post.render();
---

<Base>
  <h1>{title}</h1>
  <Content />
</Base>

We call the render() function on the post object, store it as Content, and then add that to the HTML as <Content />.

You should now see the blog post content when you go to http://localhost:4321/blog/my-first-post. Let’s add some random markdown to the my-first-post.md file to see how the Markdown is displayed on the page (styled using the Sakura classless CSS we added). You can copy and paste random markdown using Lorem Markdownum.

We also want to display the tags and the created date. Let’s bring those in as well from the props and add that to the HTML.

src/pages/blog/[slug].astro
---
import { getCollection } from "astro:content";
import Base from "@/layouts/Base.astro";

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

const { post } = Astro.props;
const { title, summary, createdDate, tags } = post.data;
const { Content } = await post.render();
---

<Base>
  <h1>{title}</h1>
  <div class="justify-between">
    <div>
      {
        tags.map((t) => {
          return (
            <small>
              <i>#{t}</i>
            </small>
          );
        })
      }
    </div>
    <small>{createdDate}</small>
  </div>
  <hr />
  <Content />
</Base>

Let’s add more posts to play around with. Go to the blog folder and create more files. For this example, we will create my-second-post.md and my-third-post.md with the same content as my-first-post.md and change only the frontmatter details like title, summary, createdDate, and tags.

With that, you should now be able to access the following URLs: http://localhost:4321/blog/my-first-post http://localhost:4321/blog/my-second-post http://localhost:4321/blog/my-third-post

Great! But we now need a blog listing page where readers can find all the blog posts as a list.

10. Create a blog listing page

We want the listing page to be available at /blog which means, you guessed it, we have to add either a blog.astro find directly inside the pages folder or add an index.astro page inside the pages\blog folder. We will do the latter. We will also bring in the Base.astro layout (see how we are reusing the layout?).

src\pages\blog\index.astro
---
import Base from "@/layouts/Base.astro";
---

<Base>
    <h1>Posts</h1>
</Base>

If you navigate to http://localhost:4321/blog, you should see the blog listing page. Now we want to list the blog posts here.

We will use the getCollection() function to get the list of posts from the blog folder and then map over each item to display them. We will also sort the posts based on the createdDate we have defined in the frontmatter.

src\pages\blog\index.astro
---
import { getCollection } from "astro:content";
import Base from "@/layouts/Base.astro";

const posts = await getCollection("blog");
const sortedPosts = posts.sort((a, b) => {
  return +new Date(b.data.createdDate) - +new Date(a.data.createdDate);
});
---

<Base>
  <h1>Posts</h1>
  <ul>
    {
      sortedPosts.map((p) => {
        return (
          <li>
            <a href={"/blog/" + p.slug}>{p.data.title}</a>
          </li>
        );
      })
    }
  </ul>
</Base>

We can also do this for the home page and list the five most recent blog posts by editing the src\pages\index.astro file.

src\pages\index.astro
---
import { getCollection } from "astro:content";
import Base from "@/layouts/Base.astro";
import { AppConfig } from "@/utils/AppConfig";

const posts = await getCollection("blog");

const sortedPosts = posts.sort((a, b) => {
  return +new Date(b.data.createdDate) - +new Date(a.data.createdDate);
});
---

<Base>
  <h1>{AppConfig.title}</h1>
  <p>{AppConfig.description}</p>
  <h3>Posts</h3>
  <ul>
    {
      sortedPosts.slice(0, 5).map((p) => {
        return (
          <li>
            <a href={"/blog/" + p.slug}>{p.data.title}</a>
          </li>
        );
      })
    }
  </ul>
  <a href="/blog">Click here</a> to view the archive.
</Base>

Let’s add some links for easy navigation.

First, we will add a link for our blog listing page to the header. Since the links in the header come from the pages property in AppConfig.ts file, we will add a link to the blog listing page to the pages array.

src\utils\AppConfig.ts
export const AppConfig = {
        author: "Author Name",
        title: "My personal website",
        description: "This is my personal website",
        image: "/images/social.png", // this will be used as the default social preview image
        twitter: "@handle",
        site: "https://yourwebsite.com/", // this is your website URL
        pages: [{
            name: "Blog",
            link: "/blog"
        },{
            name: "About",
            link: "/about"
        }]

    }

We will also add the previous post and the next post (if available) to the end of the blog post. To do this, we will retrieve the list of blog posts using the getCollection() function, sort the posts, find the index of the current post in the sorted list, and then identify the previous and next posts to display in the HTML. We do this by editing the [slug].astro file.

src/pages/blog/[slug].astro
---
import { getCollection } from "astro:content";
import Base from "@/layouts/Base.astro";

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

const { post } = Astro.props;
const { title, summary, createdDate, tags } = post.data;
const { Content } = await post.render();
const posts = await getCollection("blog");

const sortedPosts = posts.sort((a, b) => {
  return +new Date(b.data.createdDate) - +new Date(a.data.createdDate);
});

const index = sortedPosts.findIndex((c: any) => {
  return c.slug == post.slug;
});

const prev = index == 0 ? undefined : sortedPosts[index - 1];
const next =
  index == sortedPosts.length - 1 ? undefined : sortedPosts[index + 1];
---

<Base>
  <h1>{title}</h1>
  <div class="justify-between">
    <div>
      {
        tags.map((t) => {
          return (
            <small>
              <i>#{t}</i>
            </small>
          );
        })
      }
    </div>
    <small>{createdDate}</small>
  </div>
  <hr />
  <Content />
  <hr />
  <div class="justify-between">
    {
      prev && (
        <a href={`/blog/${prev.slug}`}>
          <small>&larr; {prev.data.title}</small>
        </a>
      )
    }
    {
      next && (
        <a href={`/blog/${next.slug}`}>
          <small>{next.data.title} &rarr;</small>
        </a>
      )
    }
  </div>
</Base>

We should now have navigation links at the end of the blog post. You can verify that by going to http://localhost:4321/blog/my-second-post.

You may have noticed that the browser tab title always says Astro. We want this to be dynamic based on the page we are on. Introducing you to the world of Astro Integrations.

11. Using our first Astro Integration to add SEO

Astro provides the ability for us to use plugins either offered directly by Astro or created by the community to build things faster through Astro Integrations. We will use the astro-seo integration to:

  1. Fixing the title
  2. Add SEO to our page (we want the search engines to find our website)
  3. Add social tags (so our links will look when shared on Facebook, Twitter, etc.)

Install astro-seo by running the following command:

npm install astro-seo

We are going to import SEO from the astro-seo integration. This component expects a few props like title, description, OG info, Twitter info, etc.

Since we want to use the information corresponding to each page, we are going to define the props for our Head.astro component. We are also creating an interface to get type safety.

Let’s create the interface first. We will create a file named types.ts inside the utils folder (src\utils\types.ts).

src\utils\types.ts
export interface HeadProps {
    props: {
      title: string;
      description: string;
      image?: string | undefined;
    };
  }

Let’s create a component named Head.astro inside the components folder (src\components\Head.astro) with the following content.

src\components\Head.astro
---
import { SEO } from "astro-seo";
import { AppConfig } from "@/utils/AppConfig";
import { type HeadProps } from "@/utils/types";

const {
  props: { title, description, image },
} = Astro.props as Props;
---

<SEO
  title={title || AppConfig.title}
  description={description || AppConfig.description}
  openGraph={{
    basic: {
      title: title || AppConfig.title,
      type: description || AppConfig.description,
      image: AppConfig.site + (image || AppConfig.image || ""),
    },
  }}
  twitter={{
    creator: AppConfig.twitter,
  }}
  extend={{
    link: [{ rel: "icon", href: "/favicon.svg" }],
    meta: [
      {
        name: "twitter:image",
        content: AppConfig.site + (image || AppConfig.image || ""),
      },
      { name: "twitter:title", content: title || AppConfig.title },
      {
        name: "twitter:description",
        content: description || AppConfig.description,
      },
    ],
  }}
/>

Now that we have the Head.astro component created, we want to add this to our Base.astro layout page so that we will have the SEO feature applied to all the pages.

We will remove the existing <title> tag from the Base.astro file and add the <Head /> component we just created. You will immediately see an error because we have to pass the mandatory props to the <Head> component.

Again, instead of passing the values directly from the <Base> layout, we will define a prop for the layout of type HeadProps that we created before and have the pages that use the layout pass this information to it.

src\layouts\Base.astro
---
import Head, { type HeadProps } from "@/components/Head.astro";
import Nav from "@/components/Nav.astro";

const {
  props: { title, description, image },
} = Astro.props as HeadProps;
---

<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
    <meta name="viewport" content="width=device-width" />
    <meta name="generator" content={Astro.generator} />
    <link rel="stylesheet" href="/css/style.css" type="text/css" />
    <Head props={{ title, description, image }} />
  </head>
  <body>
    <Nav />
    <slot />
  </body>
</html>

You will get errors in every file that uses the Base.astro file because we are not providing the value for the props. Let’s do that for each page.

First, let’s update the src\pages\index.astro (homepage). For this, we will page the values from the AppConfig.ts file.

src\pages\index.astro
---
import { getCollection } from "astro:content";
import Base from "@/layouts/Base.astro";
import { AppConfig } from "@/utils/AppConfig";

const posts = await getCollection("blog");

const sortedPosts = posts.sort((a, b) => {
  return +new Date(b.data.createdDate) - +new Date(a.data.createdDate);
});
---

<Base
  props={{
    title: AppConfig.title,
    description: AppConfig.description,
    image: AppConfig.image,
  }}
>
  <h1>{AppConfig.title}</h1>
  <p>{AppConfig.description}</p>
  <h3>Posts</h3>
  <ul>
    {
      sortedPosts.slice(0, 5).map((p) => {
        return (
          <li>
            <a href={"/blog/" + p.slug}>{p.data.title}</a>
          </li>
        );
      })
    }
  </ul>
  <a href="/blog">Click here</a> to view the archive.
</Base>

Next, let’s fix the blog listing page src\pages\blog\index.astro.

src\pages\blog\index.astro
---
import { getCollection } from "astro:content";
import Base from "@/layouts/Base.astro";
import { AppConfig } from "@/utils/AppConfig";

const posts = await getCollection("blog");

const sortedPosts = posts.sort((a, b) => {
  return +new Date(b.data.createdDate) - +new Date(a.data.createdDate);
});
---

<Base
  props={{
    title: "My collection of essays",
    description: AppConfig.description,
    image: AppConfig.image,
  }}
>
  <h1>Posts</h1>
  <ul>
    {
      sortedPosts.map((p) => {
        return (
          <li>
            <a href={"/blog/" + p.slug}>{p.data.title}</a>
          </li>
        );
      })
    }
  </ul>
</Base>

Let’s fix the About page.

src\pages\about.astro
---
import Base from "@/layouts/Base.astro";
import { AppConfig } from "@/utils/AppConfig";
---

<Base
  props={{
    title: "About | " + AppConfig.author,
    description: AppConfig.description,
    image: AppConfig.image,
  }}
>
  <h1>About</h1>
</Base>

Finally, we will fix the blog slug file src\pages\blog\[slug].astro.

src/pages/blog/[slug].astro
---
import { getCollection } from "astro:content";
import Base from "@/layouts/Base.astro";
import { AppConfig } from "@/utils/AppConfig";

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

const { post } = Astro.props;
const { title, summary, createdDate, tags } = post.data;
const { Content } = await post.render();
const posts = await getCollection("blog");

const sortedPosts = posts.sort((a, b) => {
  return +new Date(b.data.createdDate) - +new Date(a.data.createdDate);
});

const index = sortedPosts.findIndex((c: any) => {
  return c.slug == post.slug;
});

const prev = index == 0 ? undefined : sortedPosts[index - 1];
const next =
  index == sortedPosts.length - 1 ? undefined : sortedPosts[index + 1];
---

<Base props={{ title: title, description: summary, image: AppConfig.image }}>
  <h1>{title}</h1>
  <div class="justify-between">
    <div>
      {
        tags.map((t) => {
          return (
            <small>
              <i>#{t}</i>
            </small>
          );
        })
      }
    </div>
    <small>{createdDate}</small>
  </div>
  <hr />
  <Content />
  <hr />
  <div class="justify-between">
    {
      prev && (
        <a href={`/blog/${prev.slug}`}>
          <small>&larr; {prev.data.title}</small>
        </a>
      )
    }
    {
      next && (
        <a href={`/blog/${next.slug}`}>
          <small>{next.data.title} &rarr;</small>
        </a>
      )
    }
  </div>
</Base>

Alright, we have fixed pretty much everything. The final thing related to SEO that we need to fix is the social image. We are using the value of image property from the AppConfig.ts file everywhere but we don’t have that image. You can add the image you want to display as a preview when sharing links. I usually take a screenshot of the homepage and use that. Once you choose the image, add it to public\images\ with the name social.png since that’s the value of AppConfig.image.

Alright, we are almost there setting up the blog. There are a couple more things we need for the blog to be complete.

12. Create an RSS feed for the blog

We have a blog but we need an RSS feed so that people can subscribe to our blog (yes, people still subscribe to blogs).

We will use another Astro integration for this called @astro/rss. Let’s install it using the below command:

npm install @astrojs/rss

Let’s create a file named rss.xml.js inside the pages folder (src\pages\rss.xml.js) with the following content.

src\pages\rss.xml.js
import { AppConfig } from "@/utils/AppConfig";
import rss from "@astrojs/rss";
import { getCollection } from "astro:content";

export async function GET() {
  const blog = await getCollection("blog");

  return rss({
    title: AppConfig.title,
    description: AppConfig.description,
    site: AppConfig.site,
    items: blog.map((post) => ({
      title: post.data.title,
      pubDate: post.data.createdDate,
      description: post.data.summary,
      link: `/blog/${post.slug}/`,
    })),
  });
}

We should also add a <link> to our Base.astro file that allows browsers and other apps to auto-discover the RSS feed from our website.

Let’s add the below line to the Base.astro file just above the </head> tag.

<link
  rel="alternate"
  type="application/rss+xml"
  title="{AppConfig.title}"
  href="{`${AppConfig.site}rss.xml`}"
/>

We also have to create a sitemap and a robots.txt file so that search engines can crawl our website.

13. Add Sitemap and robots.txt file

We will use another Astro integration called sitemap. Instead of running npm install, we will run the below command which will both install the integration as well as auto-configure the sitemap for us.

npx astro add sitemap

We have to add our website URL to astro.config.mjs file.

astro.config.mjs
import { defineConfig } from "astro/config";
import sitemap from "@astrojs/sitemap";

// https://astro.build/config
export default defineConfig({
  site: "https://yourwebsite.com",
  integrations: [sitemap()],
});

You can verify that the sitemap-index.xml file gets generated by running npm run build and then going to the dist folder created in the root of your project.

Similar to how we added a <link> to the RSS feed to the Base.astro layout file, we have to do the same for the sitemap-index.xml file. Let’s add the below line to the src\layouts\Base.astro file just above the </head> tag.

<link rel="sitemap" href="/sitemap-index.xml" />

Finally, let’s create a robots.txt file inside the public folder (public\robots.txt) with the below content.

User-agent: *
Allow: /

Sitemap: https://<YOUR SITE>/sitemap-index.xml

Congrats! If you followed the tutorial till now, you have a fully functional blog.

14. Preparing for deployment

We have a few dummy values that we need to change before we deploy this blog.

  1. Update the AppConfig.ts file with the right information
  2. Update the dummy URL in astro.config.mjs file
  3. Delete the dummy Markdown files from the content\blog folder and add your blog posts. Don’t forget to add the necessary frontmatter to each post.
  4. Update the social image with the image you would like public\images\social.png
  5. Update the src\pages\about.astro page with details about you

Once you’ve made these changes, you can deploy to one of the many services that provide free hosting for static websites.

  1. Vercel - Deploy your Astro Site to Vercel | Docs
  2. Cloudflare - Astro · Cloudflare Pages docs
  3. Netlify - Astro on Netlify | Netlify Docs

15. Next steps

You have a proper blog in place right now. There are a few things you can add to this to make it better.

  1. Create a src\components\footer.astro component and add it to the Base.astro layout to make it part of every page
  2. Add a src\pages\now.astro page to tell your readers about what you are doing now (following The /now page movement | Derek Sivers)
  3. Add analytics to your website (for ex: GoatCounter – open source web analytics)
  4. Add separate collection for notes where you can write short notes instead of long blog posts and make it available as part of \notes URL similar to \blog

Happy coding and happy writing!


Notes on SaaS Multitenancy →