DEV Community

Cover image for Astro & Strapi Website Tutorial: Part 3 - Project Build
Theodore Kelechukwu Onyejiaku
Theodore Kelechukwu Onyejiaku

Posted on • Originally published at strapi.io

Astro & Strapi Website Tutorial: Part 3 - Project Build

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

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
Enter fullscreen mode Exit fullscreen mode

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;
}

Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

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:

home-page-preview.png

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'
Enter fullscreen mode Exit fullscreen mode

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;
}

Enter fullscreen mode Exit fullscreen mode

Here is what the code above does:

  • We extended the fetchApi() function from the Astro documentation to include page and parameter.
  • It takes an object parameter containing endpoint, query, wrappedByKey, wrappedByList, page, and locale.
  • 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 as endpoint, locale, and query 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 the fetch API. The response is transformed using the json() function.
  • Finally, we return the transformed response as data.
  • Later on, we will talk about what the wrappedByKey and wrappedByList perform in the code.

NOTE: In the code above, we used import.meta.env.STRAPI_URL to access the STRAPI_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.

landing-page-components.png

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>

Enter fullscreen mode Exit fullscreen mode

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.

about-photo-url.png

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" />

Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Here is what the code above does:

  • We import the fetchAPI function, the Hero and About components and the Layout 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 be pages. 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 the LandingPage 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 the data property. So with the wrappedByKey, we can reference the information returned without having to manually go into the data property.
  • Because the hero and about 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.

the-component-property.png

  • Finally, we passed the extracted data aboutText, aboutPhoto, heroText, and heroDescription as props to the About and Hero components.

If we view the result on our browser, we can see that we now have a landing page built using Strapi Dynamic Zones.

landin-page.png

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:

meta-information.png

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>

Enter fullscreen mode Exit fullscreen mode

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 property Astro object, we get currentPage. The currentPage 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.

manual-pagination.gif

  • 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 and page.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.

automatic-pagination.gif

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>

Enter fullscreen mode Exit fullscreen mode

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 a GET request to the blogs endpoint.
  • Recall that in the beginning of the tutorial, the Part 1, we said that for dynamic endpoints, the getStaticPaths must return a params object. So we loop through all the articles and returned their slugs. Also, we ruturned articles 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>

Enter fullscreen mode Exit fullscreen mode

When we click on an article as shown below, we see its page content, which is the title. Bravo!

visit-a-page-by-slug.gif

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
Enter fullscreen mode Exit fullscreen mode

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} />
Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

When this is done, we should now see the content of a blog post.

content-of-a-blog.png

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>

Enter fullscreen mode Exit fullscreen mode

In the image below, we can see the article's image displayed.

display-image-of-content.png

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"],
  },
});
Enter fullscreen mode Exit fullscreen mode

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>


Enter fullscreen mode Exit fullscreen mode

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!

page-1-of-spanish-blog.png

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.

relative-time-information.png

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>

Enter fullscreen mode Exit fullscreen mode

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 the publishedAt 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 difference hoursDifference and the minutes difference minutesDifference.
  • 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:

relative-time.png

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.

relative-time-in-spanish.png

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:

  1. 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.
  2. 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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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 the slug can change at any request.
  • With a try/catch block, we get all the blog posts using the fetchApi function. This time around, notice that we added a query and wrappedByList.
  • The wrappedByList is a boolean parameter to โ€œunwrapโ€ the list returned by Strapi headless CMS, and return only the first item.
  • The queryis going to be sent to the Strapi to filter out only the slug 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>
Enter fullscreen mode Exit fullscreen mode

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.

add-new-entry.png

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!

new-entry-dynamic-update.png

However, without rebuilding our application, notice that the new article doesn't appear in the pagination.

new-entry-not-in-pagination.png

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>

Enter fullscreen mode Exit fullscreen mode

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:

pagination-error.png

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>

Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

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!

final-preview.gif

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 take locale 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

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)