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:
- Create an Astro project
- Add some basic styling
- Create a layout
- Set up default website configurations
- Create an About page
- Create a Nav header
- Create a folder to add the blog content (the easy route)
- Use Content Collections for our blog
- Setting up the dynamic routes for our blog
- Getting Markdown content from collection entry
- Create a blog listing page
- Using our first Astro Integration to add SEO
- Create an RSS feed for the blog
- Add Sitemap and robots.txt file
- Preparing for deployment
- 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.
- Download the CSS file
- Create a folder named
css
inside thepublic
folder - 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.
- Create a folder named
layouts
under thesrc
folder (\src\layouts
) - 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.
---
import Base from "@/layouts/Base.astro";
---
<Base />
You will notice a couple of things:
- In Astro, you write JavaScript code within the
three dash separators
. - 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.
---
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.
- Create a folder named
utils
inside thesrc
folder (src\utils
) - Create a file named
AppConfig.ts
inside theutils
folder (AppConfig.js
if you don't want TypeScript)
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.
---
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.
---
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
).
- Create a folder named
components
inside thesrc
folder (src\components
) - Create a file named
Nav.astro
Let's display our name on the left and the navigation links on the right.
---
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.
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.
---
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.
---
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:
.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.
- Create a folder named
blog
inside thepages
folder (pages\blog
) - Create a sample Markdown file inside the
blog
folder
I'm going to create a file named 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 astro@2.0.0
. 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.
---
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).
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.
---
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.
---
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.
---
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?).
---
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.
---
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.
---
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.
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 links to 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.
---
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>← {prev.data.title}</small>
</a>
)
}
{
next && (
<a href={`/blog/${next.slug}`}>
<small>{next.data.title} →</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:
- Fixing the title
- Add SEO to our page (we want the search engines to find our website)
- 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
).
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.
---
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.
---
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.
---
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
.
---
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.
---
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
.
---
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>← {prev.data.title}</small>
</a>
)
}
{
next && (
<a href={`/blog/${next.slug}`}>
<small>{next.data.title} →</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.
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.
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.
- Update the
AppConfig.ts
file with the right information - Update the dummy URL in
astro.config.mjs
file - 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. - Update the social image with the image you would like
public\images\social.png
- 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.
- Vercel - Deploy your Astro Site to Vercel | Docs
- Cloudflare - Astro · Cloudflare Pages docs
- 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.
- Create a
src\components\footer.astro
component and add it to theBase.astro
layout to make it part of every page - Add a
src\pages\now.astro
page to tell your readers about what you are doing now (following The /now page movement | Derek Sivers) - Add analytics to your website (for ex: GoatCounter – open source web analytics)
- 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!
This post was originally published on my blog: Create an Astro blog from scratch
Top comments (0)