Skip to content

Commit

Permalink
Merge main and fix conflicts.
Browse files Browse the repository at this point in the history
  • Loading branch information
andreyjan committed Oct 22, 2024
2 parents bda685d + c68c8fb commit 08dd65d
Show file tree
Hide file tree
Showing 30 changed files with 1,795 additions and 1,463 deletions.
38 changes: 28 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,36 +7,45 @@ The starterkit's cornerstone is our data-fetching solution and it's typesafety.
## Features

- **NextJS App Router** usage for modern **React Server Components** approach - we think this is the way the industry will move and it is a huge benefit for traditional websites to do data fetching only on the server and keep client-bundle lean
- **GraphQL** + **GraphQL Codegen** with **client-preset** plugin + Typescript for data fetching - we believe the best way to benefit from a GraphQL backend on the frontend is to use Typescript and be informed of what is available to you when you do data fetching, and GraphQL Codegen ensures you are not “lying to yourself” in your types by generating them from the API.
- **GraphQL** + **gql.tada** plugin + Typescript for data fetching - we believe the best way to benefit from a GraphQL backend on the frontend is to use Typescript and be informed of what is available to you when you do data fetching, and gql.tada ensures you are not “lying to yourself” in your types by inferring them from the API.
- **Cypress testing** - we include a configured Cypress.io testing suite with ability to do both Component and E2E testing using Cucumber/BDD style syntax (optionally you can use traditional spec files too)
- **Component Renderer** - example of how to take a full tree of components and render them using a mapping of contentTypes to React components
- **Draft Mode** - preview mode for your application for Contentful Preview API usage
- Use of **Contentful Live Preview** - contentful live previews let you edit components side by side with a visual representation and Live Preview SDK also lets you annotate specific fields you are editing to get to the editor screen by just clicking “Edit” button on the frontend. We also integrated live updates, which will show result of content changes immediately as opposed to waiting for content to auto-save in Contentful
- Use of **Contentful Live Preview** - contentful live previews let you edit components side by side with a visual representation and Live Preview SDK also lets you annotate specific fields you are editing to get to the editor screen by just clicking “Edit” button on the frontend. We also integrated live updates, which will show result of content changes immediately as opposed to waiting for content to auto-save in Contentful.

## Getting Started

Clone the repo of course ;)
1. This project is a template, feel free to either clone it (to preserve project history) or use click "Use this template" to create a repository with a single init commit.
As soon as you have a repository, clone it locally
2. Install dependencies with:

### Contentful access
```
yarn install
```

To develop locally, you will want to connect to a Contentful instance that has the same data model as we use to develop, there are 2 ways to do that:
3. Setup Contenful access
To develop locally, you will want to connect to a Contentful instance that has the same data model as we use to develop, there are 2 ways to do that:

1. You could get access to an existing space that follows Contentful Marketing Template content model, for example a collegue could share his space with you
2. You could create your own space with https://www.contentful.com/starter-templates/marketing-website/. Keep in mind, new templates today can only be deployed on brand new Contentful accounts, so you might have to create a new account with a new email to do that, but this shouldn't be a problem, as it's free.
- You could get access to an existing space that follows Contentful Marketing Template content model, for example a collegue could share his space with you
- You could create your own space with https://www.contentful.com/starter-templates/marketing-website/. Keep in mind, new templates today can only be deployed on brand new Contentful accounts, so you might have to create a new account with a new email to do that, but this shouldn't be a problem, as it's free.

### Configure environment
You will want to get a CDA and CPA API keys by using this [guide](https://www.contentful.com/developers/docs/references/authentication/#api-keys-in-the-contentful-web-app)

4. Configure environment

Create .env.local in root directory of the repo with the following contents:

```
CONTENTFUL_SPACE=<space id>
CONTENTFUL_DELIVERY_API=<delivery api key>
CONTENTFUL_PREVIEW_API=<preview api key>****
CONTENTFUL_PREVIEW_API=<preview api key>
CONTENTFUL_ENVIRONMENT=master
CONTENTFUL_PREVIEW_SECRET=secret
# Enable or disable Content Source Maps, see https://www.contentful.com/developers/docs/tools/vercel/content-source-maps-with-vercel/
CONTENTFUL_USE_CONTENT_SOURCE_MAPS=true
```

### Dev Server
5. Run Dev Server

```bash
yarn dev
Expand Down Expand Up @@ -74,6 +83,15 @@ yarn generate:output
This command will also run `gql.tada turbo` which will generate a cache file that should also be commited. This cache file will speed up inference for new users who just checked out a new branch.
More info [here](https://gql-tada.0no.co/devlog/2024-04-15)

### Content Source Maps in Live Preview and on Vercel

Contentful has released a new live-preview API compatible content source maps spec, you can read more [here](https://www.contentful.com/developers/docs/tools/vercel/content-source-maps-with-vercel/). This implementation enabled effortless inspector mode annotations in Live Preview, as well as full Edit Mode support on Vercel. Unfortunately for some folks testing the starterkit on FREE plan, this feature is only available on Pro+. While Content Source Maps enables Visual Editing on Vercel, Visual Editing is also only available in Pro+. If you're testing the starterkit and would like to not use content source maps, you are free to opt out by:

1. Settings `CONTENTFUL_USE_CONTENT_SOURCE_MAPS=false` in .env.local or in Vercel
2. Removing `@contentSourceMaps` directive from GraphQL queries

** Note: @contentSourceMaps ideally could be added conditionally, but gql.tada prevents any dynamic strings from being typed TadaDocumentNode. In future we will add the @contentSourceMaps directive to all queries in urql exchange.**

## NextJS Docs

To learn more about Next.js, take a look at the following resources:
Expand Down
30 changes: 21 additions & 9 deletions app/[locale]/[[...slug]]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import { Metadata } from 'next';
import { setStaticParamsLocale } from 'next-international/server';
import { draftMode } from 'next/headers';

import { encodeGraphQLResponse } from '@contentful/content-source-maps';
import { graphql } from 'gql.tada';

import { ComponentRenderer } from '#/components/component-renderer';
import DebugMode from '#/components/debug-mode/debug-mode';
import { ComponentDuplexFieldsFragment } from '#/components/duplex-ctf/duplex-ctf';
import { ComponentHeroBannerFieldsFragment } from '#/components/hero-banner-ctf/hero-banner-ctf';
import { LanguageDataSetter } from '#/components/language-data-provider/language-data-setter';
import { ComponentTopicBusinessInfoFieldsFragment } from '#/components/topic-business-info/topic-business-info';
import { graphqlClient } from '#/lib/graphqlClient';
import { getLocaleFromPath } from '#/locales/get-locale-from-path';
import { getStaticParams } from '#/locales/server';
Expand All @@ -20,7 +22,7 @@ type PageProps = {
const getPage = async (slug: string, locale: string, preview = false) => {
const pageQuery = graphql(
`
query PageQuery($slug: String, $locale: String, $preview: Boolean) {
query PageQuery($slug: String, $locale: String, $preview: Boolean) @contentSourceMaps {
pageCollection(locale: $locale, preview: $preview, limit: 1, where: { slug: $slug }) {
items {
topSectionCollection(limit: 10) {
Expand All @@ -29,21 +31,29 @@ const getPage = async (slug: string, locale: string, preview = false) => {
...ComponentDuplexFields
}
}
pageContent {
...ComponentTopicBusinessInfo
}
slugEn: slug(locale: "en-US")
slugDe: slug(locale: "de-DE")
}
}
}
`,
[ComponentHeroBannerFieldsFragment, ComponentDuplexFieldsFragment]
[ComponentHeroBannerFieldsFragment, ComponentDuplexFieldsFragment, ComponentTopicBusinessInfoFieldsFragment]
);
return (
await graphqlClient(preview).query(pageQuery, {
locale,
preview,
slug,
})
).data?.pageCollection?.items?.[0];
const response = await graphqlClient(preview).query(pageQuery, {
locale,
preview,
slug,
});
const formattedData = preview
? encodeGraphQLResponse({
data: response.data,
extensions: response.extensions,
})
: response;
return formattedData?.data?.pageCollection?.items?.[0];
};

const getPageSlugs = async (locale: string) => {
Expand Down Expand Up @@ -102,6 +112,7 @@ export default async function LandingPage({ params }: PageProps) {
const pageData = await getPage(slug, getLocaleFromPath(locale), isDraftMode);

const topComponents = pageData?.topSectionCollection?.items;
const pageContent = pageData?.pageContent;

return (
<div>
Expand All @@ -113,6 +124,7 @@ export default async function LandingPage({ params }: PageProps) {
}}
/>
{topComponents ? <ComponentRenderer data={topComponents} /> : null}
{pageContent ? <ComponentRenderer data={pageContent} /> : null}
</div>
);
}
Expand Down
9 changes: 6 additions & 3 deletions app/[locale]/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { draftMode } from 'next/headers';

import { VercelToolbar } from '@vercel/toolbar/next';

import { graphqlClient } from '#/lib/graphqlClient';

import '../globals.css';

import { graphql } from 'gql.tada';

import '@contentful/live-preview/style.css';

import { LocaleProvider } from '#/app/[locale]/locale-provider';
import { ContentfulPreviewProvider } from '#/components/contentful-preview-provider';
import { LanguageDataProvider } from '#/components/language-data-provider';
import { NavigationFieldsFragment } from '#/components/navigation';
import { SiteHeader } from '#/components/site-header';
import { isContentSourceMapsEnabled } from '#/lib/contentSourceMaps';
import { fontSans } from '#/lib/fonts';
import { cn } from '#/lib/utils';
import { getLocaleFromPath } from '#/locales/get-locale-from-path';
Expand All @@ -24,6 +25,7 @@ export default async function RootLayout({
children: React.ReactNode;
params: { locale: string };
}) {
const shouldInjectToolbar = process.env.NODE_ENV === 'development';
const { locale } = params;
const { isEnabled: isDraftMode } = draftMode();

Expand Down Expand Up @@ -55,12 +57,13 @@ export default async function RootLayout({
*/}
<head />
<body className={cn('min-h-screen bg-background font-sans antialiased', fontSans.variable)}>
<ContentfulPreviewProvider isDraftMode={isDraftMode}>
<ContentfulPreviewProvider isDraftMode={isDraftMode} isContentSourceMapsEnabled={isContentSourceMapsEnabled}>
<LocaleProvider locale={locale}>
<LanguageDataProvider>
<div className="relative flex min-h-screen flex-col">
<SiteHeader navigationData={layoutData.data?.navigationMenuCollection} />
<div className="flex-1">{children}</div>
{shouldInjectToolbar && <VercelToolbar />}
</div>
</LanguageDataProvider>
</LocaleProvider>
Expand Down
4 changes: 4 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,7 @@
@apply bg-background text-foreground;
}
}

.wysiwyg p {
margin-block: 16px;
}
2 changes: 2 additions & 0 deletions components/component-renderer/mappings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@ import dynamic from 'next/dynamic';
export const componentMap = {
ComponentHeroBanner: dynamic(() => import('#/components/hero-banner-ctf').then((mod) => mod.HeroBannerCtf)),
ComponentDuplex: dynamic(() => import('#/components/duplex-ctf').then((mod) => mod.DuplexCtf)),
TopicBusinessInfo: dynamic(() => import('#/components/topic-business-info').then((mod) => mod.TopicBusinessInfo)),
TopicPersons: dynamic(() => import('#/components/topic-person').then((mod) => mod.TopicPerson)),
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,22 @@

import { ContentfulLivePreviewProvider } from '@contentful/live-preview/react';

const ContentfulPreviewProvider = ({ isDraftMode, children }: { isDraftMode: boolean; children: any }) => {
const ContentfulPreviewProvider = ({
isDraftMode,
isContentSourceMapsEnabled,
children,
}: {
isDraftMode: boolean;
isContentSourceMapsEnabled: boolean;
children: any;
}) => {
const previewActive = isDraftMode;
return (
<ContentfulLivePreviewProvider
locale={'en-US'}
enableInspectorMode={previewActive}
enableLiveUpdates={previewActive}
experimental={{ ignoreManuallyTaggedElements: isContentSourceMapsEnabled }}
>
{children}
</ContentfulLivePreviewProvider>
Expand Down
2 changes: 2 additions & 0 deletions components/hero-banner-ctf/hero-banner-ctf-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export const HeroBannerCtfClient: React.FC<{
priority: true,
})
}
size={data.heroSize}
colorPalette={data.colorPalette}
addAttributes={addAttributes}
/>
);
Expand Down
8 changes: 8 additions & 0 deletions components/icons/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,12 @@ export const Icons = {
</g>
</svg>
),
altLogo: (props: React.SVGProps<SVGSVGElement>) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 267.998 267.998" {...props}>
<g fill="#030104">
<path d="M243.378 112.894c0-11.622-21.626-21.782-53.851-27.296l23.658-77.62a5.139 5.139 0 0 0-3.417-6.419l-4.38-1.333a5.137 5.137 0 0 0-6.415 3.417l-24.341 79.834c-12.562-1.456-26.27-2.26-40.627-2.26-60.411 0-109.384 14.187-109.384 31.678 0 6.328 6.408 12.232 17.47 17.178l-.153.33c-20.466 27.539-9.843 68.85-9.843 68.85 3.238 13.608 10.943 23.73 20.83 31.254h-.142v16.004c-4.149-.914-7.909.604-8.516 3.537-.638 3.049 2.376 6.46 6.728 7.62 4.344 1.157 8.385-.368 9.017-3.417l.056-.352.202-18.488c21.307 12.53 49.031 15.752 65.63 16.482l.161 12.301c.632 3.05 4.671 4.574 9.016 3.417 4.351-1.159 7.366-4.571 6.728-7.62-.605-2.934-4.367-4.451-8.516-3.537v-4.369c13.895-.104 45.322-1.905 69.779-14.936l.184 16.75.057.352c.631 3.049 4.671 4.574 9.016 3.417 4.351-1.16 7.365-4.571 6.727-7.62-.606-2.934-4.366-4.451-8.516-3.537v-13.865c11.305-7.712 20.238-18.451 23.795-33.393 0 0 10.564-41.088-9.672-68.627h.004c11.812-5.066 18.715-11.167 18.715-17.732zm-109.372 23.409c-50.637 0-91.678-10.918-91.678-24.398 0-13.467 41.041-24.385 91.678-24.385 13.83 0 26.945.817 38.703 2.276l-1.122 3.674h-.001c-8.16.798-14.539 7.683-14.539 16.055 0 .666.053 1.324.129 1.968h32.002c.08-.644.133-1.302.133-1.968 0-4.005-1.465-7.67-3.881-10.49l2.11-6.925c23.098 4.425 38.133 11.643 38.133 19.795 0 13.479-41.041 24.398-91.667 24.398z" />
<path d="M99.498 113.853c-6.299 0-11.408 5.112-11.408 11.412 0 .469.038.933.094 1.389h22.629c.059-.456.097-.92.097-1.389 0-6.3-5.108-11.412-11.412-11.412zm24.197-19.365a8.262 8.262 0 0 0-8.262 8.262c0 .342.028.677.07 1.006h16.388a8.12 8.12 0 0 0 .066-1.006 8.26 8.26 0 0 0-8.262-8.262z" />
</g>
</svg>
),
};
11 changes: 5 additions & 6 deletions components/navigation/navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ export const Navigation = async (props: NavigationProps) => {
<div className="mx-auto flex w-full max-w-6xl items-center justify-between">
<div className="flex items-center justify-center sm:justify-start">
<Link href="/">
<Icons.logo className="h-8 w-8 md:mr-10" />
<Icons.altLogo className="h-8 w-8 md:mr-10" />
</Link>
<MainMenuDesktop />
</div>
Expand Down Expand Up @@ -235,11 +235,10 @@ export const Navigation = async (props: NavigationProps) => {
<SheetTitle>Notifications</SheetTitle>
</SheetHeader>
<SheetDescription className="mt-2">
This is an example of the shadcn/ui{' '}
<a className="font-bold" href="https://ui.shadcn.com/docs/components/sheet">
Sheet
</a>{' '}
component used to display a notification sidebar.
<strong>
Your inbox is as quiet as a wizard’s spellbook at midnight. Check back later for magical updates,
order statuses, or special offers from The Alchemist’s Vault.
</strong>
</SheetDescription>
</SheetContent>
</Sheet>
Expand Down
19 changes: 13 additions & 6 deletions components/rich-text-ctf/rich-text-ctf.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useMemo } from 'react';
import { documentToReactComponents, Options } from '@contentful/rich-text-react-renderer';
import { BLOCKS, INLINES, Block as RichtextBlock } from '@contentful/rich-text-types';

import { TopicPersonClient } from '#/components/topic-person/topic-person-client';
import { OmitRecursive, tryget } from '#/lib/utils';

import { AssetCtf, AssetFieldsFragment } from '../asset-ctf';
Expand Down Expand Up @@ -51,14 +52,20 @@ export const RichTextCtf = (props: RichTextProps) => {
const id = tryget(() => node.data.target.sys.id);
if (id) {
const entry = entryBlocks.find((block: any) => block!.sys.id === id);
let entryComponent = (
<>
<div>Entry:</div>
<pre>{JSON.stringify(entry, null, 2)}</pre>
</>
);

if (entry) {
return (
<>
<div>Entry:</div>
<pre>{JSON.stringify(entry, null, 2)}</pre>
</>
);
switch (entry.__typename) {
case 'TopicPerson':
return (entryComponent = <TopicPersonClient data={entry} />);
}

return entryComponent;
}
}
return <>{`${node.nodeType} ${id}`}</>;
Expand Down
1 change: 1 addition & 0 deletions components/topic-business-info/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { TopicBusinessInfo } from './topic-business-info';
40 changes: 40 additions & 0 deletions components/topic-business-info/topic-business-info-client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
'use client';

import { ResultOf } from 'gql.tada';

import { getImageChildProps } from '#/components/image-ctf';
import { RichTextCtf } from '#/components/rich-text-ctf';
import { TopicBusinessInfo } from '#/components/ui/topic-business-info';

import { useComponentPreview } from '../hooks/use-component-preview';
import { ComponentTopicBusinessInfoFieldsFragment } from './topic-business-info';

export const TopicBusinessInfoClient: React.FC<{
data: ResultOf<typeof ComponentTopicBusinessInfoFieldsFragment>;
}> = (props) => {
const { data: originalData } = props;
const { data, addAttributes } = useComponentPreview<typeof originalData>(originalData);

return (
<TopicBusinessInfo
name={data.name}
shortDescription={data.shortDescription}
body={
data.body && (
<div {...addAttributes('bodyText')}>
<RichTextCtf {...data.body} />
</div>
)
}
featuredImage={
data.featuredImage &&
getImageChildProps({
data: data.featuredImage,
sizes: '100vw',
priority: true,
})
}
addAttributes={addAttributes}
/>
);
};
Loading

0 comments on commit 08dd65d

Please sign in to comment.