Added Agility CMS example 🏆 (#12788)
4
examples/cms-agilitycms/.env.local.example
Normal 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
|
@ -0,0 +1,2 @@
|
|||
.env*.local
|
||||
.vercel
|
325
examples/cms-agilitycms/README.md
Normal 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`.
|
42
examples/cms-agilitycms/components/alert.js
Normal 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>
|
||||
)
|
||||
}
|
12
examples/cms-agilitycms/components/avatar.js
Normal 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>
|
||||
)
|
||||
}
|
3
examples/cms-agilitycms/components/container.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
export default function Container({ children }) {
|
||||
return <div className="container mx-auto px-5">{children}</div>
|
||||
}
|
28
examples/cms-agilitycms/components/cover-image.js
Normal 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>
|
||||
)
|
||||
}
|
6
examples/cms-agilitycms/components/date.js
Normal 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>
|
||||
}
|
30
examples/cms-agilitycms/components/footer.js
Normal 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>
|
||||
)
|
||||
}
|
12
examples/cms-agilitycms/components/header.js
Normal 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>
|
||||
)
|
||||
}
|
49
examples/cms-agilitycms/components/hero-post.js
Normal 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
|
||||
}
|
28
examples/cms-agilitycms/components/intro.js
Normal 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>
|
||||
)
|
||||
}
|
16
examples/cms-agilitycms/components/layout.js
Normal 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 />
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -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;
|
||||
}
|
42
examples/cms-agilitycms/components/meta.js
Normal 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>
|
||||
)
|
||||
}
|
39
examples/cms-agilitycms/components/more-stories.js
Normal 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,
|
||||
}
|
||||
}
|
12
examples/cms-agilitycms/components/post-body.js
Normal 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>
|
||||
)
|
||||
}
|
40
examples/cms-agilitycms/components/post-details.js
Normal 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,
|
||||
}
|
||||
}
|
31
examples/cms-agilitycms/components/post-header.js
Normal 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>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
35
examples/cms-agilitycms/components/post-preview.js
Normal 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>
|
||||
)
|
||||
}
|
7
examples/cms-agilitycms/components/post-title.js
Normal 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>
|
||||
)
|
||||
}
|
3
examples/cms-agilitycms/components/section-separator.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
export default function SectionSeparator() {
|
||||
return <hr className="border-accent-2 mt-28 mb-24" />
|
||||
}
|
204
examples/cms-agilitycms/lib/api.js
Normal 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,
|
||||
}
|
||||
}
|
18
examples/cms-agilitycms/lib/components/content-zone.js
Normal 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>
|
||||
)
|
||||
}
|
223
examples/cms-agilitycms/lib/components/image.js
Normal 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
|
|
@ -0,0 +1,9 @@
|
|||
import ContentZone from './content-zone'
|
||||
|
||||
export default function OneColumnTemplate(props) {
|
||||
return (
|
||||
<>
|
||||
<ContentZone name="MainContentZone" {...props} />
|
||||
</>
|
||||
)
|
||||
}
|
8
examples/cms-agilitycms/lib/components/page-template.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { requireComponentDependancyByName } from '../dependancies'
|
||||
|
||||
export default function CMSPageTemplate(props) {
|
||||
const AgilityPageTemplateToRender = requireComponentDependancyByName(
|
||||
props.pageTemplateName
|
||||
)
|
||||
return <AgilityPageTemplateToRender {...props} />
|
||||
}
|
13
examples/cms-agilitycms/lib/components/rich-text-area.js
Normal 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 }
|
||||
}
|
7
examples/cms-agilitycms/lib/constants.js
Normal 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'
|
51
examples/cms-agilitycms/lib/dependancies.js
Normal 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
|
||||
}
|
56
examples/cms-agilitycms/lib/normalize.js
Normal 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
|
||||
}
|
119
examples/cms-agilitycms/lib/preview.js
Normal 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
|
||||
}
|
20
examples/cms-agilitycms/lib/use-preview-redirect.js
Normal 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])
|
||||
}
|
14
examples/cms-agilitycms/lib/utils.js
Normal 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 }
|
27
examples/cms-agilitycms/package.json
Normal 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"
|
||||
}
|
||||
}
|
76
examples/cms-agilitycms/pages/[...slug].js
Normal 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,
|
||||
}
|
||||
}
|
7
examples/cms-agilitycms/pages/_app.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
import '../styles/index.css'
|
||||
|
||||
function MyApp({ Component, pageProps }) {
|
||||
return <Component {...pageProps} />
|
||||
}
|
||||
|
||||
export default MyApp
|
15
examples/cms-agilitycms/pages/_document.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
8
examples/cms-agilitycms/pages/api/exit-preview.js
Normal 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()
|
||||
}
|
24
examples/cms-agilitycms/pages/api/preview.js
Normal 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()
|
||||
}
|
5
examples/cms-agilitycms/pages/index.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
import Page, { getStaticProps } from './[...slug]'
|
||||
|
||||
export default Page
|
||||
|
||||
export { getStaticProps }
|
19
examples/cms-agilitycms/postcss.config.js
Normal 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',
|
||||
],
|
||||
}
|
After Width: | Height: | Size: 4.7 KiB |
After Width: | Height: | Size: 14 KiB |
BIN
examples/cms-agilitycms/public/favicon/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
9
examples/cms-agilitycms/public/favicon/browserconfig.xml
Normal 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>
|
BIN
examples/cms-agilitycms/public/favicon/favicon-16x16.png
Normal file
After Width: | Height: | Size: 595 B |
BIN
examples/cms-agilitycms/public/favicon/favicon-32x32.png
Normal file
After Width: | Height: | Size: 880 B |
BIN
examples/cms-agilitycms/public/favicon/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
examples/cms-agilitycms/public/favicon/mstile-150x150.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
33
examples/cms-agilitycms/public/favicon/safari-pinned-tab.svg
Normal 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 |
19
examples/cms-agilitycms/public/favicon/site.webmanifest
Normal 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"
|
||||
}
|
5
examples/cms-agilitycms/styles/index.css
Normal file
|
@ -0,0 +1,5 @@
|
|||
/* purgecss start ignore */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
/* purgecss end ignore */
|
||||
@tailwind utilities;
|
32
examples/cms-agilitycms/tailwind.config.js
Normal 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)',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
16
examples/cms-agilitycms/vercel.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|