Introduction
Building an SEO-friendly blog requires the right tools and technologies, and using Ghost, Next.js, and Tailwind CSS can streamline the process. Ghost serves as an efficient content management system (CMS) while Next.js helps create a fast, static frontend, ensuring optimal SEO performance. With Tailwind CSS for design flexibility and ease, you can create a clean, modern interface for your blog. In this article, we’ll guide you through setting up Ghost on a server, integrating it with Next.js, and styling your blog using Tailwind CSS to create a seamless, high-performance website.
What is ?
Step 1 — Publishing Posts on Ghost
Alright, here’s the deal. In this step, you’re going to set up an account on Ghost and publish a few sample posts so you’ve got content ready for the next steps. After you’ve got Ghost up and running, the first thing you need to do is create an admin account to manage your blog’s content.
So, to kick things off, just open your browser and type in the URL you set when you installed Ghost. It’s going to be something like YOUR_DOMAIN/ghost . Once you’re there, you’ll be greeted by the “Welcome to Ghost” page, where it’ll ask you to create an admin account.
All you need to do now is fill out the form with your site title, your name, email, and password. When you’re done with that, hit the “Create account & start publishing” button. And boom, you’re in. This will take you straight to your blog’s admin dashboard.
Now that you’re logged in, you’ll be on the Ghost dashboard. On the left side, you’ll see a section that says “What do you want to do first?” and right under it, there’s a button that says “Write your first post.” Go ahead and click on that to start writing.
Once you’re in the editor, type up a catchy title and add your content in the body section. After you’re done, click the “Publish” button at the top right of the screen. You’ll then see a little confirmation asking if you want to “Continue, final review.” Once you’re all set, click “Publish post, right now” to make it go live.
To check out your blog live, go back to YOUR_DOMAIN/ghost and navigate to the dashboard. There, on the left sidebar, you’ll find a “View site” option. Click it, and voila, your blog is now live!
At this point, you’ve created your first post, and your Ghost site is officially up and running. Next, you’ll want to add more content. From the dashboard, select “Posts” on the left, and add two more blog posts. Make sure to give each post a unique title so you can easily spot them later.
Right now, your blog is using the default Ghost template. While it works, it doesn’t give you much room to play around with design. But don’t worry, in the next step, you’re going to set up a
Next.js
Next.js
For more details on managing and publishing content effectively with Ghost, check out this guide on publishing and managing posts on Ghost.
Step 2 — Creating a Next.js Project
Now that you’ve got Ghost all set up and ready for your blog, the next step is to create the frontend for your blog using Next.js, and of course, spice things up with Tailwind CSS for styling.
First things first, open up the Next.js project you created earlier in your favorite code editor. Then, go ahead and open the pages/index.js file by running this command:
$ nano pages/index.js
Once the index.js file is open, go ahead and clear out whatever content is already in there. Then, add this code to import the Head component from next/head and set up a basic React component:
import Head from ‘next/head’;</p>
<p>export default function Home() {
return (
// Code to be added in the next steps
);
}
So, here’s the thing. The Head component from Next.js is your go-to for adding stuff like the <title> and <meta> tags that you usually find in the <head> section of an HTML document. These are super important because they help define the metadata for your page—things like the title that shows up in the search results and the description that social media platforms use to generate previews.
Next, to make the Head component work for your blog, we’ll add a return statement that includes a <div> element. Inside this <div> , we’ll nest a <Head> tag, and inside that, we’ll place a <title> and <meta> tag like this:
return (
<div>
<Head>
<title>My First Blog</title>
<meta name=”description” content=”My personal blog created with Next.js and Ghost” />
</Head>
</div>
);
Here’s how it breaks down:
- The <title> tag sets the name of your webpage, which is what appears on the browser tab when someone visits your site. In this case, it’s “My First Blog.”
- The <meta> tag defines the “description” of your site, which is a brief blurb about what your website is about. In this case, “My personal blog created with Next.js and Ghost,” which will show up in search engine results and give people a preview of what your site offers.
Now that you’ve set the title and description, it’s time to add a list of blog articles. For now, we’ll use some mock data as placeholders—don’t worry, we’ll fetch the real blog posts from Ghost in the next section. Below the <Head> tag, you can add a list of articles using <li> tags, just like this:
<div>
<Head>
<title>My First Blog</title>
<meta name=”description” content=”My personal blog created with Next.js and Ghost” />
</Head>
<main className=”container mx-auto py-10″>
<h1 className=”text-center text-3xl”>My Personal Blog</h1>
<div className=”flex justify-center mt-10″>
<ul className=”text-xl”>
<li>How to build and deploy your blog on Caasify</li>
<li>How to style a Next.js website</li>
<li>How to cross-post your articles automatically</li>
</ul>
</div>
</main>
</div>
Let’s break that down:
- We’ve added a <main> tag with a header ( <h1> ) for the blog title, and we’re using a <div> that centers the content.
- Inside this <div> , we use the flex utility class from Tailwind CSS to center the list of articles horizontally.
- The articles themselves are wrapped in an unordered list ( <ul> ) with each item inside a <li> tag. We’ve also applied the text-xl Tailwind CSS class to make the list items a bit bigger.
Your index.js file should now look like this:
import Head from ‘next/head’;</p>
<p>export default function Home() {
return (
<div>
<Head>
<title>My First Blog</title>
<meta name=”description” content=”My personal blog created with Next.js and Ghost” />
</Head>
</div>
<main className=”container mx-auto py-10″>
<h1 className=”text-center text-3xl”>My Personal Blog</h1>
<div className=”flex justify-center mt-10″>
<ul className=”text-xl”>
<li>How to build and deploy your blog on Caasify</li>
<li>How to style a Next.js website</li>
<li>How to cross-post your articles automatically</li>
</ul>
</div>
</main>
</div>
}
Once you’ve saved that, it’s time to fire up the web server locally. Whether you’re using Yarn or npm, just run the appropriate command:
- For Yarn:
$ yarn dev - For npm:
$ npm run dev
Now, when the server is running, open your browser and go to https://localhost:3000. You should see your homepage with the list of mock blog articles we just added. Of course, this is just placeholder content for now. In the next step, we’ll replace it with the actual blog posts pulled from Ghost.
And just like that, your blog’s Home page is all set up, and it’s ready to show dynamic content fetched from Ghost!
To dive deeper into creating and customizing projects with Next.js, you can explore this comprehensive guide on Next.js documentation and project setup.
Step 3 — Fetching All Blog Posts from Ghost
In this step, you will fetch the blog posts you created in Ghost and display them in your browser. To fetch your articles from Ghost, you will first need to install the JavaScript library for the Ghost Content API. Start by stopping the server using the keyboard shortcut CTRL+C. Then, run the following command in your terminal to install the library:
If you are using Yarn, run:
$ yarn add @tryghost/content-api
If you are using npm, run:
$ npm i @tryghost/content-api
With the library successfully installed, the next step is to create a file that will store the logic required to fetch your blog posts. Inside the pages directory, create a new folder named utils:
$ mkdir utils
Now, create a new file within the utils folder named ghost.js:
$ nano pages/utils/ghost.js
In the ghost.js file, you will import the GhostContentAPI module from the Ghost Content API library. Then, initialize a new instance of the GhostContentAPI and store the resulting value in a constant variable called api. You will need to provide values for the host URL, API key, and API version. The code should look like this:
import GhostContentAPI from “@tryghost/content-api”;
const api = new GhostContentAPI({
url: YOUR_URL,
key: YOUR_API_KEY,
version: ‘v5.0’
});
In this code, YOUR_URL refers to the domain name you configured when installing Ghost, including the protocol (i.e., https://). To obtain your Ghost API key, follow these steps:
- Navigate to YOUR_DOMAIN/ghost (where YOUR_DOMAIN is the URL you set up during Ghost installation) and log in with your admin credentials.
- Click the gear icon at the bottom of the left sidebar to access the settings page.
- In the “Advanced” category, click “Integrations” from the left sidebar.
- On the Integrations page, scroll down to the “Custom Integrations” section and click “+ Add custom integration.”
- A pop-up will appear asking you to name your integration. Enter a name for your integration in the “Name” field and click “Create.”
- This will take you to a page to configure your custom integration. Copy the Content API Key (note: use the Content API Key, not the Admin API key).
- Press “Save” to store the integration settings.
In the ghost.js file, replace YOUR_API_KEY with the copied API key. Once you have initialized the GhostContentAPI, you will write an asynchronous function to fetch all the articles from your Ghost installation. This function will retrieve the blog posts regardless of their tags. Here is the code to add to your ghost.js file:
export async function getPosts() {
return await api.posts
.browse({
include: “tags”,
limit: “all”
})
.catch(err => {
console.error(err);
});
}
The getPosts() function calls api.posts.browse() to fetch posts from your Ghost installation. The include parameter is set to “tags”, meaning that it will fetch the tags associated with each post along with the content itself. The limit parameter is set to “all” to retrieve all available posts. If an error occurs while fetching the posts, it will be logged to the browser console.
At this point, your ghost.js file will look like this:
import GhostContentAPI from ‘@tryghost/content-api’;
const api = new GhostContentAPI({
url: YOUR_URL,
key: YOUR_API_KEY,
version: ‘v5.0’,
}); </p>
<p>export async function getPosts() {
return await api.posts
.browse({
include: ‘tags’,
limit: ‘all’,
})
.catch((err) > {
console.error(err);
});
}
Save and close the file. The next step is to update your index.js file to display the list of posts. Open index.js and add the following line to import the getPosts function above the Head import:
import { getPosts } from ‘./utils/ghost’;
import Head from ‘next/head’;
You will now create an async function called getStaticProps(). This function allows Next.js to pre-render the page at build time, which is beneficial for static generation. In getStaticProps(), call the getPosts() method and return the posts as props. The code should look like this:
export async function getStaticProps() {
const posts = await getPosts();
return {
props: { posts },
};
}
Save the file. Now that you’ve defined the getStaticProps() method, restart the server by running one of the following commands, depending on your package manager:
If you’re using Yarn, run:
$ yarn dev
If you’re using npm, run:
$ npm run dev
In your browser, the page will initially show the static data. However, because you are fetching the posts asynchronously but not rendering them yet, you need to make some changes to the index.js file. Press CTRL+C to stop the server, then open index.js for editing:
$ nano pages/index.js
Make the following highlighted changes to index.js to render the posts:
export default function Home({ posts }) {
return (
<div>
<Head>
<title>My First Blog</title>
<meta name=”description” content=”My personal blog created with Next.js and Ghost”>
</Head>
<main className=”container mx-auto py-10″>
<h1 className=”text-center text-3xl”>My Personal Blog</h1>
<div className=”flex justify-center mt-10″>
<ul className=”text-xl”>
{posts.map((post) >
<li key={post.title}>{post.title}</li>
)}
</ul>
</div>
</main>
</div>
);
}
Save and close the file. Restart the server again using either npm run dev or yarn dev , and navigate to localhost:3000 in your browser. Your homepage should now display a list of blog articles fetched from Ghost.
At this stage, the blog has successfully retrieved and displayed the post titles from your Ghost CMS. However, it still doesn’t render individual posts. In the next section, you will create dynamic routes to display the content of each post.
For more details on integrating APIs with static sites, check out this useful guide on Using the Fetch API in JavaScript.
Step 4 — Rendering Each Individual Post
In this step, you will write code to fetch the content of each blog post from Ghost, create dynamic routes, and add the post title as a link on the homepage. Next.js allows you to create dynamic routes, which makes it easier to render pages with the same layout. By using dynamic routes, you can reduce redundancy in your code, as you won’t need to create a separate page for each post. Instead, all of your posts will use the same template file to render.
To create dynamic routes and render individual posts, you will need to:
- Write a function to fetch the content of a specific blog post.
- Create dynamic routes to display each post.
- Add blog post links to the list of articles on the homepage.
In the ghost.js file, you already wrote the getPosts() function to fetch a list of all your blog posts. Now, you will add another function called getSinglePost() that fetches the content of a specific post based on its slug. Ghost automatically generates a slug for each article using its title. For example, if your article is titled “My First Post,” Ghost will generate a slug like my-first-post . This slug will be used to identify the post and can be appended to your domain URL to display the content.
The getSinglePost() function will take the postSlug as a parameter and return the content of the corresponding blog post. Follow the steps below to add and configure this function.
Step-by-step implementation:
- Stop the server if it’s still running.
- Open the pages/utils/ghost.js file for editing.
- Below the getPosts() function in the ghost.js file, add and export the getSinglePost() function like this:
export async function getSinglePost(postSlug) {
return await api.posts
.read({ slug: postSlug })
.catch((err) => {
console.error(err);
});
}
The getSinglePost() function uses the posts.read() method from the GhostContentAPI to fetch a single post by its slug. If an error occurs during the API call, it will be logged to the browser’s console.
Now, your updated ghost.js file should look like this:
import GhostContentAPI from ‘@tryghost/content-api’;</p>
<p>const api = new GhostContentAPI({
url: YOUR_URL,
key: YOUR_API_KEY,
version: ‘v5.0’,
});</p>
<p>export async function getPosts() {
return await api.posts
.browse({
include: ‘tags’,
limit: ‘all’,
})
.catch((err) => {
console.error(err);
});
}</p>
<p>export async function getSinglePost(postSlug) {
return await api.posts
.read({ slug: postSlug })
.catch((err) => {
console.error(err);
});
}
Save and close the ghost.js file.
Next, to render individual blog posts dynamically in Next.js, you will use a dynamic route. In Next.js, dynamic routes are created by adding brackets ([ ]) to a filename. For example, creating a file named [slug].js in the pages/post/ directory will match any slug passed in the URL after /post/ and display the corresponding post.
Create the dynamic route file /post/[slug].js :
nano pages/post/[slug].js
Note: The backslashes () in the filename are necessary for escaping the brackets in the terminal when using the nano editor.
Inside the [slug].js file, import the getPosts() and getSinglePost() functions from the ../utils/ghost.js file. Then, create the template for rendering the post:
import { getPosts, getSinglePost } from ‘../utils/ghost’;</p>
<p>export default function PostTemplate(props) {
const { post } = props;
const { title, html, feature_image } = post;
return (
<main className="container mx-auto py-10"></p>
<h1 className="text-center text-3xl">{title}</h1>
<article
className="mt-10 leading-7 text-justify"
dangerouslySetInnerHTML={{ __html: html }}
/>
</main>
);
}</p>
<p>export const getStaticProps = async ({ params }) => {
const post = await getSinglePost(params.slug);
return {
props: { post },
};
};
In this code, the PostTemplate() function receives the post object as a prop, and it destructures the title, html, and feature_image properties from it. The post content (HTML) is injected into the <article> tag using React’s dangerouslySetInnerHTML attribute.
Note: This is a React feature that allows you to insert raw HTML, but you should sanitize the content if it’s coming from an untrusted source to prevent security risks.
The getStaticProps() function fetches the content of the blog post corresponding to the slug parameter in the URL. This ensures that each post is pre-rendered at build time.
Next, you need to create another function, getStaticPaths() , to tell Next.js which URLs need to be generated during build time. The function will return a list of slugs to generate paths for all posts.
Add the getStaticPaths() function to the [slug].js file:
export const getStaticPaths = async () => {
const allPosts = await getPosts();
return {
paths: allPosts.map(({ slug }) => {
return {
params: { slug },
};
}),
fallback: false,
};
};
Here, getStaticPaths() fetches all the posts using the getPosts() function and returns a list of slugs as the paths. By setting fallback: false , any paths that do not match the list will result in a 404 page.
The final version of the /post/[slug].js file should now look like this:
import { getPosts, getSinglePost } from ‘../utils/ghost’;</p>
<p>export default function PostTemplate(props) {
const { post } = props;
const { title, html, feature_image } = post;
return (
<main className="container mx-auto py-10"></p>
<h1 className="text-center text-3xl">{title}</h1>
<article
className="mt-10 leading-7 text-justify"
dangerouslySetInnerHTML={{ __html: html }}
/>
</main>
);
}</p>
<p>export const getStaticProps = async ({ params }) => {
const post = await getSinglePost(params.slug);
return {
props: { post },
};
};</p>
<p>export const getStaticPaths = async () => {
const allPosts = await getPosts();
return {
paths: allPosts.map(({ slug }) => {
return {
params: { slug },
};
}),
fallback: false,
};
};
Save and close the file.
Next Step: To navigate between the homepage and individual posts, you will add links on the homepage that point to each post. For this, stop the server if it’s still running, then open pages/index.js for editing.
Add an import statement for Link from next/link and create links to the individual posts:
import { getPosts } from ‘./utils/ghost’;
import Head from ‘next/head’;
import Link from ‘next/link’;</p>
<p>export default function Home(props) {
return (</p>
<div>
<Head>
<title>My First Blog</title>
<meta name="description" content="My personal blog created with Next.js and Ghost" />
</Head>
<main className="container mx-auto py-10"></p>
<h1 className="text-center text-3xl">My Personal Blog</h1>
<div className="flex justify-center mt-10">
<ul className="text-xl">
{props.posts.map((post) => (</p>
<li key={post.title}>
<Link href={`post/${post.slug}`}>{post.title}</Link>
</li>
<p> ))}
</ul>
</p></div>
<p> </main>
</div>
<p> );
}</p>
<p>export async function getStaticProps() {
const posts = await getPosts();
return {
props: { posts },
};
}
In this updated code, the Link component from next/link is used to create clickable links to each individual post. The href attribute uses the post’s slug to navigate to the corresponding post page.
Save the file, restart the server using npm run dev or yarn dev , and navigate to localhost:3000 . Now, when you click on any post title, you will be redirected to the corresponding post page where its content is displayed.
Your homepage will now show the list of blog titles, and clicking on a title will take you to the individual blog post page. This marks the completion of rendering individual posts using dynamic routes.
To learn more about building dynamic web pages with Next.js, check out this detailed guide on Dynamic Routing in Next.js.
Conclusion
In conclusion, building an SEO-friendly blog with Ghost, Next.js, and Tailwind CSS is a powerful combination that ensures a fast, flexible, and visually appealing website. By using Ghost as your CMS, you can efficiently manage and publish content, while Next.js provides the performance and SEO benefits of static site generation. Tailwind CSS offers seamless design customization, making your blog stand out with a modern, responsive layout. With this tutorial, you now have the knowledge to set up a scalable, SEO-optimized blog that delivers exceptional performance. As web development continues to evolve, adopting these tools will keep your blog ahead of the curve, providing improved functionality and user experience.