I have written articles on different platforms including LinkedIn, The Network Bits (Substack), and freeCodeCamp. So I wanted to bring all of these articles together in a single place where someone could go and see all my work.
A blog sounded like a good solution for this, so I set out to build one. In this article, I will walk you through how I did it with Next.js.
The basic idea here was to build a website where I wouldn’t need to write code in the future. I just wanted to be able to add the URL of a new article to a JSON file, and the website would extract information like the title, date, cover image, and description and then update itself with it. No database.
To understand how I would go about it, I checked the metadata of the HTML text from each of the platforms I considered. I used my articles, of course, like the one in the project folder. I found out that most of them used Open Graph metadata. So, that was easy to scrape. But, I also found out that some information wasn’t in the meta tags – instead, it was in the JSON-LD. At the end of the day, I ended up using both in my functions.
What we’ll cover:
Pre-requisites
Understanding this article requires some knowledge of programming and web development. You need to have basic knowledge of HTTP, HTML, CSS, JavaScript, and React to be able to follow along easily.
If you don’t have those skills, you may still be able to understand the general structure and working principles.
How the Blog Site Works
The project consists of client components and server components. It is a website, so ideally, it’s just a front-end. But it has to fetch data from URLs – and doing that from the client-side won’t work due to CORS blocking, as the requests will be emanating from a browser. So, it has to run on the server.
The fetchArticles()
function runs on the server – then this happens:
The fetchArticles()
function accesses the URLs, extracts and processes the HTML and JSON Linked Data objects from the response, and returns an array of Article objects to the Home page.
The HomePage
component is a client side component that has another component in it, named HomeClient
. This HomeClient
is a client side component. It has to be because it has useState hooks.
But the HomePage
component calls the fetchArticles()
function and sets the articles
constant (which is an array of Article
objects, as defined by the interface in the ArticleCard.tsx
file). The articles
constant is then passed down to the HomeClient
component as a prop.
Inside the HomeClient
component, there are two components – the Hero
component, and the MainBody
component. The Hero component shows the welcome message, and also has the search bar. The MainBody component is where the tags and the article grid are. Logic for filtering articles are also in the MainBody component.
Inside the MainBody component, there is the ArticleCard
component that takes the filtered array of Article objects from the MainBody as props, and renders an article card for each. These cards are rendered inside the grid in the MainBody component.
What does an article look like?
The articles are defined by an interface:
<span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> Article {
id: <span class="hljs-built_in">number</span>;
title?: <span class="hljs-built_in">string</span>;
description?: <span class="hljs-built_in">string</span>;
publishedDate?: <span class="hljs-built_in">string</span>;
url: <span class="hljs-built_in">string</span>;
imgUrl?: <span class="hljs-built_in">string</span>;
siteName?: <span class="hljs-built_in">string</span>;
tags?: <span class="hljs-built_in">string</span>[];
}
The interface, as shown above, specifies that the object will have eight properties, of which only the id
and url
are compulsory. Those compulsory properties are actually what’s needed in the JSON file from which the web server will read.
When the URL is visited by the server, the title, description, and other properties (except the tags) are obtained automatically and populated. Then the object is created.
The article cards consist of the article’s cover image, the name of the platform where it was published, the date published, the title, and a description. All of this is wrapped in an anchor linking to the URL. The tags are not visible on the cards, but are used in filtering operations.
How does the search feature work?
There’s a reason why the Hero component and the MainBody component are in the same parent component. That wasn’t my initial design, but after I saw that the search bar would look better in the Hero component, and that I needed to set the searchTerm
state in the Hero component and use it in the MainBody component, that became the best option for me: to put both of them in the same parent, so I could pass down the useState hook as props into both of them.
The search feature works basically by filtering the articles
array based on the tags selected, or the search term entered. Here is what the code looks like:
useEffect(<span class="hljs-function">() =></span> {
<span class="hljs-keyword">const</span> anyTagActive = isActive.some(<span class="hljs-function">(<span class="hljs-params">val</span>) =></span> val);
<span class="hljs-keyword">const</span> filtered = articles.filter(<span class="hljs-function">(<span class="hljs-params">article</span>) =></span> {
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Search term: '</span> + searchTerm || <span class="hljs-string">'searchTerm'</span>);
<span class="hljs-keyword">const</span> searchMatch =
article.title?.toLowerCase().includes(searchTerm.toLowerCase()) ||
article.description?.toLowerCase().includes(searchTerm.toLowerCase()) ||
article.tags?.some(<span class="hljs-function">(<span class="hljs-params">tag</span>) =></span> tag.toLowerCase().includes(searchTerm.toLowerCase())) ||
article.siteName?.toLowerCase().includes(searchTerm.toLowerCase()) ||
article.publishedDate?.toLowerCase().includes(searchTerm.toLowerCase());
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'This is the searchMarch: '</span> + searchMatch || <span class="hljs-string">'FALSE searchMatch'</span>);
<span class="hljs-built_in">console</span>.log(article.title || <span class="hljs-string">'article.title no wan show'</span>);
<span class="hljs-keyword">const</span> tagMatch = article.tags?.some(<span class="hljs-function">(<span class="hljs-params">tag</span>) =></span> {
<span class="hljs-keyword">const</span> index = tags.indexOf(tag);
<span class="hljs-keyword">return</span> index !== <span class="hljs-number">-1</span> && isActive[index];
}) || <span class="hljs-literal">false</span>;
<span class="hljs-keyword">if</span> (anyTagActive) {
<span class="hljs-keyword">return</span> tagMatch && searchMatch; <span class="hljs-comment">// Only return articles if tag is active and search matches</span>
}
<span class="hljs-keyword">return</span> searchMatch; <span class="hljs-comment">// If no tags active, return all that match the search term</span>
});
setFilteredArticles(filtered);
}, [articles, searchTerm, isActive]);
Here, we use a useEffect()
hook to monitor for changes in the articles
, searchTerm
, and isActive
constants. isActive
is a useState()
hook that has an array of boolean values the length of the tags array.
<span class="hljs-keyword">const</span> [isActive, setIsActive] = useState(tags.map(<span class="hljs-function">() =></span> <span class="hljs-literal">false</span>));
Here, the filtered
constant is equal to the filtered values of articles
.
<span class="hljs-keyword">const</span> filtered = articles.filter();
Inside the filter method is where the arrow function with the logic for filtering is written – (article) => {//logic}
. We have two constants: tagMatch
and searchMatch
. The searchMatch
constant is true when the title, description, tags, site name, or published date includes the search term. Else, it’s false. The tagMatch
constant is true when any tag from the article’s array of tag is present in the tag list, and also has a corresponding isActive
value of true.
If any tag at all is active, then the results for both tagMatch
and searchMatch
are returned, but if no tag at all is active, then only the searchMatch
is returned as true.
The filtered article list is what is then passed into the ArticleCard
component.
<ArticleCard articles={filteredArticles} />
Project Structure
This is what the project file structure looks like:
At the root, we have the config files and node_modules
which is not displayed here. The public
folder holds all the images and icons. Then, in the src
folder, we have app
, component
, and utils
.
The components
folder holds the files for the components – the nav bar, footer, hero, main body and article card. The utils
folder has all the functions that run in the background and do not need to render anything. The fetchArticles
function is there, along with other functions for extracting the date published, title, description, image URL, and others from HTTP responses gotten from the article URLs. The app
folder has the favicon, the global CSS stylesheet, the page
and layout
files, articles.json
which is the JSON file where I add new article URLs for rendering, a test HTML file (wsl.html), and the about/
and api/
directories.
Inside the about folder, we have the about page, and inside the API folder, we have tthe folder, metadata-local-test
which is no longer relevant to the project. I used it initially to create an internal API to fetch from the URLs. But I later restructured the codebase.
Steps to Build the Blog
1. Install Next.js
To install Next.js, navigate to the folder where you want the project to reside and open that location in your terminal. Then type the following:
npx create-next-app@latest
You’re going to be met with the following prompts:
2. Navigate to your newly created project folder and install dependencies
In the newly created project folder, run the project in development mode to preview your newly created Next project. You will be shown a message directing you to localhost on port 3000. Now, it’s time for us to start creating what we want.
Now, one more thing you’ll need to do. In the project, I used lucide-react to get one of the icons, and cheerio to extract data from the HTML. So, you’ll need to install those dependencies.
To install lucide-react, use this command in the project folder:
npm install lucide-react
Then install cheerio:
npm install cheerio
3. Change the title and description in the page metadata
The title is what shows up at the top of your browser tab when you open up the website. Right now, it should be showing ‘Create Next App.’ We don’t want that.
Since this is not just HTML, there is no index.html
to change the title in the header element. Rather, Next.js provides us a Metadata
object we can use to change things like that. And it’ll be in the layout.tsx
file in the app
or src
folder. Head over there and change it to whatever you want the title to be. I’m using “Chidiadi Portfolio Blog”.
4. Create the necessary components
Navigate to the side panel, and under the src
folder, create a components folder. This is where the components will live. Here, create the article card, footer, main body and nav bar.
For the Navbar, this is the code:
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Navbar</span>(<span class="hljs-params"></span>)</span>{
<span class="hljs-keyword">return</span>(
<>
<div className=<span class="hljs-string">"text-3xl md:text-base flex w-[100vw] md:w-[98.2vw] lg:w-[98.8vw] h-[60px] bg-black text-white px-0 md:px-7 md:py-2 items-center justify-center md:justify-between"</span>>
<h1 className=<span class="hljs-string">"font-bold"</span>>CHIDIADI ANYANWU</h1>
<div className=<span class="hljs-string">"hidden md:block flex space-x-4"</span>>
<a href=<span class="hljs-string">"/"</span> className=<span class="hljs-string">"hover:text-gray-400"</span>>Blog</a>
<a href=<span class="hljs-string">"/about"</span> className=<span class="hljs-string">"hover:text-gray-400"</span>>About</a>
</div>
</div>
</>
);
}
Here’s what the Hero component looks like:
<span class="hljs-string">"use client"</span>;
<span class="hljs-keyword">import</span> { Search } <span class="hljs-keyword">from</span> <span class="hljs-string">'lucide-react'</span>;
<span class="hljs-keyword">import</span> { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">interface</span> HeroProps {
searchTerm: <span class="hljs-built_in">string</span>;
setSearchTerm: React.Dispatch<React.SetStateAction<<span class="hljs-built_in">string</span>>>;
}
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Hero</span>(<span class="hljs-params">{ searchTerm, setSearchTerm }: HeroProps</span>) </span>{
<span class="hljs-keyword">const</span> [buttonColor, setButtonColor] = useState(<span class="hljs-string">''</span>);
<span class="hljs-keyword">return</span> (
<div className=<span class="hljs-string">"bg-[url('/img-one-1.jpg')] bg-cover bg-center bg-no-repeat flex flex-col items-center justify-center h-[400px] relative"</span>>
<div className=<span class="hljs-string">" absolute inset-0 bg-black opacity-60"</span>></div>
<h1 className=<span class="hljs-string">"text-4xl text-white font-bold text-center z-10"</span>>My Portfolio Blog</h1>
<p className=<span class="hljs-string">"mt-4 mx-4 text-xlarge text-white md:text-xl text-justify md:text-center z-10"</span> style={{ fontFamily: <span class="hljs-string">"Cormorant Garamond"</span> }}>
My name is Chidiadi Anyanwu. I am a technical writer <span class="hljs-keyword">with</span> a strong background <span class="hljs-keyword">in</span> networking.
I write about Networking, Cloud, DevOps, and even sometimes web development like <span class="hljs-built_in">this</span> one. I built <span class="hljs-built_in">this</span>
website <span class="hljs-keyword">with</span> Next.js, and there<span class="hljs-string">'s also an <a href="/" className="text-blue-500 hover:text-blue-700 hover:underline">article about that.</a>
This website holds my technical articles in one place. It is a repository of my written works.
</p>
<div id="searchbar" className="h-9xl mt-4 flex align-items-center justify-center w-full" >
<form onSubmit={(e) => {e.preventDefault(); setSearchTerm(searchTerm);}} className="group mt-4 relative w-[70%] md:w-[50%]">
<input value={searchTerm} onChange={(e) => setSearchTerm(e.target.value) } onFocus={()=>{setButtonColor('</span>bg-blue<span class="hljs-number">-500</span><span class="hljs-string">'); console.log('</span>input focused<span class="hljs-string">')}} onBlur={()=>{setButtonColor('</span><span class="hljs-string">');}}type="search" placeholder="Search Chidiadi'</span>s articles<span class="hljs-string">" className="</span>h-[<span class="hljs-number">50</span>px] w-full px-[<span class="hljs-number">48</span>px] border<span class="hljs-number">-3</span> border-blue<span class="hljs-number">-300</span> rounded-[<span class="hljs-number">25</span>px] focus:outline-none focus:border-blue<span class="hljs-number">-500</span> text-black bg-white<span class="hljs-string">"/>
<button className={`h-[42px] w-[42px] absolute right-0 mr-1.5 mt-1 rounded-[50%] bg-blue-300 ${buttonColor}`}>
<Search className='m-auto text-white'/>
</button>
</form>
</div>
</div>
);
}</span>
In this file, we created the HeroProps interface to accept the search props. Then we deconstructed both searchTerm
and setSearchTerm
from it as props to the Hero component. We’ll make it a client component 'use client'
because of the buttonColor useState()
hook that changes when the search bar is clicked and sets the search button background color.
The MainBody component looks like this:
<span class="hljs-string">"use client"</span>;
<span class="hljs-keyword">import</span> { useEffect, useState } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> ArticleCard, { Article } <span class="hljs-keyword">from</span> <span class="hljs-string">'./ArticleCard'</span>;
<span class="hljs-keyword">interface</span> MainBodyProps {
searchTerm: <span class="hljs-built_in">string</span>;
articles: Article[];
}
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">MainBody</span>(<span class="hljs-params">{ searchTerm, articles }: MainBodyProps</span>) </span>{
<span class="hljs-comment">// Get articles from JSON file and create array of article objects</span>
<span class="hljs-keyword">const</span> [filteredArticles, setFilteredArticles] = useState<Article[]>([]);
<span class="hljs-keyword">const</span> tags = [<span class="hljs-string">"Networking"</span>, <span class="hljs-string">"Cloud"</span>, <span class="hljs-string">"DevOps"</span>, <span class="hljs-string">"Web Dev"</span>, <span class="hljs-string">"Cybersecurity"</span>];
<span class="hljs-keyword">const</span> [isActive, setIsActive] = useState(tags.map(<span class="hljs-function">() =></span> <span class="hljs-literal">false</span>));
<span class="hljs-comment">// Filter articles based on search term and active tags</span>
useEffect(<span class="hljs-function">() =></span> {
<span class="hljs-keyword">const</span> anyTagActive = isActive.some(<span class="hljs-function">(<span class="hljs-params">val</span>) =></span> val);
<span class="hljs-keyword">const</span> filtered = articles.filter(<span class="hljs-function">(<span class="hljs-params">article</span>) =></span> {
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Search term: '</span> + searchTerm || <span class="hljs-string">'searchTerm'</span>);
<span class="hljs-keyword">const</span> searchMatch =
article.title?.toLowerCase().includes(searchTerm.toLowerCase()) ||
article.description?.toLowerCase().includes(searchTerm.toLowerCase()) ||
article.tags?.some(<span class="hljs-function">(<span class="hljs-params">tag</span>) =></span> tag.toLowerCase().includes(searchTerm.toLowerCase())) ||
article.siteName?.toLowerCase().includes(searchTerm.toLowerCase()) ||
article.publishedDate?.toLowerCase().includes(searchTerm.toLowerCase());
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'This is the searchMarch: '</span> + searchMatch || <span class="hljs-string">'FALSE searchMatch'</span>);
<span class="hljs-built_in">console</span>.log(article.title || <span class="hljs-string">'article.title no wan show'</span>);
<span class="hljs-keyword">const</span> tagMatch = article.tags?.some(<span class="hljs-function">(<span class="hljs-params">tag</span>) =></span> {
<span class="hljs-keyword">const</span> index = tags.indexOf(tag);
<span class="hljs-keyword">return</span> index !== <span class="hljs-number">-1</span> && isActive[index];
}) || <span class="hljs-literal">false</span>;
<span class="hljs-keyword">if</span> (anyTagActive) {
<span class="hljs-keyword">return</span> tagMatch && searchMatch; <span class="hljs-comment">// Only return articles if tag is active and search matches</span>
}
<span class="hljs-keyword">return</span> searchMatch; <span class="hljs-comment">// If no tags active, return all that match the search term</span>
});
setFilteredArticles(filtered);
}, [articles, searchTerm, isActive]);
<span class="hljs-built_in">console</span>.log(filteredArticles);
<span class="hljs-keyword">return</span> (
<div className=<span class="hljs-string">'scroll-smooth'</span>>
<div id=<span class="hljs-string">"tags"</span> className=<span class="hljs-string">"flex w-full h-[200px] md:h-[60px] justify-center gap-5 py-4 flex-wrap max-w-[100vw] scroll-smooth"</span>>
{tags.map(<span class="hljs-function">(<span class="hljs-params">tag, index</span>) =></span> (
<p
key={index}
onClick={<span class="hljs-function">() =></span> {
<span class="hljs-keyword">const</span> newIsActive = [...isActive];
newIsActive[index] = !newIsActive[index];
setIsActive(newIsActive);
}}
className={<span class="hljs-string">`h-[48px] w-[140px] border-3 rounded-[40px] px-2 py-2 text-center font-bold <span class="hljs-subst">${
isActive[index]
? <span class="hljs-string">'bg-black border-black text-white hover:bg-gray-700 hover:border-gray-700'</span>
: <span class="hljs-string">'border-blue-500 hover:bg-blue-500 hover:text-white'</span>
}</span>`</span>}>
{tag}
</p>
))}
</div>
<div id=<span class="hljs-string">"articlegrid"</span> className=<span class="hljs-string">"w-[100vw] md:w-[98vw] grid gap-2 grid-cols-1 md:grid-cols-2 xl:grid-cols-3 mt-5 px-3 py-3"</span>>
<ArticleCard articles={filteredArticles} />
</div>
</div>
);
}
Here, we have props from the parent component too, but we only need the articles fetched and the search term. We don’t need to set the fetch term from this component.
To render the tags, I first created the array of tags and an array of boolean values to record the states of the tags (whether they’re active or inactive).
<span class="hljs-keyword">const</span> tags = [<span class="hljs-string">"Networking"</span>, <span class="hljs-string">"Cloud"</span>, <span class="hljs-string">"DevOps"</span>, <span class="hljs-string">"Web Dev"</span>, <span class="hljs-string">"Cybersecurity"</span>];
<span class="hljs-keyword">const</span> [isActive, setIsActive] = useState(tags.map(<span class="hljs-function">() =></span> <span class="hljs-literal">false</span>));
Then, inside the return statement, I mapped through the tag array to render them one by one. The onClick event handler also works here to make sure that the isActive
state for that particular tag is toggled when it is clicked.
So how does this work? It creates a new array called newIsActive
that is a copy of the isActive
array. It then gets the particular tag by index number and inverts it. Then it sets the isActive
array to this new array.
{tags.map(<span class="hljs-function">(<span class="hljs-params">tag, index</span>) =></span> (
<p
key={index}
onClick={<span class="hljs-function">() =></span> {
<span class="hljs-keyword">const</span> newIsActive = [...isActive];
newIsActive[index] = !newIsActive[index];
setIsActive(newIsActive);
}} . . .
This is the code for the ArticleCard:
<span class="hljs-keyword">import</span> React, { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> Image <span class="hljs-keyword">from</span> <span class="hljs-string">'next/image'</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> Article {
id: <span class="hljs-built_in">number</span>;
title?: <span class="hljs-built_in">string</span>;
description?: <span class="hljs-built_in">string</span>;
publishedDate?: <span class="hljs-built_in">string</span>;
url: <span class="hljs-built_in">string</span>;
imgUrl?: <span class="hljs-built_in">string</span>;
siteName?: <span class="hljs-built_in">string</span>;
tags?: <span class="hljs-built_in">string</span>[];
}
<span class="hljs-keyword">interface</span> ArticleProps {
articles: Article[];
}
<span class="hljs-keyword">const</span> ArticleCard = <span class="hljs-function">(<span class="hljs-params">{ articles }: ArticleProps</span>) =></span> {
<span class="hljs-keyword">return</span> (
<>
{articles ?
(articles.map(<span class="hljs-function">(<span class="hljs-params">item, id</span>) =></span> (
<span class="hljs-comment">//anchor tag for the link</span>
<a key={id} href={item.url} className=<span class="hljs-string">'max-w-[350px] mx-auto mb-5'</span>>
<div className=<span class="hljs-string">"sm:w-[350px] hover:brightness-70"</span> data-title={item.title} data-description={item.description} data-published-date={item.publishedDate} data-tag=<span class="hljs-string">"Networking"</span> data-site-name={item.siteName}>
<Image
src={item.imgUrl || <span class="hljs-string">'/img-2.jpg'</span>}
alt={item.title || <span class="hljs-string">'Article Image'</span>}
width={<span class="hljs-number">350</span>}
height={<span class="hljs-number">400</span>}
className=<span class="hljs-string">"object-cover rounded-[10px]"</span>
/>
<div className=<span class="hljs-string">"flex h-[43px] text-[14px] text-gray-500 gap-2"</span>>
<p id=<span class="hljs-string">"Platform"</span> className=<span class="hljs-string">"py-2 h-[42px] md:text-sm mt-auto mb-auto"</span>>{item.siteName}</p>
<div className=<span class="hljs-string">"h-1 w-1 bg-black rounded-full mt-auto mb-auto bg-gray-500"</span>></div>
<p id=<span class="hljs-string">"publishedDate"</span> className=<span class="hljs-string">"py-2 h-[42px] mt-auto mb-auto"</span>>{item.publishedDate}</p>
</div>
<h1 id=<span class="hljs-string">"titleOfArticle"</span> className=<span class="hljs-string">"font-bold text-base md:text-3xl"</span>>{item.title}</h1>
<br/>
<p className=<span class="hljs-string">'w-full md:w-[350px]'</span>>{item.description}</p>
</div>
</a>
)))
:
( <span class="hljs-built_in">Array</span>(<span class="hljs-number">6</span>).fill(<span class="hljs-number">0</span>).map(<span class="hljs-function">(<span class="hljs-params">item, id</span>) =></span> (
<div key={id} className=<span class="hljs-string">"w-full md:w-[350px] h-[350px] bg-gray-500 mx-auto mb-5 hover:brightness-80 rounded-[10px] animate-pulse"</span>></div>
)))
}
</>
);
};
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> ArticleCard;
Here, we defined and exported the Article
interface so that we can create Article
objects in the MainBody
. Then, we created an interface to pass down the props of an array of Article
objects.
Next, there’s this part to ensure it renders something even if for some reason no Article object was passed:
{
article?
( {<span class="hljs-comment">/*If article exists, render this*/</span>} )
:
( {<span class="hljs-comment">/*Else, render this */</span>} )
}
Our fail-safe here is an empty array of six objects with the Tailwind animate-pulse
:
( <span class="hljs-built_in">Array</span>(<span class="hljs-number">6</span>).fill(<span class="hljs-number">0</span>).map(<span class="hljs-function">(<span class="hljs-params">item, id</span>) =></span> (
<div key={id} className=<span class="hljs-string">"w-full md:w-[350px] h-[350px] bg-gray-500 mx-auto mb-5 hover:brightness-80 rounded-[10px] animate-pulse"</span>></div>
)))
I could have made this part much better, but I was feeling a little lazy. I also used the Image
from Next, instead of the regular img
. This requires that you edit the next.config.ts
file. I had to go add all the paths that the images could possibly be loaded from:
Just like in the screenshot above, the syntax is:
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { NextConfig } <span class="hljs-keyword">from</span> <span class="hljs-string">"next"</span>;
<span class="hljs-keyword">const</span> nextConfig: NextConfig = {
images: {
remotePatterns: [
{
protocol:<span class="hljs-string">"https"</span>,
hostname:<span class="hljs-string">"licdn.com"</span>,
pathname:<span class="hljs-string">"/**"</span>
},
{
protocol:<span class="hljs-string">""</span>,
hostname:<span class="hljs-string">""</span>,
pathname:<span class="hljs-string">""</span>
}
],
},
};
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> nextConfig;
It takes a remotePatterns
array that consists of remote pattern objects, which have a protocol, hostname, and pathname property. Make sure the protocol and hostname properties are not empty like in the second object in the code sample above. That would cause errors. It’s either the objects are populated properly or they’re deleted.
The Footer looks like this:
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Footer</span>(<span class="hljs-params"></span>) </span>{
<span class="hljs-keyword">return</span> (
<footer className=<span class="hljs-string">"bg-gray-100 text-center py-4 mt-10"</span>>
<div className=<span class="hljs-string">"flex align-items-center justify-center text-sm text-blue-400 font-bold"</span>>
<a href=<span class="hljs-string">"/"</span> className=<span class="hljs-string">"hover:text-blue-600"</span>>Home</a>
<p> | </p>
<a href=<span class="hljs-string">"/about"</span> className=<span class="hljs-string">"hover:text-blue-600"</span>>About</a>
</div>
<p className=<span class="hljs-string">"text-sm text-gray-600"</span>>© {<span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>().getFullYear()} Chidiadi Anyanwu. All rights reserved.</p>
<p className=<span class="hljs-string">"text-sm text-gray-600"</span>>Built <span class="hljs-keyword">with</span> Next.js and Tailwind CSS</p>
</footer>
);
}
This new Date().getFullYear()
helps me get the current year all the time.
5. Place the components properly
The nav bar and footer components are things that will not change no matter the page you visit. So they should be placed in a more permanent and untouched location. We can put both of them in the root layout.tsx
file like this:
<body className={<span class="hljs-string">`<span class="hljs-subst">${geistSans.variable}</span> <span class="hljs-subst">${geistMono.variable}</span> antialiased scroll-smooth`</span>}>
<Navbar />
{children}
<Footer />
</body>
{children}
is where the contents from page.tsx
will enter. So, we sandwiched all the other content in the Nav bar and footer. Apart from adding <link />
tags for fonts (because this is where the root HTML is), we really don’t have business with this file again.
Now, in the same app/
folder where this layout file is, create the <HomeClient />
file. This is how it looks:
<span class="hljs-string">'use client'</span>;
<span class="hljs-keyword">import</span> { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> Hero <span class="hljs-keyword">from</span> <span class="hljs-string">'../components/hero'</span>;
<span class="hljs-keyword">import</span> MainBody <span class="hljs-keyword">from</span> <span class="hljs-string">'../components/mainbody'</span>;
<span class="hljs-keyword">import</span> { Article } <span class="hljs-keyword">from</span> <span class="hljs-string">'../components/ArticleCard'</span>;
<span class="hljs-keyword">interface</span> Props {
initialArticles: Article[];
}
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">HomeClient</span>(<span class="hljs-params">{ initialArticles }: Props</span>) </span>{
<span class="hljs-keyword">const</span> [searchTerm, setSearchTerm] = useState<<span class="hljs-built_in">string</span>>(<span class="hljs-string">''</span>);
<span class="hljs-keyword">const</span> [articles, setArticles] = useState<Article[]>(initialArticles);
<span class="hljs-keyword">return</span> (
<div>
<Hero searchTerm={searchTerm} setSearchTerm={setSearchTerm} />
<MainBody searchTerm={searchTerm} articles={articles} />
</div>
);
}
Then, put the HomeClient
component inside the page.tsx
file:
<span class="hljs-keyword">import</span> { fetchArticles } <span class="hljs-keyword">from</span> <span class="hljs-string">'../utils/fetchArticles'</span>;
<span class="hljs-keyword">import</span> HomeClient <span class="hljs-keyword">from</span> <span class="hljs-string">'./HomeClient'</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> revalidate = <span class="hljs-number">3600</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">HomePage</span>(<span class="hljs-params"></span>) </span>{
<span class="hljs-keyword">const</span> articles = <span class="hljs-keyword">await</span> fetchArticles();
<span class="hljs-keyword">return</span> <HomeClient initialArticles={articles} />;
}
The server is set to fetch the articles at build time, and fetch again (revalidate) every hour (3600s). So, it doesn’t fetch the articles from the URLs upon user request of the page.
Initially, it worked by fetching any time the component was mounted, but I noticed that this caused the page to load very slowly. The articles didn’t pop-up on time, because there’s a lot of fetching to be done.
In that same app/
directory, create an about/
folder, and create the page.tsx
for that route:
<span class="hljs-keyword">import</span> Image <span class="hljs-keyword">from</span> <span class="hljs-string">"next/image"</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">About</span>(<span class="hljs-params"></span>) </span>{
<span class="hljs-keyword">return</span> (
<>
<div className=<span class="hljs-string">"flex items-center justify-center"</span>>
<div className=<span class="hljs-string">"margin-auto w-[90vw] md:w-[60vw] lg:w-[50vw] h-[450px] hover:bg-gray-100 border-1 md:border-2 border-gray-200 shadow-sm flex flex-wrap items-center justify-center gap-2 mt-10 mb-10 rounded-lg"</span>>
<Image
src=<span class="hljs-string">"/MyPhotoChidiadi.jpg"</span>
alt=<span class="hljs-string">"Avatar"</span>
className=<span class="hljs-string">"rounded-[50%] h-30 w-30"</span>
width={<span class="hljs-number">120</span>}
height={<span class="hljs-number">120</span>}
/>
<div className=<span class="hljs-string">"w-[90%] mx-auto"</span>>
<h1 className=<span class="hljs-string">"text-xl text-center my-1 font-bold"</span>>About Me</h1>
<p className=<span class="hljs-string">"text-justify my-3"</span>>
My name is Chidiadi Anyanwu. I love breaking down complex concepts.
I write about Networking, Cloud, DevOps, and even sometimes web development.
You can connect <span class="hljs-keyword">with</span> me by following <span class="hljs-built_in">any</span> <span class="hljs-keyword">of</span> the links below.
</p>
<hr className=<span class="hljs-string">"border-gray-300 my-3"</span> />
<div className=<span class="hljs-string">"flex gap-7 w-full my-3 justify-center"</span>>
<a href=<span class="hljs-string">"https://github.com/chidiadi01"</span>>
<Image src=<span class="hljs-string">'/github-icon.svg'</span> alt=<span class="hljs-string">"github logo"</span> width={<span class="hljs-number">24</span>} height={<span class="hljs-number">24</span>} />
</a>
<a href=<span class="hljs-string">"https://linkedin.com/in/chidiadi-anyanwu"</span>>
<Image src=<span class="hljs-string">'linkedin-icon.svg'</span> alt=<span class="hljs-string">"linkedin logo"</span> width={<span class="hljs-number">24</span>} height={<span class="hljs-number">24</span>}/>
</a>
<a href=<span class="hljs-string">"https://x.com/chidiadi01"</span>>
<Image src=<span class="hljs-string">'x-2.svg'</span> alt=<span class="hljs-string">"x logo"</span> width={<span class="hljs-number">24</span>} height={<span class="hljs-number">24</span>}/>
</a>
</div>
</div>
</div>
</div>
</>
);
}
6. Create the utils folder and all the functions
The next step is to create all these files.
Under the same app/
directory, create the utils/
folder. app/utils/
. Then start with the fetchArticles()
function. The fetchArticles()
function is what accesses the API route in the project to obtain the array of Article objects from an array of URLs. The fetchArticles()
function returns an array of those objects which are then stored in the articles
variable. It looks like this:
<span class="hljs-keyword">import</span> { getPublishedDate } <span class="hljs-keyword">from</span> <span class="hljs-string">'./getPublishedDate'</span>;
<span class="hljs-keyword">import</span> { getTitle } <span class="hljs-keyword">from</span> <span class="hljs-string">'./getTitle'</span>;
<span class="hljs-keyword">import</span> { getImageURL } <span class="hljs-keyword">from</span> <span class="hljs-string">'./getImageURL'</span>;
<span class="hljs-keyword">import</span> { getDescription } <span class="hljs-keyword">from</span> <span class="hljs-string">'./getDescription'</span>;
<span class="hljs-keyword">import</span> { getPlatform } <span class="hljs-keyword">from</span> <span class="hljs-string">'./getPlatform'</span>;
<span class="hljs-keyword">import</span> articleFile <span class="hljs-keyword">from</span> <span class="hljs-string">'../app/articles.json'</span>;
<span class="hljs-keyword">import</span> { Article } <span class="hljs-keyword">from</span> <span class="hljs-string">'../components/ArticleCard'</span>;
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> cheerio <span class="hljs-keyword">from</span> <span class="hljs-string">'cheerio'</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">fetchArticles</span>(<span class="hljs-params"></span>): <span class="hljs-title">Promise</span><<span class="hljs-title">Article</span>[]> </span>{
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Fetching articles...'</span>);
<span class="hljs-keyword">const</span> results = <span class="hljs-keyword">await</span> <span class="hljs-built_in">Promise</span>.all(
articleFile.articles.map(<span class="hljs-keyword">async</span> (item) => {
<span class="hljs-comment">//Validate URL first</span>
<span class="hljs-keyword">if</span> (!item.url || <span class="hljs-keyword">typeof</span> item.url !== <span class="hljs-string">'string'</span> || item.url.trim() === <span class="hljs-string">''</span>) {
<span class="hljs-built_in">console</span>.warn(<span class="hljs-string">`Invalid URL: <span class="hljs-subst">${item.url}</span>`</span>);
<span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>; <span class="hljs-comment">// Skip this item</span>
}
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'The URL: '</span> + item.url);
<span class="hljs-keyword">let</span> data;
<span class="hljs-keyword">try</span> {
<span class="hljs-comment">// Fetch metadata and HTML from the URL</span>
<span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(item.url, {
headers: {
<span class="hljs-string">'User-Agent'</span>: <span class="hljs-string">'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36'</span>,
<span class="hljs-string">'Accept'</span>: <span class="hljs-string">'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'</span>,
<span class="hljs-string">'Accept-Language'</span>: <span class="hljs-string">'en-US,en;q=0.5'</span>,
<span class="hljs-string">'Referer'</span>: <span class="hljs-string">'https://www.google.com/'</span>,
},
});
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Fetched: '</span>+ item.url);
<span class="hljs-keyword">if</span> (!response.ok) {
<span class="hljs-built_in">console</span>.error(<span class="hljs-string">`HTTP error! Status: <span class="hljs-subst">${response.status}</span> for URL: <span class="hljs-subst">${item.url}</span>`</span>);
<span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">`HTTP error! Status: <span class="hljs-subst">${response.status}</span>`</span>);
}
<span class="hljs-keyword">const</span> html = <span class="hljs-keyword">await</span> response.text();
<span class="hljs-keyword">const</span> $ = cheerio.load(html);
<span class="hljs-keyword">const</span> jsonScript = $(<span class="hljs-string">'script[type="application/ld+json"]'</span>).html();
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Gotten HTML response'</span>);
<span class="hljs-keyword">if</span> (!jsonScript) {
<span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'No JSON-LD script found on page'</span>);
}
<span class="hljs-keyword">const</span> metadata = <span class="hljs-built_in">JSON</span>.parse(jsonScript);
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Gotten metadata'</span>);
<span class="hljs-comment">// Combine metadata and HTML into a single object</span>
data = { metadata, html };
} <span class="hljs-keyword">catch</span> (error) {
<span class="hljs-built_in">console</span>.error(<span class="hljs-string">`Failed to fetch metadata for URL: <span class="hljs-subst">${item.url}</span>`</span>, error);
<span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'The default empty object has been returned here'</span>);
}
<span class="hljs-comment">// Use the combined data (metadata and HTML) to construct the article object</span>
<span class="hljs-keyword">if</span>(getTitle(data) && getDescription(data) &&
getPublishedDate(data) && getImageURL(data) &&
getPlatform(data) || (item.title && item.description &&
item.image)) {
<span class="hljs-keyword">return</span> {
...item,
id: item.id ?? <span class="hljs-number">0</span>,
tags: item.tags ?? [],
title: getTitle(data) || item.title || <span class="hljs-string">'No title'</span>,
description: item.description || getDescription(data) || <span class="hljs-string">'No description'</span>,
publishedDate: getPublishedDate(data) ?? <span class="hljs-string">'No date'</span>,
imgUrl: getImageURL(data) || item.image || <span class="hljs-string">'/img-2.jpg'</span>,
siteName: getPlatform(data) || data.metadata?.publisher?.name || <span class="hljs-string">'Unknown site'</span>,
url: item.url || <span class="hljs-string">''</span>,
} <span class="hljs-keyword">as</span> Article;
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Proper item returned'</span>);
} <span class="hljs-keyword">else</span> { <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>; }
})
);
<span class="hljs-comment">// Filter out null values and sort the articles by published date in descending order</span>
<span class="hljs-keyword">const</span> filteredResults = results.filter((article): article is Article => article !== <span class="hljs-literal">null</span>);
<span class="hljs-keyword">const</span> sortedResults = filteredResults.sort(<span class="hljs-function">(<span class="hljs-params">a, b</span>) =></span> {
<span class="hljs-keyword">const</span> dateA = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(a.publishedDate || <span class="hljs-string">''</span>).getTime();
<span class="hljs-keyword">const</span> dateB = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(b.publishedDate || <span class="hljs-string">''</span>).getTime();
<span class="hljs-keyword">return</span> dateB - dateA;
});
<span class="hljs-built_in">console</span>.log(sortedResults);
<span class="hljs-keyword">return</span> sortedResults;
}
It maps through the articles in the articleFile, which is the JSON file with an array of objects with article URLs. For each of them, it sends a request to the URL, and from the data gotten, returns an Article object. Then, the array of objects created, results
, is first filtered to remove null objects, and sorted in descending order by their date properties. So, the latest article shows up first.
It’s then assigned in the HomeClient
component:
<span class="hljs-keyword">const</span> articles = <span class="hljs-keyword">await</span> fetchArticles();
In the fetchArticles()
code above, you can see that some other functions were used to extract the properties from the URLs, and assign them. Also, during deployment, I found that Substack couldn’t be accessed by the server, so I’m going to add code to allow creation of Article objects from an RSS feed. That will be in the project repository.
Now, let’s talk about the other functions.
The getTitle()
function:
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> cheerio <span class="hljs-keyword">from</span> <span class="hljs-string">'cheerio'</span>;
<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getTitle</span>(<span class="hljs-params">data:<span class="hljs-built_in">any</span></span>): <span class="hljs-title">string</span> </span>{
<span class="hljs-keyword">if</span>(!data) <span class="hljs-keyword">return</span> <span class="hljs-string">'Title Loading . . .'</span>;
<span class="hljs-keyword">if</span> (data?.html) {
<span class="hljs-keyword">const</span> $ = cheerio.load(data?.html);
<span class="hljs-keyword">const</span> ogTitle = $(<span class="hljs-string">'meta[property="og:title"]'</span>).attr(<span class="hljs-string">'content'</span>) || $(<span class="hljs-string">'title'</span>).text();
<span class="hljs-keyword">return</span> ogTitle;
}
<span class="hljs-keyword">return</span> <span class="hljs-string">'The Title of The Article'</span>;
}
This is a very simple function. It takes the data
parameter, and if there’s no data, it returns Title loading . . .
. But if there is data, it checks to see if there’s HTML in the data. If there is, it then uses cheerio to load the HTML text and extract the title from the Open Graph title
metadata or from the <title>
tag in the HTML header. Else, it returns The Title of the Article
.
Here, we use jQuery-like syntax $
to select the HTML elements, like in $('title')
. The data taken as a parameter is the response gotten from a HTTP request to the article’s URL.
The getDescription()
function:
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> cheerio <span class="hljs-keyword">from</span> <span class="hljs-string">'cheerio'</span>;
<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getDescription</span>(<span class="hljs-params">data: <span class="hljs-built_in">any</span></span>): <span class="hljs-title">string</span> </span>{
<span class="hljs-keyword">if</span> (!data) <span class="hljs-keyword">return</span> <span class="hljs-string">'Description Loading . . .'</span>;
<span class="hljs-keyword">if</span> (data?.metadata || data?.html) {
<span class="hljs-keyword">const</span> $ = cheerio.load(data?.html || <span class="hljs-string">''</span>);
<span class="hljs-keyword">const</span> description = data?.metadata?.description ?? $(<span class="hljs-string">'meta[property="og:description"]'</span>).attr(<span class="hljs-string">'content'</span>) ?? <span class="hljs-string">'No description found'</span>;
<span class="hljs-keyword">return</span> description;
}
<span class="hljs-keyword">return</span> <span class="hljs-string">'No description found'</span>;
}
The getURL()
function:
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> cheerio <span class="hljs-keyword">from</span> <span class="hljs-string">'cheerio'</span>;
<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getURL</span>(<span class="hljs-params">data: <span class="hljs-built_in">any</span></span>): <span class="hljs-title">string</span></span>{
<span class="hljs-keyword">if</span>(!data) <span class="hljs-keyword">return</span> <span class="hljs-string">'url'</span>;
<span class="hljs-keyword">if</span>(data?.metadata || data?.html){
<span class="hljs-keyword">const</span> $ = cheerio.load(data?.html);
<span class="hljs-keyword">const</span> url = data?.metadata.url || $(<span class="hljs-string">'meta[property="og:url"]'</span>).attr(<span class="hljs-string">'content'</span>);
<span class="hljs-keyword">return</span> url;
}
<span class="hljs-keyword">return</span> <span class="hljs-string">'url'</span>;
}
This function is not really used to get the URL of the article for use in the object. Rather, it is used to get the URL for another function, getPlatform()
. It works the same way as the ones we discussed before.
The getPlatform()
function:
<span class="hljs-keyword">import</span> { getURL } <span class="hljs-keyword">from</span> <span class="hljs-string">'./getURL'</span>;
<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getPlatform</span>(<span class="hljs-params">data: <span class="hljs-built_in">any</span></span>): <span class="hljs-title">string</span> </span>{
<span class="hljs-keyword">if</span> (!data) <span class="hljs-keyword">return</span> <span class="hljs-string">'Platform1'</span>;
<span class="hljs-keyword">const</span> url = getURL(data);
<span class="hljs-keyword">if</span> (data?.html) {
<span class="hljs-keyword">const</span> regex = <span class="hljs-regexp">/^(?:https?://)?(?:www.)?([^/n]+).(?:[a-zA-Z]{2,})/</span>;
<span class="hljs-keyword">const</span> platform = url.match(regex);
<span class="hljs-keyword">return</span> platform?.[<span class="hljs-number">1</span>].toUpperCase() || <span class="hljs-string">'Platform2'</span>;
}
<span class="hljs-keyword">return</span> <span class="hljs-string">'Platform3'</span>;
}
This function is meant to extract the name of the platform where the article is posted. I toyed with various ideas for how this should work. One of them was using the siteName
property in the OG meta tags, but I realised from my inspection that not all platforms had it populated in a helpful way. So, the results gotten from that method would be too unpredictable.
So I decided to use regex (Regular Expressions) to extract the site name from the URL. As you can see from the code, I didn’t achieve a perfect result, but it is usable.
First of all, it gets the URL of the article with the getURL()
function. Then, it uses regex:
/^(?:https?://)?(?:www.)?([^/n]+).(?:[a-zA-Z]{2,})/
Here, /
and /
at the beginning and end are to start and end the regex string. The caret ^
marks the beginning of a line.
Then, we have four groups ()()()()
. The first one is a non-captured group (?: )
. That means whatever text that matches that should be grouped together in a string, but should not be captured to be assigned to the variable. It captures any text with a ‘http’ in it, with or without the s s?
, and with two slashes after. The forward slashes were escaped with backward slashes so they can be recognised as literal characters. Then, the whole group itself is made optional by adding the question mark after it (...)?
. So, whether such a group is matched or not, the code works.
The second group is also a non-capturing group, also denoted by ?:
being the first thing inside the bracket. This one matches any ‘www.’ in the string. It’s also optional. A URL may not necessarily be written with it.
The third group is a capturing group as it doesn’t have ?:
inside the brackets. Rather, it has a character class in it []
. But it’s a negated class [^ ]
. It makes sure that the class does not contain a newline character n
(the newline character n is not a string of letter n – that’s why it is escaped) or a forward slash /
, because a URL is supposed to be one line, and not multiple lines. The +
means one or more characters, ([^/n]+)
. Whatever is in this group will get captured in the variable.
Then, the next one matches a dot (it is escaped with a backslash .
). After that is the last group which is also non-capturing and matches any character which is alphanumeric, capital or small letter [a-zA-Z]
, that occurs more than two times {2, }
.
So, if we have ‘https://www.linkedin,com’ we would have an array of captured groups [‘https://www.linkedin.com’,’https://’,’www.’,’linkedin’,’com’]. Group 1 = ‘https://’, group 2 = ‘www.’, group 3 =’linkedin’, group 4 = ‘com’. But since only group 3 is a captured group, others will be discarded, and we have an array with only two items, the full string, and the captured group: [‘https://www.linkedin.com’,’linkedin’].
So, here, we return the second item in the array. The first item is always the full string we matched.
<span class="hljs-keyword">return</span> platform?.[<span class="hljs-number">1</span>].toUpperCase()
This doesn’t account for sub domains, though. This is tricky because sometimes you want to use the name of the subdomain (as in my Substack), and sometimes you want to use the name of the domain. So, I left it like that.
The getImageURL()
function:
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> cheerio <span class="hljs-keyword">from</span> <span class="hljs-string">'cheerio'</span>;
<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getImageURL</span>(<span class="hljs-params">data: <span class="hljs-built_in">any</span></span>): <span class="hljs-title">string</span> </span>{
<span class="hljs-keyword">if</span> (!data) <span class="hljs-keyword">return</span> <span class="hljs-string">'/img-2.jpg'</span>;
<span class="hljs-keyword">if</span> (data?.metadata || data?.html) {
<span class="hljs-keyword">const</span> $ = cheerio.load(data?.html);
<span class="hljs-keyword">const</span> ogImage = $(<span class="hljs-string">'meta[property="og:image"]'</span>).attr(<span class="hljs-string">'content'</span>) || data?.metadata.image;
<span class="hljs-keyword">return</span> ogImage || <span class="hljs-string">'/img-2.jpg'</span>;
}
<span class="hljs-keyword">return</span> <span class="hljs-string">'/img-2.jpg'</span>;
}
This function works just like others, and obtains the cover image URL from either the Open Graph image meta tag $('meta[property="og:image"]').attr('content')
or ||
the image property in the JSON-LD data data?.metadata.image
.
The getPublishedDate()
function:
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> cheerio <span class="hljs-keyword">from</span> <span class="hljs-string">'cheerio'</span>;
<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getPublishedDate</span>(<span class="hljs-params">data: <span class="hljs-built_in">any</span></span>): <span class="hljs-title">string</span> </span>{
<span class="hljs-keyword">if</span> (!data) <span class="hljs-keyword">return</span> <span class="hljs-string">'Date'</span>;
<span class="hljs-keyword">const</span> publishedDate = data?.metadata?.datePublished;
<span class="hljs-keyword">if</span> (publishedDate) {
<span class="hljs-keyword">const</span> options: <span class="hljs-built_in">Intl</span>.DateTimeFormatOptions = { year: <span class="hljs-string">'numeric'</span>, month: <span class="hljs-string">'long'</span>, day: <span class="hljs-string">'numeric'</span> };
<span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(publishedDate).toLocaleDateString(<span class="hljs-string">'en-US'</span>, options);
}
<span class="hljs-keyword">if</span> (data?.html) {
<span class="hljs-keyword">const</span> $ = cheerio.load(data?.html);
<span class="hljs-keyword">const</span> ogPublishedTime = $(<span class="hljs-string">'meta[property="article:published_time"]'</span>).attr(<span class="hljs-string">'content'</span>) ||
$(<span class="hljs-string">'meta[property="og:published_time"]'</span>).attr(<span class="hljs-string">'content'</span>) ||
$(<span class="hljs-string">'meta[name="pubdate"]'</span>).attr(<span class="hljs-string">'content'</span>);
<span class="hljs-keyword">if</span> (ogPublishedTime) {
<span class="hljs-keyword">const</span> options: <span class="hljs-built_in">Intl</span>.DateTimeFormatOptions = { year: <span class="hljs-string">'numeric'</span>, month: <span class="hljs-string">'long'</span>, day: <span class="hljs-string">'numeric'</span> };
<span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(ogPublishedTime).toLocaleDateString(<span class="hljs-string">'en-US'</span>, options);
}
}
<span class="hljs-keyword">return</span> <span class="hljs-string">'Date'</span>;
}
This function is especially useful because of the need to convert the date from the ISO 8601 format (2025-04-07T10:47:19+00:00) to the more readable format I want (April 7, 2025). Here, I used the .toLocaleDateString()
JavaScript function to make it work (see the (MDN).
7. Create your JSON file
Now, remember that we’re building this to be able to pull URLs from a JSON file to put together and render the web page. That JSON file is the starting point of everything. I believe by now you’re getting an error concerning that. So we need to create the JSON file.
In the app/
directory, create a new file and name it articles.json
.
Then populate it like in this file below – an array of objects with id, URL, tags, and so on. Even though we are not trying to get the title, description, and everything from this file directly, I put in that feature. If you go back to our fetchArticles()
function, you’ll see that for most of the properties, whatever you write here will override what was gotten from the URLs.
It was partly a fail-safe because I thought that LinkedIn would block all requests, and as you can see from my blog already, some description tags were not well organized. So, we can replace them later with a cleaner description just by modifying this file.
{
<span class="hljs-attr">"articles"</span>: [
{
<span class="hljs-attr">"id"</span>: <span class="hljs-number">1</span>,
<span class="hljs-attr">"url"</span>: <span class="hljs-string">"https://thenetworkbits.substack.com/p/an-overview-of-json"</span>,
<span class="hljs-attr">"tags"</span>: [<span class="hljs-string">"Web Dev"</span>, <span class="hljs-string">"DevOps"</span>, <span class="hljs-string">"Cloud"</span>],
<span class="hljs-attr">"title"</span>: <span class="hljs-string">""</span>,
<span class="hljs-attr">"description"</span>: <span class="hljs-string">""</span>,
<span class="hljs-attr">"image"</span>: <span class="hljs-string">""</span>
},
{
<span class="hljs-attr">"id"</span>: <span class="hljs-number">2</span>,
<span class="hljs-attr">"url"</span>: <span class="hljs-string">"https://websecuritylab.org/how-safe-is-public-wi-fi-a-network-engineer-explains/"</span>,
<span class="hljs-attr">"tags"</span>: [<span class="hljs-string">"Networking"</span>, <span class="hljs-string">"Cybersecurity"</span>],
<span class="hljs-attr">"title"</span>: <span class="hljs-string">""</span>,
<span class="hljs-attr">"description"</span>: <span class="hljs-string">""</span>,
<span class="hljs-attr">"image"</span>: <span class="hljs-string">""</span>
},
{
<span class="hljs-attr">"id"</span>: <span class="hljs-number">3</span>,
<span class="hljs-attr">"url"</span>: <span class="hljs-string">"https://www.freecodecamp.org/news/automate-cicd-with-github-actions-streamline-workflow/"</span>,
<span class="hljs-attr">"tags"</span>: [<span class="hljs-string">"DevOps"</span>],
<span class="hljs-attr">"title"</span>: <span class="hljs-string">""</span>,
<span class="hljs-attr">"description"</span>: <span class="hljs-string">""</span>,
<span class="hljs-attr">"image"</span>: <span class="hljs-string">""</span>
}
]
}
Here, we have an “articles” object with an array of objects, each of which have “id”, “url”, “tags”, “title”, “description”, and “image” properties. You don’t necessarily need the values of all of these except the ID and URL, but the keys have to be there to prevent errors.
8. Add the finishing touches
Now you can add your own favicon in the app directory. It could be a 24px by 24px file, or 48px by 48px file. It doesn’t necessarily have to be in the app directory or be an icon file or be named ‘favicon’ – but I did it that way. You can just add this in the HTML header of your layout.tsx file which is your Next.js version of index.html
. The favicon is the icon that shows on the tab in your browser when you open the page.
<span class="hljs-tag"><<span class="hljs-name">link</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"icon"</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/favicon.ico"</span> <span class="hljs-attr">sizes</span>=<span class="hljs-string">"any"</span> /></span>
You can also read the Next.js documentations on that here: Metadata Files: favicon, icon, and apple-icon | Next.js. Then add your images to your public/
directory. Be sure to name them correctly, and reference them correctly.
Now, if your development server was down, spin it up again to see your end results!
npm run dev
Conclusion
If you’ve read this far, then you must be really interested in seeing the results of all this 🙂 I already have that covered. Here’s the blog. You can go through it and interact with it.
Also, this is the codebase. Feel free to fork it, clone it, and interact with it as well. If you enjoyed the article, please share it with others. You can also connect with me on LinkedIn or X. Thanks for reading.
Source: freeCodeCamp Programming Tutorials: Python, JavaScript, Git & MoreÂ