When building a blog or content listing in Next.js, it's tempting to rely solely on client-side interactivity — such as a “Load More” button powered by JavaScript. But for Google SEO and crawlability, this alone isn't enough.
In this post, we’ll walk through how to:
Use a “Load More” button that saves posts to localStorage
for better UX when navigating back
Fall back to traditional <Prev>
/ <Next>
links when presenting a paginated view (e.g., ?page=3
)
Ensure all links are visible to search engines by rendering <a href="...">
inside <Link>
Search engines like Google don’t always execute JavaScript. So if your site loads more posts with just a button and no actual <a href>
to another page, Google won’t crawl or index those additional pages.
Good UX requires JavaScript. Good SEO requires HTML.
Page 1 starts with SSR-rendered initial posts.
Clicking “Load More” loads more posts via API and appends them to the current list.
All loaded posts are cached to localStorage
.
When viewing a post and hitting back, we restore from localStorage
to avoid re-fetching.
When visiting ?page=n
via a real URL, we show Prev / Next buttons as <a href>
links.
We render real <a>
elements inside <Link>
, so Google sees the next pages.
pages/blog/index.js
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import Link from "next/link";
import { Segment, Button } from "semantic-ui-react";
import axios from "axios";
export default function BlogPage({ initialPosts, initialPage, initialShowMore }) {
const router = useRouter();
const [posts, setPosts] = useState(() => {
if (typeof window !== "undefined") {
const cached = localStorage.getItem("blogPosts");
return cached ? JSON.parse(cached) : initialPosts;
}
return initialPosts;
});
const [currentPage, setCurrentPage] = useState(initialPage);
const [showMore, setShowMore] = useState(initialShowMore);
const [isLoading, setIsLoading] = useState(false);
const isPaginatedView = !!router.query.page;
useEffect(() => {
// On mount, if paginated view, overwrite cache
if (isPaginatedView) {
localStorage.setItem("blogPosts", JSON.stringify(initialPosts));
}
}, [initialPosts, isPaginatedView]);
const loadMore = async () => {
setIsLoading(true);
const nextPage = currentPage + 1;
const res = await axios.get(`/api/posts?page=${nextPage}`);
const newPosts = [...posts, ...res.data.posts];
setPosts(newPosts);
setCurrentPage(nextPage);
setShowMore(res.data.hasMore);
localStorage.setItem("blogPosts", JSON.stringify(newPosts));
setIsLoading(false);
router.replace("/blog"); // clean up ?page= after JS
};
const prevPage = currentPage > 1 ? currentPage - 1 : null;
const nextPage = currentPage + 1;
return (
<div className="blog-wrapper">
{posts.map((post) => (
<div key={post.id}>
<h3>
<Link href={`/blog/${post.slug}`}>
<a>{post.title}</a>
</Link>
</h3>
<p>{post.excerpt}</p>
</div>
))}
{!isPaginatedView && showMore && !isLoading && (
<Segment textAlign="center">
<Link href={`/blog?page=${nextPage}`}>
<a onClick={(e) => {
e.preventDefault();
loadMore();
}}>
<Button primary>Load More</Button>
</a>
</Link>
</Segment>
)}
{isPaginatedView && (
<Segment textAlign="center">
{prevPage && (
<Link href={`/blog?page=${prevPage}`}>
<a>
<Button secondary>Previous Page</Button>
</a>
</Link>
)}
{showMore && (
<Link href={`/blog?page=${nextPage}`}>
<a>
<Button secondary>Next Page</Button>
</a>
</Link>
)}
</Segment>
)}
</div>
);
}
getServerSideProps
export async function getServerSideProps(context) {
const page = parseInt(context.query.page || "1", 10);
const pageSize = 20;
const res = await fetch(`https://yourdomain.com/api/posts?page=${page}`);
const data = await res.json();
return {
props: {
initialPosts: data.posts,
initialPage: page,
initialShowMore: data.hasMore,
},
};
}
✅ Use <Link><a>...</a></Link>
for SEO — never wrap <Button>
directly
✅ Use localStorage
to cache posts, improving the "Back" experience
✅ SSR-render each page (?page=n
) so crawlers can reach it
✅ Avoid infinite loops in useEffect
by keeping dependencies controlled
✅ Use router.replace('/blog')
to clean up query strings after Load More
You might see a warning:
React Hook useEffect has missing dependencies: 'initialPosts', 'currentPage', etc.
Be careful: if you add all those as dependencies, you may accidentally cause infinite re-renders. It’s fine to intentionally ignore the warning in some scenarios where you’re controlling when updates should happen.
This hybrid approach gives you the best of both worlds:
Instant “Load More” interactivity for users
Crawlable <Prev>
/ <Next>
links for search engines
Cached session state for great user experience when navigating back
It’s not magic — just careful balance between JavaScript UX and HTML SEO.
Let me know if you want this packaged as a markdown post or published to your blog!