Added Agility CMS example 🏆 (#12788)

This commit is contained in:
James Vidler 2020-06-01 21:49:04 -04:00 committed by GitHub
parent 65f0574c16
commit 7c203b80c9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
55 changed files with 1821 additions and 0 deletions

View file

@ -0,0 +1,4 @@
NEXT_EXAMPLE_CMS_AGILITY_GUID=
NEXT_EXAMPLE_CMS_AGILITY_API_FETCH_KEY=
NEXT_EXAMPLE_CMS_AGILITY_API_PREVIEW_KEY=
NEXT_EXAMPLE_CMS_AGILITY_SECURITY_KEY=

2
examples/cms-agilitycms/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
.env*.local
.vercel

View file

@ -0,0 +1,325 @@
# A statically generated blog example using Next.js and Agility CMS
This example showcases Next.js's [Static Generation](https://nextjs.org/docs/basic-features/pages) feature using [Agility CMS](https://www.agilitycms.com) as the data source.
> `IMPORTANT` - This example uses Agility CMS's [**Page Management**](https://agilitycms.com/resources/posts/page-management-in-agility-cms-vs-other-headless-cmss) features. This means that the CMS ultimately drives what pages are available and what content is on each page. This enables **Editors** to focus on managing their pages, while allowing you, (the **Developer**) to focus on building UI components for the editors to compose their pages.
## Demo
- **Live**: [https://next-blog-agilitycms.now.sh/](https://next-blog-agilitycms.now.sh/)
- **Preview Mode**: [https://next-blog-agilitycms.now.sh/?agilitypreviewkey=...](https://next-blog-agilitycms.now.sh/?agilitypreviewkey=GzL%2fio1pLkfKc9BR1%2fC1cDQeKjL0AkwrTAJ22q3UEjAcOhyrqZejDkDv4kMlBKqrEuQxsuRyiP%2bUaykDYlJ%2fJg%3d%3d)
### Related examples
- [Agility CMS Sample Starter](https://github.com/agility/agilitycms-next-starter-ssg)
- [Blog Starter](/examples/blog-starter)
- [Sanity](/examples/cms-sanity)
- [TakeShape](/examples/cms-takeshape)
- [Prismic](/examples/cms-prismic)
## How to use
### Using `create-next-app`
Execute [`create-next-app`](https://github.com/zeit/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example:
```bash
npm init next-app --example cms-agilitycms cms-agilitycms-app
# or
yarn create next-app --example cms-agilitycms cms-agilitycms-app
```
### Download manually
Download the example:
```bash
curl https://codeload.github.com/zeit/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/cms-agilitycms
cd cms-agilitycms
```
## Configuration
### How is this Different from Other CMS Examples?
The key principle behind Agility CMS is that **Editors** should have full control of their pages and what content is on each page without getting into code.
This means you'll not only be definining **Content** for your `Posts` and `Authors`, but you'll also be defining UI Components to compose your pages. This site will consist of a single **Page Template** and a collection of **Modules** that represent the UI components you see on the page.
> **NOTE** - `Modules` and `Page Templates` in Agility CMS simply correspond to `React Components` in your website.
Once you've gone through the steps below, you'll be able to dynamically manage pages (and what is on them) directly through the CMS without requiring a developer.
### Step 1. Create an account and a project on `Agility CMS`
First, [create an account on Agility CMS](https://agilitycms.com).
After creating an account you'll be asked to create a new project. Use any name of your liking as the **Project Name** and select the **Blank (advanced users)** template to create a blank Agility CMS instance.
### Step 2. Create an `Author` Content Definition
From within the Agility CMS Content Manager, navigate to **Settings** > **Content Definitions** and click **New** to create a new **Content Definition**.
- The **Title** should be `Author`. This will also pre-populate **Reference Name** for you.
Next, add these fields via the **Form Builder** tab (you don't have to modify any other settings):
- `Name` - Set **Field Label** to `Name` and **Field Type** to `Text`
- `Picture` - Set **Field Label** to `Picture` and **Field Type** to `Image`
When you are done, click **Save & Close** to save your `Author` content definition.
### Step 3. Create a `List` based on your `Author` Content Definition
From within the Agility CMS Content Manager, navigate to **Shared Content** and click the **+ (New)** button, then fill the form like so:
- **Type** should be `Content List`
- **Content Definition** should be **Author**
- **Display Name** should be set to **Authors**. This will also pre-populate **Reference Name** for you.
### Step 4. Create a `Post` Content Definiton
From within the Agility CMS Content Manager, navigate to **Settings** > **Content Definitions** and click **New** to create a new **Content Definition**.
- The **Title** should be `Post`.
Next, add these fields via the **Form Builder** tab (you don't have to modify any other settings):
- `Title` - Set **Field Type** to `Text`
- `Slug` - Set **Field Type** to `Text`
- `Date` - Set **Field Type** to `Date/Time`
- `AuthorID` - Set **Field Type** to `Number` and enable **Hide field from input form**
- `Author` - Do the following:
- **Field Type** - `Linked Content`
- **Content Definition** - `Author`
- **Content View** - `Shared Content`
- **Shared Content** - `Authors`
- **Render As** - `Dropdown List`
- **Save Value To Field** - `AuthorID`
- `Excerpt` - Set **Field Type** to `Text`
- `Content` - Set **Field Type** to `HTML`
- `Cover Image` - Set **Field Type** to `Image`
When you are done, click **Save & Close** to save your `Post` content definition.
### Step 5. Create a `Dynamic Page List` based on your `Posts` Content Definition
From within the Agility CMS Content Manager, navigate to **Shared Content** and click the **+ (New)** button, then fill the form like so:
- **Type** should be `Dynamic Page List`
- **Content Definition** should be `Post`
- **Display Name** should be `Posts`. This will also pre-populate **Reference Name** for you
### Step 6. Populate Content
Go to **Shared Content**, select the **Authors** list and click the **+ New** button to create a new content item:
- You just need **1 Author content item**.
- Use dummy data for the text.
- For the image, you can download one from [Unsplash](https://unsplash.com/).
Click on **Save** and **Publish** once you're done.
Next, select the **Posts** list and click the **+ New** button to create a new content item:
- We recommend creating at least **2 Post content items**.
- Use dummy data for the text.
- You can write markdown for the **Content** field.
- For the images, you can download ones from [Unsplash](https://unsplash.com/).
- Pick the **Author** you created earlier.
For each post content item, you need to click `Publish` after saving. If not, the post will be in the `Staging` state.
### Step 7. Define your `Intro` Module
Navigate to **Settings** > **Module Definitions** and click **New** to create a new **Module Definition**.
- Set **Title** to `Intro`
- Set **Description** to `Displays an intro message.`
In this case, we are not adding any fields to control the output or behaviour, since the content is actually hard-coded in the template.
Click **Save & Close** to save the definition.
### Step 8. Define your `Hero Post` Module
Navigate to **Settings** > **Module Definitions** and click **New** to create a new **Module Definition**.
- Set **Title** to `Hero Post`
- Set **Description** to `Displays the latest Post.`
In this case, we are not adding any fields to control the output or behaviour, since the latest post will be used by default and all of the data is associated to the post itself.
Click **Save & Close** to save the definition.
### Step 9. Define your `More Stories` Module
Navigate to **Settings** > **Module Definitions** and click **New** to create a new **Module Definition**.
- Set **Title** to `More Stories`
- Set **Description** to `Displays a listing of Posts.`
Next, add the following field:
- `Title` - Set **Field Type** to `Text`
Click **Save & Close** to save the definition.
### Step 10. Define your `Post Details` Module
Navigate to **Settings** > **Module Definitions** and click **New** to create a new **Module Definition**.
- Set **Title** to `Post Details`
- Set **Description** to `Displays the details of a Post.`
In this case, we are not adding any fields to control the output or behaviour, since the data is associated to the post itself.
Click **Save & Close** to save the definition.
### Step 11. Define a `One Column` Page Template
Navigate to **Settings** > **Page Templates** and click **New** to create a new **Page Template**.
- **Name** should be `One Column Template`
- **Digital Channel Type** should be `Website`
- Under **Module Zones** click `+ (New)`
- Set **Display Name** to `Main Content Zone`, it will populate **Reference Name** for you
- Click `Save` to apply the `Main Content Zone`
Click **Save & Close** to save the page template.
### Step 12. Add a new Page called `home`
Navigate to **Pages** and click the **+ (New)** button in the page tree to create a new **Page**.
- Set **Type** to `Page`
- Set **Page Template** to `One Column Template`
- Set **Menu Text** to `Home` - **Page Title** and **Page Name** fields will be auto-populated.
Click **Save** to create the `/home` page.
Next, let's add the `Intro`, `Hero Post` and `More Stories` modules to the `Main Content Zone` of the `home` page:
- Click the **+ (New)** button on `Main Content Zone` and select `Intro` to add the module to the page
- Click **Save & Close** on the module to return back to the page
- Click the **+ (New)** button on `Main Content Zone` and select `Hero Post` to add the module to the page
- Click **Save & Close** on the module to return back to the page
- Click the **+ (New)** button on `Main Content Zone` and select `More Stories` to add the module to the page
- Set **Title** to `More Stories`
- Click **Save & Close** on the module to return back to the page
Then click **Publish** on the page in order to publish the page and all of its modules.
### Step 13. Add a new Folder called `posts`
Navigate to **Pages** and click the `Website` channel, then click the **+ (New)** button in the page tree to create a new **Folder** in the root of the site:
- Set **Type** to `Folder`
- Set **Menu Text** to `Posts`, **Folder Name** will be auto-populated to `posts`
Click **Save** to create the `/posts` folder.
**Important:** Click **Publish** on the folder.
### Step 14. Add a new Dynamic Page called `posts-dynamic`
Navigate to **Pages** and select the existing `/posts` folder. Click the **+ (New)** button in the page tree to create a new **Dynamic Page** underneath the `posts` page.
- Set **Type** to `Dynamic Page`
- Set **Page Template** to `One Column Template`
- Set **Build Pages From** to `Posts`
- Set **Sitemap Label** to `posts-dynamic`
- Set **Page Path Formula** to `##Slug##`
- Set **Page Title Formula** and **Menu Text Formula** to `##Title##`
Click **Save** to create the `/posts/posts-dynamic` dynamic page.
Next, let's add the `Post Details` and `More Stories` modules to the `Main Content Zone` of the `posts-dynamic` page:
- Click the **+ (New)** button on `Main Content Zone` and select `Post Details` to add the module to the page
- Click the **+ (New)** button on `Main Content Zone` and select `More Stories` to add the module to the page
- Set **Title** to `More Stories`
- Click **Save & Close** on the module to return back to the `posts-dynamic` page
Then click **Publish** on the page in order to publish the page and all of its modules.
### Step 15. Set up environment variables
Copy the `.env.local.example` file in this directory to `.env.local` (which will be ignored by Git):
```bash
cp .env.local.example .env.local
```
Go to the **Getting Started** section from the menu and click on **API Keys**. You should see a new modal called `Content API Details`, then click in the **Show API Key(s)** button within it.
Then set each variable on `.env.local`:
- `NEXT_EXAMPLE_CMS_AGILITY_GUID` should be the **Instance GUID** field
- `NEXT_EXAMPLE_CMS_AGILITY_API_FETCH_KEY` should be the **Live API Key** field
- `NEXT_EXAMPLE_CMS_AGILITY_API_PREVIEW_KEY` should be the **Preview API Key** field - this is used when the site is in [Preview Mode](https://nextjs.org/docs/advanced-features/preview-mode) and allows your site to pull the latest content, regardless of whether it is published or not.
- `NEXT_EXAMPLE_CMS_AGILITY_SECURITY_KEY` should be the **Security Key** field that can be found in **Settings** > **Global Security** - this is used to communicate between the CMS and your site to validate [Preview Mode](https://nextjs.org/docs/advanced-features/preview-mode)
Your `.env.local` file should look like this:
```bash
NEXT_EXAMPLE_CMS_AGILITY_GUID=...
NEXT_EXAMPLE_CMS_AGILITY_API_FETCH_KEY=...
NEXT_EXAMPLE_CMS_AGILITY_API_PREVIEW_KEY=...
NEXT_EXAMPLE_CMS_AGILITY_SECURITY_KEY=...
```
### Step 16. Run Next.js in development mode
```bash
npm install
npm run dev
# or
yarn install
yarn dev
```
Your blog should be up and running on [http://localhost:3000](http://localhost:3000)! If it doesn't work, post on [GitHub discussions](https://github.com/zeit/next.js/discussions).
### Step 17. Deploy on Vercel
You can deploy this app to the cloud with [Vercel](https://vercel.com/import?filter=next.js&utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)).
To deploy on Vercel, you need to set the environment variables using the [Vercel CLI](https://vercel.com/download) ([Documentation](https://vercel.com/docs/now-cli#commands/secrets)).
Install [Vercel CLI](https://vercel.com/download), log in to your account from the CLI, and run the following commands to add the environment variables. Replace each variable with the corresponding strings in `.env.local`:
```
vercel secrets add next_example_cms_agility_guid <NEXT_EXAMPLE_CMS_AGILITY_GUID>
vercel secrets add next_example_cms_agility_api_fetch_key <NEXT_EXAMPLE_CMS_AGILITY_API_FETCH_KEY>
vercel secrets add next_example_cms_agility_api_preview_key <NEXT_EXAMPLE_CMS_AGILITY_API_PREVIEW_KEY>
vercel secrets add next_example_cms_agility_security_key <NEXT_EXAMPLE_CMS_AGILITY_SECURITY_KEY>
```
Then push the project to GitHub/GitLab/Bitbucket and [import to Vercel](https://vercel.com/import?filter=next.js&utm_source=github&utm_medium=readme&utm_campaign=next-example) to deploy.
### Step 18. Try preview mode
Now that you've deployed your app to Vercel, take note of the URL of your deployed site. This will be registered in Agility CMS so that when editors click the `Preview` button within Agility CMS, your app is loaded in **Preview Mode**. Learn more about [NextJS Preview Mode](https://nextjs.org/docs/advanced-features/preview-mode)).
To enable the Preview Mode, you'll need to add your site to **Domain Configuration** in Agility CMS:
- Go to **Settings** > **Domain Configuration**
- Click on the existing channel in the list called `Website`
- Click on the **+ (New)** button to add a new domain:
- Set **Name** to `Production`
- Set **Domain URL** to the URL of your production deployment, it should look like `https://<your-vercel-domain>.now.sh`
- Enable **Preview Domain**
- Click **Save** to save your settings
Go to one of your `Posts` and update the title. For example, you can add `[Staging]` in front of the title. Click **Save**, but **DO NOT** click **Publish**. By doing this, the post will be in the staging state.
To enter **Preview Mode**, click the `Preview` button on the details of your `Post`. This redirects you to the `/` page, however you will now be in **Preview Mode** so you can navigate to your `Post` you want to view on the website.
You should now be able to see the updated title. To exit the preview mode, you can click **Click here to exit preview mode** at the top.
> NOTE - To set up preview on a specific `Post` (as opposed to the `/` page), click on the **Settings** tab of the `Post` list in **Shared Content**. For **Item Preview Page** set it to `~/posts/posts-dynamic` and for **Item Preview Query String Parameter** set it to `contentid`.

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/zeit/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,12 @@
export default function Avatar({ name, picture }) {
return (
<div className="flex items-center">
<img
src={picture.url}
className="w-12 h-12 rounded-full mr-4"
alt={name}
/>
<div className="text-xl font-bold">{name}</div>
</div>
)
}

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,28 @@
//import { Image } from 'react-datocms'
import Image from '../lib/components/image'
import cn from 'classnames'
import Link from 'next/link'
export default function CoverImage({ title, responsiveImage, slug }) {
const image = (
<Image
data={{
...responsiveImage,
}}
className={cn('shadow-small', {
'hover:shadow-medium transition-shadow duration-200': slug,
})}
/>
)
return (
<div className="-mx-5 sm:mx-0">
{slug ? (
<Link href="/[...slug]" as={`/posts/${slug}`}>
<a aria-label={title}>{image}</a>
</Link>
) : (
image
)}
</div>
)
}

View file

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

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/zeit/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="/">
<a className="hover:underline">Blog</a>
</Link>
.
</h2>
)
}

View file

@ -0,0 +1,49 @@
import Link from 'next/link'
import Avatar from '../components/avatar'
import Date from '../components/date'
import CoverImage from '../components/cover-image'
export default function HeroPost({
title,
coverImage,
date,
excerpt,
author,
slug,
}) {
return (
<section>
<div className="mb-8 md:mb-16">
<CoverImage
title={title}
responsiveImage={coverImage.responsiveImage}
slug={slug}
/>
</div>
<div className="md:grid md:grid-cols-2 md:col-gap-16 lg:col-gap-8 mb-20 md:mb-28">
<div>
<h3 className="mb-4 text-4xl lg:text-6xl leading-tight">
<Link href="/[...slug]" as={`/posts/${slug}`}>
<a className="hover:underline">{title}</a>
</Link>
</h3>
<div className="mb-4 md:mb-0 text-lg">
<Date dateString={date} />
</div>
</div>
{author && (
<div>
<p className="text-lg leading-relaxed mb-4">{excerpt}</p>
<Avatar name={author.name} picture={author.picture} />
</div>
)}
</div>
</section>
)
}
// The data returned here will be send as `props` to the component
HeroPost.getCustomInitialProps = async function ({ client }) {
const post = await client.getLatestPost()
return post
}

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,18 @@
.markdown {
@apply text-lg leading-relaxed;
}
.markdown p,
.markdown ul,
.markdown ol,
.markdown blockquote {
@apply my-6;
}
.markdown h2 {
@apply text-3xl mt-12 mb-4 leading-snug;
}
.markdown h3 {
@apply text-2xl mt-8 mb-4 leading-snug;
}

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,39 @@
import PostPreview from '../components/post-preview'
export default function MoreStories({ title, posts }) {
return (
<section>
<h2 className="mb-8 text-6xl md:text-7xl font-bold tracking-tighter leading-tight">
{title}
</h2>
<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-32">
{posts.map((post) => (
<PostPreview
key={post.slug}
title={post.title}
coverImage={post.coverImage}
date={post.date}
author={post.author}
slug={post.slug}
excerpt={post.excerpt}
/>
))}
</div>
</section>
)
}
// The data returned here will be send as `props` to the component
MoreStories.getCustomInitialProps = async function ({
client,
item,
pageInSitemap,
}) {
const postToExcludeContentID = pageInSitemap.contentID ?? -1
const posts = await client.getPostsForMoreStories({ postToExcludeContentID })
return {
title: item.fields.title,
posts,
}
}

View file

@ -0,0 +1,12 @@
import markdownStyles from './markdown-styles.module.css'
export default function PostBody({ content }) {
return (
<div className="max-w-2xl mx-auto">
<div
className={markdownStyles['markdown']}
dangerouslySetInnerHTML={{ __html: content }}
/>
</div>
)
}

View file

@ -0,0 +1,40 @@
import Header from './header'
import PostHeader from './post-header'
import PostBody from './post-body'
import SectionSeparator from './section-separator'
import Head from 'next/head'
import { CMS_NAME } from '../lib/constants'
export default function PostDetails({ post }) {
return (
<>
<Header />
<article>
<Head>
<title>
{post.title} | Next.js Blog Example with {CMS_NAME}
</title>
<meta property="og:image" content={post.ogImage.url} />
</Head>
<PostHeader
title={post.title}
coverImage={post.coverImage}
date={post.date}
author={post.author}
/>
<PostBody content={post.content} />
</article>
<SectionSeparator />
</>
)
}
// The data returned here will be send as `props` to the component
PostDetails.getCustomInitialProps = async function ({ client, pageInSitemap }) {
const contentID = pageInSitemap.contentID
const post = await client.getPostDetails({ contentID })
return {
post,
}
}

View file

@ -0,0 +1,31 @@
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 }) {
return (
<>
<PostTitle>{title}</PostTitle>
<div className="hidden md:block md:mb-12">
<Avatar name={author.name} picture={author.picture} />
</div>
<div className="mb-8 md:mb-16 -mx-5 sm:mx-0">
<CoverImage
title={title}
responsiveImage={coverImage.responsiveImage}
/>
</div>
{author && (
<div className="max-w-2xl mx-auto">
<div className="block md:hidden mb-6">
<Avatar name={author.name} picture={author.picture} />
</div>
<div className="mb-6 text-lg">
<Date dateString={date} />
</div>
</div>
)}
</>
)
}

View file

@ -0,0 +1,35 @@
import Avatar from '../components/avatar'
import Date from '../components/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
slug={slug}
title={title}
responsiveImage={coverImage.responsiveImage}
/>
</div>
<h3 className="text-3xl mb-3 leading-snug">
<Link href="/[...slug]" as={`/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>
{author && <Avatar name={author.name} picture={author.picture} />}
</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,204 @@
import agility from '@agility/content-fetch'
import { CMS_LANG, CMS_CHANNEL } from './constants'
import { asyncForEach } from './utils'
export { validatePreview } from './preview'
import { normalizePosts } from './normalize'
import { requireComponentDependancyByName } from './dependancies'
// Our LIVE API client
const liveClient = agility.getApi({
guid: process.env.NEXT_EXAMPLE_CMS_AGILITY_GUID,
apiKey: process.env.NEXT_EXAMPLE_CMS_AGILITY_API_FETCH_KEY,
})
// Our PREVIEW API client
const previewClient = agility.getApi({
guid: process.env.NEXT_EXAMPLE_CMS_AGILITY_GUID,
apiKey: process.env.NEXT_EXAMPLE_CMS_AGILITY_API_PREVIEW_KEY,
isPreview: true,
})
export const getClient = (preview = false) =>
preview ? previewClient : liveClient
// This client is used by nested components to fetch additional data within `getStaticProps`
export class APIClient {
constructor({ preview = false }) {
this.preview = preview
this.client = getClient(preview)
}
async getAllPosts(take) {
const data = await this.client.getContentList({
referenceName: `posts`,
languageCode: CMS_LANG,
contentLinkDepth: 1,
take: take, // TODO: Implement pagination
})
return data.items
}
async getLatestPost() {
const data = await this.getAllPosts(1)
const normalizedPosts = normalizePosts(data)
return normalizedPosts[0] || null
}
async getPostDetails({ contentID, preview }) {
const post = await this.client.getContentItem({
contentID,
languageCode: CMS_LANG,
contentLinkDepth: 1,
})
const normalizedPost = normalizePosts([post])[0]
return normalizedPost
}
async getPostsForMoreStories({ postToExcludeContentID }) {
let allPosts = await this.getAllPosts(5)
//if we don't have a post to exclude, assume we should exclude the latest one
if (postToExcludeContentID < 0) {
allPosts.shift()
}
const postsLessThisPost = allPosts.filter((p) => {
return p.contentID !== postToExcludeContentID
})
const normalizedMorePosts = normalizePosts(postsLessThisPost)
return normalizedMorePosts
}
}
export async function getAgilityPaths() {
console.log(`Agility CMS => Fetching sitemap for getAgilityPaths...`)
const sitemapFlat = await getClient().getSitemapFlat({
channelName: CMS_CHANNEL,
languageCode: CMS_LANG,
})
//returns an array of paths as a string (i.e. ['/home', '/posts']
//skips folders...
const paths = Object.keys(sitemapFlat)
.filter((s) => sitemapFlat[s].isFolder !== true)
.map((s) => {
return s
})
return paths
}
export async function getAgilityPageProps({ params, preview }) {
//determine if we are in preview mode
const client = getClient(preview)
let path = '/'
if (params) {
//build path by iterating through slugs
path = ''
params.slug.forEach((slug) => {
path += '/' + slug
})
}
console.log(`Agility CMS => Getting page props for '${path}'...`)
//get sitemap
const sitemap = await client.getSitemapFlat({
channelName: CMS_CHANNEL,
languageCode: CMS_LANG,
})
let pageInSitemap = sitemap[path]
let page = null
if (path === '/') {
let firstPagePathInSitemap = Object.keys(sitemap)[0]
pageInSitemap = sitemap[firstPagePathInSitemap]
}
if (pageInSitemap) {
//get the page
page = await client.getPage({
pageID: pageInSitemap.pageID,
languageCode: CMS_LANG,
contentLinkDepth: 1,
})
} else {
//Could not find page
console.error('page [' + path + '] not found in sitemap.')
return
}
if (!page) {
console.error('page [' + path + '] not found in getpage method.')
return
}
//resolve the page template
let pageTemplateName = page.templateName.replace(/[^0-9a-zA-Z]/g, '')
//resolve the modules per content zone
await asyncForEach(Object.keys(page.zones), async (zoneName) => {
let modules = []
//grab the modules for this content zone
const modulesForThisContentZone = page.zones[zoneName]
//loop through the zone's modules
await asyncForEach(modulesForThisContentZone, async (moduleItem) => {
let ModuleComponentToRender = requireComponentDependancyByName(
moduleItem.module
)
if (ModuleComponentToRender) {
//resolve any additional data for the modules
let moduleData = null
if (ModuleComponentToRender.getCustomInitialProps) {
//we have some additional data in the module we'll need, execute that method now, so it can be included in SSG
console.log(
`Agility CMS => Fetching additional data via getCustomInitialProps for ${moduleItem.module}...`
)
moduleData = await ModuleComponentToRender.getCustomInitialProps({
client: new APIClient({ preview }),
item: moduleItem.item,
languageCode: CMS_LANG,
channelName: CMS_CHANNEL,
pageInSitemap: pageInSitemap,
})
}
//if we have additional module data, then overwrite our props that will be sent to the module
if (moduleData != null) {
moduleItem.item = moduleData
}
modules.push({
moduleName: moduleItem.module,
item: moduleItem.item,
})
} else {
console.error(
`No react component found for the module "${moduleItem.module}". Cannot render module.`
)
}
})
//store as dictionary
page.zones[zoneName] = modules
})
return {
sitemapNode: pageInSitemap,
page: page,
pageTemplateName: pageTemplateName,
languageCode: CMS_LANG,
channelName: CMS_CHANNEL,
}
}

View file

@ -0,0 +1,18 @@
import { requireComponentDependancyByName } from '../dependancies'
export default function ContentZone(props) {
function RenderModules() {
let modules = props.page.zones[props.name]
return modules.map((m, i) => {
const AgilityModule = requireComponentDependancyByName(m.moduleName)
return <AgilityModule key={i} {...m.item} />
})
}
return (
<div>
<RenderModules />
</div>
)
}

View file

@ -0,0 +1,223 @@
import React, { useCallback, useState } from 'react'
import { useInView } from 'react-intersection-observer'
// type ResponsiveImageType = {
// aspectRatio: number;
// base64?: string | null;
// height?: number | null;
// width: number;
// sizes?: string | null;
// src?: string | null;
// srcSet?: string | null;
// webpSrcSet?: string | null;
// bgColor?: string | null;
// alt?: string | null;
// title?: string | null;
// };
// type ImagePropTypes = {
// data: ResponsiveImageType;
// className?: string;
// pictureClassName?: string;
// fadeInDuration?: number;
// intersectionTreshold?: number;
// intersectionMargin?: string;
// lazyLoad?: boolean;
// style?: React.CSSProperties;
// pictureStyle?: React.CSSProperties;
// explicitWidth?: boolean;
// };
// type State = {
// lazyLoad: boolean;
// isSsr: boolean;
// isIntersectionObserverAvailable: boolean;
// inView: boolean;
// loaded: boolean;
// };
const imageAddStrategy = ({
lazyLoad,
isSsr,
isIntersectionObserverAvailable,
inView,
loaded,
}) => {
if (!lazyLoad) {
return true
}
if (isSsr) {
return false
}
if (isIntersectionObserverAvailable) {
return inView || loaded
}
return true
}
const imageShowStrategy = ({
lazyLoad,
isSsr,
isIntersectionObserverAvailable,
loaded,
}) => {
if (!lazyLoad) {
return true
}
if (isSsr) {
return false
}
if (isIntersectionObserverAvailable) {
return loaded
}
return true
}
const Image = function ({
className,
fadeInDuration,
intersectionTreshold,
intersectionMargin,
pictureClassName,
lazyLoad = true,
style,
pictureStyle,
explicitWidth,
data,
}) {
const [loaded, setLoaded] = useState(false)
const handleLoad = useCallback(() => {
setLoaded(true)
}, [])
const [ref, inView] = useInView({
threshold: intersectionTreshold || 0,
rootMargin: intersectionMargin || '0px 0px 0px 0px',
triggerOnce: true,
})
const isSsr = typeof window === 'undefined'
const isIntersectionObserverAvailable = isSsr
? false
: !!window.IntersectionObserver
const absolutePositioning = {
position: 'absolute',
left: 0,
top: 0,
bottom: 0,
right: 0,
}
const addImage = imageAddStrategy({
lazyLoad,
isSsr,
isIntersectionObserverAvailable,
inView,
loaded,
})
const showImage = imageShowStrategy({
lazyLoad,
isSsr,
isIntersectionObserverAvailable,
inView,
loaded,
})
const webpSource = data.webpSrcSet && (
<source srcSet={data.webpSrcSet} sizes={data.sizes} type="image/webp" />
)
const regularSource = data.srcSet && (
<source srcSet={data.srcSet} sizes={data.sizes} />
)
const placeholder = (
<div
style={{
backgroundImage: data.base64 ? `url(${data.base64})` : null,
backgroundColor: data.bgColor,
backgroundSize: 'cover',
opacity: showImage ? 0 : 1,
transition:
!fadeInDuration || fadeInDuration > 0
? `opacity ${fadeInDuration || 500}ms ${fadeInDuration || 500}ms`
: null,
...absolutePositioning,
}}
/>
)
const { width, aspectRatio } = data
const height = data.height || width / aspectRatio
const sizer = (
<svg
className={pictureClassName}
style={{
width: explicitWidth ? `${width}px` : '100%',
height: 'auto',
display: 'block',
...pictureStyle,
}}
height={height}
width={width}
/>
)
return (
<div
ref={ref}
className={className}
style={{
display: 'inline-block',
overflow: 'hidden',
...style,
position: 'relative',
}}
>
{sizer}
{placeholder}
{addImage && (
<picture
style={{
...absolutePositioning,
opacity: showImage ? 1 : 0,
transition:
!fadeInDuration || fadeInDuration > 0
? `opacity ${fadeInDuration || 500}ms`
: null,
}}
>
{webpSource}
{regularSource}
{data.src && (
<img
src={data.src}
alt={data.alt}
title={data.title}
onLoad={handleLoad}
style={{ width: '100%' }}
/>
)}
</picture>
)}
<noscript>
<picture className={pictureClassName} style={{ ...pictureStyle }}>
{webpSource}
{regularSource}
{data.src && <img src={data.src} alt={data.alt} title={data.title} />}
</picture>
</noscript>
</div>
)
}
export default Image

View file

@ -0,0 +1,9 @@
import ContentZone from './content-zone'
export default function OneColumnTemplate(props) {
return (
<>
<ContentZone name="MainContentZone" {...props} />
</>
)
}

View file

@ -0,0 +1,8 @@
import { requireComponentDependancyByName } from '../dependancies'
export default function CMSPageTemplate(props) {
const AgilityPageTemplateToRender = requireComponentDependancyByName(
props.pageTemplateName
)
return <AgilityPageTemplateToRender {...props} />
}

View file

@ -0,0 +1,13 @@
export default function RichTextArea({ fields }) {
const { textblob } = fields
return (
<div
className="default-rich-text-area"
dangerouslySetInnerHTML={setHTML(textblob)}
/>
)
}
const setHTML = (textblob) => {
return { __html: textblob }
}

View file

@ -0,0 +1,7 @@
export const EXAMPLE_PATH = 'cms-agilitycms'
export const CMS_NAME = 'Agility CMS'
export const CMS_URL = 'https://www.agilitycms.com'
export const CMS_LANG = 'en-us'
export const CMS_CHANNEL = 'website'
export const HOME_OG_IMAGE_URL =
'https://og-image.now.sh/Next.js%20Blog%20Example%20with%20**Agility%20CMS**.png?theme=light&md=1&fontSize=75px&images=https%3A%2F%2Fassets.zeit.co%2Fimage%2Fupload%2Ffront%2Fassets%2Fdesign%2Fnextjs-black-logo.svg&images=https%3A%2F%2Fcdn.agilitycms.com%2Fcontent-manager%2Fimages%2Flogos%2F3d69eddf6f00b8824feb126d9ac2bed3ec6e85c0.png&widths=undefined&widths=350&heights=200&heights=auto'

View file

@ -0,0 +1,51 @@
import { convertPascalToKebabCase } from './utils'
const path = require('path')
const userComponentsPath = path.resolve('./components')
const libComponentsPath = path.resolve('./lib/components')
//Bug: when dynamic imports are used within the module, it doest not get outputted server-side
//let AgilityModule = dynamic(() => import ('../components/' + m.moduleName));
export const requireComponentDependancyByName = (name) => {
let pascalCaseName = name
let kebabCaseName = convertPascalToKebabCase(name)
let Component = null
try {
Component = requireComponent(kebabCaseName)
} catch {}
if (!Component) {
try {
Component = requireComponent(pascalCaseName)
} catch {}
}
if (!Component) {
// eslint-disable-next-line no-throw-literal
throw `Could not find a component with the name ${name}. Tried searching:
${userComponentsPath}/${kebabCaseName}.js',
${libComponentsPath}/${kebabCaseName}.js',
${userComponentsPath}/${pascalCaseName}.js',
${libComponentsPath}/${pascalCaseName}.js'.`
}
return Component
}
const requireComponent = (name) => {
let Component = null
try {
//check the user path first (must be relative paths)
Component = require(`../components/${name}.js`).default
} catch {}
if (!Component)
try {
//fallback to lib path (must be relative paths)
Component = require(`./components/${name}.js`).default
} catch {}
return Component
}

View file

@ -0,0 +1,56 @@
//Normalizes our data that we get back from Agility CMS
export function normalizePosts(postsFromAgility) {
/* Need an object like this...
- title
- slug
- excerpt
- date
- coverImage
- responsiveImage
- author
- name
- picture
- url
*/
const posts = postsFromAgility.map((p) => {
let normalizedPost = {
title: p.fields.title,
slug: p.fields.slug,
excerpt: p.fields.excerpt,
date: p.fields.date,
content: p.fields.content,
ogImage: {
url: `${p.fields.coverImage.url}?w=2000&h=1000&q=70`,
},
coverImage: {
responsiveImage: {
srcSet: null,
webpSrcSet: null,
sizes: null,
src: `${p.fields.coverImage.url}?w=2000&h=1000&q=70`,
width: 2000,
height: 1000,
aspectRatio: 100,
base64: null,
alt: p.fields.coverImage.label,
title: null,
bgColor: null,
},
},
}
if (p.fields.author) {
normalizedPost.author = {
name: p.fields.author.fields.name,
picture: {
url: `${p.fields.author.fields.picture.url}?w=100&h=100`,
},
}
}
return normalizedPost
})
return posts
}

View file

@ -0,0 +1,119 @@
import crypto from 'crypto'
import { getClient } from './api'
import { CMS_LANG, CMS_CHANNEL } from './constants'
//Validates whether the incoming preview request is valid
export async function validatePreview({ agilityPreviewKey, slug, contentID }) {
//Validate the preview key
if (!agilityPreviewKey) {
return {
error: true,
message: `Missing agilitypreviewkey.`,
}
}
//sanitize incoming key (replace spaces with '+')
if (agilityPreviewKey.indexOf(` `) > -1) {
agilityPreviewKey = agilityPreviewKey.split(` `).join(`+`)
}
//compare the preview key being used
const correctPreviewKey = generatePreviewKey()
if (agilityPreviewKey !== correctPreviewKey) {
return {
error: true,
message: `Invalid agilitypreviewkey.`,
//message: `Invalid agilitypreviewkey. Incoming key is=${agilityPreviewKey} compared to=${correctPreviewKey}...`
}
}
const validateSlugResponse = await validateSlugForPreview({ slug, contentID })
if (validateSlugResponse.error) {
//kickout
return validateSlugResponse
}
//return success
return {
error: false,
message: null,
slug: validateSlugResponse.slug,
}
}
//Checks that the requested page exists, if not return a 401
export async function validateSlugForPreview({ slug, contentID }) {
//if its for root, allow it and kick out
if (slug === `/`) {
return {
error: false,
message: null,
slug: `/`,
}
}
const client = getClient(true)
//this is a standard page
const sitemapFlat = await client.getSitemapFlat({
channelName: CMS_CHANNEL,
languageCode: CMS_LANG,
})
let sitemapNode = null
if (!contentID) {
//For standard pages
sitemapNode = sitemapFlat[slug]
} else {
console.log(contentID)
//For dynamic pages - need to adjust the actual slug
slug = Object.keys(sitemapFlat).find((key) => {
const node = sitemapFlat[key]
if (node.contentID === contentID) {
return node
}
return false
})
sitemapNode = sitemapFlat[slug]
}
if (!sitemapNode) {
return {
error: true,
message: `Invalid page. '${slug}' was not found in the sitemap. Are you trying to preview a Dynamic Page Item? If so, ensure you have your List Preview Page, Item Preview Page, and Item Preview Query String Parameter set (contentid) .`,
slug: null,
}
}
return {
error: false,
message: null,
slug: sitemapNode.path,
}
}
//Generates a preview key to compare agains
export function generatePreviewKey() {
//the string we want to encode
const str = `-1_${process.env.NEXT_EXAMPLE_CMS_AGILITY_SECURITY_KEY}_Preview`
//build our byte array
let data = []
for (var i = 0; i < str.length; ++i) {
data.push(str.charCodeAt(i))
data.push(0)
}
//convert byte array to buffer
const strBuffer = Buffer.from(data)
//encode it!
const previewKey = crypto
.createHash('sha512')
.update(strBuffer)
.digest('base64')
return previewKey
}

View file

@ -0,0 +1,20 @@
import { useEffect } from 'react'
import { useRouter } from 'next/router'
export default function usePreviewRedirect() {
const router = useRouter()
const { agilitypreviewkey, contentid } = router.query
useEffect(() => {
// kickout if we don't have an agilityPreviewKey
if (!agilitypreviewkey) return
// redirect to our preview API route
let redirectLink = `/api/preview?slug=${window.location.pathname}&agilitypreviewkey=${agilitypreviewkey}`
// Check if we have a `contentid` in the query, if so this is a preview request for a Dynamic Page Item
if (contentid) redirectLink = `${redirectLink}&contentid=${contentid}`
window.location.href = redirectLink
}, [agilitypreviewkey, contentid])
}

View file

@ -0,0 +1,14 @@
const asyncForEach = async (array, callback) => {
for (let index = 0; index < array.length; index++) {
await callback(array[index], index, array)
}
}
const convertPascalToKebabCase = (string) => {
return string
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
.replace(/([A-Z])([A-Z])(?=[a-z])/g, '$1-$2')
.toLowerCase()
}
export { asyncForEach, convertPascalToKebabCase }

View file

@ -0,0 +1,27 @@
{
"name": "cms-agilitycms",
"version": "1.0.0",
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
},
"dependencies": {
"@agility/content-fetch": "^0.8.1",
"classnames": "2.2.6",
"date-fns": "2.10.0",
"isomorphic-unfetch": "3.0.0",
"next": "latest",
"react": "^16.13.0",
"react-datocms": "1.1.0",
"react-dom": "^16.13.0",
"react-intersection-observer": "^8.26.1",
"remark": "11.0.2",
"remark-html": "10.0.0"
},
"devDependencies": {
"@fullhuman/postcss-purgecss": "^2.1.0",
"postcss-preset-env": "^6.7.0",
"tailwindcss": "^1.2.0"
}
}

View file

@ -0,0 +1,76 @@
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 { CMS_NAME } from '../lib/constants'
import { getAgilityPaths, getAgilityPageProps } from '../lib/api'
import usePreviewRedirect from '../lib/use-preview-redirect'
import CMSPageTemplate from '../lib/components/page-template'
export default function Slug({
sitemapNode,
page,
pageTemplateName,
languageCode,
channelName,
preview,
}) {
usePreviewRedirect()
const router = useRouter()
if (!router.isFallback && !page) {
return <ErrorPage statusCode={404} />
}
return (
<>
<Layout preview={preview}>
<Head>
<title>Next.js Blog Example with {CMS_NAME}</title>
</Head>
<Container>
{router.isFallback ? (
<h1>Loading...</h1>
) : (
<CMSPageTemplate
sitemapNode={sitemapNode}
page={page}
pageTemplateName={pageTemplateName}
languageCode={languageCode}
channelName={channelName}
preview={preview}
/>
)}
</Container>
</Layout>
</>
)
}
export async function getStaticProps({ params, preview = false }) {
const props = await getAgilityPageProps({ params, preview })
if (!props) {
return { props: {} }
}
return {
props: {
sitemapNode: props.sitemapNode,
page: props.page,
pageTemplateName: props.pageTemplateName,
languageCode: props.languageCode,
channelName: props.channelName,
preview,
},
}
}
export async function getStaticPaths() {
const paths = await getAgilityPaths()
return {
paths: paths,
fallback: true,
}
}

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,15 @@
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>
)
}
}

View file

@ -0,0 +1,8 @@
export default async function handler(_, res) {
// Exit the current user from "Preview Mode". This function accepts no args.
res.clearPreviewData()
// Redirect the user back to the index page.
res.writeHead(307, { Location: '/' })
res.end()
}

View file

@ -0,0 +1,24 @@
import { validatePreview } from '../../lib/api'
export default async function handler(req, res) {
// Check the secret and next parameters
// This secret should only be known to this API route and the CMS
//validate our preview key, also validate the requested page to preview exists
const validationResp = await validatePreview({
agilityPreviewKey: req.query.agilitypreviewkey,
slug: req.query.slug,
contentID: req.query.contentid,
})
if (validationResp.error) {
return res.status(401).end(`${validationResp.message}`)
}
//enable preview mode
res.setPreviewData({})
// Redirect to the slug
res.writeHead(307, { Location: validationResp.slug })
res.end()
}

View file

@ -0,0 +1,5 @@
import Page, { getStaticProps } from './[...slug]'
export default Page
export { getStaticProps }

View file

@ -0,0 +1,19 @@
module.exports = {
plugins: [
'tailwindcss',
process.env.NODE_ENV === 'production'
? [
'@fullhuman/postcss-purgecss',
{
content: [
'./pages/**/*.{js,jsx,ts,tsx}',
'./components/**/*.{js,jsx,ts,tsx}',
],
defaultExtractor: (content) =>
content.match(/[\w-/:]+(?<!:)/g) || [],
},
]
: undefined,
'postcss-preset-env',
],
}

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,32 @@
module.exports = {
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)',
},
},
},
}

View file

@ -0,0 +1,16 @@
{
"env": {
"NEXT_EXAMPLE_CMS_AGILITY_GUID": "@next_example_cms_agility_guid",
"NEXT_EXAMPLE_CMS_AGILITY_API_FETCH_KEY": "@next_example_cms_agility_api_fetch_key",
"NEXT_EXAMPLE_CMS_AGILITY_API_PREVIEW_KEY": "@next_example_cms_agility_api_preview_key",
"NEXT_EXAMPLE_CMS_AGILITY_SECURITY_KEY": "@next_example_cms_agility_security_key"
},
"build": {
"env": {
"NEXT_EXAMPLE_CMS_AGILITY_GUID": "@next_example_cms_agility_guid",
"NEXT_EXAMPLE_CMS_AGILITY_API_FETCH_KEY": "@next_example_cms_agility_api_fetch_key",
"NEXT_EXAMPLE_CMS_AGILITY_API_PREVIEW_KEY": "@next_example_cms_agility_api_preview_key",
"NEXT_EXAMPLE_CMS_AGILITY_SECURITY_KEY": "@next_example_cms_agility_security_key"
}
}
}