By John Popilek
Posted 9/16/2021
You must login to like or comment
Do you ever feel like you're stuck in an indefinite loop of doing tutorials? Whenever I find myself in this situation I get the urge to build something. The question then becomes what to build, I personally wanted to build something useful for archiving my personal studies and projects. That’s why I settled on building out this blogging platform you are reading this post on. This platform may be a little overkill for a personal blog but it seemed like a lot more fun than doing a basic MarkDown blog.
From all the tutorials I have taken I found that there are a lot of good but incomplete courses on building out applications. My gameplan for this site is to do a true Full Stack DevOps course on Udemy teaching people to build this same platform. I want to include how to build out a deployment pipeline with automation testing using Cypress or Playwright for the UI. My goal is to teach others how to take an idea into production as quickly and safely as possible.
My long term vision for this platform is to take simple concepts and use this as a medium to teach them. I will be posting more complicated concepts on YouTube or Udemy as I find video to be a more effective platform for teaching people to code, especially when the topic or problem we are solving for is complicated.
The amount of code it took to build this platform will be much better suited for video so stay tuned I will definitely be making one! In the meantime I wanted to cover my favorite pattern I discovered while building the UI for this course that really simplified my code and assisted in improving the platform's SEO.
While building out web pages it's a very common use case to want to pull data on page load. However in traditional Single Page Applications this presents a big issue. The issue this creates is that pulling data on page load is not SEO friendly and when bots go to your page the HTML that React renders is mostly empty. NextJS created a solution to this problem by offering a variety of ways to render your sites webpages. I’m not going to go into all the solutions here but if you are interested this is a great resource for learning about what you can do with NextJS
In this platform I made use of Server Side Rendering. This will be familiar to anyone who worked on the web applications of old. At a high level Next JS solves this by making an API call for data on the server side and then feeding that into your react code as props. In order to illustrate this we will look at two examples, the first example will be client side rendered, and the second will be server side rendered.
Notice here that useEffect hook is called on page load to fetch the data from the backing API and populates the post data into the page state. This is the traditional way of pulling data in a react functional component when a page is rendered. As we mentioned before this has some drawbacks for SEO reasons and if SEO is a concern for your app you will need a solution.
import React from 'react';
import {
Container,
Stack,
IconButton,
Center,
Box,
Button,
Heading,
Input,
InputGroup,
InputLeftElement,
InputRightElement,
} from '@chakra-ui/react';
import {
FiArrowRight,
FiArrowLeft,
FiRefreshCw,
FiBook,
FiSearch,
} from 'react-icons/fi';
import { useState, useEffect } from 'react';
import axios from 'axios';
import BlogContent from '../components/blog/BlogContent';
import Skeleton from 'react-loading-skeleton';
import HeadingUnderlined from '../components/headings/HeadingUnderlined';
const ArticleList = ({ data }) => {
const [posts, setPosts] = useState(data);
const [searchText, setSearchText] = useState('');
const [page, setPage] = useState(1);
const [prevEnabled, setPrevEnabled] = useState(false);
const [nextEnabled, setNextEnabled] = useState(true);
const [loadingComplete, setLoadingComplete] = useState(false);
const limit = 4;
useEffect(() => {
fetchPosts();
}, []);
const fetchPosts = async () => {
setLoadingComplete(false);
const { data } = await axios.get(`/api/post?limit=${limit}&page=1`);
if (data.success) {
setPosts(data);
}
setLoadingComplete(true);
};
const searchForPosts = async () => {
setLoadingComplete(false);
const { data } = await axios.get(`/api/post/search?text=${searchText}`);
if (data.success) {
setPosts(data);
}
setLoadingComplete(true);
};
const fetchPostsByPage = async (index) => {
setLoadingComplete(false);
const { data } = await axios.get(`/api/post?limit=${limit}&page=${index}`);
setPosts(data);
setPage(index);
setLoadingComplete(true);
if (data.pagination) {
if (data.pagination.next) {
setNextEnabled(true);
} else {
setNextEnabled(false);
}
if (data.pagination.prev) {
setPrevEnabled(true);
} else {
setPrevEnabled(false);
}
}
};
const handlePageMovementForward = () => {
const nextPage = posts.pagination.next.page;
setLoadingComplete(false);
fetchPostsByPage(nextPage ? nextPage : 1, searchText);
};
const handlePageMovementBackward = () => {
const prevPage = posts.pagination.prev.page;
setLoadingComplete(false);
fetchPostsByPage(prevPage ? posts.pagination.prev.page : 1, searchText);
};
const handlePageReset = () => {
setLoadingComplete(false);
setPrevEnabled(false);
setNextEnabled(true);
fetchPostsByPage(1);
};
const handleInputChange = (event) => {
const { value } = event.target;
setSearchText(value);
};
const resetSearch = (event) => {
event.preventDefault();
setSearchText('');
fetchPostsByPage(1);
};
return loadingComplete ? (
<Container maxW={'7xl'} p="12">
<HeadingUnderlined text={'Blog Posts'} />
<Center>
<Stack pt={4} maxW="32rem">
<Heading as="h4" size="lg" mb={4}>
Looking for a specific topic?
</Heading>
<InputGroup>
<InputLeftElement
pointerEvents="none"
color="gray.300"
fontSize="1.2em"
>
<FiBook />
</InputLeftElement>
<Input
name="searchText"
value={searchText}
onChange={handleInputChange}
placeholder="Enter topic"
/>
<InputRightElement>
<IconButton
aria-label="search"
id="search"
disabled={searchText.length == 0}
onClick={searchForPosts}
icon={<FiSearch />}
bg={'blue.400'}
color={'white'}
_hover={{
bg: 'blue.500',
}}
/>
</InputRightElement>
</InputGroup>
{searchText && <Button onClick={resetSearch}>Clear Search</Button>}
</Stack>
</Center>
{!posts.count > 0 ? (
<Center>
<p>No Post found</p>
</Center>
) : (
posts.data.map((post) => (
<BlogContent
key={post._id}
imageUrl={post.image ? post.image.Location : ''}
headingText={post.name}
description={post.description}
tags={post.tags}
authorName={post.author}
slug={post.slug}
date={post.createdAt}
/>
))
)}
{posts.numberOfPages > 1 && (
<Container>
<Center p="2">
<Stack direction={'row'} maxW={'md'}>
<IconButton
aria-label="move left"
id="move-left"
disabled={!prevEnabled}
onClick={handlePageMovementBackward}
icon={<FiArrowLeft />}
bg={'blue.400'}
color={'white'}
_hover={{
bg: 'blue.500',
}}
/>
<Box p="2">
Page {page} of {posts.numberOfPages}
</Box>
<IconButton
aria-label="move right"
id="move-right"
disabled={!nextEnabled}
onClick={handlePageMovementForward}
icon={<FiArrowRight />}
bg={'blue.400'}
color={'white'}
_hover={{
bg: 'blue.500',
}}
/>
</Stack>
</Center>
<Center>
<Button
width="190px"
leftIcon={<FiRefreshCw />}
onClick={handlePageReset}
bg={'yellow.400'}
color={'white'}
_hover={{
bg: 'yellow.500',
}}
variant="solid"
>
Reset
</Button>
</Center>
</Container>
)}
</Container>
) : (
<Container maxW={'7xl'} p="12">
<Skeleton count={40} />
</Container>
);
};
export default ArticleList;
In the server side rendered example notice how we got rid of the useEffect hook and call the built in NextJS function getServerSideProps. The result of this call is passed into your functional component as props which you can then use to store in state and render the page with minimal code changes from the above example.
import React from 'react';
import {
Container,
Stack,
IconButton,
Center,
Box,
Button,
Heading,
Input,
InputGroup,
InputLeftElement,
InputRightElement,
} from '@chakra-ui/react';
import {
FiArrowRight,
FiArrowLeft,
FiRefreshCw,
FiBook,
FiSearch,
} from 'react-icons/fi';
import { useState } from 'react';
import axios from 'axios';
import BlogContent from '../components/blog/BlogContent';
import Skeleton from 'react-loading-skeleton';
import HeadingUnderlined from '../components/headings/HeadingUnderlined';
import Head from 'next/head';
import logo from '../public/PopilekDev.png';
const ArticleList = ({ data }) => {
const [posts, setPosts] = useState(data);
const [searchText, setSearchText] = useState('');
const [page, setPage] = useState(1);
const [prevEnabled, setPrevEnabled] = useState(false);
const [nextEnabled, setNextEnabled] = useState(true);
const [loadingComplete, setLoadingComplete] = useState(true);
const limit = 4;
const searchForPosts = async () => {
setLoadingComplete(false);
const { data } = await axios.get(`/api/post/search?text=${searchText}`);
if (data.success) {
setPosts(data);
}
setLoadingComplete(true);
};
const fetchPostsByPage = async (index) => {
setLoadingComplete(false);
const { data } = await axios.get(`/api/post?limit=${limit}&page=${index}`);
setPosts(data);
setPage(index);
setLoadingComplete(true);
if (data.pagination) {
if (data.pagination.next) {
setNextEnabled(true);
} else {
setNextEnabled(false);
}
if (data.pagination.prev) {
setPrevEnabled(true);
} else {
setPrevEnabled(false);
}
}
};
const handlePageMovementForward = () => {
const nextPage = posts.pagination.next.page;
setLoadingComplete(false);
fetchPostsByPage(nextPage ? nextPage : 1);
};
const handlePageMovementBackward = () => {
const prevPage = posts.pagination.prev.page;
setLoadingComplete(false);
fetchPostsByPage(prevPage ? posts.pagination.prev.page : 1);
};
const handlePageReset = () => {
setLoadingComplete(false);
setPrevEnabled(false);
setNextEnabled(true);
fetchPostsByPage(1);
};
const handleInputChange = (event) => {
const { value } = event.target;
setSearchText(value);
};
const resetSearch = (event) => {
event.preventDefault();
setSearchText('');
fetchPostsByPage(1);
};
return loadingComplete ? (
<Container maxW={'7xl'} p="12">
<Head>
<title>PopilekDev</title>
<meta
name="description"
content="Personal blogging site to share Node, Express, React, Java, and Spring boot tutorials"
/>
<meta property="og:title" content="PopilekDev" />
<meta
property="og:description"
content="Personal blogging site to share Node, Express, React, Java, and Spring boot tutorials"
/>
<meta property="og:image" content={logo.src} key="ogimage" />
<meta property="og:site_name" content="popilekdev" />
<meta property="og:url" content="https://popilekdev.com/" />
<meta property="og:type" content="website" />
</Head>
<HeadingUnderlined text={'Blog Posts'} />
<Center>
<Stack pt={4} maxW="32rem">
<Heading as="h4" size="lg" mb={4}>
Looking for a specific topic?
</Heading>
<InputGroup>
<InputLeftElement
pointerEvents="none"
color="gray.300"
fontSize="1.2em"
>
<FiBook />
</InputLeftElement>
<Input
name="searchText"
value={searchText}
onChange={handleInputChange}
placeholder="Enter topic"
/>
<InputRightElement>
<IconButton
aria-label="search"
id="search"
disabled={searchText.length == 0}
onClick={searchForPosts}
icon={<FiSearch />}
bg={'blue.400'}
color={'white'}
_hover={{
bg: 'blue.500',
}}
/>
</InputRightElement>
</InputGroup>
{searchText && <Button onClick={resetSearch}>Clear Search</Button>}
</Stack>
</Center>
{!posts.count > 0 ? (
<Center>
<p>No Post found</p>
</Center>
) : (
posts.data.map((post) => (
<BlogContent
key={post._id}
imageUrl={post.image ? post.image.Location : ''}
headingText={post.name}
description={post.description}
tags={post.tags}
authorName={post.author}
slug={post.slug}
date={post.createdAt}
/>
))
)}
{posts.numberOfPages > 1 && (
<Container>
<Center p="2">
<Stack direction={'row'} maxW={'md'}>
<IconButton
aria-label="move left"
id="move-left"
disabled={!prevEnabled}
onClick={handlePageMovementBackward}
icon={<FiArrowLeft />}
bg={'blue.400'}
color={'white'}
_hover={{
bg: 'blue.500',
}}
/>
<Box p="2">
Page {page} of {posts.numberOfPages}
</Box>
<IconButton
aria-label="move right"
id="move-right"
disabled={!nextEnabled}
onClick={handlePageMovementForward}
icon={<FiArrowRight />}
bg={'blue.400'}
color={'white'}
_hover={{
bg: 'blue.500',
}}
/>
</Stack>
</Center>
<Center>
<Button
width="190px"
leftIcon={<FiRefreshCw />}
onClick={handlePageReset}
bg={'yellow.400'}
color={'white'}
_hover={{
bg: 'yellow.500',
}}
variant="solid"
>
Reset
</Button>
</Center>
</Container>
)}
</Container>
) : (
<Container maxW={'7xl'} p="12">
<Skeleton count={40} />
</Container>
);
};
export async function getServerSideProps(context) {
const { data } = await axios.get(`https://api.popilekdev.com/api/post`);
return { props: { data } };
}
export default ArticleList;
As you can see from the two examples the code is almost identical but one is SEO friendly and the other isn’t. With this pattern it is really easy to convert a client side rendered component to a server side rendered version. Please comment or like this post if you enjoyed it and are excited for the Udemy Course on building out this Platform!
Scott McClellan
Awesome!