Introduction
Pagination and infinite scrolling significantly improve the user experience of web applications by allowing developers to load large datasets.
Astro.js and Strapi are two powerful tools that developers often use in tandem to create dynamic and efficient web applications.
In this article, you will learn how to enable pagination and infinite scrolling in your Astro.js application using Strapi 5.
Overview of Astro.js and Strapi
Astro.js is a JavaScript framework for building modern, fast, scalable web applications. It provides developers with a clean and organized structure for their code, making it easier to manage and maintain complex projects. Astro.js offers various features and utilities, including routing, state management, and component-based architecture, making it a popular choice among developers for front-end development.
Strapi, on the other hand, is an open-source headless CMS (Content Management System) built with Node.js. It allows developers to quickly and easily create API-driven content management systems. Strapi provides a flexible and customizable content management interface and powerful APIs for managing content, making it an ideal choice for building dynamic web applications.
Importance of Pagination and Infinite Scrolling
Pagination is a technique that helps users work with a large amount of data by dividing it into smaller chunks or pages. It enhances the general user experience by limiting the amount of data loaded at a given time, improving its speed and the time it takes to load.
Infinite scrolling is when content loads as the user scrolls down the page without clicking buttons or links for page navigation. It gives a smooth and engaging browsing experience by loading new content as the user reaches the end of the content displayed. Infinite scrolling is especially effective in applications that contain a massive amount of content and whose volume increases constantly, such as social networks or news portals.
Why Combine Astro.js with Strapi
Integrating Astro.js with Strapi 5 merges the strengths of both platforms. Astro's performance-oriented frontend can consume dynamic content served by Strapi's API. By integrating Astro with Strapi, you can fetch content at build time or runtime, offering flexibility for static and dynamic rendering. Using Strapi as the backend provides a centralized content management system that editors can use without affecting the frontend codebase. Combining Astro.js and Strapi streamlines development and simplifies content updates, making it easier to build scalable websites.
Prerequisites
You will need the following to get started:
- Node.js v18 or later.
- A code editor on your computer.
Create a Strapi CMS Project
To get started, create a new Strapi 5 backend project by running the command below:
1npx create-strapi-app@rc my-project --quickstart
The above command will scaffold a new Strapi Node.js project and install the required dependencies.
Now, fill out the forms to create your administrator user account.
Create a New Astro App
Next, create a new Astro application by running the command below:
1npm create astro@latest
The above command will prompt you to select where you want to create your new project. Press the ESC key to use the default. Start your project with the Empty template, select no write in Javascript, install dependency, and initialize the new git repository. Overall, your selection should look like the screenshot below:
Creating Strapi Collection Type
To demonstrate how pagination and infinite scrolling work, create a new Article collection, add some entries, and publish them so that the API for your content can be consumed.
The Article collection will have the following data types:
title
: The article's titledescription
: The article's descriptioncontent
: The article's content
Configure Strapi Collection Type API
To access your Astro application's Article collection API, navigate to Settings > Roles > Public and select the Article collection. In the permissions tab, check to select all to give the public read, write, update, and delete access.
Connecting Astro.js with Strapi CMS
Now, let's connect your Strapi CMS with your Astro app. First, update your astro.config.mjs
configuration with the code:
1import { defineConfig } from 'astro/config';
2
3export default defineConfig({
4 env: {
5 STRAPI_API_URL: process.env.STRAPI_API_URL,
6 },
7 output: 'hybrid'
8});
This configuration:
- Sets up an environment variable
STRAPI_API_URL
environment variables. - Specifies the output as "hybrid", indicating that the project will use a hybrid approach combining static and server-rendered content.
Then add STRAPI_API_URL
variable to your .env
file:
STRAPI_API_URL=http://localhost:1337
Adding sample data for demonstration
With the collection type and Astro configurations created to connect with your Astro app, let's load some sample data to the Articles collection. Create a populate-strapi.js
file and add the code:
1import axios from "axios";
2
3const STRAPI_URL = "http://localhost:1337";
4
5async function populateArticles() {
6 try {
7 const { data: posts } = await axios.get(
8 "https://jsonplaceholder.typicode.com/posts"
9 );
10 for (const post of posts) {
11 await axios.post(
12 `${STRAPI_URL}/api/articles`,
13 {
14 data: {
15 title: post.title,
16 content: post.body,
17 },
18 },
19 {
20 headers: {
21 "Content-Type": "application/json",
22 },
23 }
24 );
25 console.log(`Created article: ${post.title}`);
26 }
27
28 console.log("All articles created successfully!");
29 } catch (error) {
30 console.error("Error:", error.response?.data || error.message);
31 }
32}
33
34populateArticles();
Here, we defined a populateArticles()
to fetch sample data from jsonplaceholder and send a POST request to Strapi CMS to save them in the Article collection.
Run the command below the code in the populateArticles
function:
node populate-strapi.js
If everything works as expected, you should see All articles created successfully!
printed on your terminal.
Implement Pagination in Astro.js
Now that you've loaded some sample data on your Article collection let's fetch paginated data from Strapi. Create a new pagination.astro
file in the pages
directory. Then, add the code below to configure the Javascript fetch API to fetch data from your Strapi CMS.
1---
2import "../styles/articles.css";
3
4export const prerender = false;
5const currentPath = Astro.url.pathname;
6const page = parseInt(Astro.url.searchParams.get("page") || "1");
7const pageSize = 6;
8
9const fetchArticles = async (page, pageSize) => {
10 const response = await fetch(
11 `${import.meta.env.STRAPI_API_URL}/api/articles?pagination[page]=${page}&pagination[pageSize]=${pageSize}&populate=*`
12 );
13 const data = await response.json();
14 return data;
15};
16
17const { data: articles, meta } = await fetchArticles(page, pageSize);
18---
The above code will:
- Set
prerender
tofalse
, indicating that this page should be server-side rendered. - Determine the current path and page number from the URL.
- Set a constant
pageSize
of 6 articles per page. - Define an asynchronous function
fetchArticles
that makes a fetch request for articles at the Strapi REST API endpoint. The data returned from this function includes the articles and metadata, such as the current page number and the total number of pages available (meta.pagination.page
andmeta.pagination.pageCount
). - The URL includes pagination parameters (
page
andpageSize
). - Uses the
STRAPI_API_URL
environment variable to construct the full API URL. Requests all populated fields with thepopulate=*
parameter. - Parses the JSON response and returns the data.
- Call the
fetchArticles
function with the currentpage
andpageSize
. - Destructure the response to extract the
articles
data andmeta
information.
Then render the articles and add buttons to navigate between the articles pages:
1<div class="articles-container">
2 <h1>Latest Articles</h1>
3
4 <div class="articles-grid">
5 {
6 articles.map((article) => (
7 <article class="article">
8 <h2>{article.title}</h2>
9 <div class="article-content" set:html={article.content} />
10 </article>
11 ))
12 }
13 </div>
14
15 <nav class="pagination">
16 <a
17 href={`${currentPath}?page=${meta.pagination.page - 1}`}
18 class={`pagination-button ${page <= 1 ? "disabled" : ""}`}
19 aria-label="Go to previous page"
20 rel={page > 1 ? "prev" : null}
21 >
22 Previous
23 </a>
24 <span class="pagination-info">
25 Page {page} of {meta.pagination.pageCount}
26 </span>
27 <a
28 href={`${currentPath}?page=${meta.pagination.page + 1}`}
29 class={`pagination-button ${page >= meta.pagination.pageCount ? "disabled" : ""}`}
30 aria-label="Go to next page"
31 rel={page < meta.pagination.pageCount ? "next" : null}
32 >
33 Next
34 </a>
35 </nav>
36</div>
To add some basic styling to the app, create a styles
directory in the root of your project and create an articles.css
file. Copy the styles here and add to the articles.css
file. Then, on your browser, navigate to /pagination
. You can see all the articles displayed on page 6 and navigate between pages by clicking the next and previous page buttons.
Implement Infinite Scrolling in Astro.js
In the previous section, you saw how to add pagination to your Astro application and tested it to see how it improves app load time and user experience. Let's move further to implement infinite scrolling, which helps you achieve the same result of increasing user experience and data load time.
First, the api
folder is in the pages
directory. In the api
directory, create a new articles.js
file. This will be where your application will ask for articles it needs from the server. Add the code snippet below:
1export async function GET({ url }) {
2 const page = url.searchParams.get("page") || "1";
3 const pageSize = url.searchParams.get("pageSize") || "10";
4
5 const response = await fetch(
6 `${
7 import.meta.env.STRAPI_API_URL
8 }/api/articles?pagination[page]=${page}&pagination[pageSize]=${pageSize}&populate=*`
9 );
10 const data = await response.json();
11
12 return new Response(
13 JSON.stringify({
14 articles: data.data,
15 hasMore: data.meta.pagination.page < data.meta.pagination.pageCount,
16 totalPages: data.meta.pagination.pageCount,
17 }),
18 {
19 headers: { "Content-Type": "application/json" },
20 }
21 );
22}
The code snippet sets up an API endpoint to fetch articles from a Strapi server. It defines a GET
function to retrieve articles based on pagination parameters like page number and page size. The function sends a request to the Strapi server, receives a list of articles and information about pagination, and checks if there are more pages of articles available. Finally, it formats the list of articles and pagination information and sends it back in a standardized JSON response.
Now, create a new file in your pages
directory named infinite-scroll.astro
and add the code.
1---
2import "../styles/articles.css";
3
4export const prerender = false;
5
6const initialPage = parseInt(Astro.url.searchParams.get("page") || "1");
7const pageSize = 10;
8
9const fetchArticles = async (page) => {
10 const response = await fetch(
11 `${import.meta.env.STRAPI_API_URL}/api/articles?pagination[page]=${page}&pagination[pageSize]=${pageSize}&populate=*`
12 );
13 const data = await response.json();
14
15 const { data: fetchedArticles, meta } = data;
16 return {
17 articles: fetchedArticles,
18 hasMore: meta.pagination.page < meta.pagination.pageCount,
19 totalPages: meta.pagination.pageCount,
20 };
21};
22
23const {
24 articles: initialArticles,
25 hasMore: initialHasMore,
26 totalPages,
27} = await fetchArticles(initialPage);
28---
The above code loads initial data when the page is first loaded. This ensures the page has some content to display before user interactions occur. Next, add the code below to display the content on the webpage and handle the infinite scrolling functionalities.
1<div class="articles-container">
2 <h1>Latest Articles</h1>
3
4 <div class="articles-grid">
5 {
6 initialArticles.map((article) => (
7 <article class="article">
8 <h2>{article.title}</h2>
9 <div class="article-content" set:html={article.content} />
10 </article>
11 ))
12 }
13 </div>
14
15 <div id="article-loader" style="display: none;">Loading more articles...</div>
16</div>
17
18<script define:vars={{ initialHasMore, initialPage, pageSize, totalPages }}>
19 let isLoading = false;
20 let hasMore = initialHasMore;
21 let currentPage = initialPage;
22
23 const articleLoader = document.getElementById("article-loader");
24 const articlesGrid = document.querySelector(".articles-grid");
25
26 const fetchMoreArticles = async () => {
27 if (isLoading || !hasMore) return;
28
29 isLoading = true;
30 articleLoader.style.display = "block";
31
32 try {
33 const response = await fetch(
34 `/api/articles?page=${currentPage + 1}&pageSize=${pageSize}`
35 );
36 const data = await response.json();
37
38 if (!data.articles || data.articles.length === 0) {
39 hasMore = false;
40 } else {
41 currentPage++;
42 data.articles.forEach((article) => {
43 const articleElement = document.createElement("article");
44 articleElement.className = "article";
45 articleElement.innerHTML = `
46 <h2>${article.title}</h2>
47 <div class="article-content">${article.content}</div>
48 `;
49 articlesGrid.appendChild(articleElement);
50 });
51 hasMore = currentPage < totalPages;
52 }
53 console.log(
54 `Fetched page ${currentPage}/${totalPages}, hasMore: ${hasMore}`
55 );
56 } catch (error) {
57 console.error("Error fetching articles:", error);
58 hasMore = false;
59 } finally {
60 isLoading = false;
61 articleLoader.style.display = hasMore ? "none" : "block";
62 articleLoader.textContent = hasMore
63 ? "Loading more articles..."
64 : "No more articles to load.";
65
66 if (!hasMore) {
67 window.removeEventListener("scroll", handleScroll);
68 console.log("No more articles, removed scroll listener");
69 }
70 }
71 };
72
73 const handleScroll = () => {
74 if (isLoading || !hasMore) return;
75
76 const scrollHeight = document.documentElement.scrollHeight;
77 const scrollTop = window.scrollY || document.documentElement.scrollTop;
78 const clientHeight =
79 window.innerHeight || document.documentElement.clientHeight;
80
81 if (scrollTop + clientHeight >= scrollHeight - 200) {
82 console.log(`Triggering fetch for page ${currentPage + 1}`);
83 fetchMoreArticles();
84 }
85 };
86
87 window.addEventListener("scroll", handleScroll);
88</script>
The above code will:
- Create a
<div>
with the class “articles-container” which includes a<h2>
“Latest Articles” and a grid layout for articles. - Use JavaScript to render the articles obtained from the server in real time.
- Create a variable
isLoading
,hasMore
,currentPage
and select necessary elements from the DOMarticleLoader
,articlesGrid
. - Create a function named
fetchMoreArticles
, which will load more articles from the server when the user scrolls down the page. WithinfetchMoreArticles
, to avoid multiple server requests while waiting for a response, we use theisLoading
flag, show a loading message (articleLoader
), and update the current page number (currentPage
). - Use the fetch API to make a GET request to our API endpoint
/api/articles
with the current page number and page size as parameters. After getting the response, we add the new articles to the articles grid and sethasMore
as true or false, depending on the availability of more articles. - Create the function
handleScroll
that will listen for scroll events and call the functionfetchMoreArticles
when the user scrolls down to the bottom of the page. - Use the
addEventListener
method to the window object to trigger thehandleScroll
function when the user scrolls.
Testing App
Now, navigate to the /infinite-scroll
endpoint on your browser to see the infinite scrolling feature in action.
You can find the code for the front end of this tutorial in this GitHub repository. Clone and start the Astro development server by running the command below:
npm run dev
Conclusion and Next Steps
By integrating pagination and infinite scrolling into your Astro.js application with Strapi 5, you've enhanced the user experience for content-rich sites by allowing efficient data loading and providing a smoother navigation experience.
Pagination and infinite scrolling should be included in any application involving a lot of data, such as a blog, news app, or social media app. The issue here is identifying when to apply each of the two methods. Pagination may be more applicable if you work with many data sets where users usually want to go to a specific page. However, if the aim is to give the users the functionality of browsing through the page and loading more content as they scroll down, then infinite scrolling can be more applicable. Finally, the decision depends on your application's specific requirements and user experience goals.
Recap of Key Concepts
We covered how to set up pagination by fetching data from Strapi's API with pagination parameters and displaying it in Astro.js. We also implemented infinite scrolling using client-side JavaScript to load more content as the user scrolls, combining static site generation with dynamic data loading.
Exploring Advanced Features
For further improvement, consider adding error handling and loading indicators to improve user experience. You might also explore alternatives like a "Load More" button for better accessibility or implement virtual scrolling for handling extremely large datasets.
Resources for Further Learning
To deepen your understanding, refer to tutorials like Implementing Pagination and Infinite Scrolling in Astro.js for detailed examples. Exploring the official documentation for Astro.js and Strapi 5 can also provide valuable insights for further development.
I'm a Software Engineer and Technical Writer who specializes in Node.js and JavaScript. I'm passionate about technology and sharing knowledge.