- Languages: Next.js
- Time saved: 3 weeks -> 30 mins
- Source Code
Don't want to spend time coding Programmatic SEO pages? I'm building a no-code tool called Launchman that does this. Try it here.
Companies like @zapier are driving massive TOTF growth from carefully crafted landing pages matching users' search intent. If you're building a "Books Marketplace" website, your SEO should be ...
Sukhpal Saini (@thisissukh_) November 22, 2021
Developers often create Landing Pages that say -
- "Best books in town - delivered to your home in under 10 hours"
when their customers are searching for -
- "Top books to read on Business"
- "Best nonfiction books to read in 2022"
Blog pages are supposed to fill this gap but they are too time-consuming to write manually. Imagine how much time + effort it requires to create and maintain a 100-page blog so it's updated. Imagine 10x, 100x that.
Can we create a ton of the pages that speak directly our target users' queries in an automatic way?
Yes. Yes, we can with programmatic SEO.
Next.js
Fortunately we have Next.js. It's the perfect solution to dynamically generate quality content pages at scale. Next.js can statically generate your dynamic React components into HTML with a single command. These HTML pages are indexable, crawlable, and Search Engine optimized out of the box. Plus a sitemap can also be generated. Let's see an example.
Example Scenario
Let's say we are a books subscription service called Bookify (similar to Spotify) that lets you read any number of books for a monthly price. Our customers are searching:
-
"Top books to read on Business"
-
"How do I learn Marketing"
-
"Business Books by Peter Thiel"
Provided that we have a database of books information, we can address query #1 and build a fully SEO'd content page that looks like so:
{
"id": 1,
"title": "The $100 Startup",
"abstract": "In The $100 Startup, Chris Guillebeau shows you how to lead of life of adventure, meaning and purpose – and earn a good living. Still in his early thirties, Chris is on the verge of completing a tour of every country on earth – he's already visited more than 175 nations – and yet he's never held a “real job” or earned a regular paycheck. Rather, he has a special genius for turning ideas into income, and he uses what he earns both to support his life of adventure and to give back. There are many others like Chris – those who've found ways to opt out of traditional employment and create the time and income to pursue what they find meaningful. Sometimes, achieving that perfect blend of passion and income doesn't depend on shelving what you currently do. You can start small with your venture, committing little time or money, and wait to take the real plunge when you're sure it's successful.",
"tag": "business",
"author": "Chris Guillebeau",
"url": "https://play.google.com/store/books/details?id=2YeBy-RlgkQC&source=gbs_api",
"thumbnail": "http://books.google.com/books/content?id=2YeBy-RlgkQC&printsec=frontcover&img=1&zoom=1&edge=curl&imgtk=AFLRE708dxWlPlj2jW6V7eRwiYB7l76qWZHr1XxmVAClpSMpQyVyGL0oIlnUrc_auYJsrb_OgMQXpWUAnk4O8zoXSCEpiv0J-tOcX0-I1XnJadQcWC1EQRiBMZvj7EGeICFSDQXfCxVI&source=gbs_api"
}
Preview 1 of an SEO page for a book recommendation website Preview 2 of an SEO page for a book recommendation website Notice that the URL is also the same as the page title.
Impressive right? Let's get building.
Step 1: Create a Next.js project
Let's create a new Next.js project by running:
npx create-next-app
Voila! Now we have a basic Next.js project to start off with.
Start the application by running:
npm run dev
This will start off a server that will both watch and live refresh your code on every change made. Very handy!
Step 2: Add Tailwind Styles (Optional)
Let's add some styling to our app before moving forward. Tailwind CSS is my go-to choice. The setup here is pretty standard.
Install required packages by running:
npm install -D tailwindcss@latest postcss@latest autoprefixer@latest
Add a ./tailwind.config.js
to specify the default config for Tailwind. We will also add the Inter font.
const defaultTheme = require('tailwindcss/defaultTheme')
module.exports = {
purge: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {
fontFamily: {
sans: ['Inter var', ...defaultTheme.fontFamily.sans],
},
},
},
variants: {
extend: {},
},
plugins: [],
}
Add a default ./postcss.config.js
as well like so:
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
We will use a single global stylesheet for this guide. Create a new file called ./styles/globals.css
and add:
@tailwind base;
@tailwind components;
@tailwind utilities;
Perfect. With the Tailwind CSS configured, we can now add it to our app like so:
<link rel="stylesheet" href="https://rsms.me/inter/inter.css"></link>
import '../styles/globals.css'
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />
}
export default MyApp
Perfect, now if you run the app you will see the font change. That's our styling done. Let's start working on the backend API.
Step 3: Make data available through an API
To build our pages, we need data to pull from. Ideally, you would want to pull from your database. For the sake of the guide, we can mock book information data with we might have collected from Google Books API or Amazon. We can slice and dice this data to present value to the end customer.
Add a new file ./data.js
:
export const books = [{
"id": 1,
"title": "The $100 Startup",
"abstract": "$100 is all you need",
"tag": "business",
"author": "Chris Guillebeau",
"url": "https://play.google.com/store/books/details?id=2YeBy-RlgkQC&source=gbs_api",
"thumbnail": "http://books.google.com/books/content?id=2YeBy-RlgkQC&printsec=frontcover&img=1&zoom=1&edge=curl&imgtk=AFLRE708dxWlPlj2jW6V7eRwiYB7l76qWZHr1XxmVAClpSMpQyVyGL0oIlnUrc_auYJsrb_OgMQXpWUAnk4O8zoXSCEpiv0J-tOcX0-I1XnJadQcWC1EQRiBMZvj7EGeICFSDQXfCxVI&source=gbs_api"
}, {
"id": 2,
"title": "The Lean Startup",
"abstract": "Most startups fail.",
"tag": "business",
"author": "Eric Ries",
"url": "https://play.google.com/store/books/details?id=r9x-OXdzpPcC&source=gbs_api",
"thumbnail": "http://books.google.com/books/content?id=r9x-OXdzpPcC&printsec=frontcover&img=1&zoom=1&edge=curl&imgtk=AFLRE70tfn2A2fRV10tSwCDlpXFp1WAyKcnYlAKWieGiQhQv7cWLN0ieaR9ewhDXIOgT5Q1OhSKU3vxpQAqzZJiE_4g1XC0HoBW2FKl2LsCVVKTrQwXuGr4nGoZRXK7SzZ-G0NtjpIyc&source=gbs_api"
}, {
"id": 3,
"title": "The Mom Test",
"abstract": "Save your time, money, and heartbreak.",
"tag": "business",
"author": "Rob Fitzpatrick",
"url": "https://www.amazon.ca/Intelligent-Investor-Definitive-Value-Investing/dp/0060555661",
"thumbnail": "http://books.google.com/books/publisher/content?id=Z5nYDwAAQBAJ&printsec=frontcover&img=1&zoom=1&edge=curl&imgtk=AFLRE73GzfMA3iaiGkXHQf0oRVSmwPRgd59Sh4qz398GhrEpjcZfKQIg1X7__1v2-DiXUj2M06is49nhTwGvMqNeyQ0okZ3aKSnSU5lnNG1Pi561LqYzC5D2avTOjLnKGzfNjXBdZQLt&source=gbs_api"
}]
We can now expose the data through an API. Super easy to do it with Next.js. Create a file in the pages
directory, add resulting json in the handler function and now you have a valid route.
For the lists
GET API route, we will simply return all the books in the database.
Add the /pages/api/lists/index.js
:
import { books } from '../../../data'
export default function handler(req, res) {
res.status(200).json(getLists(null))
}
export const getLists = (filter) => {
if (!filter) { return books; }
const filtered = books.filter(article => article.tag === filter.tag)
return filtered
}
Run using:
npm run dev
Navigate to http://localhost:3000/api/lists to see the Next.js responding back with book data as JSON. We are now ready to start rendering pages using the data from this API.
Step 4: Static Site Generation
Let's work on the secret sauce. We want to be able to create pages for a specified Google Search string. Say our user was looking for "Top books to learn business". We need a page that has the:
- URL: /lists/top-books-in-business
- Content: List of business books and their relevant information.
Let's work on #1. With Next.js, we can generate a list of URLs and create render pages for each of those URLs.
Let's see an example.
Add a new file called ./pages/list/[id]/index.js
const BookSection = ({ title }) => {
return (<>
{title}
</>
)
}
export const getStaticProps = async (context) => {
return { props: { title: "The title of this page is" + context.params.id } }
}
export const getStaticPaths = async () => {
const paths = [{ "params": { id: "top-3-books-in-business" } }]
return {
paths, fallback: false
}
}
export default BookSection
getStaticProps
: Render the page with returned propsgetStaticPaths
: Generates paths for URLs
Navigate to http://localhost:3000/list/top-3-books-in-business and you will notice that the page loads and the output on the page is the returned object on getStaticProps.
It was as easy as that! To create content, we can now:
- Generate URLs
- Create a filter string using the URL of the page
- Use that filter to get a list of books and their information
Perfect, with that in mind we can expand this to generate a list of URLs in getStaticPaths.
Update ./pages/list/[id]/index.js
:
const limit = 3
const tags = ["business"]
// ....
function buildSearchPath(filter) {
const { tag, limit } = filter;
let path = `top ${limit} books to learn ${tag}`
path = path.replace(/ /g, "-")
return path;
}
function generateAllPaths() {
const paths = []
tags.forEach((tag) => {
paths.push(buildSearchPath({ tag, limit }))
})
return paths
}
export const getStaticPaths = async () => {
const ids = generateAllPaths()
const paths = ids.map(id => ({ params: { id: id.toString() } }))
console.log("generating paths", paths);
return {
paths, fallback: false
}
}
We just built 1 path - _top-3-books-to-learn-business _for now but if we expand the list of tags, we will create that many new paths.
Now that we have the paths generated, we need to render content based on the URL. For that, we need to edit our getStaticProps
:
Update ./pages/list/[id]/index.js
:
import { getLists } from '../../api/lists/[id]'
//...
function tokenize(str) {
const filter = {}
const tokens = str.split("-")
tokens.forEach((token) => {
if (tags.includes(token)) {
filter["tag"] = token
}
})
return filter
}
export const getStaticProps = async (context) => {
let title = context.params.id.replace(/-/g, " ").split(" ")
title = title.map(w => w.charAt(0).toUpperCase() + w.substring(1)).join(' ')
const filter = tokenize(context.params.id)
const books = getLists(filter)
return { props: { books, title } }
}
function buildSearchPath(filter) {
const { tag, limit } = filter;
let path = `top ${limit} books to learn ${tag}`
path = path.replace(/ /g, "-")
return path;
}
//...
We tokenize the URL path of the current page, build a filter object and get the relevant list of books. Here's what everything combined looks like:
In ./pages/list/[id]/index.js
import { getLists } from '../../api/lists'
const limit = 3
const tags = ["business"]
const BookSection = ({ books, title }) => {
return (<>
JSON.stringify(books, title)
</>
)
}
function tokenize(str) {
const filter = {}
const tokens = str.split("-")
tokens.forEach((token) => {
if (tags.includes(token)) {
filter["tag"] = token
}
})
return filter
}
export const getStaticProps = async (context) => {
let title = context.params.id.replace(/-/g, " ").split(" ")
title = title.map(w => w.charAt(0).toUpperCase() + w.substring(1)).join(' ')
const filter = tokenize(context.params.id)
const books = getLists(filter)
return { props: { books, title } }
}
function buildSearchPath(filter) {
const { tag, limit } = filter;
let path = `top ${limit} books to learn ${tag}`
path = path.replace(/ /g, "-")
return path;
}
function generateAllPaths() {
const paths = []
tags.forEach((tag) => {
paths.push(buildSearchPath({ tag, limit }))
})
return paths
}
export const getStaticPaths = async () => {
const ids = generateAllPaths()
const paths = ids.map(id => ({ params: { id: id.toString() } }))
console.log("generating paths", paths);
return {
paths, fallback: false
}
}
export default BookSection
Run the app and navigate to http://localhost:3000/list/top-3-books-to-learn-business. You should see the list of 3 books that were tagged with business in the database. Since it's server side render, it is blazingly fast.
Expand the list of tags by updating the tags in ./pages/list/[id]/index.js
:
const tags = ["business", "marketing", "investing"]
Now when we run the app, we have:
- http://localhost:3000/list/top-3-books-to-learn-business
- http://localhost:3000/list/top-3-books-to-learn-marketing
- http://localhost:3000/list/top-3-books-to-learn-investing
All valid routes with legitimate data. We just need to make it look presentable. Good thing we have Tailwind to save the day.
Step 5: UI - Add a UI
Instead of showing a stringified list of books, let's make it visual.
Add a new component ./components/BookList.js
:
import Link from 'next/Link'
const BookList = ({ books, title }) => {
return (<>
{books.map(book => {
const thumbnail = book.thumbnail
const title = book.title
const description = book.abstract.replace(/<[^>]*>?/gm, '')
const authors = book.author
return (
<div key={book.id}>
<article >
<div className="">
<div className="max-w-none">
<div className="space-y-12">
<div className="post">
<div className="my-4">
<div className="space-y-2">
<div className="bg-gray-100 h-96 flex items-center justify-center">
<img
className="rounded-lg"
src={thumbnail}
alt={`Book cover thumbnail for ${title} by ${authors}`}
/>
</div>
<h1 className="text-2xl font-bold text-gray-800">
{title} by {authors}
</h1>
<p className="text-gray-500">
{description}
</p></div>
<p><Link
className="transition-colors duration-200 hover:text-gray-800"
href={book.url}
><a>View on Google Books <span aria-hidden="true" className="mr-2">→</span></a></Link></p>
</div>
</div>
</div>
</div>
</div>
</article>
</div>
)
})} </>
)
}
export default BookList;
We can now add the component + a header to our List pages.
In ./lists/[id]/index.js
import { getLists } from '../../api/lists'
import BookList from '../../../components/BookList'
const BookSection = ({ books, title }) => {
return (<>
<header className="pt-6 xl:pb-10">
<dd className="text-base leading-6 font-medium text-gray-500"><time dateTime="2021-02-16T16:05:00.000Z">Tuesday, Febuary 16, 2021</time></dd>
<h1 className="mt-2 text-3xl leading-9 font-bold text-gray-900 tracking-tight sm:text-4xl sm:leading-10 md:text-5xl md:leading-14">
{title}
</h1>
</header>
<BookList books={books} title={title} />
</>
)
}
//...
Step 6: UI - Add Layout
We want to be able to get our visitors to try out our app. That was the whole point. This is the top-of-the-funnel. So let's add a CTA at the bottom of every page. We can do that easily by adding a Layout component.
In ./components/Layout.js
import Meta from './Meta'
const Layout = ({ children }) => {
return (
<>
<Meta />
<div className="max-w-xl mx-auto">
{children}
<br></br>
<hr />
<br></br>
<div className="text-center space-y-5 mb-5">
<h2 className="text-xl">Discover curated books on 100+ topics</h2>
<button className="bg-blue-500 hover:bg-blue-700 text-white py-2 px-4 rounded">Try Bookify free for 7 days</button>
</div>
</div>
</>
)
}
export default Layout
In _app.js
import Layout from '../components/Layout'
<link rel="stylesheet" href="https://rsms.me/inter/inter.css"></link>
import '../styles/globals.css'
function MyApp({ Component, pageProps }) {
return (
<Layout><Component {...pageProps} />
</Layout>)
}
export default MyApp
Step 7: UI - Add SEO
Since we are already rendering out individual pages, adding SEO is a piece of cake.
Add a new ./components/meta.js
import Head from 'next/Head'
const Meta = ({ title, keywords, description }) => {
return (
<Head>
<meta
name="description"
content={description}
/>
<meta
name="keywords"
content={keywords}
/>
<meta rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="msapplication-TileColor" content="#5000ff" />
<meta name="theme-color" content="#5000ff" />
<title>{title}</title>
</Head>
)
}
Meta.defaultProps = {
title: "Bookify",
keywords: "books",
description: "Bookify provides access to 100+ hand-picked books for you to start learning new skills"
}
export default Meta
Update ./components/layout.js
import Meta from './Meta'
const Layout = ({ children }) => {
return (
<>
<Meta />
<div className="max-w-xl mx-auto">
{children}
...
Step 8: UI - Home Page (Optional)
We can add a home page that will be accessible from the /
route to link out to all our content pages.
export default function Home() {
return (
<div className="text-center mt-4">
<h1 className="text-2xl mb-4">Home Page</h1>
<ul>
<li><a href="http://localhost:3000/api/lists">All Books</a></li>
<li><a href="http://localhost:3000/list/top-3-books-to-learn-business">Top 3 Books to Learn Business</a></li>
</ul>
</div>
)
}
Navigate to http://localhost:3000/list/top-3-books-to-learn-business
Step 9: Add a Sitemap
Sitemap lets google correctly index our pages. Very important. Let's add it by installing:
npm i next-sitemap -D
We need to add a very basic configuration for the sitemap generator. Add a ./sitemap.js
:
module.exports = {
siteUrl: 'https://saasbase.dev',
generateRobotsTxt: true
}
We can add it to our build process with by editing the ./package.json
:
"scripts": {
...
"postbuild": "next-sitemap --config sitemap.js"
With the sitemap generator in place, we can run the production build of the app by running:
npm run build
When it successfully finishes building, it will generate the post build task of creating a sitemap for all the URLs that were created.
And there we have it! Our very own scalable content system is ready to bring in tons of traffic.
Conclusion
In the tutorial, we learned how to use Next.js to build content pages addressing specific customer queries so they rank higher in Google rankings and bring in top-of-the-funnel visitors. We added a UI and a dynamically generated sitemap. Combining everything we have a strategy to build content pages at scale!
How Zapier is using Dynamic SEO content to generate 1.5M in organic traffic