Astro & Strapi Website Tutorial: Part 3 - Project Build
Introduction
In the previous Part of this tutorial series, we looked at an introduction to Strapi headless CMS, Strapi installation, Strapi Content type builder, Collection type, enabling API access, internationalization in Strapi, Dynamic zones, and more.
In this final Part, we will build a Multilingual Blog Website using Astro and Strapi! Let's go!
Tutorial Outline
- Part 1: Introduction to Astro
- Part 2: Introduction to Strapi
- Part 3: Building the Main Project
Set up an Astro Project
Before we begin, we must create a basic Astro project. To see the installation process, please refer to Part 1.
npm create astro@latest ui
As seen above, the name of our project is ui
.
Create CSS Styles
Navigate to the public
folder and create a new folder called styles
. Inside the new folder styles
, create a new file styles.css
and add the following code:
html,
body {
height: 100%;
margin: 0;
padding: 0;
font-family: "Cinzel", serif;
background-color: #f0e5d8;
}
nav {
background-color: #1a1a2e;
color: #d4af37;
text-align: center;
padding: 10px 0;
position: fixed;
width: 100%;
top: 0;
left: 0;
z-index: 1000;
font-size: 18px;
font-weight: bold;
border-bottom: 2px solid #d4af37;
}
nav a {
color: #d4af37;
text-decoration: none;
padding: 0 15px;
transition: color 0.3s ease, transform 0.3s ease;
}
nav a:hover {
color: #eaeaea;
transform: scale(1.1);
}
a {
color: #645452;
text-decoration: none;
transition: color 0.5s ease-in-out, transform ease-in-out;
}
a:hover {
color: #861657;
transform: scale(1.05);
}
main {
display: flex;
justify-content: center;
align-items: center;
padding-top: 40px;
color: #3e2723;
box-sizing: border-box;
}
section {
display: flex;
flex-direction: column;
align-items: center;
margin: 20px;
}
The CSS code above represents the styles and color scheme we will use for this project. Feel free to reuse these styles or modify them as you see fit.
Building Project Layout Files
Inside the src
folder, we will create a layout folder called layouts
, which will contain some of our layout files.
Create the Head Layout
Here, we will only utilize the head
HTML element. Inside the layouts
folder, create a new file called Head.astro
and add the following code:
---
const { title, description } = Astro.props;
---
<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} />
<meta name="description" content={description} />
<title>{title}</title>
</head>
We notice title
and description
in the code above. Just like in other frameworks, we can pass and access the properties or props of a given component. This is achieved using the props
property of the global Astro
object.
In this case, we accessed the title
and description
as the props and added them to the meta
and title
tags. This is a practical example of how to pass these props to the Head
component, a key component in the Astro framework that we'll delve into in the next section.
Create the Navigation Layout
This time around, we will create a layout component for navigation. Inside the layouts
folder, create another file called Nav.astro
and add the following code:
<nav>
<a href="/"> Home </a>
|
<a href="/blog">Blog </a>๐ฌ๐ง |
<a href="/blog/es">Blog ๐ช๐ธ</a>
</nav>
Because we will be building a multilingual blog, we have created links to the home page, the blog page in English, and the blog page in Spanish.
We will modify this later when we delve deeper into this project.
Create the Main Layout File
Now, inside the main layout, we will add both the Head
and Nav
components.
Inside the layouts
folder, create a new file called Layout.astro
and add the following code:
---
import Head from "./Head.astro";
import Nav from "./Nav.astro";
const { title = "Just a title", description = "Adescription" } = Astro.props;
---
<html>
<Head {title} {description} />
<link
href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;700&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="/styles/styles.css" />
<body>
<Nav />
<main>
<section>
<slot />
</section>
</main>
</body>
</html>
We imported the Head
and Nav
components in the main layout file above. We also created the properties title
and description
, which we then passed to the Head
component.
Notice the relationship between the title
variable and the prop we passed to the Head
component. We didn't need to write it as title = "Just a title"
. This is because the value of the title
variable is the same as the prop we are sending to the Head
component, illustrating the concept of prop passing in React or similar frameworks.
Preview the Changes
To see our changes, we need to modify the index.astro
file inside the pages
folder. Replace the code inside the index.astro
file with the code below:
---
import Layout from "../layouts/Layout.astro";
---
<Layout>
<h1>Hello, World!</h1>
</Layout>
We imported the main layout file Layout.astro
from the code above and wrapped an h1
element inside it. When we now preview the home page, this is what we will see:
As seen above, we have the home page, the English blog, and the Spanish blog pages.
Next, let's build the landing page!
Create Landing Page
Recall that we have access to the landing page content coming from Strapi CMS when we open up the URL http://localhost:1337/api/pages?populate[LandingPage][populate]=*
on our browser.
The Astro docs have a dedicated section that provides clear and concise steps for integrating Astro and Strapi. As we proceed with building our application, we can confidently follow these steps in the documentation guide. The first of these steps is adding the Strapi URL to the environment variable.
Add Strapi URL to Environment Variable
Inside the root of our project folder, ui
, create a new file .env
and add the following environment variable.
STRAPI_URL='http://localhost:1337'
We added our Strapi URL running on localhost. It is important to know that we could also modify this to our Strapi URL running on production or development.
Create Fetch API
Next, let's make API requests from Astro to Strapi CMS!
It's crucial to create a new folder called lib
inside the src
folder. Equally important is to create a file called strapi.js
inside this new folder. These are the key components where we will implement the fetchApi
function as per the Astro-Strapi documentation we mentioned above.
export default async function fetchApi({
endpoint,
query,
wrappedByKey,
wrappedByList,
page,
locale,
}) {
if (endpoint.startsWith("/")) {
endpoint = endpoint.slice(1);
}
const url = new URL(`${import.meta.env.STRAPI_URL}/api/${endpoint}`);
if (locale) {
url.searchParams.append("locale", locale);
}
if (query) {
Object.entries(query).forEach(([key, value]) => {
url.searchParams.append(key, value);
});
}
if (page) {
url.searchParams.append(`populate[${page}][populate]`, "*");
} else {
url.searchParams.append("populate", "*");
}
const res = await fetch(url.toString());
let data = await res.json();
if (wrappedByKey) {
data = data[wrappedByKey];
}
if (wrappedByList) {
data = data[0];
}
if (page) {
data = data[0]["attributes"][page];
}
return data;
}
Here is what the code above does:
- We extended the
fetchApi()
function from the Astro documentation to includepage
andparameter
. - It takes an object parameter containing
endpoint
,query
,wrappedByKey
,wrappedByList
,page
, andlocale
. - When we make a request to Strapi via the URL :
${import.meta.env.STRAPI_URL}/api/${ endpoint }
, we will be able to pass some paramters such asendpoint
,locale
, andquery
to the requesting URL. -
locale
: This is the locale we want to get the contents. Just like we already know, this could be English or Spanish. Without specifying the value of this, we get by default the English blog posts. -
query
: If there is a query we want to pass, this will contain it. This can be a query containing filters, pagination, etc. You can learn more here. -
page
: Refers to the page we will be looking for when we want to paginate. - After checking and adding parameters we want to include in the requesting URL, the next step will be to make a
GET
request using thefetch
API. The response is transformed using thejson()
function. - Finally, we return the transformed response as
data
. - Later on, we will talk about what the
wrappedByKey
andwrappedByList
perform in the code.
NOTE: In the code above, we used
import.meta.env.STRAPI_URL
to access theSTRAPI_URL
environment variable.
In summary, the code above is a helper function that allows us to request the Strapi URL from the Astro UI we are building.
Create About and Hero Components
As a reminder, in our previous work with Strapi Dynamic Zones, we crafted two essential components for our landing page. The hero
component, responsible for the eye-catching top section, and the about
component, providing a detailed description of our project.
In light of that, we should also display these two components on our landing page's UI. So, create a folder called components
inside the src
and create the following files.
The Hero Component
The hero
component encapsulates both the heroText
and heroDescription
. To begin, create a file named Hero.astro
within the components
directory and insert the following code:
---
const { heroText, heroDescription } = Astro.props;
---
<h1>{heroText}</h1>
<p>{heroDescription}</p>
In the code above, we get the heroText
and heroDescription
as props of the Hero.astro
component and display them on the UI using the h1
and p
HTML elements.
The About Component
The about
component comprises the aboutText
andaboutPhoto
.
When we make request to the landing page, and we access the about
component, we realize that the aboutPhoto
comes with a URL which represents an image as shown below.
The question now is, how do we access this data? Luckily, Astro has a built-in Image component that we will use to render this image on our UI.
Start by creating a file called About.astro
in the same directory as the Hero.astro
file and populate it with the following code:
---
import { Image } from "astro:assets";
const { aboutText, aboutPhotoURL } = Astro.props;
const photoURL = `${import.meta.env.STRAPI_URL}${aboutPhotoURL}`;
---
<p>{aboutText}</p>
<Image src={photoURL} alt="Me" width="400" height="400" />
Display Page Data from Strapi Dynamic Zones
We have created both the About
and Hero
components. Let's display them on the UI. Start by modifying the index.astro
file by importing the two components:
---
import Layout from "../layouts/Layout.astro";
import Hero from "../components/Hero.astro";
import About from "../components/About.astro";
import fetchApi from "../lib/strapi";
const pageData = await fetchApi({
endpoint: "pages",
page: "LandingPage",
wrappedByKey: "data",
});
const heroData = pageData.find((pd) => pd.__component === "hero.hero");
const { heroText, heroDescription } = heroData;
const aboutData = pageData.find((pd) => pd.__component === "about.about");
const { aboutText } = aboutData;
const {
aboutPhoto: {
data: {
attributes: { url: aboutPhotoURL },
},
},
} = aboutData;
---
<Layout>
<Hero {heroText} {heroDescription} />
<About {aboutText} {aboutPhotoURL} />
</Layout>
Here is what the code above does:
- We import the
fetchAPI
function, theHero
andAbout
components and theLayout
components. -
We create
pageData
which is the data we will be using to get the data of the landing page. And then we added the following properties:- We specify the
endpoint
to bepages
. Recall that this is the endpoint for the landing page. And that we are trying to construct the URL :http://localhost:1337/api/pages?populate[LandingPage][populate]=*
-
page
here represents the page we want to populate, and which is theLandingPage
we created using the Strapi Dynamic Zones. -
wrappedByKey
here is used because anytime we make a request to Strapi, we get eevery information wrapped by thedata
property. So with thewrappedByKey
, we can reference the information returned without having to manually go into thedata
property.
- We specify the
Because the
hero
andabout
components have the property__component
as shown below, which we used to differentiate each component and their data, we used it to extract each component's data.
- Finally, we passed the extracted data
aboutText
,aboutPhoto
,heroText
, andheroDescription
as props to theAbout
andHero
components.
If we view the result on our browser, we can see that we now have a landing page built using Strapi Dynamic Zones.
Implement Pagination
The next thing we want to do is to display each blog post in English. However, because we don't want to display all articles in a single page, we instead implement pagination.
When we make an API call to http://localhost:1337/api/blogs?populate=*
, we will see that we get the English and Spanish versions, images and every other thing else. Along with it, Strapi sends a meta information with pagination as shown below:
The meta information returned can actually be manipulated by sending a query to Strapi. For example, we could specify that the pageSize
should be returned as 2, 5, or 10. It could be any way we want it.
Another advantage we have is that Astro, as we can learn from the docs, has built-in support for Pagination. This means that implementing Pagination is a straightforward process, giving you the confidence and capability to handle large data sets with ease.
Step 1: Create The blog
Folder
Create a folder called blog
inside the pages
folder. This folder will be the route to all blog posts.
Step 2: Create the [page].astro
File
Create a new file called [page].astro
inside the blog
folder. This file will help us make a fetch API request to get us all the blog posts, the pagination information, and the titles of the blog posts. Inside this file, add the following code:
---
import type { GetStaticPaths } from "astro";
import Layout from "../../layouts/Layout.astro";
import fetchApi from "../../lib/strapi";
export async function getStaticPaths({ paginate }) {
const response = await fetchApi({
endpoint: "blogs",
wrappedByKey: "data",
});
const articles = response;
return paginate(articles, { pageSize: 2 });
}
const { page } = Astro.props;
---
<Layout>
<h1>Page {page.currentPage}</h1>
<ul>
{page.data.map((article) => <li>{article.attributes.title}</li>)}
</ul>
{page.url.prev ? <a href={page.url.prev}>Previous Page</a> : null} |
{page.url.next ? <a href={page.url.next}>Next Page</a> : null}
</Layout>
In the code above:
- We make a fetch request to
/blogs
from the Strapi CMS and grab all the articles. - We then paginate the articles 2 per page.
- Using the
page
propertyAstro
object, we getcurrentPage
. ThecurrentPage
here refers to the page we are currently on. - Using the same
page
property, we map it and display each article as a list.
With the above implementation, we saw that when we access the /blog
URL together with the page number, as shown below, we can get the pagination of the articles.
- We proceeded by adding the adding some links which will allow us to go back and forth between the pages using the
page.url.prev
andpage.url.next
.
Notice that when we click on the previous page or the next page, the previous page goes to page 1. When we click on the next page, it goes to page 2, and so on.
Display Article Content
Now that we have implemented pagination, how do we view an article's content? Using each article's slug, we can create a dynamic slug page to view its content dynamically.
Head to the blog
folder and create a file called [slug].astro
for our dynamic endpoint. Inside the new file, add the following code:
---
import Layout from "../../layouts/Layout.astro";
import fetchApi from "../../lib/strapi";
export async function getStaticPaths() {
const articles = await fetchApi({
endpoint: "blogs",
wrappedByKey: "data",
});
return articles.map((article) => ({
params: { slug: article.attributes.slug },
props: article,
}));
}
const article = Astro.props;
---
<Layout>
<h1>{article.attributes.title}</h1>
</Layout>
In the code above, we implemented the following:
- We import the
Layout
component. - Because we would like to generate all these information ahead of time, we make use of the
getStaticPaths
function. - Using the
fetchApi
function, we make aGET
request to theblogs
endpoint. - Recall that in the beginning of the tutorial, the Part 1, we said that for dynamic endpoints, the
getStaticPaths
must return aparams
object. So we loop through all the articles and returned their slugs. Also, we ruturnedarticles
as the props so will can loop through it and display them on the UI.
Next, we modify the [page].astro
so that when we click on a blog article, we will be redirected to the slug page on /blog/[the slug of the blog]
.
---
import type { GetStaticPaths } from "astro";
import Layout from "../../layouts/Layout.astro";
import fetchApi from "../../lib/strapi";
export async function getStaticPaths({ paginate }) {
const response = await fetchApi({
endpoint: "blogs",
wrappedByKey: "data",
});
const articles = response;
return paginate(articles, { pageSize: 2 });
}
const { page } = Astro.props;
---
<Layout>
<h1>Page {page.currentPage}</h1>
<ul>
{
page.data.map((article) => (
<li>
<a href={`/blog/${article.attributes.slug}`}>{article.attributes.title}</a>
</li>
))
}
</ul>
{page.url.prev ? <a href={page.url.prev}>Previous Page</a> : null} |
{page.url.next ? <a href={page.url.next}>Next Page</a> : null}
</Layout>
When we click on an article as shown below, we see its page content, which is the title. Bravo!
Format Markdown Content to HTML
As we know, the content
data of each article is in a markdown format. So, we need to display the content in HTML format.
The first step is to install marked. This is a markedown parser that will convert our markdown content into HTML. Run the command below to install marked
.
Step 1: Install marked
npm i marked
Step 2: Create Markdown Component
Create a separate component to display the parsed mardown content. So inside the components
folder, create a new file called MarkdownComponent.astro
and add the following code:
---
import { marked } from "marked";
const content = marked.parse(Astro.props.content);
---
<article set:html={content} />
In the code above, we imported the installed marked
package. We used it to format the content
props, which we will pass on in the next section, and then displayed the formatted content in the article
HTML element.
Step 3: Update the Slug/Content Page
Next, we update the [slug].astro
page to display the Markdown
component. This allows us to display the content in HTML format instead of Markdown, which is the default.
---
import MarkdownComponent from "../../components/MarkdownComponent.astro";
import Layout from "../../layouts/Layout.astro";
import fetchApi from "../../lib/strapi";
export async function getStaticPaths() {
const articles = await fetchApi({
endpoint: "blogs",
wrappedByKey: "data",
});
return articles.map((article) => ({
params: { slug: article.attributes.slug },
props: article,
}));
}
const article = Astro.props;
---
<Layout>
<h1>{article.attributes.title}</h1>
<MarkdownComponent content={article.attributes.content} />
</Layout>
When this is done, we should now see the content of a blog post.
Display Article Image
As we know, each article has an image. So update the [slug].asto
file to include it.
---
import { Image } from "astro:assets";
import MarkdownComponent from "../../components/MarkdownComponent.astro";
import Layout from "../../layouts/Layout.astro";
import fetchApi from "../../lib/strapi";
export async function getStaticPaths() {
const articles = await fetchApi({
endpoint: "blogs",
wrappedByKey: "data",
});
return articles.map((article) => ({
params: { slug: article.attributes.slug },
props: article,
}));
}
const article = Astro.props;
---
<Layout>
<Image
src={import.meta.env.STRAPI_URL +
article.attributes.image.data[0].attributes.formats.small.url}
width="500"
height="500"
alt={article.attributes.title}
/>
<h1>{article.attributes.title}</h1>
<MarkdownComponent content={article.attributes.content} />
</Layout>
In the image below, we can see the article's image displayed.
Implement Internationalization
So far, we have only worked on the English blog posts. Let us implement Internationalization to enable us to render blog content in Spanish.
The Astro documentation shows that Astro has support for Internationalization routing.
Step 1: Modify the astro.config.mjs
File
Start by modifying the astro.config.mjs
file. We need to specify the i18n
object where we specify the default locale defaultLocale
to be en
, which is English. Any other available locales could be Engilish and Spanish, en
and es
.
import { defineConfig } from "astro/config";
// https://astro.build/config
export default defineConfig({
i18n: {
defaultLocale: "en",
locales: ["es", "en"],
},
});
Step 2: Create Page for Blog in Spanish
Next is to create the route for Spanish blog posts. Inside the existing blog
folder, create a new folder called es
. Inside this new folder, create the two files [page].astro
and [slug.astro]
.
The two files we have created are pretty much the same as the ones we have created initially. The only difference is that we want it displayed in Spanish and that the API request will include locale
as "es"
.
For this reason, we will copy the codes inside the blog/[page].astro
and blog/[slug].astro
and paste into the new blog/es/[page.astro]
and blog/es/[slug].astro
files respectively.
So, add the following code in the blog/es/[page].astro
file.
---
import { getRelativeLocaleUrl } from "astro:i18n";
import Layout from "../../../layouts/Layout.astro";
import fetchApi from "../../../lib/strapi";
export async function getStaticPaths({ paginate }) {
const response = await fetchApi({
endpoint: "blogs",
wrappedByKey: "data",
locale: "es",
});
const articles = response;
return paginate(articles, { pageSize: 2 });
}
const { page } = Astro.props;
---
<Layout>
<h1>Pรกgina {page.currentPage}</h1>
<ul>
{
page.data.map((article) => (
<li>
<a href={`/blog/${getRelativeLocaleUrl('es')}${article.attributes.slug}`}>
{article.attributes.title}
</a>
</li>
))
}
</ul>
{page.url.prev ? <a href={page.url.prev}>Pรกgina Anterior</a> : null} |
{page.url.next ? <a href={page.url.next}>Pรกgina Siguiente</a> : null}
</Layout>
The code above requests the Strapi endpoint localhost:1337/api/blogs?populate=*&locale=es
. The response will only be the Spanish articles in our Strapi backend. We modified the English contents of the file to Spanish and then used the getRelativeLocaleUrl('es')
from Astro Internationalization to get the right path to the article content, which is /blog/es/[the slug]
.
In the blog/es/[slug].astro
file, we only need to add locale: "es"
to the fetchApi
function.
NOTE: Remember to modify the layout and component imports now that we are one directory deeper into our project.
With that, when we access /blog/es/1
for example, we get the blog in Spanish!
Implement Relative Time
We want to print out the date an article was published, both in English and Spanish, using the JavaScript internationalization API. Luckily, this data comes back as metadata from Strapi headless CMS, as seen below.
Create a Helper Function
Next, we will create a helper function to generate the relative time an article was published.
Replace the code inside /blog/[slug].astro
with the following code:
---
import { Image } from "astro:assets";
import MarkdownComponent from "../../components/MarkdownComponent.astro";
import Layout from "../../layouts/Layout.astro";
import fetchApi from "../../lib/strapi";
export async function getStaticPaths() {
const articles = await fetchApi({
endpoint: "blogs",
wrappedByKey: "data",
});
return articles.map((article) => ({
params: { slug: article.attributes.slug },
props: article,
}));
}
const article = Astro.props;
function formatRelativeTime(dateString) {
const date = new Date(dateString);
const now = new Date();
const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
const daysDifference = Math.round((now - date) / (1000 * 60 * 60 * 24));
if (daysDifference < 1) {
const hoursDifference = Math.round((now - date) / (1000 * 60 * 60));
if (hoursDifference < 1) {
const minutesDifference = Math.round((now - date) / (1000 * 60));
return rtf.format(-minutesDifference, "minute");
}
return rtf.format(-hoursDifference, "hour");
}
return rtf.format(-daysDifference, "day");
}
const relativeTime = formatRelativeTime(article.attributes.publishedAt);
---
<Layout>
<Image
src={import.meta.env.STRAPI_URL +
article.attributes.image.data[0].attributes.formats.small.url}
width="500"
height="500"
alt={article.attributes.title}
/>
<h1>{article.attributes.title}</h1>
<p>Article written {relativeTime}</p>
<MarkdownComponent content={article.attributes.content} />
</Layout>
Here is what we did in the code above:
- We create a function called
formatRelativeTime
. - The
dateString
parameter is passed to the function created above. In this case, this is thepublishedAt
meta information provided by Strapi. - By utilizing the JavaScript Internationalization API
RelativeTimeFormat
, we set it to English by passing'en'
. - The function then calculates the days difference
daysDifference
, the hours differencehoursDifference
and the minutes differenceminutesDifference
. - With the
relativeTime
variable, we get the true relative time of which the article was published. This could be "2 days ago", "an hour ago", "2 weeks ago", etc. - And finally, we display it on the blog post.
We can see the result of the relative time in the image below:
Copy the helper function above and add it to the blog/es/[slug].asto
file. We will make only two changes. The first is to change the JavaScript Internationalization API from English "en" to Spanish
"es". The second is to change "Article Written" to "Artรญculo escrito," which is from the English version to the Spanish version.
NOTE: For redundancy and the DRY principle of Software Development, we might want to create the helper function inside the
lib
folder, import it, and call it in these files.
Below is what we should see in the Spanish blog.
Implement Pre-Rendering and Server-Side Rendering
So far, we have been doing Static Site Generation (SSG). This means that articles will still have the same information they had during the build process when we built and hosted this site. If we visit an article 100 days later, we will still see it as '2 DAYS AGO' and so on.
To correct this, there are two approaches:
- Set up a Hook on a Deployment Platform: Let's say we deploy this project to Netlify. This hook will fire up a redeployment or rebuilding of our site when our blog posts change. For example, when we post a new blog, the hook will fire up, and our site will be rebuilt to display the new blog post. We can achieve this with Netlify.
- Change Astro Default Behaviour: We could change Astro's default behavior from Static Site Generation (SSG) to Server-Side Rendering (SSR).
Change Astro Default Behaviour
As we know, Astro uses Static Site Generation by default. Also, we learned in Part 1 of this tutorial series that to change Astro from Static Site Generation (SSG) to Server-Side Rendering (SSR), we will need an Adapter. In this case, we will use the node
adapter.
Let's proceed by quitting the running development server and installing the node
adapter:
npx astro add node
The command above will add the node
adapter to the configuration file and set the output to be server
. After successful installation, we will restart our Astro development server once again by running npm run dev
.
Pre-render Some Pages
With the code below, we can make a page pre-rendered.
export const prerender = true;
Add the code above to the top of the following files:
index.astro
blog/[page].astro
blog/es/[page].astro
Our focus will be on blog/[slug].astro
and blog/es/[slug].astro
files because they have the getStaticPaths
function which is for Static Site Generation(SSG) and not Server-Side Rendering(SSR).
Add Server-Side Rendering
Inside the blog/[slug].astro
and blog/es/[slug].astro
files, we can find the getStaticPaths
. Recall that this function is only used for Static Site Generation. Because data can change, for example, when we create a new post, we want to add Server-Side rendering to these pages.
Modify The English Blog
Modify the blog/[slug].astro
file by replacing the code inside it with the following:
---
import { Image } from "astro:assets";
import MarkdownComponent from "../../components/MarkdownComponent.astro";
import Layout from "../../layouts/Layout.astro";
import fetchApi from "../../lib/strapi";
// export async function getStaticPaths() {
// const articles = await fetchApi({
// endpoint: "blogs",
// wrappedByKey: "data",
// });
// return articles.map((article) => ({
// params: { slug: article.attributes.slug },
// props: article,
// }));
// }
// const article = Astro.props;
let article;
const { slug } = Astro.params;
try {
article = await fetchApi({
endpoint: "blogs",
wrappedByKey: "data",
wrappedByList: true,
query: {
"filters[slug][$eq]": slug || "",
},
});
} catch (error) {
Astro.redirect("/404");
}
function formatRelativeTime(dateString) {
const date = new Date(dateString);
const now = new Date();
const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
const daysDifference = Math.round((now - date) / (1000 * 60 * 60 * 24));
if (daysDifference < 1) {
const hoursDifference = Math.round((now - date) / (1000 * 60 * 60));
if (hoursDifference < 1) {
const minutesDifference = Math.round((now - date) / (1000 * 60));
return rtf.format(-minutesDifference, "minute");
}
return rtf.format(-hoursDifference, "hour");
}
return rtf.format(-daysDifference, "day");
}
const relativeTime = formatRelativeTime(article.attributes.publishedAt);
---
<Layout>
<image
src="{import.meta.env.STRAPI_URL"
+
article.attributes.image.data[0].attributes.formats.small.url}
width="500"
height="500"
alt="{article.attributes.title}"
/>
<h1>{article.attributes.title}</h1>
<p>Article written {relativeTime}</p>
<MarkdownComponent content="{article.attributes.content}" />
</Layout>
In the code above:
- We comment out the
getStaticPaths
function. - We get the slug from the global
Astro.params
property. Remember that this is a dynamic page, so theslug
can change at any request. - With a
try/catch
block, we get all the blog posts using thefetchApi
function. This time around, notice that we added aquery
andwrappedByList
. - The
wrappedByList
is a boolean parameter to โunwrapโ the list returned by Strapi headless CMS, and return only the first item. - The
query
is going to be sent to the Strapi to filter out only theslug
that was passed in params, otherwise it should be empty. - If there is an error, we will ask Astro to redirect us to the
/404
page.
Modify The Spanish Blog
Inside the blog/es/[slug].astro
file, we will repeat the same steps but add the locale
parameter to the fetchApi
function and give it the value 'es'
to tell Strapi that we are fetching the Spanish blog posts. Also, we will modify the links as shown in the code below:
---
import Layout from "../../../../layouts/Layout.astro";
import fetchApi from "../../../../lib/strapi";
const page = parseInt(Astro.params.page);
const postsPerPage = 2;
const response = await fetchApi({
endpoint: "blogs",
locale: "es",
query: {
"pagination[page]": page,
"[pagination][pageSize]": postsPerPage,
},
});
const articles = response.data;
const pagination = response.meta.pagination;
---
<Layout>
<h1>Page {page}</h1>
<ul>
{
articles.map((article) => (
<li>
<a href={`/blog/es/${article.attributes.slug}`}>
{article.attributes.title}
</a>
</li>
))
}
</ul>
{
pagination.page > 1 && (
<a href={`/blog/es/page/${pagination.page - 1}`}>Previous Page</a>
)
} | {
pagination.page < pagination.pageCount && (
<a href={`/blog/es/page/${pagination.page + 1}`}>Next Page</a>
)
}
</Layout>
Add a New Blog Post
As we said, Server-Side Rendering will allow us to update the UI in real time. For example, when we add a new blog post, we should see the relative time as something very recent. See the image below after we added a new blog post.
When we visit the application with its slug URL, we notice that it was created 12 minutes ago. Bravo! This is possible only through Server-Side Rendering!
However, without rebuilding our application, notice that the new article doesn't appear in the pagination.
In the next section, we will fix this!
Final Steps (Fixing Pagination)
Recall that we implemented pre-rendering in the blog/[page].astro
and blog/es/[page].astro
files. This means that they are going to do Static Site Generation, and as such, if you create an article, even though it will show up, it won't come up in the pagination.
Start by replacing the code inside the blog/[page].astro
with the code below:
---
import Layout from "../../layouts/Layout.astro";
import fetchApi from "../../lib/strapi";
const page = parseInt(Astro.params.page);
const postsPerPage = 2;
const response = await fetchApi({
endpoint: "blogs",
query: {
'pagination[page]': page,
'[pagination][pageSize]': postsPerPage,
},
});
const articles = response.data;
const pagination = response.meta.pagination;
---
<Layout>
<h1>Page {page}</h1>
<ul>
{
articles.map((article) => (
<li>
<a href={`/blog/${article.attributes.slug}`}>
{article.attributes.title}
</a>
</li>
))
}
</ul>
{
pagination.page > 1 && (
<a href={`/blog/${pagination.page - 1}`}>Previous Page</a>
)
} | {
pagination.page < pagination.pageCount && (
<a href={`/blog/${pagination.page + 1}`}>Next Page</a>
)
}
</Layout>
When we go back to the browser, we can see that the pagination works. But when we click on a blog post from the pagination, we get the following error:
This happened because we are trying to render the same path as blog/[page].astro
and blog/[slug].astro
files. They are both reading the same data. So, Astro doesn't know if it is a number or a slug.
The solution for this will be to create a folder called [page]
inside the blog
folder and move the blog/[page].astro
file there. And then make sure to update the links inside the file. See the code below:
---
import Layout from "../../../layouts/Layout.astro";
import fetchApi from "../../../lib/strapi";
const page = parseInt(Astro.params.page);
const postsPerPage = 2;
const response = await fetchApi({
endpoint: "blogs",
query: {
'pagination[page]': page,
'[pagination][pageSize]': postsPerPage,
},
});
const articles = response.data;
console.log("Thbe artiflces ", page)
const pagination = response.meta.pagination;
---
<Layout>
<h1>Page {page}</h1>
<ul>
{
articles.map((article) => (
<li>
<a href={`/blog/${article.attributes.slug}`}>
{article.attributes.title}
</a>
</li>
))
}
</ul>
{
pagination.page > 1 && (
<a href={`/blog/page/${pagination.page - 1}`}>Previous Page</a>
)
} | {
pagination.page < pagination.pageCount && (
<a href={`/blog/page/${pagination.page + 1}`}>Next Page</a>
)
}
</Layout>
From the code above, we updated the links to paginated pages from /blog/[page]
to /blog/page/[page]
, where [page]
refers to the page number.
To ensure that this works, you can modify the astro.config.mjs
file by adding a redirect or by modifying the Nav.astro
file to ensure that there won't be errors when we navigate between pages.
<nav>
<a href="/"> Home </a>
|
<a href="/blog/page/1">Blog </a>๐ฌ๐ง |
<a href="/blog/es">Blog ๐ช๐ธ</a>
</nav>
From the Nav
component above, when we click the "Blog", we see that it takes us to /blog/page/1
. See the image below. This shows that our application is working correctly!
Congratulations!
We have reached the end of this tutorial. Here are a couple of steps. Congratulations on building a Multilingual blog using Astro and Strapi! Youโve done an excellent job. Applying your skills with hands-on projects like this is a great way to get comfortable with new techniques and technologies.
The following are some suggested next steps:
- Learn how to implement a 404 page in Astro.
- Fix the pagination error for Spanish blog posts as we did for English blog posts.
- Try modifying the
formatRelativeTime
to takelocale
as a parameter. - Deploy your code to a provider, such as Netlify, and set up a hook so that when there is a new post that is saved in Strapi, you can trigger an entire rebuild of your code.
Conclusion
Throughout this tutorial we have learnt the basics of Astro and the basics of Strapi. We explored Dynamic Zones, different content types, how to grab data from a CMS and internationalization in Strapi as well.
We also saw how both we can render pages using Stati Site Generation (SSG) and how to update our code to enable Server-Side Rendering.
And finally, we were able to build a multilingual blog website using Astro and Strapi CMS.
You can visit the Strapi blog for more exciting tutorials and blog posts.
Resources
- You can find the GitHub with the complete code for this tutorial here. Make sure to select the branch
part-3
. ### Video Resources - Astro Crash Course Part 1: Project overview and introduction
- Astro Crash Course Part 2: Astro 101 introduction to the basics
- Astro Crash Course Part 3: Server-Side Rendering
- Astro Crash Course Part 4: Introduction to Strapi
- Astro Crash Course Part 5: Building the main project
- Astro Crash Course Part 6: Final steps and wrap up
Register For Our Upcoming Stream Event with Ben Holmes from Astro
We are excited to have Ben Holmes from Astro chatting with us about why Astro is awesome and best way to build content-driven websites fast.
Topics
- What's new in Astro
- Content Layer
- Webhooks
Join us to learn more about why Astro can be great choice for your next project.
Top comments (0)