diff --git a/examples/nextjs-app-router-localization-starter/next.config.js b/examples/nextjs-app-router-localization-starter/next.config.js index 9b4e711f..5a93645f 100644 --- a/examples/nextjs-app-router-localization-starter/next.config.js +++ b/examples/nextjs-app-router-localization-starter/next.config.js @@ -1,14 +1,7 @@ const withNextIntl = require("next-intl/plugin")(); +const { withUniformConfig } = require("@uniformdev/canvas-next-rsc/config"); /** @type {import('next').NextConfig} */ -const nextConfig = { - typescript: { - // !! WARN !! - // Dangerously allow production builds to successfully complete even if - // your project has type errors. - // !! WARN !! - ignoreBuildErrors: true, - }, -}; +const nextConfig = {}; -module.exports = withNextIntl(nextConfig); +module.exports = withNextIntl(withUniformConfig(nextConfig)); diff --git a/examples/nextjs-app-router-localization-starter/package-lock.json b/examples/nextjs-app-router-localization-starter/package-lock.json index b3afc362..e343f8b8 100644 --- a/examples/nextjs-app-router-localization-starter/package-lock.json +++ b/examples/nextjs-app-router-localization-starter/package-lock.json @@ -11,6 +11,7 @@ "@types/node": "20.10.6", "@types/react": "18.2.40", "@types/react-dom": "18.2.17", + "@uniformdev/canvas": "^19.178.0", "@uniformdev/canvas-next-rsc": "^19.178.0", "@uniformdev/project-map": "^19.178.0", "autoprefixer": "10.4.16", diff --git a/examples/nextjs-app-router-localization-starter/package.json b/examples/nextjs-app-router-localization-starter/package.json index b90ea9eb..df8320b6 100644 --- a/examples/nextjs-app-router-localization-starter/package.json +++ b/examples/nextjs-app-router-localization-starter/package.json @@ -15,6 +15,7 @@ "@types/node": "20.10.6", "@types/react": "18.2.40", "@types/react-dom": "18.2.17", + "@uniformdev/canvas": "^19.178.0", "@uniformdev/canvas-next-rsc": "^19.178.0", "@uniformdev/project-map": "^19.178.0", "autoprefixer": "10.4.16", diff --git a/examples/nextjs-app-router-localization-starter/src/app/[locale]/[...path]/page.tsx b/examples/nextjs-app-router-localization-starter/src/app/[locale]/[...path]/page.tsx index 6d9ac873..ade1b2d8 100644 --- a/examples/nextjs-app-router-localization-starter/src/app/[locale]/[...path]/page.tsx +++ b/examples/nextjs-app-router-localization-starter/src/app/[locale]/[...path]/page.tsx @@ -1,20 +1,33 @@ import React from "react"; +import { Metadata } from "next"; +import { notFound } from "next/navigation"; import { unstable_setRequestLocale } from "next-intl/server"; - import Composition from "@/components/composition"; -import { getStaticParams } from "@/lib/uniform/utils"; +import { + getPageMetaData, + getStaticParams, + isRouteWithoutErrors, + getRouteData, +} from "@/lib/uniform/utils"; + +export type RouteParams = { path: string[]; locale: string }; + +export async function generateMetadata(props: { + params: RouteParams; +}): Promise { + const route = await getRouteData(props.params); + if (!isRouteWithoutErrors(route)) return notFound(); + return getPageMetaData(route); +} export async function generateStaticParams() { return await getStaticParams(); } -type Props = { - params: { locale: string; path: Array }; -}; - -export default function Home({ params: { locale, path } }: Props) { +export default function Home({ params }: { params: RouteParams }) { + const { locale } = params; unstable_setRequestLocale(locale); - return ; + return ; } export const dynamic = "force-static"; diff --git a/examples/nextjs-app-router-localization-starter/src/app/[locale]/page.tsx b/examples/nextjs-app-router-localization-starter/src/app/[locale]/page.tsx index cde94555..103de937 100644 --- a/examples/nextjs-app-router-localization-starter/src/app/[locale]/page.tsx +++ b/examples/nextjs-app-router-localization-starter/src/app/[locale]/page.tsx @@ -1,15 +1,30 @@ -import Composition from "@/components/composition"; +import { Metadata } from "next"; import { unstable_setRequestLocale } from "next-intl/server"; +import { notFound } from "next/navigation"; +import { retrieveRoute } from "@uniformdev/canvas-next-rsc"; +import Composition from "@/components/composition"; +import { getPageMetaData, isRouteWithoutErrors } from "@/lib/uniform/utils"; type Props = { params: { locale: string }; }; +export async function generateMetadata(props: { + params: { path: string[]; locale: string }; +}): Promise { + const { locale } = props.params; + const params = { params: { path: locale } }; + const route = await retrieveRoute(params); + + if (!isRouteWithoutErrors(route)) return notFound(); + return getPageMetaData(route); +} + export default function IndexPage({ params: { locale } }: Props) { // Enable static rendering unstable_setRequestLocale(locale); - return ; + return ; } export const dynamic = "force-static"; diff --git a/examples/nextjs-app-router-localization-starter/src/app/[locale]/playground/page.tsx b/examples/nextjs-app-router-localization-starter/src/app/[locale]/playground/page.tsx new file mode 100644 index 00000000..75cc2c87 --- /dev/null +++ b/examples/nextjs-app-router-localization-starter/src/app/[locale]/playground/page.tsx @@ -0,0 +1,11 @@ +import { + UniformPlayground, + UniformPlaygroundProps, +} from "@uniformdev/canvas-next-rsc"; +import { resolveComponent } from "@/lib/uniform/componentResolver"; + +export default function PlaygroundPage(props: { + searchParams: UniformPlaygroundProps["searchParams"]; +}) { + return ; +} diff --git a/examples/nextjs-app-router-localization-starter/src/app/api/preview/route.ts b/examples/nextjs-app-router-localization-starter/src/app/api/preview/route.ts index 0e1cc0d5..c4951165 100644 --- a/examples/nextjs-app-router-localization-starter/src/app/api/preview/route.ts +++ b/examples/nextjs-app-router-localization-starter/src/app/api/preview/route.ts @@ -4,6 +4,9 @@ import { createPreviewOPTIONSRouteHandler, } from "@uniformdev/canvas-next-rsc/handler"; -export const GET = createPreviewGETRouteHandler(); +export const GET = createPreviewGETRouteHandler({ + playgroundPath: "/playground", +}); + export const POST = createPreviewPOSTRouteHandler(); export const OPTIONS = createPreviewOPTIONSRouteHandler(); diff --git a/examples/nextjs-app-router-localization-starter/src/components/composition.tsx b/examples/nextjs-app-router-localization-starter/src/components/composition.tsx index d6c2e7e0..a60b5f38 100644 --- a/examples/nextjs-app-router-localization-starter/src/components/composition.tsx +++ b/examples/nextjs-app-router-localization-starter/src/components/composition.tsx @@ -1,28 +1,24 @@ -import {retrieveRoute, UniformComposition} from '@uniformdev/canvas-next-rsc'; -import {notFound} from 'next/navigation'; -import {resolveComponent} from '../lib/uniform/componentResolver'; +import { UniformComposition } from "@uniformdev/canvas-next-rsc"; +import { notFound } from "next/navigation"; +import { resolveComponent } from "@/lib/uniform/componentResolver"; +import { getRouteData } from "@/lib/uniform/utils"; -export default async function Composition({ - locale, - path = '/' -}: { +export default async function Composition(params: { locale: string; - path: string; + path: string[]; }) { - const routePath = path !== '/' ? `${locale}/${path}` : locale; - const params = {params: {path: routePath}}; - const route = await retrieveRoute(params); - + const { path } = params; + const route = await getRouteData(params); if ( - route.type === 'notFound' || - (route.type === 'composition' && - route.compositionApiResponse.errors?.some((e) => e.type === 'data')) + route.type === "notFound" || + (route.type === "composition" && + route.compositionApiResponse.errors?.some((e) => e.type === "data")) ) { // if we got data errors, we could not resolve a data resource and we choose to return a 404 instead of partial content // eslint-disable-next-line no-console - console.log('Returning 404 because data errors: ', { + console.log("Returning 404 because data errors: ", { path, - route + route, }); return notFound(); } @@ -32,7 +28,7 @@ export default async function Composition({ mode="static" resolveComponent={resolveComponent} route={route} - {...params} + params={{ path: Array.isArray(path) ? path.join("/") : path }} /> ); } diff --git a/examples/nextjs-app-router-localization-starter/src/components/header.tsx b/examples/nextjs-app-router-localization-starter/src/components/header.tsx index defd2761..718260db 100644 --- a/examples/nextjs-app-router-localization-starter/src/components/header.tsx +++ b/examples/nextjs-app-router-localization-starter/src/components/header.tsx @@ -4,11 +4,13 @@ import Link from "next/link"; export const Header = () => { const locale = useLocale(); + // @ts-expect-error const t = useTranslations("Global"); return ( <> + {/* @ts-expect-error */} {t("homeLink")}
diff --git a/examples/nextjs-app-router-localization-starter/src/i18n.ts b/examples/nextjs-app-router-localization-starter/src/i18n.ts index 56232fa7..d9f0e070 100644 --- a/examples/nextjs-app-router-localization-starter/src/i18n.ts +++ b/examples/nextjs-app-router-localization-starter/src/i18n.ts @@ -2,7 +2,7 @@ import { getRequestConfig } from "next-intl/server"; import { notFound } from "next/navigation"; // Can be imported from a shared config -const locales = ['en', 'de']; +const locales = ['en', 'de', 'ja']; /** Configure i18n static translation messages */ export default getRequestConfig(async ({ locale }) => { diff --git a/examples/nextjs-app-router-localization-starter/src/lib/uniform/mappings.ts b/examples/nextjs-app-router-localization-starter/src/lib/uniform/mappings.ts index 3b2e316d..5f497a1a 100644 --- a/examples/nextjs-app-router-localization-starter/src/lib/uniform/mappings.ts +++ b/examples/nextjs-app-router-localization-starter/src/lib/uniform/mappings.ts @@ -1,3 +1,3 @@ // components will be registered here export { pageMapping } from "@/components/page/mapping"; -export { heroMapping } from "@/components/hero/mapping"; +export { heroMapping } from "@/components/hero/mapping"; \ No newline at end of file diff --git a/examples/nextjs-app-router-localization-starter/src/lib/uniform/utils.ts b/examples/nextjs-app-router-localization-starter/src/lib/uniform/utils.ts index 773a1e96..60b75193 100644 --- a/examples/nextjs-app-router-localization-starter/src/lib/uniform/utils.ts +++ b/examples/nextjs-app-router-localization-starter/src/lib/uniform/utils.ts @@ -1,30 +1,80 @@ +import { flattenValues, LocaleClient, LocalesGetResponse, ResolvedRouteGetResponse, RootComponentInstance, RouteGetResponseEdgehancedComposition } from "@uniformdev/canvas"; import { ProjectMapClient } from "@uniformdev/project-map"; +import { retrieveRoute } from "@uniformdev/canvas-next-rsc"; +import { RouteParams } from "@/app/[locale]/[...path]/page"; -export async function getStaticParams(defaultLocale: string = "en") { +export async function getRouteData(params: RouteParams): Promise { + const { path, locale } = params; + const routePath = Array.isArray(path) ? `${locale}/${path.join("/")}` : locale; + const data = { params: { path: routePath } }; + const routeData = await retrieveRoute(data); + return routeData; +} + +export const isRouteWithoutErrors = ( + route: ResolvedRouteGetResponse +): route is RouteGetResponseEdgehancedComposition => + "compositionApiResponse" in route && + route.compositionApiResponse !== undefined && + "composition" in route.compositionApiResponse; + +export const getPageMetaData = (compositionResponse: RouteGetResponseEdgehancedComposition) => { + const { composition } = compositionResponse.compositionApiResponse || {}; + const compositionParameters = flattenValues(composition); + const { + metaTitle, + metaDescription, + pageKeywords, + } = compositionParameters || {}; + + return { + metadataBase: new URL(process.env.SITE_URL || 'http://localhost:3000'), + title: metaTitle as string ?? 'Home', + description: metaDescription as string, + keywords: pageKeywords as string, + }; +}; + +export async function getLocales(): Promise { + const client = new LocaleClient({ + apiKey: process.env.UNIFORM_API_KEY, + projectId: process.env.UNIFORM_PROJECT_ID, + }); + + const localeResponse: LocalesGetResponse = await client.get(); + + const { results: localeDefinitions } = localeResponse; + return localeDefinitions.map((locale) => locale.locale); +} + +export async function getStaticParams() { const client = getProjectMapClient(); const { nodes } = await client.getNodes({}); const resolvedPaths: { path: string[], locale: string }[] = []; - + const locales = await getLocales(); if (nodes) { for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; const path = node.path.replace("/:locale", "/"); if (path !== "/") { - resolvedPaths.push({ path: path.split('/').filter(Boolean), locale: defaultLocale }); - - // adding localized paths - const locales = node.locales; - if (locales) { - Object.keys(locales).forEach((locale) => { - resolvedPaths.push({ path: locales[locale].pathSegment.split('/').filter(Boolean), locale: locale }); - }) + const nodeLocales = node.locales; + locales.forEach((locale: string) => { + // if there are any locales defined for the node + if (nodeLocales && Object.keys(nodeLocales).indexOf(locale) !== -1) { + const nodePath = nodeLocales[locale].pathSegment.split('/').filter(Boolean); + resolvedPaths.push({ path: nodePath, locale: locale }); + } else { + resolvedPaths.push({ path: path.split('/').filter(Boolean), locale: locale }); + } } + ); } } - } + }; return resolvedPaths; -}; +} + export const getProjectMapClient = () => { const manifestClient = new ProjectMapClient({