35
loading...
This website collects cookies to deliver better user experience
Next.js - Next.js is our static site generator of choice for this tutorial. Next.js is an open-source react framework used for building production-grade react websites. It comes with a preview API that we will use in this guide.
Tailwind CSS - Tailwind CSS is a utility-first CSS framework. It enables you to add CSS directly to your Html for easier development and scaling.
Strapi - Strapi is the leading open-source headless CMS used for managing content efficiently and designing secure production-ready RestFul or GraphQL APIs.
Markdown-to-jsx - We will use the Markdown-to-jsx npm package for converting the markdown files returned from strapi into jsx to be rendered by Next.js
npx create-strapi-app blog-backend --template https://github.com/strapi/strapi-template-blog
quickstart
to use the preconfigured settings. This command will create a new blog-backend folder ****with the Strapi backend fully set up and all dependencies installed during installation./api/article/models/article.settings.json
file and replace the code with these lines of code.{
"kind": "collectionType",
"collectionName": "articles",
"info": {
"name": "article",
"description": ""
},
"options": {
"increments": true,
"timestamps": true,
"draftAndPublish": false
},
"pluginOptions": {},
"attributes": {
"title": {
"type": "string",
"required": true
},
"description": {
"type": "text",
"required": true
},
"content": {
"type": "richtext",
"required": true
},
"publishedAt": {
"type": "datetime",
"required": true
},
"status": {
"type": "enumeration",
"enum": [
"draft",
"published"
],
"required": true,
"default": "draft"
},
"slug": {
"type": "uid",
"targetField": "title",
"required": true
},
"category": {
"model": "category",
"via": "articles"
},
"image": {
"model": "file",
"via": "related",
"allowedTypes": [
"files",
"images",
"videos"
],
"plugin": "upload",
"required": true,
"pluginOptions": {}
},
"author": {
"via": "articles",
"model": "writer"
}
}
}
yarn develop
to register the changes. Then, log in to Strapi's dashboard again and set the **Status**
of the articles in Collection types to either **published**
or **draft**
. Also, add a **PublishedAt**
date, and make sure to save your changes.npx create-next-app blogs-frontend
npm run dev
to start the frontend server. You can view your frontend locally using http://localhost:3000.npm install -D tailwindcss@latest postcss@latest autoprefixer@latest
npx tailwindcss init -p
**postcss.config.js**
and a **tailwind.config.js**
file to the root directory of your project. We will use these two files for any configurations relating to tailwind CSS in our project.**pages/app.js**
file and replace import '../styles/globals.css'
with import 'tailwindcss/tailwind.css'
. You can delete the **styles**
****folder since we will not be using any custom CSS for our blog.npm install markdown-to-jsx
**lib**
folder in your frontend folder root folder, and add a file named **api.js**
to the **lib**
folder.export function getStrapiURL(path = "") {
return ${
process.env.NEXT_PUBLIC_STRAPI_API_URL || "http://localhost:1337"
}${path};
}
// Helper to make GET requests to Strapi
export async function fetchAPI(path) {
const requestUrl = getStrapiURL(path);
const response = await fetch(requestUrl);
const data = await response.json();
return data;
}
Layout.js
file into the folder. Next, add the following code into the **components/layout.js**
file.import Header from './header';
import Footer from './footer';
const Layout = ({ children }) => (
<>
<Header />
{children}
<Footer />
</>
);
export default Layout;
header.js
and a footer.js
file to the component folder. header.js
fileimport React from "react";
import Link from "next/link";
const Header = () => {
return (
<div>
<nav className="flex justify-center items-center h-16 py-2 mb-2 bg-gray-100 w-full border-b">
<div>
<Link href="/">
<a href="/" className="px-2 font-black lg:px-0 text-lg">
Sheriff's Blog
</a>
</Link>
</div>
</nav>
</div>
);
};
export default Header;
footer.js
file.import React from "react";
const Footer = () => {
return (
<footer className="border-t mt-10 pt-12 pb-32 px-4 lg:px-0">
<div className="px-4 pt-3 pb-4 border-b -mx-4 border-gray-400">
<div className="max-w-xl mx-auto">
<h2 className="text-xl text-left inline-block font-semibold text-gray-800">Join Our Newsletter</h2>
<p className="text-gray-700 text-xs pl-px">
Latest news ,articles and updates montly delevered to your inbox.
</p>
<form action="#" className="mt-2">
<div className="flex items-center">
<input type="email" className="w-full py-4 mr-2 bg-gray-100 shadow-inner rounded-md border border-gray-400 focus:outline-none" required />
<button className="bg-blue-600 text-gray-200 text-sm rounded">Sign Up</button>
</div>
</form>
</div>
</div>
</footer>
);
};
export default Footer;
**index.js**
file and discard the pre-existing code there. Then add the following lines of code to the **index.js**
file.import Link from "next/link";
import Layout from "../components/layout";
import { fetchAPI } from "../lib/api";
export default function Home({ articles }) {
return (
<>
<Layout>
<body className="antialiased md:bg-gray-100">
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
{articles.map((article) => (
<div key={article.id} className="md:p-8 p-2 bg-white">
<div>
{article.image && (
<img src={`http://localhost:1337${article.image.url}`} />
)}
</div>
{article.title}
<div>
{article.category && (
<p className="text-indigo-500 font-semibold text-base mt-2">
{article.category.name}
</p>
)}
</div>
<h1 className="font-semibold text-gray-900 leading-none text-xl mt-1 capitalize truncate">
{article.title}
</h1>
<Link href={`/article/${article.slug}`}>
<div className="max-w-full">
<p className="text-base font-medium tracking-wide text-gray-600 mt-1">
{article.description}
</p>
</div>
</Link>
<div className="flex items-center space-x-2 mt-20">
<div>
{article.author && (
<p className="text-gray-900 font-semibold pb-2">
{article.author.name}
</p>
)}
<p className="text-gray-500 font-semibold text-sm">
Created on - {new Date(article.created_at).toDateString()}
</p>
<p className="text-gray-500 font-semibold text-sm">
Updated on - {new Date(article.updated_at).toDateString()}
</p>
</div>
</div>
</div>
))}
</div>
</body>
</Layout>
</>
);
}
export async function getStaticProps() {
try {
const articles = await fetchAPI("/articles?status=published");;
return {
props: {
articles
},
};
} catch (error) {
return { error };
}
}
getStaticProps()
. The getStaticProps()
is a Next.js API that will pre-render our page at build time using the props returned in it. <Home />
component. The props are then rendered in a UI built with Tailwind CSS. We will also wrap the page in the <Layout />
component to access our pre-made header and footer components.**pages**
folder, and add a **[slug].js**
file to the **pages/article**
folder. Add the following lines of code to the **pages/article/[slug].js**
file.import Markdown from "markdown-to-jsx";
import { fetchAPI } from "../../lib/api";
import Layout from "../../components/layout";
const Article = ({ article }) => {
return (
<>
<Layout>
<div className="mt-10">
<div className="mb-4 md:mb-0 w-full max-w-screen-md mx-auto">
<div className="absolute left-0 bottom-0 w-full h-full" />
<div>
{article.image && (
<img src={`http://localhost:1337${article.image.url}`} />
)}
</div>
<div>
{article.category && (
<a
href="#"
className="px-4 py-1 bg-black text-blue-200 inline-flex text-md items-center justify-center mb-2"
>
{article.category.name}
</a>
)}
</div>
<h2 className="text-4xl pt-2 font-semibold text-gray-500 leading-tigh`t">
{article.description}
</h2>
<div className="mt-3">
{article.author && (
<p className="text-blue-900 font-semibold pb-2">
Written by - {article.author.name}
</p>
)}
</div>
</div>
<article className="prose lg:prose-xl px-4 lg:px-0 mt-12 text-gray-700 max-w-screen-md mx-auto text-lg leading-relaxed">
<Markdown>{article.content}</Markdown>
</article>
</div>
</Layout>
</>
);
};
export default Article;
export async function getStaticPaths() {
const articles = await fetchAPI("/articles");
return {
paths: articles.map((article) => ({
params: {
slug: article.slug,
},
})),
fallback: false,
};
}
export async function getStaticProps({ params }) {
const article = await fetchAPI(/articles/${params.slug}`);
return {
props: { article },
revalidate: 1,
};
}
getStaticPaths()
API. Then, we get the slug and store them in the slug params. getStaticProps()
API. The getStaticProps queries the backend with a single slug and returns the data collected from the backend as props. The props data are then passed into the < Article />
component and rendered with our UI created with tailwind CSS. PREVIEW_SECRET=****
**/lib/api.js**
file. We will use this helper to query our backend for preview data.export async function getPageData(slug, preview = false) {
// Find the pages that match this slug
const pagesData = await fetchAPI(
`/articles?slug=${slug}&status=published${preview ? "&status=draft" : ''}`
);
// Make sure we found something, otherwise return null
if (pagesData == null || pagesData.length === 0) {
return null;
}
// Return the first item since there should only be one result per slug
return pagesData[0];
}
**/pages/API**
, add a new file called **preview.js**
. Then, add the following code to the file.import { getPageData } from "../../lib/api"
export default async function handler(req, res) {
if (req.query.secret !== (process.env.PREVIEW_SECRET || 'secret-token')) {
return res.status(401).json({ message: "Invalid token" });
}
const pageData = await getPageData(req.query.slug, true);
if (!pageData) {
return res.status(401).json({ message: "Invalid slug" });
}
res.setPreviewData({});
res.writeHead(307, { Location: `/article/${req.query.slug}` });
res.end();
};
.env
file. After verifying the preview request, the API then activates the preview mode and redirects to the article location.**[slug].js**
file, import the getPageData helper from the **lib/api**
page.import { fetchAPI, getPageData } from "../../lib/api";
export async function getStaticProps({ params, preview = null }) {
const article = await getPageData(params.slug, preview);
return {
props: { article, preview },
revalidate: 1,
};
}
[http://localhost:3000/api/preview?secret=](http://localhost:3000/api/preview?secret=)
<preview-secret>&slug=<slug>
.env
file**pages/API**
folder and add a new file named **exit-preview.js**
. Next, add the following lines of code to the **exit-preview.js**
file.export default function handler(req, res) {
try {
// Clears the preview mode cookies.
// This function accepts no arguments.
res.clearPreviewData()
return res.status(200).json({ message: 'Cookies Cleared' })
} catch (error) {
return res.status(500).json({ message: error })
}
}
pages/articles/[slug].js
and change the <Article />
component to this -const Article = ({ article, preview }) => {
return (
<>
<div>
{preview ? (
<div className="relative bg-indigo-600">
<div className="max-w-7xl mx-auto py-3 px-3 sm:px-6 lg:px-8">
<div className="pr-16 sm:text-center sm:px-16">
<p className="font-medium text-white">
<span>Preview mode is on,</span>
<span className="block sm:ml-2 sm:inline-block">
<a
href="/api/exit-preview"
className="underline hover:text-cyan transition-colors"
>
turn off
</a>
</span>
</p>
</div>
</div>
</div>
) : null}
</div>
<Layout>
<div className="mt-10">
<div className="mb-4 md:mb-0 w-full max-w-screen-md mx-auto">
<div className="absolute left-0 bottom-0 w-full h-full" />
<div>
{article.image && (
<img src={`http://localhost:1337${article.image.url}`} />
)}
</div>
<div>
{article.category && (
<a
href="#"
className="px-4 py-1 bg-black text-blue-200 inline-flex text-md items-center justify-center mb-2"
>
{article.category.name}
</a>
)}
</div>
<h2 className="text-4xl pt-2 font-semibold text-gray-500 leading-tight">
{article.description}
</h2>
<div className="mt-3">
{article.author && (
<p className="text-blue-900 font-semibold pb-2">
Written by - {article.author.name}
</p>
)}
</div>
</div>
<article className="prose lg:prose-xl px-4 lg:px-0 mt-12 text-gray-700 max-w-screen-md mx-auto text-lg leading-relaxed">
<Markdown>{article.content}</Markdown>
</article>
</div>
</Layout>
</>
);
};
.
## Conclusion
The preview mode is an essential Static site generator tool that can improve the content editor experience when using the Jamstack architecture.
To learn more about Next.js preview mode, visit this link - [](https://nextjs.org/docs/advanced-features/preview-mode)https://nextjs.org/docs/advanced-features/preview-mode [](https://nextjs.org/docs/advanced-features/preview-mode).
Source Code - https://github.com/Quadrisheriff/previews-tutorial-
>This article is a guest post by [**Quadri Sheriff**](http://twitter.com/QuadriSheriff3). He wrote this blog post through the [Write for the Community](https://strapi.io/write-for-the-community) program.