😍CMS Wordpress (ok)
https://github.com/vercel/next.js/tree/main/examples/cms-wordpress
Last updated
Was this helpful?
https://github.com/vercel/next.js/tree/main/examples/cms-wordpress
Last updated
Was this helpful?
tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./pages/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
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: [],
};
package.json
{
"private": true,
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@types/node": "^18.6.3",
"@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6",
"classnames": "^2.3.1",
"date-fns": "^2.28.0",
"next": "latest",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"typescript": "^4.7.4"
},
"devDependencies": {
"autoprefixer": "10.4.7",
"postcss": "8.4.31",
"tailwindcss": "^3.0.24"
}
}
next.config.js
if (!URL.canParse(process.env.WORDPRESS_API_URL)) {
throw new Error(`
Please provide a valid WordPress instance URL.
Add to your environment variables WORDPRESS_API_URL.
`);
}
const { protocol, hostname, port, pathname } = new URL(
process.env.WORDPRESS_API_URL,
);
/** @type {import('next').NextConfig} */
module.exports = {
reactStrictMode: true,
images: {
domains: [
'wpclidemo.dev',
"2.gravatar.com",
"0.gravatar.com",
"secure.gravatar.com",
],
remotePatterns: [
{
protocol: 'https',
hostname: 'wpclidemo.dev',
port: '',
pathname: '**',
},
],
},
};
.env.local
WORDPRESS_API_URL=https://wpclidemo.dev/graphql
NODE_TLS_REJECT_UNAUTHORIZED=0
# Only required if you want to enable preview mode
# WORDPRESS_AUTH_REFRESH_TOKEN=
# WORDPRESS_PREVIEW_SECRET=
styles\index.css
@tailwind base;
/* Write your own custom base styles here */
/* Start purging... */
@tailwind components;
/* Stop purging. */
/* Write you own custom component styles here */
/* Start purging... */
@tailwind utilities;
/* Stop purging. */
/* Your own custom utilities */
.has-text-align-left {
@apply text-left;
}
.has-text-align-center {
@apply text-center;
}
.has-text-align-right {
@apply text-right;
}
.has-large-font-size {
@apply text-4xl;
}
.alignfull {
@apply w-screen relative;
left: 50%;
margin-left: -50vw;
margin-right: -50vw;
max-width: 100vw;
right: 50%;
}
.wp-block-image img {
@apply max-w-full mt-2;
}
.wp-block-image.aligncenter {
@apply text-center;
}
.wp-block-image.alignfull img,
.wp-block-image.alignwide img {
@apply w-full;
}
.wp-block-image .alignleft,
.wp-block-image .alignright,
.wp-block-image .aligncenter,
.wp-block-image.is-resized {
@apply table ml-0 mr-0;
}
.wp-block-image .alignleft > figcaption,
.wp-block-image .alignright > figcaption,
.wp-block-image .aligncenter > figcaption,
.wp-block-image.is-resized > figcaption {
@apply table-caption;
caption-side: bottom;
}
.wp-block-image .alignleft {
@apply float-left mr-4;
}
.wp-block-image .alignright {
@apply float-right ml-4;
}
.wp-block-image .aligncenter {
@apply m-auto;
}
.wp-block-button a,
.wp-block-file a.wp-block-file__button {
@apply bg-blue-500 text-white no-underline py-2 px-4;
}
.wp-block-button a:hover,
.wp-block-file a.wp-block-file__button:hover {
@apply bg-blue-400 cursor-pointer;
}
.wp-block-file a:first-of-type {
@apply mr-4;
}
.wp-block-cover {
@apply flex flex-wrap justify-center items-center bg-cover bg-center overflow-hidden;
min-height: 430px;
}
.wp-block-verse {
@apply font-sans;
}
.wp-block-media-text {
@apply grid grid-cols-2 gap-4;
}
pages\index.tsx
import Head from "next/head";
import { GetStaticProps } from "next";
import Container from "../components/container";
import MoreStories from "../components/more-stories";
import HeroPost from "../components/hero-post";
import Intro from "../components/intro";
import Layout from "../components/layout";
import { getAllPostsForHome } from "../lib/api";
import { CMS_NAME } from "../lib/constants";
export default function Index({ allPosts: { edges }, preview }) {
const heroPost = edges[0]?.node;
const morePosts = edges.slice(1);
return (
<Layout preview={preview}>
<Head>
<title>{`Next.js Blog Example with ${CMS_NAME}`}</title>
</Head>
<Container>
<Intro />
{heroPost && (
<HeroPost
title={heroPost.title}
coverImage={heroPost.featuredImage}
date={heroPost.date}
author={heroPost.author}
slug={heroPost.slug}
excerpt={heroPost.excerpt}
/>
)}
{morePosts.length > 0 && <MoreStories posts={morePosts} />}
</Container>
</Layout>
);
}
export const getStaticProps: GetStaticProps = async ({ preview = false }) => {
const allPosts = await getAllPostsForHome(preview);
return {
props: { allPosts, preview },
revalidate: 10,
};
};
pages\_document.tsx
import { Html, Head, Main, NextScript } from "next/document";
export default function Document() {
return (
<Html lang="en">
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
pages\_app.tsx
import { AppProps } from "next/app";
import "../styles/index.css";
function MyApp({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />;
}
export default MyApp;
pages\posts\[slug].tsx
import { useRouter } from "next/router";
import ErrorPage from "next/error";
import Head from "next/head";
import { GetStaticPaths, GetStaticProps } from "next";
import Container from "../../components/container";
import PostBody from "../../components/post-body";
import MoreStories from "../../components/more-stories";
import Header from "../../components/header";
import PostHeader from "../../components/post-header";
import SectionSeparator from "../../components/section-separator";
import Layout from "../../components/layout";
import PostTitle from "../../components/post-title";
import Tags from "../../components/tags";
import { getAllPostsWithSlug, getPostAndMorePosts } from "../../lib/api";
import { CMS_NAME } from "../../lib/constants";
export default function Post({ post, posts, preview }) {
const router = useRouter();
const morePosts = posts?.edges;
if (!router.isFallback && !post?.slug) {
return <ErrorPage statusCode={404} />;
}
return (
<Layout preview={preview}>
<Container>
<Header />
{router.isFallback ? (
<PostTitle>Loading…</PostTitle>
) : (
<>
<article>
<Head>
<title>
{`${post.title} | Next.js Blog Example with ${CMS_NAME}`}
</title>
<meta
property="og:image"
content={post.featuredImage?.node.sourceUrl}
/>
</Head>
<PostHeader
title={post.title}
coverImage={post.featuredImage}
date={post.date}
author={post.author}
categories={post.categories}
/>
<PostBody content={post.content} />
<footer>
{post.tags.edges.length > 0 && <Tags tags={post.tags} />}
</footer>
</article>
<SectionSeparator />
{morePosts.length > 0 && <MoreStories posts={morePosts} />}
</>
)}
</Container>
</Layout>
);
}
export const getStaticProps: GetStaticProps = async ({
params,
preview = false,
previewData,
}) => {
const data = await getPostAndMorePosts(params?.slug, preview, previewData);
return {
props: {
preview,
post: data.post,
posts: data.posts,
},
revalidate: 10,
};
};
export const getStaticPaths: GetStaticPaths = async () => {
const allPosts = await getAllPostsWithSlug();
return {
paths: allPosts.edges.map(({ node }) => `/posts/${node.slug}`) || [],
fallback: true,
};
};
pages\api\preview.ts
import type { NextApiRequest, NextApiResponse } from "next";
import { getPreviewPost } from "../../lib/api";
export default async function preview(
req: NextApiRequest,
res: NextApiResponse,
) {
const { secret, id, slug } = req.query;
// Check the secret and next parameters
// This secret should only be known by this API route
if (
!process.env.WORDPRESS_PREVIEW_SECRET ||
secret !== process.env.WORDPRESS_PREVIEW_SECRET ||
(!id && !slug)
) {
return res.status(401).json({ message: "Invalid token" });
}
// Fetch WordPress to check if the provided `id` or `slug` exists
const post = await getPreviewPost(id || slug, id ? "DATABASE_ID" : "SLUG");
// If the post doesn't exist prevent preview mode from being enabled
if (!post) {
return res.status(401).json({ message: "Post not found" });
}
// Enable Preview Mode by setting the cookies
res.setPreviewData({
post: {
id: post.databaseId,
slug: post.slug,
status: post.status,
},
});
// Redirect to the path from the fetched post
// We don't redirect to `req.query.slug` as that might lead to open redirect vulnerabilities
res.writeHead(307, { Location: `/posts/${post.slug || post.databaseId}` });
res.end();
}
pages\api\exit-preview.ts
import { NextApiResponse } from "next";
export default async function exit(_, res: NextApiResponse) {
// Exit Draft Mode by removing the cookie
res.setDraftMode({ enable: false });
// Redirect the user back to the index page.
res.writeHead(307, { Location: "/" });
res.end();
}
lib\constants.ts
export const EXAMPLE_PATH = "cms-wordpress";
export const CMS_NAME = "WordPress";
export const CMS_URL = "https://wordpress.org";
export const HOME_OG_IMAGE_URL ="https://og-image.vercel.app/Next.js%20Blog%20Example%20with%20**WordPress**.png?theme=light&md=1&fontSize=75px&images=https%3A%2F%2Fassets.vercel.com%2Fimage%2Fupload%2Ffront%2Fassets%2Fdesign%2Fnextjs-black-logo.svg&images=data%3Aimage%2Fsvg%2Bxml%2C%253C%253Fxml+version%3D%271.0%27+encoding%3D%27UTF-8%27%253F%253E%253Csvg+preserveAspectRatio%3D%27xMidYMid%27+version%3D%271.1%27+viewBox%3D%270+0+256+255%27+xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%253E%253Cg+fill%3D%27%2523464342%27%253E%253Cpath+d%3D%27m18.124+127.5c0+43.295+25.161+80.711+61.646+98.441l-52.176-142.96c-6.0691+13.603-9.4699+28.657-9.4699+44.515zm183.22-5.5196c0-13.518-4.8557-22.88-9.0204-30.166-5.5446-9.01-10.742-16.64-10.742-25.65+0-10.055+7.6259-19.414+18.367-19.414+0.48494+0+0.94491+0.060358+1.4174+0.087415-19.46-17.828-45.387-28.714-73.863-28.714-38.213+0-71.832+19.606-91.39+49.302+2.5662+0.077008+4.9847+0.13112+7.039+0.13112+11.441+0+29.151-1.3882+29.151-1.3882+5.8963-0.34758+6.5915+8.3127+0.7014+9.01+0+0-5.9255+0.69724-12.519+1.0427l39.832+118.48+23.937-71.79-17.042-46.692c-5.8901-0.3455-11.47-1.0427-11.47-1.0427-5.8942-0.3455-5.2033-9.3575+0.69099-9.01+0+0+18.064+1.3882+28.811+1.3882+11.439+0+29.151-1.3882+29.151-1.3882+5.9005-0.34758+6.5936+8.3127+0.7014+9.01+0+0-5.938+0.69724-12.519+1.0427l39.528+117.58+10.91-36.458c4.7287-15.129+8.3273-25.995+8.3273-35.359zm-71.921+15.087l-32.818+95.363c9.7988+2.8805+20.162+4.4561+30.899+4.4561+12.738+0+24.953-2.202+36.323-6.2002-0.29346-0.46829-0.55987-0.96572-0.77841-1.5069l-33.625-92.112zm94.058-62.046c0.47037+3.4841+0.73678+7.2242+0.73678+11.247+0+11.1-2.073+23.577-8.3169+39.178l-33.411+96.599c32.518-18.963+54.391-54.193+54.391-94.545+0.002081-19.017-4.8557-36.899-13.399-52.48zm-95.977-75.023c-70.304+0-127.5+57.196-127.5+127.5+0+70.313+57.2+127.51+127.5+127.51+70.302+0+127.51-57.194+127.51-127.51-0.002082-70.304-57.209-127.5-127.51-127.5zm0+249.16c-67.08+0-121.66-54.578-121.66-121.66+0-67.08+54.576-121.65+121.66-121.65+67.078+0+121.65+54.574+121.65+121.65+0+67.084-54.574+121.66-121.65+121.66z%27%2F%253E%253C%2Fg%253E%253C%2Fsvg%253E";
lib\api.ts
const API_URL = process.env.WORDPRESS_API_URL;
async function fetchAPI(query = "", { variables }: Record<string, any> = {}) {
const headers = { "Content-Type": "application/json" };
if (process.env.WORDPRESS_AUTH_REFRESH_TOKEN) {
headers["Authorization"] =
`Bearer ${process.env.WORDPRESS_AUTH_REFRESH_TOKEN}`;
}
// WPGraphQL Plugin must be enabled
const res = await fetch(API_URL, {
headers,
method: "POST",
body: JSON.stringify({
query,
variables,
}),
});
const json = await res.json();
if (json.errors) {
console.error(json.errors);
throw new Error("Failed to fetch API");
}
return json.data;
}
export async function getPreviewPost(id, idType = "DATABASE_ID") {
const data = await fetchAPI(
`
query PreviewPost($id: ID!, $idType: PostIdType!) {
post(id: $id, idType: $idType) {
databaseId
slug
status
}
}`,
{
variables: { id, idType },
},
);
return data.post;
}
export async function getAllPostsWithSlug() {
const data = await fetchAPI(`
{
posts(first: 10000) {
edges {
node {
slug
}
}
}
}
`);
return data?.posts;
}
export async function getAllPostsForHome(preview) {
const data = await fetchAPI(
`
query AllPosts {
posts(first: 20, where: { orderby: { field: DATE, order: DESC } }) {
edges {
node {
title
excerpt
slug
date
featuredImage {
node {
sourceUrl
}
}
author {
node {
name
firstName
lastName
avatar {
url
}
}
}
}
}
}
}
`,
{
variables: {
onlyEnabled: !preview,
preview,
},
},
);
return data?.posts;
}
export async function getPostAndMorePosts(slug, preview, previewData) {
const postPreview = preview && previewData?.post;
// The slug may be the id of an unpublished post
const isId = Number.isInteger(Number(slug));
const isSamePost = isId
? Number(slug) === postPreview.id
: slug === postPreview.slug;
const isDraft = isSamePost && postPreview?.status === "draft";
const isRevision = isSamePost && postPreview?.status === "publish";
const data = await fetchAPI(
`
fragment AuthorFields on User {
name
firstName
lastName
avatar {
url
}
}
fragment PostFields on Post {
title
excerpt
slug
date
featuredImage {
node {
sourceUrl
}
}
author {
node {
...AuthorFields
}
}
categories {
edges {
node {
name
}
}
}
tags {
edges {
node {
name
}
}
}
}
query PostBySlug($id: ID!, $idType: PostIdType!) {
post(id: $id, idType: $idType) {
...PostFields
content
${
// Only some of the fields of a revision are considered as there are some inconsistencies
isRevision
? `
revisions(first: 1, where: { orderby: { field: MODIFIED, order: DESC } }) {
edges {
node {
title
excerpt
content
author {
node {
...AuthorFields
}
}
}
}
}
`
: ""
}
}
posts(first: 3, where: { orderby: { field: DATE, order: DESC } }) {
edges {
node {
...PostFields
}
}
}
}
`,
{
variables: {
id: isDraft ? postPreview.id : slug,
idType: isDraft ? "DATABASE_ID" : "SLUG",
},
},
);
// Draft posts may not have an slug
if (isDraft) data.post.slug = postPreview.id;
// Apply a revision (changes in a published post)
if (isRevision && data.post.revisions) {
const revision = data.post.revisions.edges[0]?.node;
if (revision) Object.assign(data.post, revision);
delete data.post.revisions;
}
// Filter out the main post
data.posts.edges = data.posts.edges.filter(({ node }) => node.slug !== slug);
// If there are still 3 posts, remove the last one
if (data.posts.edges.length > 2) data.posts.edges.pop();
return data;
}
components\tags.tsx
export default function Tags({ tags }) {
return (
<div className="max-w-2xl mx-auto">
<p className="mt-8 text-lg font-bold">
Tagged
{tags.edges.map((tag, index) => (
<span key={index} className="ml-4 font-normal">
{tag.node.name}
</span>
))}
</p>
</div>
);
}
components\section-separator.tsx
export default function SectionSeparator() {
return <hr className="border-accent-2 mt-28 mb-24" />;
}
components\post-title.tsx
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"
dangerouslySetInnerHTML={{ __html: children }}
/>
);
}
components\post-preview.tsx
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 title={title} coverImage={coverImage} slug={slug} />
)}
</div>
<h3 className="text-3xl mb-3 leading-snug">
<Link
href={`/posts/${slug}`}
className="hover:underline"
dangerouslySetInnerHTML={{ __html: title }}
></Link>
</h3>
<div className="text-lg mb-4">
<Date dateString={date} />
</div>
<div
className="text-lg leading-relaxed mb-4"
dangerouslySetInnerHTML={{ __html: excerpt }}
/>
<Avatar author={author} />
</div>
);
}
components\post-header.tsx
import Avatar from "./avatar";
import Date from "./date";
import CoverImage from "./cover-image";
import PostTitle from "./post-title";
import Categories from "./categories";
export default function PostHeader({
title,
coverImage,
date,
author,
categories,
}) {
return (
<>
<PostTitle>{title}</PostTitle>
<div className="hidden md:block md:mb-12">
<Avatar author={author} />
</div>
<div className="mb-8 md:mb-16 sm:mx-0">
<CoverImage title={title} coverImage={coverImage} />
</div>
<div className="max-w-2xl mx-auto">
<div className="block md:hidden mb-6">
<Avatar author={author} />
</div>
<div className="mb-6 text-lg">
Posted <Date dateString={date} />
<Categories categories={categories} />
</div>
</div>
</>
);
}
components\post-body.tsx
import styles from "./post-body.module.css";
export default function PostBody({ content }) {
return (
<div className="max-w-2xl mx-auto">
<div
className={styles.content}
dangerouslySetInnerHTML={{ __html: content }}
/>
</div>
);
}
components\post-body.module.css
.content {
@apply text-lg leading-relaxed;
}
.content p,
.content ul,
.content ol,
.content blockquote {
@apply my-6;
}
.content a {
@apply underline;
}
.content ul,
.content ol {
@apply pl-4;
}
.content ul {
@apply list-disc;
}
.content ol {
@apply list-decimal;
}
.content ul > li > ul,
.content ol > li > ol {
@apply my-0 ml-4;
}
.content ul > li > ul {
list-style: circle;
}
.content h2 {
@apply text-3xl mt-12 mb-4 leading-snug;
}
.content h3 {
@apply text-2xl mt-8 mb-4 leading-snug;
}
.content h4 {
@apply text-xl mt-6 mb-4 leading-snug;
}
.content pre {
@apply whitespace-pre overflow-x-auto p-4 text-sm leading-tight border border-gray-400 bg-gray-100;
}
.content code {
@apply text-sm;
}
.content figcaption {
@apply text-center text-sm;
}
.content blockquote {
@apply border-l-4 border-gray-500 bg-gray-200 italic ml-0 py-4 px-6;
}
.content blockquote p {
@apply mt-0;
}
.content blockquote cite {
@apply not-italic;
}
.content audio {
@apply w-full;
}
components\more-stories.tsx
import PostPreview from "./post-preview";
export default function MoreStories({ posts }) {
return (
<section>
<h2 className="mb-8 text-6xl md:text-7xl font-bold tracking-tighter leading-tight">
More Stories
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 md:gap-x-16 lg:gap-x-32 gap-y-20 md:gap-y-32 mb-32">
{posts.map(({ node }) => (
<PostPreview
key={node.slug}
title={node.title}
coverImage={node.featuredImage}
date={node.date}
author={node.author}
slug={node.slug}
excerpt={node.excerpt}
/>
))}
</div>
</section>
);
}
components\meta.tsx
import Head from "next/head";
import { CMS_NAME, HOME_OG_IMAGE_URL } from "../lib/constants";
export default function Meta() {
return (
<Head>
<link
rel="apple-touch-icon"
sizes="180x180"
href="/favicon/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/favicon/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/favicon/favicon-16x16.png"
/>
<link rel="manifest" href="/favicon/site.webmanifest" />
<link
rel="mask-icon"
href="/favicon/safari-pinned-tab.svg"
color="#000000"
/>
<link rel="shortcut icon" href="/favicon/favicon.ico" />
<meta name="msapplication-TileColor" content="#000000" />
<meta name="msapplication-config" content="/favicon/browserconfig.xml" />
<meta name="theme-color" content="#000" />
<link rel="alternate" type="application/rss+xml" href="/feed.xml" />
<meta
name="description"
content={`A statically generated blog example using Next.js and ${CMS_NAME}.`}
/>
<meta property="og:image" content={HOME_OG_IMAGE_URL} />
</Head>
);
}
components\layout.tsx
import Alert from "./alert";
import Footer from "./footer";
import Meta from "./meta";
export default function Layout({ preview, children }) {
return (
<>
<Meta />
<div className="min-h-screen">
<Alert preview={preview} />
<main>{children}</main>
</div>
<Footer />
</>
);
}
components\intro.tsx
import { CMS_NAME, CMS_URL } from "../lib/constants";
export default function Intro() {
return (
<section className="flex-col md:flex-row flex items-center md:justify-between mt-16 mb-16 md:mb-12">
<h1 className="text-6xl md:text-8xl font-bold tracking-tighter leading-tight md:pr-8">
Blog.
</h1>
<h4 className="text-center md:text-left text-lg mt-5 md:pl-8">
A statically generated blog example using{" "}
<a
href="https://nextjs.org/"
className="underline hover:text-success duration-200 transition-colors"
>
Next.js
</a>{" "}
and{" "}
<a
href={CMS_URL}
className="underline hover:text-success duration-200 transition-colors"
>
{CMS_NAME}
</a>
.
</h4>
</section>
);
}
components\hero-post.tsx
import Avatar from "./avatar";
import Date from "./date";
import CoverImage from "./cover-image";
import Link from "next/link";
export default function HeroPost({
title,
coverImage,
date,
excerpt,
author,
slug,
}) {
return (
<section>
<div className="mb-8 md:mb-16">
{coverImage && (
<CoverImage title={title} coverImage={coverImage} slug={slug} />
)}
</div>
<div className="md:grid md:grid-cols-2 md:gap-x-16 lg:gap-x-8 mb-20 md:mb-28">
<div>
<h3 className="mb-4 text-4xl lg:text-6xl leading-tight">
<Link
href={`/posts/${slug}`}
className="hover:underline"
dangerouslySetInnerHTML={{ __html: title }}
></Link>
</h3>
<div className="mb-4 md:mb-0 text-lg">
<Date dateString={date} />
</div>
</div>
<div>
<div
className="text-lg leading-relaxed mb-4"
dangerouslySetInnerHTML={{ __html: excerpt }}
/>
<Avatar author={author} />
</div>
</div>
</section>
);
}
components\header.tsx
import Link from "next/link";
export default function Header() {
return (
<h2 className="text-2xl md:text-4xl font-bold tracking-tight md:tracking-tighter leading-tight mb-20 mt-8">
<Link href="/" className="hover:underline">
Blog
</Link>
.
</h2>
);
}
components\footer.tsx
import Container from "./container";
import { EXAMPLE_PATH } from "../lib/constants";
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.
</h3>
<div className="flex flex-col lg:flex-row justify-center items-center lg:pl-4 lg:w-1/2">
<a
href="https://nextjs.org/docs/basic-features/pages"
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/vercel/next.js/tree/canary/examples/${EXAMPLE_PATH}`}
className="mx-3 font-bold hover:underline"
>
View on GitHub
</a>
</div>
</div>
</Container>
</footer>
);
}
components\date.tsx
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>;
}
components\cover-image.tsx
import cn from "classnames";
import Image from "next/image";
import Link from "next/link";
interface Props {
title: string;
coverImage: {
node: {
sourceUrl: string;
};
};
slug?: string;
}
export default function CoverImage({ title, coverImage, slug }: Props) {
const image = (
<Image
width={2000}
height={1000}
alt={`Cover Image for ${title}`}
src={coverImage?.node.sourceUrl}
className={cn("shadow-small", {
"hover:shadow-medium transition-shadow duration-200": slug,
})}
/>
);
return (
<div className="sm:mx-0">
{slug ? (
<Link href={`/posts/${slug}`} aria-label={title}>
{image}
</Link>
) : (
image
)}
</div>
);
}
components\container.tsx
export default function Container({ children }) {
return <div className="container mx-auto px-5">{children}</div>;
}
components\categories.tsx
export default function Categories({ categories }) {
return (
<span className="ml-1">
under
{categories.edges.length > 0 ? (
categories.edges.map((category, index) => (
<span key={index} className="ml-1">
{category.node.name}
</span>
))
) : (
<span className="ml-1">{categories.edges.node.name}</span>
)}
</span>
);
}
components\avatar.tsx
import Image from "next/image";
export default function Avatar({ author }) {
const isAuthorHaveFullName =
author?.node?.firstName && author?.node?.lastName;
const name = isAuthorHaveFullName
? `${author.node.firstName} ${author.node.lastName}`
: author.node.name || null;
return (
<div className="flex items-center">
<div className="w-12 h-12 relative mr-4">
<Image
src={author.node.avatar.url}
layout="fill"
className="rounded-full"
alt={name}
/>
</div>
<div className="text-xl font-bold">{name}</div>
</div>
);
}
components\alert.tsx
import Container from "./container";
import cn from "classnames";
import { EXAMPLE_PATH } from "../lib/constants";
export default function Alert({ preview }) {
return (
<div
className={cn("border-b", {
"bg-accent-7 border-accent-7 text-white": preview,
"bg-accent-1 border-accent-2": !preview,
})}
>
<Container>
<div className="py-2 text-center text-sm">
{preview ? (
<>
This is a page preview.{" "}
<a
href="/api/exit-preview"
className="underline hover:text-cyan duration-200 transition-colors"
>
Click here
</a>{" "}
to exit preview mode.
</>
) : (
<>
The source code for this blog is{" "}
<a
href={`https://github.com/vercel/next.js/tree/canary/examples/${EXAMPLE_PATH}`}
className="underline hover:text-success duration-200 transition-colors"
>
available on GitHub
</a>
.
</>
)}
</div>
</Container>
</div>
);
}