first commit
Some checks failed
Test examples / Test Examples (20) (push) Has been cancelled
Test examples / Test Examples (22) (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Trigger Release / start (push) Has been cancelled
Stale issue handler / stale (push) Has been cancelled
Update Font Data / create-pull-request (push) Has been cancelled
build-and-deploy / deploy-target (push) Has been cancelled
build-and-deploy / build (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / build-wasm (nodejs) (push) Has been cancelled
build-and-deploy / build-wasm (web) (push) Has been cancelled
build-and-deploy / Deploy preview tarball (push) Has been cancelled
build-and-deploy / Potentially publish release (push) Has been cancelled
build-and-deploy / publish-turbopack-npm-packages (push) Has been cancelled
build-and-deploy / Deploy examples (push) Has been cancelled
build-and-deploy / thank you, build (push) Has been cancelled
build-and-deploy / Upload Turbopack Bytesize metrics to Datadog (push) Has been cancelled
Rspack Next.js development integration tests / Rspack integration tests (push) Has been cancelled
Rspack Next.js production integration tests / Rspack integration tests (push) Has been cancelled
Turbopack Next.js development integration tests / Next.js integration tests (push) Has been cancelled
Turbopack Next.js production integration tests / Next.js integration tests (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack development test manifest (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack production test manifest (push) Has been cancelled
Upload bundler test manifests to areweturboyet.com / Upload test results (push) Has been cancelled
Update React / create-pull-request (push) Has been cancelled
test-e2e-project-reset-cron / reset-test-project (push) Has been cancelled
Notify about the top 15 issues/PRs/feature requests (most reacted) in the last 90 days / run (push) Has been cancelled

This commit is contained in:
Arian Tron
2026-03-10 19:37:31 +03:30
commit 61f56f997c
27684 changed files with 2784175 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
NEXT_PUBLIC_DOTCMS_HOST=""
NEXT_PREVIEW_SECRET_TOKEN=""
DOTCMS_PREVIEW_SECRET=""

40
examples/cms-dotcms/.gitignore vendored Normal file
View File

@@ -0,0 +1,40 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

View File

@@ -0,0 +1,73 @@
# A statically generated blog example using Next.js and dotCMS
This example showcases Next.js's [Static Generation](https://nextjs.org/docs/basic-features/pages) feature using [dotCMS](https://dotcms.com/) as the data source.
## Demo
### [https://nextjs-dotcms-blog.vercel.app/](https://nextjs-dotcms-blog.vercel.app/)
## Deploy your own
Using the Deploy Button below, you'll deploy the Next.js project.
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FdotCMS%2Fnext.js%2Ftree%2Fcanary%2Fexamples%2Fcms-dotcms&project-name=nextjs-dotcms-blog&repository-name=nextjs-dotcms-blog&demo-title=Next.js+Blog&demo-description=Static+blog+with+multiple+authors+using+Preview+Mode&demo-url=https%3A%2F%2Fnext-blog-dotcms.vercel.app%2F)
### Related examples
- [AgilityCMS](/examples/cms-agilitycms)
- [Builder.io](/examples/cms-builder-io)
- [ButterCMS](/examples/cms-buttercms)
- [Contentful](/examples/cms-contentful)
- [Cosmic](/examples/cms-cosmic)
- [DatoCMS](/examples/cms-datocms)
- [DotCMS](/examples/cms-dotcms)
- [Drupal](/examples/cms-drupal)
- [Enterspeed](/examples/cms-enterspeed)
- [Ghost](/examples/cms-ghost)
- [GraphCMS](/examples/cms-graphcms)
- [Kontent.ai](/examples/cms-kontent-ai)
- [MakeSwift](/examples/cms-makeswift)
- [Payload](/examples/cms-payload)
- [Plasmic](/examples/cms-plasmic)
- [Prepr](/examples/cms-prepr)
- [Prismic](/examples/cms-prismic)
- [Sanity](/examples/cms-sanity)
- [Sitecore XM Cloud](/examples/cms-sitecore-xmcloud)
- [Sitefinity](/examples/cms-sitefinity)
- [Storyblok](/examples/cms-storyblok)
- [TakeShape](/examples/cms-takeshape)
- [Tina](/examples/cms-tina)
- [Umbraco](/examples/cms-umbraco)
- [Umbraco heartcore](/examples/cms-umbraco-heartcore)
- [Webiny](/examples/cms-webiny)
- [WordPress](/examples/cms-wordpress)
- [Blog Starter](/examples/blog-starter)
## How to use
Rename `.env.local.example` to `.env.local` and complete the variables:
`NEXT_PUBLIC_DOTCMS_HOST` is the dotCMS host, you can use `https://demo.dotcms.com`
`DOTCMS_TOKEN` for the demo site, you can generate the token using:
```
curl -H "Content-Type:application/json" --insecure -X POST -d '
{ "user":"admin@dotcms.com", "password":"admin", "expirationDays": 10 }
' http://demo.dotcms.com:8080/api/v1/authentication/api-token
```
Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init), [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/), or [pnpm](https://pnpm.io) to bootstrap the example:
```bash
npx create-next-app --example cms-dotcms cms-dotcms-app
```
```bash
yarn create next-app --example cms-dotcms cms-dotcms-app
```
```bash
pnpm create next-app --example cms-dotcms cms-dotcms-app
```
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fnext.js%2Ftree%2Fcanary%2Fexamples%2Fcms-dotcms&project-name=nextjs-dotcms-blog&repository-name=nextjs-dotcms-blog&demo-title=Next.js+Blog&demo-description=Static+blog+with+multiple+authors+using+Preview+Mode&demo-url=https%3A%2F%2Fnext-blog-dotcms.vercel.app%2F)

View File

@@ -0,0 +1,42 @@
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 page is a 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>
);
}

View File

@@ -0,0 +1,19 @@
import DotCmsImage from "./dotcms-image";
export default function Avatar({ name, picture }) {
return (
<div className="flex items-center ">
<div className="w-12 h-12 relative mr-4">
{picture?.idPath ? (
<DotCmsImage
src={picture?.idPath}
layout="fill"
className="rounded-full"
alt={name}
/>
) : null}
</div>
<div className="text-xl font-bold">{name}</div>
</div>
);
}

View File

@@ -0,0 +1,98 @@
import cn from "classnames";
import DotCmsImage from "./dotcms-image";
import Link from "next/link";
export const Bold = ({ children }) => <strong>{children}</strong>;
export const Italic = ({ children }) => <em>{children}</em>;
export const Strike = ({ children }) => <s>{children}</s>;
export const Underline = ({ children }) => <u>{children}</u>;
export const DotLink = ({ attrs: { href, target }, children }) => {
const regEx = /https?:\/\//;
return regEx.test(href) ? (
<a href={href} rel="noopener noreferrer" target="_blank">
{children}
</a>
) : (
<Link href={href} target={target || "_self"}>
{children}
</Link>
);
};
const nodeMarks = {
link: DotLink,
bold: Bold,
underline: Underline,
italic: Italic,
strike: Strike,
};
export const TextNode = (props) => {
const { marks = [], text } = props;
const mark = marks[0] || { type: "", attrs: {} };
const newProps = { ...props, marks: marks.slice(1) };
const Component = nodeMarks[mark?.type];
if (!Component) {
return text;
}
return (
<Component attrs={mark.attrs}>
<TextNode {...newProps} />
</Component>
);
};
export const DotImage = ({ attrs: { textAlign, data } }) => {
const { asset, title } = data;
const [imgTitle] = title.split(".");
return (
<DotCmsImage
objectFit="cover"
style={{ textAlign: textAlign }}
width="800"
height="400"
alt={`Cover Image for ${title}`}
className={cn("shadow-small", {
"hover:shadow-medium transition-shadow duration-200": imgTitle,
})}
src={asset}
/>
);
};
export const ListItem = ({ children }) => {
return <li>{children}</li>;
};
export const OrderedList = ({ children }) => {
return <ol>{children}</ol>;
};
export const Paragraph = ({ children }) => {
return <p>{children}</p>;
};
export const BulletList = ({ children }) => {
return <ul>{children}</ul>;
};
export const Heading = ({ level, children }) => {
const Tag = `h${level}` as keyof JSX.IntrinsicElements;
return <Tag>{children}</Tag>;
};
export const BlockQuote = ({ children }) => {
return <blockquote>{children}</blockquote>;
};
export const CodeBlock = ({ language, children }) => {
return (
<pre data-language={language}>
<code>{children}</code>
</pre>
);
};

View File

@@ -0,0 +1,3 @@
export default function Container({ children }) {
return <div className="container mx-auto px-5">{children}</div>;
}

View File

@@ -0,0 +1,90 @@
import {
BlockQuote,
BulletList,
CodeBlock,
DotImage,
Heading,
ListItem,
OrderedList,
Paragraph,
TextNode,
} from "./blocks";
/*
dotCMS Block Editor is a new rich content editor that allows you to create your content as building blocks.
More info: https://dotcms.com/docs/latest/block-editor
*/
export const ContentBlocks = ({ content }) => {
return (
<>
{content?.map((data, index) => {
switch (data.type) {
case "paragraph":
return (
<Paragraph key={index}>
<ContentBlocks content={data.content} />
</Paragraph>
);
case "heading":
return (
<Heading key={index} level={data.attrs.level}>
<ContentBlocks content={data.content} />
</Heading>
);
case "bulletList":
return (
<BulletList key={index}>
<ContentBlocks content={data.content} />
</BulletList>
);
case "orderedList":
return (
<OrderedList key={index}>
<ContentBlocks content={data.content} />
</OrderedList>
);
case "dotImage":
return <DotImage key={index} {...data} />;
case "horizontalRule":
return <hr key={index} />;
case "blockquote":
return (
<BlockQuote key={index}>
<ContentBlocks content={data.content} />
</BlockQuote>
);
case "codeBlock":
return (
<CodeBlock language={data.attrs.language} key={index}>
<ContentBlocks content={data.content} />
</CodeBlock>
);
case "hardBreak":
return <br key={index} />;
case "text":
return <TextNode key={index} {...data} />;
case "listItem":
return (
<ListItem key={index}>
<ContentBlocks content={data.content} />
</ListItem>
);
default:
return <p>Block not supported</p>;
}
})}
</>
);
};

View File

@@ -0,0 +1,27 @@
import DotCmsImage from "./dotcms-image";
import Link from "next/link";
import cn from "classnames";
export default function CoverImage(props) {
const image = (
<DotCmsImage
{...props}
alt={`Cover Image for ${props.title}`}
className={cn("shadow-small", {
"hover:shadow-medium transition-shadow duration-200": props.slug,
})}
/>
);
return (
<div className="-mx-5 sm:mx-0">
{props.slug ? (
<Link href={`/posts/${props.slug}`} aria-label={props.title}>
{image}
</Link>
) : (
image
)}
</div>
);
}

View File

@@ -0,0 +1,9 @@
import { format } from "date-fns";
export default function DateComponent({ dateString }) {
return (
<time dateTime={dateString}>
{format(new Date(dateString), "LLLL d, yyyy")}
</time>
);
}

View File

@@ -0,0 +1,31 @@
import Image from "next/image";
const DEFAULT_QUALITY = 20;
// https://dotcms.com/docs/latest/image-resizing-and-processing
const getUrlWithResizingParameters = ({
src,
width,
quality = DEFAULT_QUALITY,
}) => {
const urlParams = [];
const lastSeparatorIdx = src.lastIndexOf("/");
const imageIdentifierAndField = src.slice(0, lastSeparatorIdx);
urlParams.push(imageIdentifierAndField);
urlParams.push(width + "w");
urlParams.push(quality + "q");
return urlParams.join("/");
};
const dotCmsLoader = (props) => {
return `${process.env.NEXT_PUBLIC_DOTCMS_HOST}${getUrlWithResizingParameters(
props,
)}`;
};
const DotCmsImage = (params) => {
return <Image {...params} loader={dotCmsLoader} />;
};
export default DotCmsImage;

View File

@@ -0,0 +1,30 @@
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>
);
}

View File

@@ -0,0 +1,12 @@
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>
);
}

View File

@@ -0,0 +1,59 @@
import Link from "next/link";
import Avatar from "@components/avatar";
import DateComponent from "@components/date";
import CoverImage from "@components/cover-image";
import cn from "classnames";
export default function HeroPost({
title,
coverImage,
date,
excerpt,
author,
slug,
}) {
return (
<section>
<div className="mb-8 md:mb-16">
<CoverImage
width={2000}
height={1000}
title={title}
slug={slug}
objectFit="cover"
layout={"intrinsic"}
src={coverImage.idPath}
alt={`Cover Image for ${title}`}
className={cn("shadow-small", {
"hover:shadow-medium transition-shadow duration-200": slug,
})}
/>
</div>
<div className="md:grid md:grid-cols-2 md:gap-16 lg:col-gap-8 mb-20 md:mb-28">
<div>
<h3 className="mb-4 text-4xl leading-tight lg:text-6xl">
<Link
as={`/posts/${slug}`}
href="/posts/[slug]"
className="hover:underline"
>
{title}
</Link>
</h3>
<div className="mb-4 text-lg md:mb-0">
<DateComponent dateString={date} />
</div>
</div>
<div>
<p className="mb-4 text-lg leading-relaxed">{excerpt}</p>
{author.length ? (
<Avatar
name={`${author[0].firstName} ${author[0].lastName}`}
picture={author[0].profilePhoto}
/>
) : null}
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,28 @@
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>
);
}

View File

@@ -0,0 +1,16 @@
import Alert from "@components/alert";
import Footer from "@components/footer";
import Meta from "@components/meta";
export default function Layout({ preview, children }) {
return (
<>
<Meta />
<div className="min-h-screen">
<Alert preview={preview} />
<main>{children}</main>
</div>
<Footer />
</>
);
}

View File

@@ -0,0 +1,42 @@
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>
);
}

View File

@@ -0,0 +1,24 @@
import PostPreview from "@components/post-preview";
export default function MoreStories({ posts }) {
return (
<section>
<h2 className="mb-8 text-6xl font-bold leading-tight tracking-tighter md:text-7xl">
More Stories
</h2>
<div className="grid grid-cols-1 gap-20 mb-32 md:grid-cols-2 md:gap-y-16 lg:gap-y-32 md:gap-x-32">
{posts.map((post) => (
<PostPreview
key={post.urlTitle}
title={post.title}
coverImage={post.image}
date={post.postingDate}
author={post.author}
slug={post.urlTitle}
excerpt={post.teaser}
/>
))}
</div>
</section>
);
}

View File

@@ -0,0 +1,26 @@
import { ContentBlocks } from "./content-blocks";
import DateComponent from "./date";
import Avatar from "./avatar";
export default function PostBody({ content }) {
return (
<div className="prose lg:prose-xl mx-auto max-w-2xl">
<div className="mb-6 block md:hidden">
{content.author.length ? (
<Avatar
name={`${content.author[0].firstName} ${content.author[0].lastName}`}
picture={content.author[0].profilePhoto}
/>
) : null}
</div>
<div className="mb-6 text-lg">
{content.postingDate !== "now" ? (
<div className="mb-6 text-lg">
Posted <DateComponent dateString={content.postingDate} />
</div>
) : null}
</div>
<ContentBlocks content={content.blogContent.json.content} />
</div>
);
}

View File

@@ -0,0 +1,29 @@
import Avatar from "@components/avatar";
import CoverImage from "@components/cover-image";
import PostTitle from "@components/post-title";
export default function PostHeader({ title, coverImage, author }) {
return (
<>
<PostTitle>{title}</PostTitle>
<div className="hidden md:block md:mb-12">
{author.length ? (
<Avatar
name={`${author[0].firstName} ${author[0].lastName}`}
picture={author[0].profilePhoto}
/>
) : null}
</div>
<div className="mb-8 md:mb-16 sm:mx-0">
<CoverImage
title={title}
width={2000}
height={1000}
src={coverImage.idPath}
objectFit="cover"
layout={"intrinsic"}
/>
</div>
</>
);
}

View File

@@ -0,0 +1,46 @@
import Link from "next/link";
import Avatar from "@components/avatar";
import DateComponent from "@components/date";
import CoverImage from "./cover-image";
export default function PostPreview({
title,
coverImage,
date,
excerpt,
author,
slug,
}) {
return (
<div>
<div className="mb-5">
<CoverImage
width={1200}
height={600}
title={title}
slug={slug}
src={coverImage.idPath}
objectFit="cover"
layout={"intrinsic"}
/>
</div>
<h3 className="mb-3 text-3xl leading-snug">
<Link href={`/posts/${slug}`} className="hover:underline">
{title}
</Link>
</h3>
{date !== "now" ? (
<div className="mb-4 text-lg">
<DateComponent dateString={date} />
</div>
) : null}
<p className="mb-4 text-lg leading-relaxed">{excerpt}</p>
{author.length ? (
<Avatar
name={`${author[0].firstName} ${author[0].lastName}`}
picture={author[0].profilePhoto}
/>
) : null}
</div>
);
}

View File

@@ -0,0 +1,7 @@
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>
);
}

View File

@@ -0,0 +1,3 @@
export default function SectionSeparator() {
return <hr className="border-accent-2 mt-28 mb-24" />;
}

View File

@@ -0,0 +1,176 @@
/**
* A helper for the GraphQL API.
*
* @param {String} query - The query to fetch for
* @param {Object} param1.variables - The variables to pass to the query
* @param {Object} param1.preview - Indicate if the query should be previewed
* @returns {Promise} - A promise that resolves to the result of the query
*/
async function fetchAPI(query, { variables } = { variables: null }) {
const res = await fetch(
process.env.NEXT_PUBLIC_DOTCMS_HOST + "/api/v1/graphql",
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.DOTCMS_TOKEN}`,
},
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;
}
/**
* Get the correct type to filter post using preview flag
*
* @param preview
* @returns {string}
*/
const showPreviewPosts = (preview) => {
return preview === true
? "+working:true +deleted:false"
: "+live:true +deleted:false";
};
/**
* Fetch a single post and more posts
*
* @param {String} slug - The slug of the post to fetch
* @param {boolean} preview - Whether or not to fetch the live post
* @returns An object with a post and more posts array
*/
export async function getPostAndMorePosts(slug, preview) {
const data = await fetchAPI(
`
query PostBySlug($query: String!, $morePostsQuery: String!) {
post: BlogCollection(query: $query, limit: 1) {
title
urlTitle
blogContent {
json
}
postingDate
image {
idPath
}
author {
firstName
lastName
profilePhoto {
idPath
}
}
}
morePosts: BlogCollection(query: $morePostsQuery, limit: 2) {
title
urlTitle
teaser
postingDate
image {
idPath
}
author {
firstName
lastName
profilePhoto {
idPath
}
}
}
}
`,
{
variables: {
query: `+urlmap:/blog/post/${slug} ${showPreviewPosts(preview)}`,
morePostsQuery: `-urlmap:/blog/post/${slug} ${showPreviewPosts(
preview,
)}`,
},
},
);
return {
post: data?.post[0] ?? {},
morePosts: data?.morePosts ?? [],
};
}
/**
* Fetch one post and more post with preview mode flag.
*
* @param slug
* @param isPreview
* @returns {Promise<{post, morePosts}>}
*/
export async function getPreviewPostBySlug(slug, isPreview) {
return await getPostAndMorePosts(slug, isPreview);
}
/**
* Fetch all posts with slug
*
* @returns An array of posts with the following shape:
* {
* urlTitle: string
* }
*/
export async function getAllPostsWithSlug() {
const entries = await fetchAPI(`
query getAllPostsWithSlug {
BlogCollection(query: "+live:true +deleted:false") {
urlTitle
}
}
`);
return entries?.BlogCollection ?? [];
}
/**
* Fetch all posts
*
* @param {boolean} preview - If true, return a preview of the post
* @returns An array of posts
*/
export async function getAllPostsForHome(preview) {
const entries = await fetchAPI(
`
query getAllPostsForHome($query: String!) {
BlogCollection(query: $query) {
title
teaser
postingDate
author {
firstName
lastName
profilePhoto {
idPath
}
}
urlTitle
image {
idPath
}
}
}
`,
{
variables: {
query: `${showPreviewPosts(preview)}`,
},
},
);
return entries?.BlogCollection ?? [];
}

View File

@@ -0,0 +1,5 @@
export const EXAMPLE_PATH = "cms-dotcms";
export const CMS_NAME = "dotCMS";
export const CMS_URL = "https://dotcms.com/";
export const HOME_OG_IMAGE_URL =
"https://og-image.vercel.app/Next.js%20Blog%20Example%20with%20**dotCMS**.png?theme=light&md=1&fontSize=100px&images=https%3A%2F%2Fassets.vercel.com%2Fimage%2Fupload%2Ffront%2Fassets%2Fdesign%2Fnextjs-black-logo.svg&images=https%3A%2F%2Fcdn.dotcms.com%2FdA%2F99fe3769-d649%2F256w%2Fdotcms.png&widths=undefined&widths=350&heights=undefined&heights=auto";

View File

@@ -0,0 +1,15 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
async rewrites() {
const baseUrl = process.env.NEXT_PUBLIC_DOTCMS_HOST;
return [
{
source: "/images/:slug*",
destination: `${baseUrl}/images/:slug*`,
},
];
},
reactStrictMode: true,
};
module.exports = nextConfig;

View File

@@ -0,0 +1,25 @@
{
"private": true,
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
},
"dependencies": {
"@tailwindcss/typography": "^0.5.7",
"classnames": "^2.3.2",
"date-fns": "^2.29.3",
"next": "latest",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tailwindcss": "^3.1.8"
},
"devDependencies": {
"@types/node": "^18.7.18",
"@types/react": "^18.0.21",
"@types/react-dom": "^18.0.6",
"autoprefixer": "^10.4.12",
"postcss": "^8.4.16",
"typescript": "^4.8.3"
}
}

View File

@@ -0,0 +1,7 @@
import "@styles/index.css";
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />;
}
export default MyApp;

View File

@@ -0,0 +1,8 @@
export default async function exit(_, res) {
// 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();
}

View File

@@ -0,0 +1,31 @@
import { getPreviewPostBySlug } from "@lib/api";
export default async function preview(req, res) {
const { secret, slug } = req.query;
if (secret !== process.env.DOTCMS_PREVIEW_SECRET || !slug) {
return res.status(401).json({ message: "Invalid token" });
}
// Fetch the headless CMS to check if the provided `slug` exists
const post = await getPreviewPostBySlug(slug, true);
// If the slug doesn't exist prevent preview mode from being enabled
if (Object.keys(post.post).length < 1) {
return res.status(401).json({ message: "Invalid slug" });
}
// Enable Draft Mode by setting the cookie
res.setDraftMode({ enable: true });
// Redirect to the path from the fetched post
const url = `/posts/${post.post.urlTitle}`;
res.setHeader("Content-Type", "text/html");
res.write(
`<!DOCTYPE html><html><head><meta http-equiv="Refresh" content="0; url=${url}" />
<script>window.location.href = '${url}'</script>
</head>
</html>`,
);
res.end();
}

View File

@@ -0,0 +1,45 @@
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 Head from "next/head";
import { CMS_NAME } from "@lib/constants";
export default function Index({ preview, allPosts }) {
const heroPost = allPosts[0];
const morePosts = allPosts.slice(1);
const title = `Next.js Blog Example with ${CMS_NAME}`;
return (
<>
<Layout preview={preview}>
<Head>
<title>{title}</title>
</Head>
<Container>
<Intro />
{heroPost && (
<HeroPost
title={heroPost.title}
coverImage={heroPost.image}
date={heroPost.postingDate}
author={heroPost.author}
slug={heroPost.urlTitle}
excerpt={heroPost.teaser}
/>
)}
{morePosts.length > 0 && <MoreStories posts={morePosts} />}
</Container>
</Layout>
</>
);
}
export async function getStaticProps({ preview = false }) {
const allPosts = await getAllPostsForHome(preview);
return {
props: { preview, allPosts },
};
}

View File

@@ -0,0 +1,82 @@
import { useRouter } from "next/router";
import Head from "next/head";
import ErrorPage from "next/error";
import Container from "@components/container";
import MoreStories from "@components/more-stories";
import Header from "@components/header";
import PostHeader from "@components/post-header";
import PostBody from "@components/post-body";
import SectionSeparator from "@components/section-separator";
import Layout from "@components/layout";
import PostTitle from "@components/post-title";
import { CMS_NAME } from "@lib/constants";
import { getAllPostsWithSlug, getPostAndMorePosts } from "@lib/api";
export default function Post({ post, morePosts, preview }) {
const router = useRouter();
if (!router.isFallback && !post) {
return <ErrorPage statusCode={404} />;
}
const title = `${
post?.title || "dotcms"
} | Next.js Blog Example with ${CMS_NAME}`;
return (
<Layout preview={preview}>
<Container>
<Header />
{router.isFallback ? (
<PostTitle>Loading</PostTitle>
) : (
<>
<article>
<Head>
<title>{title}</title>
<meta
property="og:image"
content={`${process.env.NEXT_PUBLIC_DOTCMS_HOST}${post.image.idPath}`}
/>
</Head>
<PostHeader
title={post.title}
coverImage={post.image}
author={post.author}
/>
<PostBody content={post} />
</article>
<SectionSeparator />
{morePosts && morePosts.length > 0 && (
<MoreStories posts={morePosts} />
)}
</>
)}
</Container>
</Layout>
);
}
export async function getStaticProps({ params, preview = false }) {
const data = await getPostAndMorePosts(params.slug, preview);
return {
props: {
preview,
...data,
},
};
}
export async function getStaticPaths() {
const allPosts = await getAllPostsWithSlug();
return {
paths: allPosts?.map((post) => `/posts/${post.urlTitle}`) || [],
fallback: true,
};
}

View File

@@ -0,0 +1,8 @@
// If you want to use other PostCSS plugins, see the following:
// https://tailwindcss.com/docs/using-with-preprocessors
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/favicons/mstile-150x150.png"/>
<TileColor>#000000</TileColor>
</tile>
</msapplication>
</browserconfig>

Binary file not shown.

After

Width:  |  Height:  |  Size: 595 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 880 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="1024.000000pt" height="1024.000000pt" viewBox="0 0 1024.000000 1024.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.11, written by Peter Selinger 2001-2013
</metadata>
<g transform="translate(0.000000,1024.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M4785 10234 c-22 -2 -92 -9 -155 -14 -1453 -131 -2814 -915 -3676
-2120 -480 -670 -787 -1430 -903 -2235 -41 -281 -46 -364 -46 -745 0 -381 5
-464 46 -745 278 -1921 1645 -3535 3499 -4133 332 -107 682 -180 1080 -224
155 -17 825 -17 980 0 687 76 1269 246 1843 539 88 45 105 57 93 67 -8 6 -383
509 -833 1117 l-818 1105 -1025 1517 c-564 834 -1028 1516 -1032 1516 -4 1 -8
-673 -10 -1496 -3 -1441 -4 -1499 -22 -1533 -26 -49 -46 -69 -88 -91 -32 -16
-60 -19 -211 -19 l-173 0 -46 29 c-30 19 -52 44 -67 73 l-21 45 2 2005 3 2006
31 39 c16 21 50 48 74 61 41 20 57 22 230 22 204 0 238 -8 291 -66 15 -16 570
-852 1234 -1859 664 -1007 1572 -2382 2018 -3057 l810 -1227 41 27 c363 236
747 572 1051 922 647 743 1064 1649 1204 2615 41 281 46 364 46 745 0 381 -5
464 -46 745 -278 1921 -1645 3535 -3499 4133 -327 106 -675 179 -1065 223 -96
10 -757 21 -840 13z m2094 -3094 c48 -24 87 -70 101 -118 8 -26 10 -582 8
-1835 l-3 -1798 -317 486 -318 486 0 1307 c0 845 4 1320 10 1343 16 56 51 100
99 126 41 21 56 23 213 23 148 0 174 -2 207 -20z"/>
<path d="M7843 789 c-35 -22 -46 -37 -15 -20 22 13 58 40 52 41 -3 0 -20 -10
-37 -21z"/>
<path d="M7774 744 c-18 -14 -18 -15 4 -4 12 6 22 13 22 15 0 8 -5 6 -26 -11z"/>
<path d="M7724 714 c-18 -14 -18 -15 4 -4 12 6 22 13 22 15 0 8 -5 6 -26 -11z"/>
<path d="M7674 684 c-18 -14 -18 -15 4 -4 12 6 22 13 22 15 0 8 -5 6 -26 -11z"/>
<path d="M7598 644 c-38 -20 -36 -28 2 -9 17 9 30 18 30 20 0 7 -1 6 -32 -11z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,19 @@
{
"name": "Next.js",
"short_name": "Next.js",
"icons": [
{
"src": "/favicons/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/favicons/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#000000",
"background_color": "#000000",
"display": "standalone"
}

View File

@@ -0,0 +1,5 @@
/* purgecss start ignore */
@tailwind base;
@tailwind components;
/* purgecss end ignore */
@tailwind utilities;

View File

@@ -0,0 +1,48 @@
module.exports = {
content: [
"./pages/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
container: {
screens: {
sm: "640px",
md: "768px",
lg: "1024px",
xl: "1280px",
},
},
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)",
},
},
},
variants: {
extend: {},
},
plugins: [require("@tailwindcss/typography")],
};

View File

@@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"baseUrl": ".",
"paths": {
"@components/*": ["components/*"],
"@lib/*": ["lib/*"],
"@styles/*": ["styles/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}