Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: app router [CFISO-1826] #183

Merged
merged 11 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@contentful:registry=https://registry.yarnpkg.com
1 change: 0 additions & 1 deletion .nvmrc

This file was deleted.

8 changes: 2 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,21 +208,17 @@ For custom components, you can find the instructions at our [guide](https://www.

1. Set a unique value for `process.env.CONTENTFUL_PREVIEW_SECRET` in your environment variables. This value should be kept secret and only known to the API route and the CMS.
2. Configure the entry preview URLs in Contentful to match the draft API route's URL structure. This can be done in the Contentful web interface under "Settings" for each content type. For more information see: https://www.contentful.com/help/setup-content-preview/#preview-content-in-your-online-environment
3. The draft mode API route is already written in the app and can be found in `pages/api/draft.page.tsx`. This route checks for a valid secret and slug before redirecting to the corresponding page\*.
4. To disable draft mode, navigate to the `/api/disable-draft` route. This route already exists in the app and can be found in `pages/api/disable-draft.page.tsx`.
3. The draft mode API route is already written in the app and can be found in `src/app/api/enable-draft/route.ts`. This route checks for a valid secret and slug before redirecting to the corresponding page\*.

_\*The `slug` field is optional; When not passed we redirect the page to the root of the domain._

### Adjustments in Contentful

1. Next, you will need to configure your Contentful space to use the correct preview URLs. To do this, go to the "Settings" section of your space, and click on the "Content Preview" tab. From here, you can configure the preview URLs for each of your content models.
2. Edit all content models that need a preview url. We usually expect that to only be the models prefixed with `📄 page -`.
3. Add a new URL with the following format: `https://<your-site>/api/draft?secret=<token>&slug={entry.fields.slug}`. Make sure to replace `<your-site>` with the URL of your Next.js site, and `<token>` with the value of `process.env.CONTENTFUL_PREVIEW_SECRET`. Optionally, a `locale` parameter can be passed.
3. Add a new URL with the following format: `https://<your-site>/api/enable-draft?path=%2F{locale}%2F{entry.fields.slug}&x-contentful-preview-secret=<token>`. Make sure to replace `<your-site>` with the URL of your Next.js site, and `<token>` with the value of `process.env.CONTENTFUL_PREVIEW_SECRET`.
4. Now, when you view an unpublished entry in Contentful, you should see a "Preview" button that will take you to the preview URL for that entry. Clicking this button should show you a preview of the entry on your Next.js site, using the draft API route that we set up earlier.

### Exiting the Content Preview

To disable draft mode, navigate to the `/api/disable-draft` route. This route already exists in the app and can be found in `pages/api/disable-draft.page.tsx`.

$~$

Expand Down
5 changes: 4 additions & 1 deletion codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import { CodegenConfig } from '@graphql-codegen/cli';

const endpointOverride = process.env.CONTENTFUL_GRAPHQL_ENDPOINT;
const productionEndpoint = 'https://graphql.contentful.com/content/v1/spaces';
export const endpoint = `${endpointOverride || productionEndpoint}/${process.env.CONTENTFUL_SPACE_ID}`;
export const endpoint = `${endpointOverride || productionEndpoint}/${
process.env.CONTENTFUL_SPACE_ID
}/environments/${process.env.CONTENTFUL_SPACE_ENVIRONMENT || 'master'}`;

export const config: CodegenConfig = {
overwrite: true,
ignoreNoDocuments: true,
Expand Down
13 changes: 0 additions & 13 deletions config/plugins.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,6 @@
const withBundleAnalyzer = require('@next/bundle-analyzer');
const withPWA = require('next-pwa');

module.exports = [
[
withPWA,
{
pwa: {
disable: process.env.NODE_ENV !== 'production',
dest: `public`,
register: false,
swSrc: './service-worker.js',
publicExcludes: ['!favicon/**/*'],
},
},
],
[
withBundleAnalyzer,
{
Expand Down
10 changes: 0 additions & 10 deletions next-i18next.config.js

This file was deleted.

16 changes: 10 additions & 6 deletions next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ const nextComposePlugins = require('next-compose-plugins');

const headers = require('./config/headers');
const plugins = require('./config/plugins');
const { i18n } = require('./next-i18next.config.js');

/**
* https://github.com/cyrilwanner/next-compose-plugins/issues/59
Expand All @@ -15,7 +14,6 @@ const { withPlugins } = nextComposePlugins.extend(() => ({}));
* documentation: https://nextjs.org/docs/api-reference/next.config.js/introduction
*/
module.exports = withPlugins(plugins, {
i18n,
/**
* add the environment variables you would like exposed to the client here
* documentation: https://nextjs.org/docs/api-reference/next.config.js/environment-variables
Expand All @@ -42,7 +40,6 @@ module.exports = withPlugins(plugins, {
// swcMinify: true,

poweredByHeader: false,
reactStrictMode: false,
compress: true,

/**
Expand All @@ -57,11 +54,18 @@ module.exports = withPlugins(plugins, {
* Settings are the defaults
*/
images: {
domains: ['images.ctfassets.net','images.eu.ctfassets.net'],
remotePatterns: [
{
protocol: 'https',
hostname: 'images.ctfassets.net',
},
{
protocol: 'https',
hostname: 'images.eu.ctfassets.net',
},
],
},

pageExtensions: ['page.tsx', 'page.ts', 'page.jsx', 'page.js'],

webpack(config) {
config.module.rules.push({
test: /\.svg$/,
Expand Down
33 changes: 17 additions & 16 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,56 +27,57 @@
},
"license": "MIT",
"dependencies": {
"@contentful/f36-icons": "^4.23.2",
"@contentful/f36-tokens": "^4.0.1",
"@contentful/f36-icons": "^4.29.0",
"@contentful/f36-tokens": "^4.0.5",
"@contentful/live-preview": "^4.5.6",
"@contentful/rich-text-react-renderer": "^15.16.2",
"@next/bundle-analyzer": "^13.0.4",
"@next/bundle-analyzer": "^14.2.6",
"dotenv": "^16.0.3",
"graphql": "^16.6.0",
"next": "^13.4.1",
"i18next-browser-languagedetector": "^8.0.0",
"i18next-resources-to-backend": "^1.2.1",
"next": "^14.2.6",
"next-compose-plugins": "^2.2.1",
"next-i18next": "^12.1.0",
"next-pwa": "^5.6.0",
"next-seo": "^5.15.0",
"next-sitemap": "^3.1.32",
"react": "18.2.0",
"react-dom": "18.2.0",
"next-i18n-router": "^5.5.1",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-focus-lock": "^2.9.2",
"sharp": "^0.32.6"
"react-i18next": "^15.0.1",
"sharp": "^0.33.5"
},
"devDependencies": {
"@babel/eslint-parser": "^7.19.1",
"@contentful/rich-text-types": "^16.0.2",
"@graphql-codegen/cli": "2.13.12",
"@graphql-codegen/cli": "5.0.2",
"@graphql-codegen/client-preset": "1.1.4",
"@graphql-codegen/introspection": "2.2.1",
"@graphql-codegen/typescript-graphql-request": "^6.2.0",
"@svgr/webpack": "^6.5.1",
"@tailwindcss/typography": "^0.5.8",
"@types/negotiator": "^0.6.3",
"@types/node": "18.11.9",
"@types/react": "18.0.25",
"@types/react-dom": "18.0.9",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.32.0",
"autoprefixer": "^10.4.13",
"eslint": "8.26.0",
"eslint-config-next": "13.0.1",
"eslint-config-next": "14.2.7",
"eslint-config-prettier": "^8.3.0",
"eslint-import-resolver-typescript": "^2.4.0",
"eslint-plugin-import": "^2.23.4",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.22.0",
"eslint-plugin-react-hooks": "^4.2.0",
"husky": "^8.0.2",
"i18next": "^21.9.2",
"i18next": "^23.14.0",
"i18next-http-backend": "^1.4.4",
"lint-staged": "^13.0.3",
"postcss": "^8.4.19",
"prettier": "^2.7.1",
"prettier-plugin-tailwindcss": "^0.2.1",
"tailwind-merge": "^1.8.0",
"tailwindcss": "^3.2.4",
"typescript": "4.9.3",
"typescript-graphql-request": "^4.4.6"
"typescript": "5.5.4"
}
}
5 changes: 5 additions & 0 deletions src/app/[locale]/[...notFound]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { notFound } from 'next/navigation';

export default function NotFoundCatchAll() {
notFound();
}
71 changes: 71 additions & 0 deletions src/app/[locale]/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { draftMode } from 'next/headers';
import { notFound } from 'next/navigation';

import { ArticleContent, ArticleHero, ArticleTileGrid } from '@src/components/features/article';
import { Container } from '@src/components/shared/container';
import initTranslations from '@src/i18n';
import { client, previewClient } from '@src/lib/client';

export async function generateStaticParams({
params: { locale },
}: {
params: { locale: string };
}): Promise<BlogPageProps['params'][]> {
const gqlClient = client;
const { pageBlogPostCollection } = await gqlClient.pageBlogPostCollection({ locale, limit: 100 });

if (!pageBlogPostCollection?.items) {
throw new Error('No blog posts found');
}

return pageBlogPostCollection.items
.filter((blogPost): blogPost is NonNullable<typeof blogPost> => Boolean(blogPost?.slug))
.map(blogPost => {
return {
locale,
slug: blogPost.slug!,
};
});
}

interface BlogPageProps {
params: {
locale: string;
slug: string;
};
}

export default async function Page({ params: { locale, slug } }: BlogPageProps) {
const { isEnabled: preview } = draftMode();
const gqlClient = preview ? previewClient : client;
const { t } = await initTranslations({ locale });
const { pageBlogPostCollection } = await gqlClient.pageBlogPost({ locale, slug, preview });
const { pageLandingCollection } = await gqlClient.pageLanding({ locale, preview });
const landingPage = pageLandingCollection?.items[0];
const blogPost = pageBlogPostCollection?.items[0];
const relatedPosts = blogPost?.relatedBlogPostsCollection?.items;
const isFeatured = Boolean(
blogPost?.slug && landingPage?.featuredBlogPost?.slug === blogPost.slug,
);

if (!blogPost) {
notFound();
}

return (
<>
<Container>
<ArticleHero article={blogPost} isFeatured={isFeatured} isReversedLayout={true} />
</Container>
<Container className="mt-8 max-w-4xl">
<ArticleContent article={blogPost} />
</Container>
{relatedPosts && (
<Container className="mt-8 max-w-5xl">
<h2 className="mb-4 md:mb-6">{t('article.relatedArticles')}</h2>
<ArticleTileGrid className="md:grid-cols-2" articles={relatedPosts} />
</Container>
)}
</>
);
}
76 changes: 76 additions & 0 deletions src/app/[locale]/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { dir } from 'i18next';
import type { Metadata, Viewport } from 'next';
import { Urbanist } from 'next/font/google';
import { draftMode } from 'next/headers';

import { ContentfulPreviewProvider } from '@src/components/features/contentful';
import TranslationsProvider from '@src/components/shared/i18n/TranslationProvider';
import { Footer } from '@src/components/templates/footer';
import { Header } from '@src/components/templates/header';
import initTranslations from '@src/i18n';
import { locales } from '@src/i18n/config';

export async function generateMetadata() {
const metatadata: Metadata = {
metadataBase: new URL(process.env.NEXT_PUBLIC_BASE_URL!),
} as Metadata;

return metatadata;
}

export const viewport: Viewport = {
themeColor: '#ffffff',
};

export async function generateStaticParams(): Promise<LayoutProps['params'][]> {
return locales.map(locale => ({ locale }));
}

const urbanist = Urbanist({ subsets: ['latin'], variable: '--font-urbanist' });

const allowedOriginList = [
'https://app.contentful.com',
'https://app.eu.contentful.com',
'https://app.flinkly.com',
'https://app.eu.flinkly.com',
'https://app.quirely.com',
'https://app.eu.quirely.com',
'http://localhost:3001',
];

interface LayoutProps {
children: React.ReactNode;
params: { locale: string };
}

export default async function PageLayout({ children, params }: LayoutProps) {
const { isEnabled: preview } = draftMode();
const { locale } = params;
const { resources } = await initTranslations({ locale });

return (
<html lang={locale} dir={dir(locale)}>
<head>
<link rel="mask-icon" href="/favicons/safari-pinned-tab.svg" color="#5bbad5" />
</head>

<body>
<TranslationsProvider locale={locale} resources={resources}>
<ContentfulPreviewProvider
locale={locale}
enableInspectorMode={preview}
enableLiveUpdates={preview}
targetOrigin={allowedOriginList}
>
<main className={`${urbanist.variable} font-sans`}>
<Header />
{children}
<Footer />
</main>
<div id="portal" className={`${urbanist.variable} font-sans`} />
</ContentfulPreviewProvider>
</TranslationsProvider>
</body>
</html>
);
}
24 changes: 24 additions & 0 deletions src/app/[locale]/not-found.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { headers } from 'next/headers';
import Link from 'next/link';
import { Trans } from 'react-i18next/TransWithoutContext';

import { Container } from '@src/components/shared/container';
import initTranslations from '@src/i18n';
import { defaultLocale } from '@src/i18n/config';

export default async function NotFound() {
const headersList = headers();
const locale = headersList.get('x-next-i18n-router-locale') || defaultLocale;
const { t } = await initTranslations({ locale });

return (
<Container>
<h1 className="h2">{t('notFound.title')}</h1>
<p className="mt-4">
<Trans i18nKey="notFound.description" t={t}>
<Link className="text-blue500" href="/" />
</Trans>
</p>
</Container>
);
}
Loading
Loading