Repository: ButterCMS/react-cms-blog-with-next-js
Branch: master
Commit: d18b1c86cb75
Files: 33
Total size: 37.9 KB
Directory structure:
gitextract_qb8etyuo/
├── .gitignore
├── README.md
├── components/
│ ├── avatar.js
│ ├── container.js
│ ├── cover-image.js
│ ├── date.js
│ ├── footer.js
│ ├── header.js
│ ├── layout.js
│ ├── post-body.js
│ ├── post-header.js
│ ├── post-preview.js
│ └── post-title.js
├── jsconfig.json
├── lib/
│ └── api.js
├── next.config.js
├── package.json
├── pages/
│ ├── _app.js
│ ├── _document.js
│ ├── atom.js
│ ├── case-studies/
│ │ └── [slug].js
│ ├── case-studies.js
│ ├── faq.js
│ ├── index.js
│ ├── posts/
│ │ ├── [slug].js
│ │ ├── categories.js
│ │ ├── category/
│ │ │ └── [slug].js
│ │ └── page/
│ │ └── [page].js
│ ├── rss.js
│ └── sitemap.js
├── postcss.config.js
├── styles/
│ └── index.css
└── tailwind.config.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
dist
.next
node_modules
npm-debug.log
.DS_Store
.env
.vercel
================================================
FILE: README.md
================================================
# React CMS-powered application built with Next.js
## Important Notice
This project was created as an example use case of ButterCMS with React and Next.JS, and will not be actively maintained.
If you’re interested in exploring the best, most up-to-date way to integrate Butter into javascript frameworks like React and Next.js, you can check out the following resources:
### Starter Projects
The following turn-key starters are fully integrated with dynamic sample content from your ButterCMS account, including main menu, pages, blog posts, categories, and tags, all with a beautiful, custom theme with already-implemented search functionality. All of the included sample content is automatically created in your account dashboard when you sign up for a free trial of ButterCMS.
- [Next.js Starter](https://buttercms.com/starters/nextjs-starter-project/)
- [Angular Starter](https://buttercms.com/starters/angular-starter-project/)
- [React Starter](https://buttercms.com/starters/react-starter-project/)
- [Vue.js Starter](https://buttercms.com/starters/vuejs-starter-project/)
- Or see a list of all our [currently-maintained starters](https://buttercms.com/starters/). (Over a dozen and counting!)
### Other Resources
- Check out the [official ButterCMS Docs](https://buttercms.com/docs/)
- Check out the [official ButterCMS API docs](https://buttercms.com/docs/api/)
## Project Description
[](https://vercel.com/new/git/external?repository-url=https%3A%2F%2Fgithub.com%2Fbuttercms%2Freact-cms-blog-with-next-js)
- Demo: https://react-cms-blog-with-next-js.orlyohreally.vercel.app/
[Next.js](https://github.com/vercel/next.js/) is a small framework for building universal React webapps. Next.js comes with Webpack and Babel built-in. You can read more about the philosophy behind Next.js [here](https://zeit.co/blog/next).
[ButterCMS](https://buttercms.com) is a hosted API-based CMS and blog engine that lets you build CMS-powered apps using any programming language. You can think of Butter as similar to WordPress except that you build your website in your language of choice and then plug-in the dynamic content using an API.
This projects shows how to integrate ButterCMS into Next.js application:
- How to create page with a list of blog posts and pages for each post. Learn more about blog integration [here](https://buttercms.com/docs/api/#get-your-blog-posts).
- How to dynamically create pages using data from ButterCMS pages. Learn more about fetching single pages and pages with types [here](https://buttercms.com/docs/api/#pages).
- How to display collections items on the page. Learn more on how to query collections [here](https://buttercms.com/docs/api/#retrieve-a-collection).
- How to get all posts categories. Learn more about categories [here](https://buttercms.com/docs/api/#categories).
- How to create pages with RSS, Atom, and Sitemap feeds. Learn more [here](https://buttercms.com/docs/api/#feeds).
Link to ButterCMS API documentation is https://buttercms.com/docs/api/.
## Set up ButterCMS account
1. Create a free account on ButterCMS - https://buttercms.com/.
2. To see the project locally you need to create this data in your account:
- Create and publish customer case study pages (page with type `customer_case_study`) with this structure:

Learn more about page types [here](https://buttercms.com/docs/api-client/nextjs#PagesPageType).
- Create collection `faq_items` with this structure:

Learn more about collections [here](https://buttercms.com/docs/api-client/nextjs#Collections).
3. Find your read API token from the home page or from settings page, it will be needed later.
## Requirements
- Node >= 12.0.0
## Running project locally
1. Clone and cd into project:
```
git clone https://github.com/ButterCMS/react-cms-blog-with-next-js.git
cd react-cms-blog-with-next-js.git
```
2. Install all dependencies
```
npm install
```
or
```
yarn install
```
1. Copy `.env.example` file as `.env` and replace `BUTTER_CMS_API_KEY` value with your read API token
2. Run the app in development mode
```
npm run dev
```
or
```
yarn dev
```
The application should be available at `http://localhost:3000`.
To build the project run `npm run build` and to start run `npm start`
## Deploy on Vercel
1. Create a Vercel account at https://vercel.com/signup and download [the CLI](https://vercel.com/download)
2. Run `vercel` at the project root
## Webhooks to trigger deployment
We can make use of the ButterCMS webhooks to notify the application every time a post, page or collection item are added or modified, and reload the Next.js application.
You can add a new webhook by going to the [https://buttercms.com/webhooks/](https://buttercms.com/webhooks/) page.

If you use Vercel, you can create deploy webhooks there. 
## Wrap up
Next.js is a powerful framework that makes it easy to build universal React apps. With ButterCMS you can quickly build CMS-powered applications and websites with React and Node.js.
We hope you enjoyed this tutorial. If you have any questions about setting up your ButterCMS-powered application feel free to contact ButterCMS support.
## Other
View ReactJS [Blog engine](https://buttercms.com/react-blog-engine/) and [Full CMS](https://buttercms.com/react-cms/) for other examples of using ButterCMS with ReactJS. And check out [Next.js Blog engine](https://buttercms.com/nextjs-blog-engine/) and [Next.js CMS](https://buttercms.com/nextjs-cms/) for other Next.js examples.
================================================
FILE: components/avatar.js
================================================
import Image from "next/image";
export default function Avatar({ name, picture }) {
return (
<div className="flex items-center">
<div className=" mr-4">
<Image
src={picture}
height="48px"
width="48px"
className="w-12 h-12 rounded-full grayscale"
alt={name}
/>
</div>
<div className="text-xl font-bold">{name}</div>
</div>
);
}
================================================
FILE: components/container.js
================================================
export default function Container({ children }) {
return <div className="container mx-auto px-5">{children}</div>;
}
================================================
FILE: components/cover-image.js
================================================
import Link from "next/link";
import Image from "next/image";
export default function CoverImage({ title, url, slug }) {
return (
<div className="sm:mx-0">
{slug ? (
<Link href={`/posts/${slug}`}>
<a aria-label={title}>
<div style={{ position: "relative", height: "300px" }}>
<Image
alt={title}
src={url}
layout="fill"
objectFit="cover"
quality={100}
className="rounded-lg"
/>
</div>
</a>
</Link>
) : (
<div style={{ position: "relative", height: "300px" }}>
<Image
alt={title}
src={url}
layout="fill"
objectFit="cover"
quality={100}
className="rounded-lg"
/>
</div>
)}
</div>
);
}
================================================
FILE: components/date.js
================================================
import { parseISO, format } from "date-fns";
export default function Date({ dateString }) {
const date = parseISO(dateString);
return <time dateTime={dateString}>{format(date, "LLLL d, yyyy")}</time>;
}
================================================
FILE: components/footer.js
================================================
import Container from "./container";
export default function Footer() {
return (
<footer className="bg-accent-1 border-t border-accent-2">
<Container>
<div className="py-28 flex flex-col lg:flex-row items-center">
<h3 className="text-4xl lg:text-5xl font-bold tracking-tighter leading-tight text-center lg:text-left mb-10 lg:mb-0 lg:pr-4 lg:w-1/2">
Statically Generated with Next.js and ButterCMS
</h3>
<div className="flex flex-col lg:flex-row justify-center items-center lg:pl-4 lg:w-1/2">
<a
href="https://buttercms.com/docs/api/?javascript#"
className="mx-3 bg-black hover:bg-white hover:text-black border border-black text-white font-bold py-3 px-12 lg:px-8 duration-200 transition-colors mb-6 lg:mb-0"
>
Read Documentation
</a>
<a
href={`https://github.com/ButterCMS/react-cms-blog-with-next-js/`}
className="mx-3 font-bold hover:underline"
>
View on GitHub
</a>
</div>
</div>
</Container>
</footer>
);
}
================================================
FILE: components/header.js
================================================
export default function Header({ title }) {
return (
<h2 className="text-2xl md:text-4xl font-bold tracking-tight md:tracking-tighter leading-tight px-4 mb-20 mt-8">
{title}
</h2>
);
}
================================================
FILE: components/layout.js
================================================
import Link from "next/link";
import Footer from "./footer";
export default function Layout({ children }) {
return (
<>
<div className="min-h-screen mb-20">
<div className="relative pt-6 pb-6 px-4 sm:px-6 lg:px-8">
<nav
className="relative flex items-center justify-between sm:h-10 lg:justify-start"
aria-label="Global"
>
<div className="ml-10 pr-4 space-x-8">
<Link href="/">
<a className="font-medium text-gray-500 hover:text-gray-900">
Home
</a>
</Link>
<Link href="/posts">
<a className="font-medium text-gray-500 hover:text-gray-900">
Blog
</a>
</Link>
<Link href="/case-studies">
<a className="font-medium text-gray-500 hover:text-gray-900">
Pages with types
</a>
</Link>
<Link href="/faq">
<a className="font-medium text-gray-500 hover:text-gray-900">
Collections
</a>
</Link>
</div>
</nav>
</div>
<main>{children}</main>
</div>
<Footer />
</>
);
}
================================================
FILE: components/post-body.js
================================================
export default function PostBody({ content }) {
return (
<div className="max-w-2xl mx-auto">
<div className="prose" dangerouslySetInnerHTML={{ __html: content }} />
</div>
);
}
================================================
FILE: components/post-header.js
================================================
import Link from "next/link";
import Avatar from "@/components/avatar";
import Date from "@/components/date";
import CoverImage from "@/components/cover-image";
import PostTitle from "@/components/post-title";
export default function PostHeader({
title,
coverImage,
date,
author,
categories,
}) {
return (
<>
<PostTitle>{title}</PostTitle>
<div className="hidden md:block md:mb-12">
<Avatar
name={`${author.first_name} ${author.last_name}`}
picture={author.profile_image}
/>
</div>
<div className="mb-8 md:mb-16 sm:mx-0">
<CoverImage title={title} url={coverImage} />
</div>
<div className="max-w-2xl mx-auto">
<div className="block md:hidden mb-6">
<Avatar
name={`${author.first_name} ${author.last_name}`}
picture={author.profile_image}
/>
</div>
<div className="mb-6 text-lg">
{categories.map(({ name, slug }) => {
return (
<Link href={`/posts/category/${slug}`} key={slug}>
<a className="mr-2 hover:underline leading-snug">{name}</a>
</Link>
);
})}
</div>
<div className="mb-6 text-lg">
<Date dateString={date} />
</div>
</div>
</>
);
}
================================================
FILE: components/post-preview.js
================================================
import Avatar from "./avatar";
import Date from "./date";
import CoverImage from "./cover-image";
import Link from "next/link";
export default function PostPreview({
title,
coverImage,
date,
excerpt,
author,
slug,
}) {
return (
<div>
<div className="mb-5">
{coverImage && (
<CoverImage slug={slug} title={title} url={coverImage} />
)}
</div>
<h3 className="text-3xl mb-3 leading-snug">
<Link href={`/posts/${slug}`}>
<a className="hover:underline">{title}</a>
</Link>
</h3>
<div className="text-lg mb-4">
<Date dateString={date} />
</div>
<p className="text-lg leading-relaxed mb-4">{excerpt}</p>
<Avatar
name={`${author.first_name} ${author.last_name}`}
picture={author.profile_image}
/>
</div>
);
}
================================================
FILE: components/post-title.js
================================================
export default function PostTitle({ children }) {
return (
<h1 className="text-6xl md:text-7xl lg:text-8xl font-bold tracking-tighter leading-tight md:leading-none mb-12 text-center md:text-left">
{children}
</h1>
)
}
================================================
FILE: jsconfig.json
================================================
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/components/*": ["components/*"],
"@/lib/*": ["lib/*"],
"@/styles/*": ["styles/*"]
}
}
}
================================================
FILE: lib/api.js
================================================
import Butter from "buttercms";
const butter = Butter(process.env.BUTTER_CMS_API_KEY);
const postsPageSize = 10;
export async function getPreviewPostBySlug(slug) {
const postResponse = await butter.post.retrieve(slug, {
preview: 1,
});
return postResponse?.data?.data;
}
export async function getPostsData(page = 1, pageSize = postsPageSize) {
// https://buttercms.com/docs/api/node?javascript#get-your-blog-posts
const response = await butter.post.list({
page_size: pageSize,
page: page,
});
return {
posts: response?.data?.data,
prevPage: response?.data?.meta.previous_page,
nextPage: response?.data?.meta.next_page,
};
}
export async function getAllPostsPaginated(pageSize = postsPageSize) {
const paginatedPosts = [];
let currentPage = 1;
while (!!currentPage) {
const pagePostsData = await getPostsData(currentPage, pageSize);
paginatedPosts[currentPage - 1] = pagePostsData.posts;
currentPage = pagePostsData.nextPage;
}
return paginatedPosts;
}
export async function getCategories() {
const categories = await butter.category.list();
return categories?.data?.data;
}
export async function getCategoryWithPosts(slug) {
const response = await butter.category.retrieve(slug, {
include: "recent_posts",
});
return response?.data?.data;
}
export async function getAtomData() {
const atom = await butter.feed.retrieve("atom");
return atom?.data?.data;
}
export async function getRssData() {
const rss = await butter.feed.retrieve("rss");
return rss?.data?.data;
}
export async function getSitemapData() {
const sitemap = await butter.feed.retrieve("sitemap");
return sitemap?.data?.data;
}
export async function getPagesByType(type) {
const pages = await butter.page.list(type);
return pages?.data?.data;
}
export async function getPageWithType(type, slug) {
const page = await butter.page.retrieve(type, slug);
return page?.data?.data;
}
export async function getPost(slug) {
const post = await butter.post.retrieve(slug);
return post?.data?.data;
}
export async function getCollectionsItems(collectionsSlugs) {
const collectionsItems = await butter.content.retrieve(collectionsSlugs);
return collectionsItems?.data?.data;
}
export async function getPostAndMorePosts(slug, preview) {
const postResponse = await butter.post.retrieve(slug, {
preview: preview ? 1 : 0,
});
const postListResponse = await butter.post.list();
return {
post: postResponse?.data?.data,
morePosts: postListResponse?.data?.data.filter(
({ slug: postSlug }) => postSlug !== slug
),
};
}
================================================
FILE: next.config.js
================================================
module.exports = {
async rewrites() {
return [
{
source: "/posts",
destination: "/posts/page/1",
},
];
},
async redirects() {
return [
{
source: "/posts/page",
destination: "/posts",
permanent: true,
},
];
},
images: {
domains: ["cdn.buttercms.com"],
},
};
================================================
FILE: package.json
================================================
{
"name": "react-blog",
"scripts": {
"start": "next start",
"dev": "next",
"build": "next build"
},
"engines": {
"node": ">=12.0.0"
},
"dependencies": {
"@tailwindcss/typography": "^0.3.1",
"autoprefixer": "^10.0.4",
"buttercms": "^1.2.3",
"date-fns": "^2.16.1",
"next": "latest",
"postcss": "^8.2.13",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"tailwindcss": "^2.0.1"
},
"devDependencies": {
"postcss-flexbugs-fixes": "4.2.1",
"postcss-preset-env": "^6.7.0"
}
}
================================================
FILE: pages/_app.js
================================================
import '@/styles/index.css'
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />
}
export default MyApp
================================================
FILE: pages/_document.js
================================================
import Document, { Html, Head, Main, NextScript } from 'next/document'
export default class MyDocument extends Document {
render() {
return (
<Html lang="en">
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
}
================================================
FILE: pages/atom.js
================================================
import { getAtomData } from "@/lib/api";
export default function Atom({ atom }) {
return atom;
}
export async function getStaticProps() {
const atom = await getAtomData();
return { props: { atom } };
}
================================================
FILE: pages/case-studies/[slug].js
================================================
import React from "react";
import Head from "next/head";
import { useRouter } from "next/router";
import ErrorPage from "next/error";
import Image from "next/image";
import Layout from "@/components/layout";
import Date from "@/components/date";
import Header from "@/components/header";
import Container from "@/components/container";
import { getPagesByType, getPageWithType } from "@/lib/api";
export default function caseStudy({
slug,
name,
customerLogo,
headline,
customerIndustry,
customerSubindustry,
studyBody,
studyDate,
customerReviewedCaseStudy,
}) {
const router = useRouter();
if (!router.isFallback && !slug) {
return <ErrorPage statusCode={404} />;
}
return (
<Layout>
<Container>
{router.isFallback ? (
<div>Loading…</div>
) : (
<article>
<Head>
<title>{name}</title>
</Head>
<Header title={headline}></Header>
<div className="grid grid-flow-col auto-cols-max gap-4 mb-5">
<div
style={{
position: "relative",
width: "300px",
height: "300px",
}}
>
<Image
alt={name}
src={customerLogo}
layout="fill"
objectFit="cover"
quality={100}
className="rounded-lg"
/>
</div>
<div>
<div>
Study date: <Date dateString={studyDate}></Date>
</div>
<div>Industry: {customerIndustry}</div>
<div>Subindustry: {customerSubindustry}</div>
{customerReviewedCaseStudy && <div>Reviewed by customer </div>}
</div>
</div>
<div
className="mb-10"
dangerouslySetInnerHTML={{ __html: studyBody }}
/>
</article>
)}
</Container>
</Layout>
);
}
export async function getStaticProps({ params }) {
const page = await getPageWithType("customer_case_study", params.slug);
return {
props: {
slug: page.slug,
name: page.name,
headline: page.fields.headline,
customerLogo: page.fields.customer_logo,
customerIndustry: page.fields.customer_industry,
customerSubindustry: page.fields.customer_subindustry,
studyBody: page.fields.study_body,
studyDate: page.fields.study_date,
customerReviewedCaseStudy: page.fields.customer_reviewed_case_study,
},
};
}
export async function getStaticPaths() {
const caseStudiesPages = await getPagesByType("customer_case_study");
return {
paths: caseStudiesPages?.map((page) => `/case-studies/${page.slug}`) || [],
fallback: true,
};
}
================================================
FILE: pages/case-studies.js
================================================
import Link from "next/link";
import Head from "next/head";
import Image from "next/image";
import Header from "@/components/header";
import Layout from "@/components/layout";
import Date from "@/components/date";
import Container from "@/components/container";
import { getPagesByType } from "@/lib/api";
export default function caseStudies({ pages }) {
return (
<Layout>
<Container>
<Head>
<title>Case studies</title>
</Head>
<Header title="Case studies"></Header>
{pages.map(({ slug, fields }, key) => {
return (
<div
key={key}
className="grid grid-flow-col auto-rows-max gap-4 mb-5"
>
<div
style={{
position: "relative",
width: "300px",
height: "300px",
}}
>
<Link href={`/case-studies/${slug}`}>
<a>
<Image
alt={fields.headline}
src={fields.customer_logo}
layout="fill"
objectFit="cover"
quality={100}
className="rounded-lg"
/>
</a>
</Link>
</div>
<div>
<h3 className="text-3xl mb-3 mt-3 leading-snug">
<Link href={`/case-studies/${slug}`}>
<a className="hover:underline">{fields.headline}</a>
</Link>
</h3>
Study date: <Date dateString={fields.study_date}></Date>
{fields.customer_reviewed_case_study && (
<div>Reviewed by customer </div>
)}
</div>
</div>
);
})}
</Container>
</Layout>
);
}
export async function getStaticProps() {
const pages = await getPagesByType("customer_case_study");
return { props: { pages } };
}
================================================
FILE: pages/faq.js
================================================
import Head from "next/head";
import Layout from "@/components/layout";
import Container from "@/components/container";
import Header from "@/components/header";
import { getCollectionsItems } from "@/lib/api";
export default function FAQ({ faqItems }) {
return (
<Layout>
<Container>
<Head>
<title>FAQ</title>
</Head>
<>
<Header title="FAQ"></Header>
<ul>
{faqItems.map(({ question, answer }, index) => {
return (
<li key={index} className="mb-5">
<div className="text-lg leading-6 font-medium text-gray-900">
{question}
</div>
<div className="mt-4 text-base text-gray-500">{answer}</div>
</li>
);
})}
</ul>
</>
</Container>
</Layout>
);
}
export async function getStaticProps() {
const { faq_items: faqItems } = await getCollectionsItems(["faq_items"]);
return { props: { faqItems } };
}
================================================
FILE: pages/index.js
================================================
import Head from "next/head";
import Link from "next/link";
import Container from "@/components/container";
import Header from "@/components/header";
import Layout from "@/components/layout";
export default function Posts() {
return (
<Layout>
<Container>
<Head>
<title></title>
</Head>
<Header title="ButterCMS. Headless CMS you'll melt over"></Header>
<div className="bg-gray-50 mb-5 rounded-lg">
<div className="max-w-7xl mx-auto py-12 px-4 sm:px-6">
<h2 className="text-3xl font-extrabold tracking-tight text-gray-900 sm:text-4xl lg:mr-5">
<span className="block">Blog Engine</span>
<span className="block text-indigo-600">
You've got better things to do than building another blog.
</span>
</h2>
<div className="mt-8">
<div className="inline-flex rounded-md shadow">
<Link href="/posts">
<a className="inline-flex items-center justify-center px-5 py-3 border border-transparent text-base font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700">
Preview integration
</a>
</Link>
</div>
<div className="ml-3 inline-flex rounded-md shadow">
<a
href="https://buttercms.com/features/#flexiblecontentmodeling-blog-engine"
className="inline-flex items-center justify-center px-5 py-3 border border-transparent text-base font-medium rounded-md text-indigo-600 bg-white hover:bg-indigo-50"
>
Learn more
</a>
</div>
</div>
</div>
</div>
<div className="bg-gray-50 mb-5 rounded-lg">
<div className="max-w-7xl mx-auto py-12 px-4 sm:px-6">
<h2 className="text-3xl font-extrabold tracking-tight text-gray-900 sm:text-4xl lg:mr-5">
<span className="block">Pages</span>
<span className="block text-indigo-600">
Build SEO landing pages, knowledge base, news articles, and more
by using Page Types.
</span>
</h2>
<div className="mt-8">
<div className="inline-flex rounded-md shadow">
<Link href="/case-studies">
<a className="inline-flex items-center justify-center px-5 py-3 border border-transparent text-base font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700">
Preview integration
</a>
</Link>
</div>
<div className="ml-3 inline-flex rounded-md shadow">
<a
href="https://buttercms.com/features/#flexiblecontentmodeling-page-types"
className="inline-flex items-center justify-center px-5 py-3 border border-transparent text-base font-medium rounded-md text-indigo-600 bg-white hover:bg-indigo-50"
>
Learn more
</a>
</div>
</div>
</div>
</div>
<div className="bg-gray-50 mb-5 rounded-lg">
<div className="max-w-7xl mx-auto py-12 px-4 sm:px-6">
<h2 className="text-3xl font-extrabold tracking-tight text-gray-900 sm:text-4xl lg:mr-5">
<span className="block">Collections</span>
<span className="block text-indigo-600">
Create reusable promotional content and more with Collections.
</span>
</h2>
<div className="mt-8">
<div className="inline-flex rounded-md shadow">
<Link href="/faq">
<a className="inline-flex items-center justify-center px-5 py-3 border border-transparent text-base font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700">
Preview integration
</a>
</Link>
</div>
<div className="ml-3 inline-flex rounded-md shadow">
<a
href="https://buttercms.com/features/#flexiblecontentmodeling-collections"
className="inline-flex items-center justify-center px-5 py-3 border border-transparent text-base font-medium rounded-md text-indigo-600 bg-white hover:bg-indigo-50"
>
Learn more
</a>
</div>
</div>
</div>
</div>
</Container>
</Layout>
);
}
================================================
FILE: pages/posts/[slug].js
================================================
import { useRouter } from "next/router";
import ErrorPage from "next/error";
import Head from "next/head";
import Container from "@/components/container";
import PostBody from "@/components/post-body";
import PostHeader from "@/components/post-header";
import Layout from "@/components/layout";
import { getAllPostsPaginated, getPost } from "@/lib/api";
export default function Post({ post }) {
const router = useRouter();
if (!router.isFallback && !post?.slug) {
return <ErrorPage statusCode={404} />;
}
return (
<Layout>
<Container>
{router.isFallback ? (
<div>Loading…</div>
) : (
<>
<article>
<Head>
<title>{post.seo_title}</title>
<meta name="description" content={post.meta_description} />
<meta name="og:image" content={post.featured_image} />
</Head>
<PostHeader
title={post.title}
coverImage={post.featured_image}
date={post.published}
author={post.author}
categories={post.categories}
/>
<PostBody content={post.body} />
</article>
</>
)}
</Container>
</Layout>
);
}
export async function getStaticProps({ params }) {
const post = await getPost(params.slug);
return {
props: {
post,
},
};
}
export async function getStaticPaths() {
const allPosts = await getAllPostsPaginated(100);
const paths = Object.entries(allPosts).reduce((res, [pageIndex, posts]) => {
const pagePaths = posts.map((post) => `/posts/${post.slug}`);
return [...res, ...pagePaths];
}, []);
return {
paths,
fallback: true,
};
}
================================================
FILE: pages/posts/categories.js
================================================
import Head from "next/head";
import Link from "next/link";
import Layout from "@/components/layout";
import Header from "@/components/header";
import Container from "@/components/container";
import { getCategories } from "@/lib/api";
export default function Categories({ categories }) {
return (
<Layout>
<Container>
<Head>
<title>Post categories</title>
</Head>
<Header title="Posts categories"></Header>
<ul>
{categories.map(({ name, slug }, key) => {
return (
<li key={key} className="mb-5">
<Link href={`/posts/category/${slug}`}>
<a className="text-lg leading-6 font-medium">{name}</a>
</Link>
</li>
);
})}
</ul>
</Container>
</Layout>
);
}
export async function getStaticProps() {
const categories = await getCategories();
return { props: { categories } };
}
================================================
FILE: pages/posts/category/[slug].js
================================================
import Head from "next/head";
import ErrorPage from "next/error";
import { useRouter } from "next/router";
import Layout from "@/components/layout";
import Container from "@/components/container";
import PostPreview from "@/components/post-preview";
import { getCategories, getCategoryWithPosts } from "@/lib/api";
import Header from "@/components/header";
export default function Category({ name, slug, recentPosts }) {
const router = useRouter();
if (!router.isFallback && !slug) {
return <ErrorPage statusCode={404} />;
}
return (
<Layout>
<Container>
<Head>
<title>{name} | Post category</title>
</Head>
{router.isFallback ? (
<div>Loading…</div>
) : (
<article>
<Header title={`Blog posts with category "${name}"`}></Header>
<div className="grid grid-cols-1 md:grid-cols-2 md:col-gap-16 lg:col-gap-32 row-gap-20 md:row-gap-32 mb-10">
{recentPosts.map(
({
slug: postSlug,
title: postTitle,
featured_image: featuredImage,
published,
author,
summary,
}) => {
return (
<PostPreview
key={postSlug}
title={postTitle}
coverImage={featuredImage}
date={published}
author={author}
slug={postSlug}
excerpt={summary}
/>
);
}
)}
</div>
</article>
)}
</Container>
</Layout>
);
}
export async function getStaticProps({ params }) {
const { name, slug, recent_posts } = await getCategoryWithPosts(params.slug);
return {
props: { name, slug, recentPosts: recent_posts },
};
}
export async function getStaticPaths() {
const categories = await getCategories();
return {
paths:
categories?.map((category) => `/posts/category/${category.slug}`) || [],
fallback: true,
};
}
================================================
FILE: pages/posts/page/[page].js
================================================
import Link from "next/link";
import Head from "next/head";
import { useRouter } from "next/router";
import ErrorPage from "next/error";
import Layout from "@/components/layout";
import Header from "@/components/header";
import Container from "@/components/container";
import PostPreview from "@/components/post-preview";
import { getPostsData, getAllPostsPaginated } from "@/lib/api";
export default function Posts({ posts, prevPage, nextPage }) {
const router = useRouter();
if (!router.isFallback && !posts) {
return <ErrorPage statusCode={404} />;
}
return (
<Layout>
<Container>
{router.isFallback ? (
<div>Loading…</div>
) : (
<>
<Head>
<title>Blog posts</title>
<meta
name="description"
content="Blog posts fetched from ButterCMS"
/>
</Head>
<Header title="Blog"></Header>
<div className="grid grid-cols-1 md:grid-cols-2 md:gap-x-8 lg:gap-x-16 gap-y-10 md:gap-y-16">
{posts.map((post) => (
<PostPreview
key={post.slug}
title={post.title}
coverImage={post.featured_image}
date={post.published}
author={post.author}
slug={post.slug}
excerpt={post.summary}
/>
))}
</div>
{(prevPage || nextPage) && (
<div className="text-right mb-10">
<nav
className="relative z-0 inline-flex shadow-sm -space-x-px"
aria-label="Pagination"
>
{prevPage && (
<Link href={`/posts/page/${prevPage}`}>
<a className="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
<span className="sr-only">Previous</span>
<svg
className="h-5 w-5"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
</a>
</Link>
)}
{nextPage && (
<Link href={`/posts/page/${nextPage}`}>
<a className="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
<span className="sr-only">Next</span>
<svg
className="h-5 w-5"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clipRule="evenodd"
/>
</svg>
</a>
</Link>
)}
</nav>
</div>
)}
</>
)}
</Container>
</Layout>
);
}
export async function getStaticProps({ params }) {
const page = parseInt(params.page, 10);
const { posts, prevPage, nextPage } = await getPostsData(page);
return {
props: { posts, prevPage, nextPage },
revalidate: 1,
};
}
export async function getStaticPaths() {
const allPosts = await getAllPostsPaginated();
const paths = Object.keys(allPosts).map(
(pageIndex) => `/posts/page/${parseInt(pageIndex, 10) + 1}`
);
return {
paths,
fallback: true,
};
}
================================================
FILE: pages/rss.js
================================================
import { getRssData } from "@/lib/api";
export default function Rss({ rss }) {
return rss;
}
export async function getStaticProps() {
const rss = await getRssData();
return { props: { rss } };
}
================================================
FILE: pages/sitemap.js
================================================
import { getSitemapData } from "@/lib/api";
export default function Sitemap({ sitemap }) {
return sitemap;
}
export async function getStaticProps() {
const sitemap = await getSitemapData();
return { props: { sitemap } };
}
================================================
FILE: postcss.config.js
================================================
module.exports = {
plugins: [
'tailwindcss',
'postcss-flexbugs-fixes',
[
'postcss-preset-env',
{
autoprefixer: {
flexbox: 'no-2009',
},
stage: 3,
features: {
'custom-properties': false,
},
},
],
],
}
================================================
FILE: styles/index.css
================================================
@tailwind base;
@tailwind components;
@tailwind utilities;
.grayscale {
filter: grayscale(1);
}
iframe {
max-width: 100%;
}
================================================
FILE: tailwind.config.js
================================================
module.exports = {
purge: ["./components/**/*.js", "./pages/**/*.js"],
theme: {
extend: {
fontFamily: {
sans:
'-apple-system, "Helvetica Neue", "Segoe UI", Roboto, Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"',
},
colors: {
"accent-1": "#FAFAFA",
"accent-2": "#EAEAEA",
"accent-7": "#333",
success: "#0070f3",
cyan: "#79FFE1",
},
spacing: {
28: "7rem",
},
letterSpacing: {
tighter: "-.04em",
},
lineHeight: {
tight: 1.2,
},
fontSize: {
"5xl": "2.5rem",
"6xl": "2.75rem",
"7xl": "4.5rem",
"8xl": "6.25rem",
},
boxShadow: {
small: "0 5px 10px rgba(0, 0, 0, 0.12)",
medium: "0 8px 30px rgba(0, 0, 0, 0.12)",
},
},
},
plugins: [require("@tailwindcss/typography")],
};
gitextract_qb8etyuo/ ├── .gitignore ├── README.md ├── components/ │ ├── avatar.js │ ├── container.js │ ├── cover-image.js │ ├── date.js │ ├── footer.js │ ├── header.js │ ├── layout.js │ ├── post-body.js │ ├── post-header.js │ ├── post-preview.js │ └── post-title.js ├── jsconfig.json ├── lib/ │ └── api.js ├── next.config.js ├── package.json ├── pages/ │ ├── _app.js │ ├── _document.js │ ├── atom.js │ ├── case-studies/ │ │ └── [slug].js │ ├── case-studies.js │ ├── faq.js │ ├── index.js │ ├── posts/ │ │ ├── [slug].js │ │ ├── categories.js │ │ ├── category/ │ │ │ └── [slug].js │ │ └── page/ │ │ └── [page].js │ ├── rss.js │ └── sitemap.js ├── postcss.config.js ├── styles/ │ └── index.css └── tailwind.config.js
SYMBOL INDEX (54 symbols across 26 files)
FILE: components/avatar.js
function Avatar (line 3) | function Avatar({ name, picture }) {
FILE: components/container.js
function Container (line 1) | function Container({ children }) {
FILE: components/cover-image.js
function CoverImage (line 4) | function CoverImage({ title, url, slug }) {
FILE: components/date.js
function Date (line 3) | function Date({ dateString }) {
FILE: components/footer.js
function Footer (line 3) | function Footer() {
FILE: components/header.js
function Header (line 1) | function Header({ title }) {
FILE: components/layout.js
function Layout (line 4) | function Layout({ children }) {
FILE: components/post-body.js
function PostBody (line 1) | function PostBody({ content }) {
FILE: components/post-header.js
function PostHeader (line 8) | function PostHeader({
FILE: components/post-preview.js
function PostPreview (line 6) | function PostPreview({
FILE: components/post-title.js
function PostTitle (line 1) | function PostTitle({ children }) {
FILE: lib/api.js
function getPreviewPostBySlug (line 6) | async function getPreviewPostBySlug(slug) {
function getPostsData (line 13) | async function getPostsData(page = 1, pageSize = postsPageSize) {
function getAllPostsPaginated (line 27) | async function getAllPostsPaginated(pageSize = postsPageSize) {
function getCategories (line 38) | async function getCategories() {
function getCategoryWithPosts (line 43) | async function getCategoryWithPosts(slug) {
function getAtomData (line 50) | async function getAtomData() {
function getRssData (line 55) | async function getRssData() {
function getSitemapData (line 60) | async function getSitemapData() {
function getPagesByType (line 65) | async function getPagesByType(type) {
function getPageWithType (line 70) | async function getPageWithType(type, slug) {
function getPost (line 75) | async function getPost(slug) {
function getCollectionsItems (line 80) | async function getCollectionsItems(collectionsSlugs) {
function getPostAndMorePosts (line 85) | async function getPostAndMorePosts(slug, preview) {
FILE: next.config.js
method rewrites (line 2) | async rewrites() {
method redirects (line 10) | async redirects() {
FILE: pages/_app.js
function MyApp (line 3) | function MyApp({ Component, pageProps }) {
FILE: pages/_document.js
class MyDocument (line 3) | class MyDocument extends Document {
method render (line 4) | render() {
FILE: pages/atom.js
function Atom (line 3) | function Atom({ atom }) {
function getStaticProps (line 7) | async function getStaticProps() {
FILE: pages/case-studies.js
function caseStudies (line 11) | function caseStudies({ pages }) {
function getStaticProps (line 65) | async function getStaticProps() {
FILE: pages/case-studies/[slug].js
function caseStudy (line 13) | function caseStudy({
function getStaticProps (line 77) | async function getStaticProps({ params }) {
function getStaticPaths (line 95) | async function getStaticPaths() {
FILE: pages/faq.js
function FAQ (line 8) | function FAQ({ faqItems }) {
function getStaticProps (line 35) | async function getStaticProps() {
FILE: pages/index.js
function Posts (line 8) | function Posts() {
FILE: pages/posts/[slug].js
function Post (line 11) | function Post({ post }) {
function getStaticProps (line 46) | async function getStaticProps({ params }) {
function getStaticPaths (line 56) | async function getStaticPaths() {
FILE: pages/posts/categories.js
function Categories (line 9) | function Categories({ categories }) {
function getStaticProps (line 33) | async function getStaticProps() {
FILE: pages/posts/category/[slug].js
function Category (line 11) | function Category({ name, slug, recentPosts }) {
function getStaticProps (line 59) | async function getStaticProps({ params }) {
function getStaticPaths (line 67) | async function getStaticPaths() {
FILE: pages/posts/page/[page].js
function Posts (line 12) | function Posts({ posts, prevPage, nextPage }) {
function getStaticProps (line 103) | async function getStaticProps({ params }) {
function getStaticPaths (line 113) | async function getStaticPaths() {
FILE: pages/rss.js
function Rss (line 3) | function Rss({ rss }) {
function getStaticProps (line 7) | async function getStaticProps() {
FILE: pages/sitemap.js
function Sitemap (line 3) | function Sitemap({ sitemap }) {
function getStaticProps (line 7) | async function getStaticProps() {
Condensed preview — 33 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (42K chars).
[
{
"path": ".gitignore",
"chars": 62,
"preview": "dist\n.next\nnode_modules\nnpm-debug.log\n.DS_Store\n\n.env\n.vercel\n"
},
{
"path": "README.md",
"chars": 5819,
"preview": "# React CMS-powered application built with Next.js\n\n## Important Notice\nThis project was created as an example use case "
},
{
"path": "components/avatar.js",
"chars": 424,
"preview": "import Image from \"next/image\";\n\nexport default function Avatar({ name, picture }) {\n return (\n <div className=\"flex"
},
{
"path": "components/container.js",
"chars": 119,
"preview": "export default function Container({ children }) {\n return <div className=\"container mx-auto px-5\">{children}</div>;\n}\n"
},
{
"path": "components/cover-image.js",
"chars": 905,
"preview": "import Link from \"next/link\";\nimport Image from \"next/image\";\n\nexport default function CoverImage({ title, url, slug }) "
},
{
"path": "components/date.js",
"chars": 208,
"preview": "import { parseISO, format } from \"date-fns\";\n\nexport default function Date({ dateString }) {\n const date = parseISO(dat"
},
{
"path": "components/footer.js",
"chars": 1160,
"preview": "import Container from \"./container\";\n\nexport default function Footer() {\n return (\n <footer className=\"bg-accent-1 b"
},
{
"path": "components/header.js",
"chars": 203,
"preview": "export default function Header({ title }) {\n return (\n <h2 className=\"text-2xl md:text-4xl font-bold tracking-tight "
},
{
"path": "components/layout.js",
"chars": 1301,
"preview": "import Link from \"next/link\";\nimport Footer from \"./footer\";\n\nexport default function Layout({ children }) {\n return (\n"
},
{
"path": "components/post-body.js",
"chars": 195,
"preview": "export default function PostBody({ content }) {\n return (\n <div className=\"max-w-2xl mx-auto\">\n <div className="
},
{
"path": "components/post-header.js",
"chars": 1341,
"preview": "import Link from \"next/link\";\n\nimport Avatar from \"@/components/avatar\";\nimport Date from \"@/components/date\";\nimport Co"
},
{
"path": "components/post-preview.js",
"chars": 856,
"preview": "import Avatar from \"./avatar\";\nimport Date from \"./date\";\nimport CoverImage from \"./cover-image\";\nimport Link from \"next"
},
{
"path": "components/post-title.js",
"chars": 236,
"preview": "export default function PostTitle({ children }) {\n return (\n <h1 className=\"text-6xl md:text-7xl lg:text-8xl font-bo"
},
{
"path": "jsconfig.json",
"chars": 175,
"preview": "{\n \"compilerOptions\": {\n \"baseUrl\": \".\",\n \"paths\": {\n \"@/components/*\": [\"components/*\"],\n \"@/lib/*\": ["
},
{
"path": "lib/api.js",
"chars": 2613,
"preview": "import Butter from \"buttercms\";\nconst butter = Butter(process.env.BUTTER_CMS_API_KEY);\n\nconst postsPageSize = 10;\n\nexpor"
},
{
"path": "next.config.js",
"chars": 353,
"preview": "module.exports = {\n async rewrites() {\n return [\n {\n source: \"/posts\",\n destination: \"/posts/page"
},
{
"path": "package.json",
"chars": 542,
"preview": "{\n \"name\": \"react-blog\",\n \"scripts\": {\n \"start\": \"next start\",\n \"dev\": \"next\",\n \"build\": \"next build\"\n },\n "
},
{
"path": "pages/_app.js",
"chars": 134,
"preview": "import '@/styles/index.css'\n\nfunction MyApp({ Component, pageProps }) {\n return <Component {...pageProps} />\n}\n\nexport "
},
{
"path": "pages/_document.js",
"chars": 290,
"preview": "import Document, { Html, Head, Main, NextScript } from 'next/document'\n\nexport default class MyDocument extends Document"
},
{
"path": "pages/atom.js",
"chars": 210,
"preview": "import { getAtomData } from \"@/lib/api\";\n\nexport default function Atom({ atom }) {\n return atom;\n}\n\nexport async functi"
},
{
"path": "pages/case-studies/[slug].js",
"chars": 2858,
"preview": "import React from \"react\";\nimport Head from \"next/head\";\nimport { useRouter } from \"next/router\";\nimport ErrorPage from "
},
{
"path": "pages/case-studies.js",
"chars": 2045,
"preview": "import Link from \"next/link\";\nimport Head from \"next/head\";\nimport Image from \"next/image\";\n\nimport Header from \"@/compo"
},
{
"path": "pages/faq.js",
"chars": 1051,
"preview": "import Head from \"next/head\";\n\nimport Layout from \"@/components/layout\";\nimport Container from \"@/components/container\";"
},
{
"path": "pages/index.js",
"chars": 4625,
"preview": "import Head from \"next/head\";\nimport Link from \"next/link\";\n\nimport Container from \"@/components/container\";\nimport Head"
},
{
"path": "pages/posts/[slug].js",
"chars": 1770,
"preview": "import { useRouter } from \"next/router\";\nimport ErrorPage from \"next/error\";\nimport Head from \"next/head\";\n\nimport Conta"
},
{
"path": "pages/posts/categories.js",
"chars": 969,
"preview": "import Head from \"next/head\";\nimport Link from \"next/link\";\n\nimport Layout from \"@/components/layout\";\nimport Header fro"
},
{
"path": "pages/posts/category/[slug].js",
"chars": 2157,
"preview": "import Head from \"next/head\";\nimport ErrorPage from \"next/error\";\nimport { useRouter } from \"next/router\";\n\nimport Layou"
},
{
"path": "pages/posts/page/[page].js",
"chars": 4409,
"preview": "import Link from \"next/link\";\nimport Head from \"next/head\";\nimport { useRouter } from \"next/router\";\nimport ErrorPage fr"
},
{
"path": "pages/rss.js",
"chars": 203,
"preview": "import { getRssData } from \"@/lib/api\";\n\nexport default function Rss({ rss }) {\n return rss;\n}\n\nexport async function g"
},
{
"path": "pages/sitemap.js",
"chars": 231,
"preview": "import { getSitemapData } from \"@/lib/api\";\n\nexport default function Sitemap({ sitemap }) {\n return sitemap;\n}\n\nexport "
},
{
"path": "postcss.config.js",
"chars": 298,
"preview": "module.exports = {\n plugins: [\n 'tailwindcss',\n 'postcss-flexbugs-fixes',\n [\n 'postcss-preset-env',\n "
},
{
"path": "styles/index.css",
"chars": 130,
"preview": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n.grayscale {\n filter: grayscale(1);\n}\n\niframe {\n max-width"
},
{
"path": "tailwind.config.js",
"chars": 958,
"preview": "module.exports = {\n purge: [\"./components/**/*.js\", \"./pages/**/*.js\"],\n theme: {\n extend: {\n fontFamily: {\n "
}
]
About this extraction
This page contains the full source code of the ButterCMS/react-cms-blog-with-next-js GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 33 files (37.9 KB), approximately 9.8k tokens, and a symbol index with 54 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.