Astro is a modern, lightweight frontend framework for building fast, content-driven websites.
It is particularly popular among developers who want to build static websites, and it uses HTML as its primary rendering method while allowing for the integration of multiple JavaScript frameworks such as React, Vue, and Svelte.
Astro gives you the flexibility to create dynamic components while maintaining the simplicity and speed of a static site.
In this tutorial, we’ll be building a blog application powered by Astro 4 and Strapi 5.
Strapi will serve as our content management system (CMS), allowing us to easily manage blog content and it is easily configurable, while Astro will power our frontend.
Here's what our final blog application will look like:
So why are we building a blog application? Building a functional blog application is a great way to learn how modern tools like Astro and Strapi can simplify web development, while exploring concepts like web APIs, database, and frontend design.
Here are some things you'll need to have or do so you can follow along with this tutorial:
By the end of this tutorial, we'll have a fully functional blog application and a solid understanding of how Strapi and Astro work together. Here are a few things you'll learn along the way:
In this section, we'll set up Strapi as the backend CMS that will allow us to set up RESTful API to fetch the blog data.
First, we'll create a directory for our project. This directory will contain the project's Strapi backend folder and the Astro frontend folder.
mkdir blog-app && cd blog-app
Create a new Strapi 5 project using one of the following commands:
npx create-strapi@latest backend --quickstart
OR
yarn create strapi-app@latest backend --quickstart
We'll proceed with the installation and login to our Strapi account when prompted (you'll have to signup instead if this is your first time using Strapi).
This will create a new Strapi 5 project in the 'blog-app' directory and install necessary dependencies.
After creating the Strapi project, we'll navigate to the project folder:
cd backend
Start up the Strapi server (if you're not automatically directed to the Strapi panel on your browser) using:
npm run develop
OR
yarn develop
This will open up the Strapi admin dashboard of our newly created project at this url "http://localhost:1337/admin", which will be provided in our terminal. We'll then input our details, including name, email, and password to assess our admin panel.
This is where we’ll create collections, manage content types, and configure API settings for our blog application.
Here's a breakdown of the collection types we'll create, along with their respective fields:
Name
slug
name
bio
bioImage
title
content
featuredImage
excerpt
readingTime
- (this we will automatically update based on the content)slug
category
author
To get started, we'll first create content type for the collection by navigating to the Content-Type Builder page. We'll click the "Go to the Content type builder
" button on our dashboard or click the fourth icon on the sidebar navigation labelled "Content-Type Builder".
We'll then create our first collection by clicking the "Create new collection type" option. A modal opens up where we'll be prompted to input our first collection name - Category, then hit "Continue".
NB: Name your collection in the singular, as Strapi automatically pluralizes the word.
Next, we'll be asked to select our collection field type. Choose the field types according to the fields we discussed above, i.e, Category collection will have two fields- Name
and slug
.
The Name field will be a text (short text) type, so select the 'Text' option and tick the 'Short text' radio button. The slug field will be a 'UID' type, so select the 'UID' option.
We'll then click the "Save" button located at the top-right hand corner of our page to save the fields created.
Follow the same steps to create the other two collections (Author and Post) and their respective fields.
Authors will manage details about blog post authors. We'll go to Content-Types Builder again, create a new collection type called "Author" and add fields for name (Text), bio (Text), and bioImage (Single Media).
The Post collection holds the main content of our blog. We'll create a collection type called "Post" with fields for title (Text), content (Rich Text-Markddown), excerpt (Text), visibility (Boolean), readingTime (Text), published_at (Date), and any other relevant fields like featuredImage (Media) as shown in the image below.
To link authors with their blog posts, we need to create a relationship. In the Post collection, we'll add a field of type Relation. Then we'll set it to relate one Author to many Posts. This way, each post can only have one author, but an author can have multiple posts.
Similarly, we'll link categories to posts by adding a Relation field in the Post collection. We'll set this to category to many posts, so each post can belong to one category, and each category can relate to many posts.
We'll save oour fields and collections after creating them.
One important thing in setting up a Strapi project is to configure roles and permissions to control who can access our blog data and grant public access to view and perform CRUD operations.
To set up roles and permission to have public access for our front end to fetch the posts, we'll navigate to Settings → Users & Permissions Plugin → Roles → Public.
Click the 'edit' icon in Public.
Toggle the Author, Category, and Post collections under "Permission" and tick the find and findOne checkboxes for each of the collection.
These are the only two permissions we'll need for our blog application.
We'll save our settings by clicking the Save button at the top right-hand corner of the page.
Strapi automatically generates REST API endpoints for each content type. To access any API endpoint in Strapi, add the pluralized name of the collection type to the base URL.
In our case, for us to access all posts, we can use the endpoint: http://localhost:1337/api/posts. This URL retrieves all posts in JSON format.
We can access this endpoint because we made the /posts endpoints public.
The reason we are getting an empty array JSON response for our endpoint is because we haven't added any post entry in our Strapi admin panel.
Before we finalize the Strapi backend for this blog app, let's modify the Post functionality to automatically include the reading time when a post is created or edited. We can achieve this by modifying the Post Collection Model.
Strapi, by default, is extensible, meaning it allows us to modify the functionality as we like. For more on Strapi's customization, visit the Strapi customization documentation.
To add this reading time functionality to the post collection, we'll open our Strapi project in our code editor. Restart using yarn develop
or npm run develop
if the server has stopped. If it hasn't, go on.
Models represent the database's structure. We will be updating the post.js
file which contains lifecycle hooks or functions that are called when Strapi queries are executed or database interactions are performed. In this case, we want to update the reading time whenever we create or update/edit a post.
We'll navigate to src/api/post/content-types/post
and create a new file called lifecycles.js
and paste the following code inside it:
1const readingTime = require("reading-time");
2
3module.exports = {
4 async beforeCreate(event) {
5 console.log("########## BEFORE CREATE ##########");
6 if (event.params.data.content) {
7 event.params.data.readingTime =
8 readingTime(event.params.data.content)?.text || null;
9 }
10 },
11
12 async beforeUpdate(event) {
13 console.log(, "########## BEFORE UPDATE ##########");
14 if (event.params.data.content) {
15 event.params.data.readingTime =
16 readingTime(event.params.data.content)?.text || null;
17 }
18 },
19};
Save the file.
NB: The
lifecycles.js
file defines lifecycle hooks for the post content type in our Strapi project. These hooks enable us to perform specific actions automatically before certain events occur in a database, such as creating or updating a post.
Quick breakdown of what we did in the lifecycles component:
The reading-time library is used to calculate how long it would take to read a given text which is useful for enhancing blog posts with a "Reading Time" indicator.
The beforeCreate
lifecycle hook is triggered before a new post is created in the Strapi database. The hook checks if the content
field exists in the data being created. If content
exists, the reading-time package is used to calculate the text's reading time, which is then assigned to event.params.data.readingTime
. If the content
is empty, readingTime
is set to null.
The beforeUpdate
lifecycle hook is triggered before an existing post is updated in our Strapi database. It performs the same actions as the beforeCreate
hook. It checks to see if the updated data contains content
before recalculating and updating the post's readingTime
.
If we create a new post with content that takes ~3 minutes to read, the readingTime
field in the Strapi is automatically adjusted to "3 min read".
If we later update the content to make it shorter or longer, the readingTime
field will reflect the changes before they are saved.
NB: For a detailed guide on how to work with Strapi hooks, check out our Understanding the different types/categories of Strapi Hooks article.
After this, we'll install the reading-time package by navigating to the backend folder of our project and running any one of these commands:
yarn add reading-time
#OR
npm i reading-time
We'll then stop and restart our Strapi server using the npm run develop
or yarn develop
command so the changes can take effect.
Now we've set up our Strapi backend and configured some settings. Let's add some entries in each of the collection we created in our admin panel.
To do this, we'll navigate to the Content Manager page and click the Create new entry button.
Create a few entries for the Post, Author, and Category collections.
If we check out the api endpoints (http://localhost:1337/api/categories, http://localhost:1337/api/authors, and http://localhost:1337/api/posts), we'll see the entries we made in the admin panel in JSON format, like this:
That is all for the Strapi backend configuration. Since we're sure the Strapi backend and API endpoints are working well, let's move on to the frontend.
Here, we'll get to build the UI and app's functionality using Astro.
npm create astro@latest
This will install the latest versio of Astro, which is Astro 4.
After installation, we'll change the directory into astro-blog, which is the name we gave our Astro project during installation (cd astro-blog
) and install Tailwind CSS, which is the CSS framework we'll use to style the application.
We'll need to run the following command to install Tailwind CSS into our project:
npx astro add tailwind
Answer yes to every question asked and wait for it to finish installing.
To start up the frontend astro project, run this command:
npm run dev
OR
yarn dev
You'll get this welcome page on your browser:
Great! Let's move on.
NB: For those not familiar with Astro or if this is your first time building with Astro, it's recommended you install the Astro VS Code Extension to properly format your Astro code, if VS Code is your preferred IDE.
If you're using any other IDE asides VS Code, check out the Astro's editor setup page to find out the recommended extension for your preferred IDE.
Check out the default folder structure of an Astro project:
Astro comes with its component file. It is a single-file component framework and the '.astro' files houses all the HTML, JS, and styles as one single file.
If you want to learn more about Astro's folder structures and files, check out the Astro project structure documentation.
Astro supports other frameworks and styling options but in this tutorial, we are going to choose HTML, JS, and Tailwind. Check out the various renderers and style options to learn more about them.
We'll build only two pages, the:
Individual blog page, which will display the full content for individual blogs.
We can start by modifying the Layout
component in the layouts folder. This layout component will serve as a template for each page and will include the header and footer (optional), and it will be consistent across all pages, along with a slot for the main content.
To get started, let's create the base layout.
1---
2interface Props {
3 title: string;
4}
5
6const { title } = Astro.props;
7---
8
9<!doctype html>
10<html lang="en">
11 <head>
12 <meta charset="UTF-8" />
13 <meta name="description" content="Astro description" />
14 <meta name="viewport" content="width=device-width" />
15 <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
16 <meta name="generator" content={Astro.generator} />
17 <title>{title}</title>
18 </head>
19 <body>
20 <main>
21 <header class="shadow mb-8">
22 <div>
23 <a
24 href="/"
25 class="block text-black text-2xl font-bold text-center py-4"
26 >
27 Astro Blog
28 </a>
29 </div>
30 </header>
31 <div class="container mx-auto my-4">
32 <slot />
33 </div>
34 </main>
35 </body>
36</html>
37
38<style is:global></style>
Notice how all of this code is wrapped between two ""--- /"" code fences? This is the Frontmatter Script part of the Astro component, which allows dynamic building components.
If you want to learn more about the layouts in Astro, check out this section of the Astro documentation.
Since we've set up roles and permissions, we'll fetch blog data from Strapi's REST API for use in our Astro components.
We'll start by creating 3 components for the UI:
1. BlogGridItem
2. BlogGrid
3. SingleBlogItem
Before we create the blog grid items component, let's create an '.env' variable file in the root of our Astro project to store the Strapi endpoint URL:
1STRAPI_URL=http://localhost:1337
This blog grid item component will represent a single blog post in the grid. It will display the title of the blog, featured image, excerpt, and the author's details.
src/components/BlogGridItem.astro
and paste the following lines of code inside it:1 ---
2const { post } = Astro.props;
3const { slug, featuredImage, title, excerpt, author } = post;
4
5const url = import.meta.env.STRAPI_URL;
6
7const authorImage = author.bioImage?.url || null;
8const postImage = post.featuredImage?.url || null;
9
10console.log("Logging posts here", post)
11---
12
13<div
14 class="rounded-md overflow-hidden shadow-sm p-4 transition-transform h-auto"
15>
16 <a href={`/post/${slug}`}>
17 <div class="rounded-md h-48 w-full overflow-hidden">
18 <img
19 class="object-cover w-full h-full"
20 src={postImage
21 ? url + postImage
22 : "https://via.placeholder.com/1080"}
23 />
24 </div>
25 <div>
26 <h1 class="my-2 font-bold text-xl text-gray-900">{title}</h1>
27 <p class="my-2 text-gray-700 line-clamp-3">{excerpt}</p>
28 </div>
29 <div class="flex justify-between my-4 items-center">
30 <div class="flex space-x-2 items-center overflow-hidden">
31 <img
32 class="inline-block h-8 w-8 rounded-full ring-2 ring-white"
33 src={authorImage
34 ? url + authorImage
35 : "https://via.placeholder.com/1080"}
36 alt="author picture"
37 />
38 <p class="font-medium text-xs text-gray-600 cursor-pointer">
39 {author?.name}
40 </p>
41 </div>
42 <div class="inline-flex rounded-md">
43 <a
44 href={`/post/${slug}`}
45 class="inline-flex items-center justify-center px-5 py-2 border border-transparent text-base font-medium rounded-md text-white bg-yellow-500 hover:bg-yellow-400"
46 >
47 Read article
48 </a>
49 </div>
50 </div>
51 </a>
52</div>
Code explanation:
First we destructured post data to extract key properties of the post object, such as slug for navigation, title for the headline, and excerpt for a short description.
After that, we provided fallback URLs for the featured & bio images so that if the API doesn’t return valid image paths, it will dynamically construct URLs for post and author images.
We added an href link tag to construct a link to the single post page using the post's slug. This makes it possible that any part of a blog item that is clicked takes the user to that particular blog post page.
We then rendered the post metadata (title, excerpt, and author details) to display on the page and of course, styled the page using Tailwind CSS.
The BlogGrid
component will contain a list of BlogGridItem
. This component will display a grid of blog posts. Each post will be rendered using the BlogGridItem
component.
src/components/BlogGrid.astro
and pasting the following code inside it:1---
2import BlogGridItem from "./BlogGridItem.astro";
3const { posts } = Astro.props;
4
5console.log("Posts in parent component:", posts);
6---
7
8<div class="grid grid-cols-3 gap-6">
9 {
10 posts?.data && posts.data.length > 0 ? (
11 posts.data.map((post, index) => <BlogGridItem key={index} post={post} />)
12 ) : (
13 <p>No posts found</p>
14 )
15 }
16</div>
Code explanation:
First, we checked if posts are available (posts?.data)
If posts are available, we'll map through them to render BlogGridItem
. If they aren't available, we'll render a text saying "No posts found" instead.
This component will render the full content of a single blog post.
npm install marked date-fns qs
OR
yarn add marked date-fns qs
The marked library converts Markdown to HTML. If we recall, while we were creating the fields for the Post collection in our Strapi panel, we set the field type for content to be "Rich Text-Markddown" and not "Text". This means we want the content of the blog to be in markdown format, not just as regular text.
We'll use the date-fns library to manipulate JavaScript dates in a browser, while the qs library converts a JavaScript object into a query string that is sent to Strapi. This enables us to define complex populate and filter logic directly in JavaScript.
src/components/SingleBlogItem.astro
and paste the following lines of code inside it:1---
2import { marked } from "marked";
3import { formatDistance, format } from 'date-fns';
4
5const { post } = Astro.props;
6
7const { featuredImage, title, content, author, readingTime, publishedAt } = post;
8const url = import.meta.env.STRAPI_URL;
9
10const authorImage = author.bioImage?.url || null;
11const postImage = post.featuredImage?.url || null;
12---
13
14
15<div class="container mx-auto">
16 <div class="w-full flex justify-start rounded-md">
17 <a
18 href={`/`}
19 class="inline-flex items-center justify-center px-5 py-2 border border-transparent text-base font-medium rounded-md text-white bg-yellow-500 hover:bg-yellow-400"
20 >
21 Back
22 </a>
23 </div>
24 <div class="my-4 text-center">
25 <h1 class="text-center text-4xl leading-tight text-gray-900 my-4 font-bold">
26 {title}
27 </h1>
28 <div class="text-gray-500 flex justify-center items-center space-x-2">
29 <span class="flex space-x-2 items-center overflow-hidden">
30 <img
31 class="inline-block h-8 w-8 rounded-full ring-2 ring-white"
32 src={authorImage
33 ? url + authorImage
34 : "https://via.placeholder.com/1080"}
35 alt="author picture"
36 />
37 <p class="font-medium text-xs text-gray-600 cursor-pointer">
38 {author?.name}
39 </p>
40 </span>
41 <span>·</span>
42 <span>{format(new Date(publishedAt), 'MM/dd/yyyy')}</span>
43 <span>·</span>
44 <span>{readingTime}</span>
45 </div>
46 </div>
47 <div class="rounded-md h-full w-full overflow-hidden">
48 <img
49 class="object-cover w-full h-full"
50 src={postImage
51 ? url + postImage
52 : "https://via.placeholder.com/1080"}
53 />
54 </div>
55 <article class=" prose max-w-full w-full my-4">
56 <div class="rich-text" set:html={marked.parse(content)} />
57 </article>
58</div>
59
60<style is:global>
61
62 /*******************************************
63 Rich Text Styles
64 *******************************************/
65
66 /* Headers */
67 article .rich-text h1 {
68 @apply text-4xl font-bold mb-8 text-gray-800;
69 }
70
71 article .rich-text h2 {
72 @apply text-3xl font-bold mb-8 text-gray-800;
73 }
74
75 article .rich-text h3 {
76 @apply text-2xl font-bold mb-6 text-gray-800;
77 }
78
79 article .rich-text h4 {
80 @apply text-xl font-bold mb-4 text-gray-800;
81 }
82
83 article.rich-text h5 {
84 @apply text-lg font-bold mb-4 text-gray-800;
85 }
86
87 article .rich-text h6 {
88 @apply text-base font-bold mb-4 text-gray-800;
89 }
90
91 /* Horizontal rules */
92 article .rich-text hr {
93 @apply text-gray-800 my-8;
94 }
95
96 article .rich-text a {
97 @apply text-gray-900 underline text-xl leading-relaxed;
98 }
99
100 /* Typographic replacements */
101 article .rich-text p {
102 @apply mb-8 text-xl leading-relaxed text-gray-700;
103 }
104
105 /* Emphasis */
106 article .rich-text strong {
107 @apply font-bold text-xl leading-relaxed;
108 }
109
110 article .rich-text em {
111 @apply italic text-xl leading-relaxed;
112 }
113
114 article .rich-text del {
115 @apply line-through text-xl leading-relaxed;
116 }
117
118 /* Blockquotes */
119 article .rich-text blockquote {
120 @apply border-l-4 border-gray-400 pl-4 py-2 mb-4;
121 }
122
123 /* Lists */
124 article .rich-text ul {
125 @apply list-disc pl-4 mb-4 text-gray-800;
126 }
127
128 article .rich-text ol {
129 @apply list-decimal pl-4 mb-4 text-gray-800;
130 }
131
132 article .rich-text li {
133 @apply mb-2 text-gray-800;
134 }
135
136 article .rich-text li > ul {
137 @apply list-disc pl-4 mb-2;
138 }
139
140 article.rich-text li > ol {
141 @apply list-decimal pl-4 mb-2;
142 }
143
144 /* Code blocks */
145 article .rich-text pre {
146 @apply font-mono text-gray-800 text-gray-800 rounded p-4 my-6;
147 }
148
149 article .rich-text code {
150 @apply font-mono text-gray-800 text-gray-800 rounded px-2 py-1;
151 }
152
153 /* Tables */
154 article .rich-text table {
155 @apply w-full border-collapse text-gray-800 my-6;
156 }
157
158 article .rich-text th {
159 @apply text-gray-800 text-left py-2 px-4 font-semibold border-b text-gray-800;
160 }
161
162 article .rich-text td {
163 @apply py-2 px-4 border-b text-gray-800;
164 }
165
166 /* Images */
167 article .rich-text img {
168 @apply w-full object-cover rounded-xl my-6;
169 }
170
171 /* Custom containers */
172 article .rich-text .warning {
173 @apply bg-yellow-100 border-yellow-500 text-yellow-700 px-4 py-2 rounded-lg mb-4;
174 }
175</style>
Code explanation:
First, we destructured the post data to retrieve the data necessary to populate the single post view, including the post's main content and metadata (e.g., title, author, content, publicationDate, featuredImage, readingTime, etc).
We then created a fallback for the media (featuredImage and bioImage) to ensure the UI doesn’t break if image URLs are unavailable. this will set a default image for the post and bio, if the image URLs aren't available
We also formatted the post's publication date publishedAt
using the date-fns library we previously installed.
To render the full content for each post, we used the marked library to render Markdown content into HTML (set:html={marked.parse(content)})
. This displays our post metadata, including title, featured image, and content.
After all that, we styled the page.
We'll now need to create the page to put all our components together.
One thing to note is that any '.astro' file created inside the pages directory will be treated as a route because Astro is a file-based routing framework.
Even '.md' files created inside the pages directory will be treated as routes, but other file extensions would not be treated as routes. Go through this Astro pages documentation to learn more about pages in Astro.
The index.astro component is the entry point for the blog application. This is where the blog posts from Strapi will be fetched, and will pass them to the BlogGrid
component, and wrap everything in a Layout
.
To start building our main page, we'll open the index.astro
file and paste these lines of code inside it:
1---
2import qs from "qs";
3
4import Layout from "../layouts/Layout.astro";
5import BlogGrid from "../components/BlogGrid.astro";
6
7let baseUrl = import.meta.env.STRAPI_URL + "/api/posts";
8
9const query = qs.stringify({
10 populate: {
11 featuredImage: {
12 fields: ["name", "width", "height", "url"],
13 },
14 author: {
15 populate: {
16 bioImage: {
17 fields: ["name", "width", "height", "url"],
18 },
19 },
20 },
21 category: {
22 populate: true,
23 },
24 },
25}, { encode: false });
26
27const url = `${baseUrl}?${query}`;
28
29const posts = await fetch(url)
30 .then((response) => response.json())
31 .catch((error) => {
32 console.error("Error fetching posts:", error);
33 return null;
34 });
35
36console.log(posts);
37---
38
39<Layout title="Welcome to Astro.">
40 <BlogGrid posts={posts} />
41</Layout>
Code explanation:
We created a baseUrl
that points to the posts endpoint in our Strapi API.
The qs.stringify
function builds a query string that requests additional data (like featured images, authors, and categories). By using the populate feature in Strapi, we were able to include related entities in the API response.
The query builder here requests the featuredImage
field of the post, includes the author relation, populates their bioImage
field, and keeps the query human-readable using the { encode: false }
flag. Refer to the official Strapi Interactive Query Builder documentation for the latest usage.
We fetched the related data like the featured image, author bio image, and category details, and populated it.
We then used the fetch
method to fetch data from Strapi API using the url. If an error occurs, it will be logged and will return 'null'.
Finally, we wrapped the content in a Layout
and passed the fetched posts to BlogGrid
component to render and display on the browser.
Note: The qs.stringify
method builds a query string for API requests. In Strapi, this query can include fields like populate, filters, and sort.
Our blog application is almost ready.
Let's move on to create the last component, which is the individual post item page.
Astro uses file-based routing which supports dynamic parameters using bracket notation into the filename. This allows us to map a specific file to make different routes.
We'll create a [slug].astro
component which will be the dynamic route for individual blog posts. It will fetch data for a specific post based on its slug and render it using the SingleBlogItem
component.
[slug].astro
component inside it. This component has bracket notation so the route will be mapped to anything that follows /post. Eg: /post/hello-world
, /post/lorem-ipsum
, etc.Paste these lines of code inside the [slug].astro
component:
1---
2import qs from "qs";
3import Layout from "../../layouts/Layout.astro";
4import SingleBlogItem from "../../components/SingleBlogItem.astro";
5
6export async function getStaticPaths() {
7 const baseUrl = import.meta.env.STRAPI_URL + "/api/posts";
8
9 const query = qs.stringify({
10 populate: {
11 featuredImage: {
12 fields: ["name", "width", "height", "url"],
13 },
14 author: {
15 populate: {
16 bioImage: {
17 fields: ["name", "width", "height", "url"],
18 },
19 },
20 },
21 category: {
22 populate: true,
23 },
24 },
25 }, { encode: false });
26
27 const url = `${baseUrl}?${query}`;
28
29 const data = await fetch(url)
30 .then((response) => response.json())
31 .catch((error) => {
32 console.error("Error fetching posts:", error);
33 return null;
34 });
35
36 return data.data.map((post) => ({
37 params: { slug: post.slug },
38 props: { post },
39 })) || [];
40}
41
42const { post } = Astro.props;
43---
44
45<Layout title="Blog">
46 <SingleBlogItem post={post} />
47</Layout>
Code explanation:
Similar to the index.astro
component, we created a baseUrl
that points to the posts endpoint in our Strapi API.
We then created the qs.stringify
function to build a query string that requests additional data (like featuredImage, author, and category)
The getStaticPaths
function fetches all posts from Strapi to generate static paths for each blog post dynamically during the build process.
Each path corresponds to a slug, which is a unique identifier for the blog post.
This then extracts the slug for each post and assigns the full post data as props, allowing each post to have its own URL, such as "/post/blog-slug".
Finally, we wrapped the post content in a Layout and passed the post data to the SingleBlogItem
component to render and display the individual blog post.
The index.astro
fetches all posts and passes them to BlogGrid.
The BlogGrid.astro
maps over the posts and passes each post to BlogGridItem.
The BlogGridItem.astro
component displays a summary of the post with links to its detailed page.
The [slug].astro
component dynamically generates pages for individual blog posts.
Lastly, the SingleBlogItem.astro
component displays the full details of a blog post.
And that's it! We're done.
Let's stop our development server and start it up again so that any changes we made can be applied.
Run the npm run dev
or yarn dev
command.
Here's what our blog application should look like this now:
We’ve succesfully built a fully functioning blog application using Strapi 5 for our backend, Astro 4 for the frontend, and Tailwind CSS to style the blog application.
In this tutorial, we were able to leverage Strapi’s easy-to-manage content capabilities and Astro’s fast and simple rendering to build a functional blog application.
You see how easy it was for us to complete our blog application's backend functionality using Strapi without having to spend so much time building the APIs.
You can find the complete code for this tutorial here on GitHub.
Juliet is a developer and technical writer whose work aims to break down complex technical concepts and empower developers of all levels.