A fictional Travel Agency landing page & blog built using Next.js & SCSS modules. Strapi CMS was used to build custom content-types for holiday packages, blog articles & about page content. Using a CMS such as Strapi allows content managers to update site content with minimal coding knowledge using the Strapi dashboard.
Table of Contents
- 1 | Application Technologies & Features
- 2 | What I Learned
- 3 | Issues Faced During Development
- Frontend:
- Backend (link to repo):
- Strapi CMS with GraphQL & Cloudinary plugins
- Image slider implemented using Swiper.js
- Horizontal scrolling cards that display info for Holiday Packages
- Animated image reel to show company partners; pauses animation on hover
- Used sampleSize from Lodash to generate array of 7 random articles for "Other Articles" section (on
/blog
route) - Custom solution for pagination of blog articles on
/blog/all-articles
route
- The Strapi backend uses a GraphQL plugin that maps each Strapi Content-type to its own Type in a GraphQL API; such types can be viewed in a GraphQL Playground, although this must be configured for production (click here for details).
- I created a utility function (queryStrapi) that wraps the fetch function and queries the GraphQL API created by Strapi. It accepts a GraphQL query string as argument.
- All pages use Static Site Generation (SSG) to render pages.
- Incremental Static Generation (ISR) is used to revalidate data (on static pages) once it becomes stale. It works by checking if fetched data is stale on specified intervals. If stale, the data is re-fetched and the page will re-render with fresh data.
- The SWR package was used for Client-Side Rendering (CSR) of paginated articles (on
/blog/all-articles
route). SWR is leveraged for its abilities to:- Cache data fetched on the client - thus preventing requests for data that was previously fetched and reducing server load.
- Revalidate stale data - ensuring that the blog articles in-view reflect the most up-to-date articles from the database.
- The difference between client-side & server-side rendering
- How to implement numerous rendering strategies in Next.js and what benefits each provides
- Routing
- Generating dynamic routes using
getStaticPaths
alongsidegetStaticProps
- SEO benefits of Next.js - including rendering strategies and the Image component
- Using SCSS modules to scope styles to components
- Using Strapi to create & manage content-types
- Creating entries (i.e. instances of a particular content-type)
- Configuring plugins:
- Image hosting with Cloudinary
- Using GraphQL with Strapi
- Configuring Strapi for different environments (i.e. development & production)
- Using Conventional Commits pattern for cleaner git history
For the Pagination component design, I took inspiration from Material UI's Pagination component, where only n pages precede & following the current page. The final solution computes the relevant pages to show based on the currently selected page.
I had difficulty figuring out how to display the correct number of pages that are adjacent to the current page whilst also showing the ellipsis at the correct times. For example, if the current page was 5
, the pages shown would be [ 1, ..., 4, 5, 6, ..., n ] // where n > (last sibling + 2)
.
I then came across the idea of thinking of the adjacent numbers as "siblings". By specifying a siblingCount
value I was able to create a reusable component which could show any number of specified siblings.
For example, if siblingCount === 2
, then pages would be [ 1, ..., 5, 6, 7, 8, 9, ..., n ]
where 7
is the current page. Notice that siblingCount
is the number of siblings shown on one side of the current page (left or right).
From this point, the ellipsis could be shown by checking if the number of pages between the extremes of siblings (i.e. first or last sibling) and the first or last page (respectively) was greater than 2
. This check ensures that an ellipsis is only shown if there is at least 1
page between a sibling and first/last page.
Example: page range = [1, 10]
, siblingCount = 1
, currentPage = 3
- Siblings of
3
are2
(firstSibling) &4
(lastSibling) - Left ellipsis is NOT shown because there are no pages between
1
(firstPage) and firstSibling2
- i.e.
firstSibling > 2 === false
- i.e.
- Right ellipsis IS shown because pages DO EXIST between
10
(lastPage) and lastSibling4
- i.e.
lastSibling < (lastPage - 1) === true
- i.e.
- Resulting page range shown:
[ 1, 2, 3, 4, ..., 10 ]
For pagination implementation, see:
For Pagination component use, see:
How Pagination Works With Strapi's GraphQL API
Strapi's GraphQL API allows us to paginate requests "by page" (see GraphQL Pagination in Strapi Docs). So in the GraphQL query, I simply provide 2 parameters:
page
- number of the page being requestedpageSize
- how many articles to display on the page
Fetching paginated articles could be done with either:
- Client-side Rendering (CSR)
- Server-side Rendering (SSR)
Using SSR for Pagination
getServerSideProps()
(or gSSP
) runs at request-time on server-side only. It receives the context
object as an argument, which can be used to access query parameters.
One option to fetch the paginated articles is to send the GraphQL pagination parameters (page
& pageSize
) as query parameters to the server using Next's built-in Router. gSSP
can then access the parameters via the context
object and then send a GraphQL query to Strapi for the paginated articles.
Using CSR for Pagination
With this approach I could use getStaticProps()
(or gSP
) to statically render the /blog/all-articles
page which would improve the page load-time (as the page is pre-rendered at build-time as opposed to run-time). After the page has loaded I could request the paginated articles in useEffect
and populate the UI appropriately, according to the response.
CSR inherently takes some time and I would use a spinner (or loading icon) to indicate that the data is being fetched.
Regardless of whether I chose CSR or SSR, on click of each pagination-page the articles would always be requested. This means that data would be re-requested when:
- clicking the current page
- clicking a previously requested page
To resolve problem #1, I simply disabled the button for the current page, making it non-clickable.
Problem #2 could be resolved using caching. The methods of caching vary depending on whether I chose SSR or CSR. My options were:
- cache in local storage
- use caching headers (i.e.
Cache-Control
) insidegSSP
to set caching directives - use SWR package to cache responses from data fetched on client-side
This section outlines why I opted to go with CSR using the SWR package.
Evaluation of Rendering Methods
The rendering method I chose would also partially dictate which caching option I went with. The drawback of SSR (via gSSP
) was that on each request for paginated articles, the entire page would be re-rendered and sent back to the client.
The fetched data is simply to be displayed in the UI, it does not influence the overall shape of the UI, therefore the re-render by gSSP
is unnecessary. As a result of this, I decided to go with CSR.
Evaluation of Caching Methods
With SSR off the table, I had to choose between caching in Local Storage vs using the SWR package.
Local Storage would be a simple solution to cache the data on initial load, however it would be tedious to sync Local Storage when the articles data is updated (i.e. articles are added/removed).
The SWR package provides the useSWR
hook, which I could use to client-side-fetch the articles on click of each pagination-page. The nice thing about useSWR
is that it handles caching automatically with configurable options.
SWR is derived from the stale-while-revalidate
(SWR) caching directive. It decreases page load-times like so:
- on initial request (i.e. no cache), data is fetched then cached. This request results in the longest latency and occurs once.
- on subsequent requests data is served from the cache.
The cache is essentially updated in the background:
- if cached data is fresh, it's served as-is.
- if cached data is stale, it is:
- served as the response;
- then revalidated, i.e. data is re-fetched and the fresh response is cached.
Ultimately, statically rendering the page allowed me to ensure that page load-times are optimised for SEO (where SSR may have increased load-times). Using Static Site Generation meant that I had to fetch data on the client, and useSWR
was the easiest option to do so, whilst also ensuring that cached data was kept up-to-date without increasing page load-times.
Very simply, close the mobile dropdown navigation menu when the user clicks outside of it. This functionality was implemented with useClickOutside
.
ImageSlider
renders a Swiper component to create a swipeable image carousel. This swipe/touch-detection was taking precedence over the check for clicking outside of the navigation menu. As a result of this, the navigation menu would not close if the user clicked on the ImageSlider component.
In the documentation for Swiper, I noticed that you could disable the swiping functionality with a CSS class: swiper-no-swiping
. I used Chrome Devtools to add the CSS class to the Swiper component and then checked if the external click was registered when clicking the Swiper component. The menu closed as desired.
To resolve the issue, I created a utility function - handleSwiperNoSwiping() - that adds/removes the swiper-no-swiping
CSS class to all Swiper elements on a page. I used this function to:
- add
swiper-no-swiping
when the navigation menu is open - remove
swiper-no-swiping
when the navigation menu is closed
This ensured that:
- the menu would close on external clicks;
- the ImageSlider component was still swipeable.
- Backend repo link: https://github.com/Bilaal96/blog-strapi