-
Notifications
You must be signed in to change notification settings - Fork 1
Revise Uniform SDK documentation for Next.js App Router #5
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
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
71bd594
Revise Uniform SDK documentation for Next.js App Router
alexshyba 75cc8b1
Add Next.js app router SDK v2 migration guide
alexshyba 3268fb4
Rename app-router-sdk-v2-migration to app-router-sdk-v2-migration.mdc
alexshyba 14afd81
Revise Next.js app router SDK v2 migration guide
alexshyba 9e2c101
Migrate to @uniformdev/next-app-router package
alexshyba 79aa2dc
fix: approuter sdk v2 rules
alexshyba File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,353 @@ | ||
| --- | ||
| alwaysApply: true | ||
| --- | ||
| # Next.js app router SDK v2 migration guide | ||
|
|
||
| ### Install Packages | ||
|
|
||
| Install the following packages with npm: `npm i @uniformdev/next-app-router --save` | ||
|
|
||
| ```jsx | ||
| "@uniformdev/next-app-router": "20.48.0", | ||
| ``` | ||
|
|
||
| It’s best to reference all three in your `package.json` to avoid version mismatches as new packages are released. | ||
|
|
||
| ### New Middleware | ||
|
|
||
| Middleware is now required to route requests and evaluate compositions, the basic structure should look like this: | ||
|
|
||
| ```jsx | ||
| import { uniformMiddleware } from '@uniformdev/next-app-router/middleware'; | ||
|
|
||
| export default uniformMiddleware(); | ||
|
|
||
| // IMPORTANT: runtime: "experimental-edge" is required for the middleware to work correctly for preview in Next.js 16 | ||
| export const config = { | ||
| matcher: [ | ||
| "/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)", | ||
| ], | ||
| runtime: "experimental-edge", | ||
| }; | ||
|
|
||
| ``` | ||
|
|
||
| (Optionally) You can also rewrite where the middleware will redirect to. This can be useful if you are trying to code split your components and do not want to define them all on every page. | ||
|
|
||
| ```jsx | ||
| import { handleUniformRoute } from "@uniformdev/next-app-router/middleware"; | ||
| import { NextRequest } from "next/server"; | ||
|
|
||
| export default (request: NextRequest) => { | ||
| return handleUniformRoute({ | ||
| request, | ||
| rewrite: async ({ code, route }) => { | ||
| if (route.compositionApiResponse.composition.type === "page") { | ||
| return `/rewrite-here-instead/${code}`; | ||
| } | ||
|
|
||
| return `/default-rewrite/${code}`; | ||
| }, | ||
| }); | ||
| }; | ||
|
|
||
| ``` | ||
|
|
||
| ### Server config is optional now | ||
|
|
||
| If you have a `uniform.server.config.ts` or `uniform.server.config.js` in the application root, you can remove it if it matches the following default: | ||
|
|
||
| ```ts | ||
| import { UniformServerConfig } from "@uniformdev/next-app-router/config"; | ||
|
|
||
| const config: UniformServerConfig = { | ||
| defaultConsent: true, | ||
| quirkSerialization: true, | ||
| middlewareRuntimeCache: true, | ||
| playgroundPath: "/playground", | ||
| }; | ||
|
|
||
| export default config; | ||
| ``` | ||
|
|
||
| ### New File Structure | ||
|
|
||
| Before the expectation was that you could have a catch all page route, now by default we expect you to have the following file structure: | ||
|
|
||
| - app | ||
| - uniform | ||
| - [code] | ||
| - page.tsx | ||
|
|
||
| The default implementation of `page.tsx` should be: | ||
|
|
||
| ```jsx | ||
| import { | ||
| UniformComposition, | ||
| UniformPageParameters, | ||
| createUniformStaticParams, | ||
| resolveRouteFromCode, | ||
| } from "@uniformdev/next-app-router"; | ||
| import { CustomUniformClientContext } from "@/components/CustomUniformClientContext"; | ||
|
|
||
| import { resolveComponent } from "../../../components/resolveComponent"; | ||
|
|
||
| export const generateStaticParams = async () => { | ||
| return createUniformStaticParams({ | ||
| // paths: ["/"], | ||
| // Important: for localized sites, you need to add the locales to the paths | ||
| paths: ["/en"], | ||
| }); | ||
| }; | ||
|
|
||
| export default async function UniformPage(props: UniformPageParameters) { | ||
| const { code } = await props.params; | ||
| return ( | ||
| <UniformComposition | ||
| code={code} | ||
| resolveRoute={resolveRouteFromCode} | ||
| resolveComponent={resolveComponent} | ||
| clientContextComponent={CustomUniformClientContext} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| ``` | ||
|
|
||
| ## New Playground implementation | ||
|
|
||
| - app | ||
| - playground | ||
| - [code] | ||
| - page.tsx | ||
|
|
||
| The default implementation of `page.tsx` should be: | ||
|
|
||
| ```jsx | ||
| import { | ||
| createUniformPlaygroundStaticParams, | ||
| PlaygroundParameters, | ||
| resolvePlaygroundRoute, | ||
| UniformPlayground, | ||
| } from "@uniformdev/next-app-router"; | ||
|
|
||
| import { resolveComponent } from "@/components/resolveComponent"; | ||
|
|
||
| export const generateStaticParams = async () => { | ||
| return createUniformPlaygroundStaticParams({ | ||
| // Important: for localized sites, you need to add the locales to the paths | ||
| paths: ["/en"], | ||
| }); | ||
| }; | ||
|
|
||
| export default async function PlaygroundPage({ params }: PlaygroundParameters) { | ||
| const { code } = await params; | ||
| return ( | ||
| <UniformPlayground | ||
| code={code} | ||
| resolveRoute={resolvePlaygroundRoute} | ||
| resolveComponent={resolveComponent} | ||
| /> | ||
| ); | ||
| } | ||
| ``` | ||
|
|
||
| ### ComponentProps | ||
|
|
||
| This type was reworked a little, the older interface used to look like this: | ||
|
|
||
| ```jsx | ||
| export type ComponentProps<TProps = unknown, TSlotNames extends string = string> = TProps & { | ||
| component: ComponentInstance; | ||
| context: CompositionContext; | ||
| slots: Record<TSlotNames, SlotDefinition>; | ||
| slotName: string | undefined; | ||
| slotIndex: number | undefined; | ||
| }; | ||
| ``` | ||
|
|
||
| Now it looks like this: | ||
|
|
||
| ``` | ||
| export type ComponentProps< | ||
| TParameters extends Record<string, ComponentParameter> | unknown = Record<string, ComponentParameter>, | ||
| TSlotNames extends string = string, | ||
| > = { | ||
| type: string; | ||
| variant: string | null; | ||
| slots: Record<TSlotNames, SlotDefinition>; | ||
| parameters: TParameters; | ||
| component: ComponentContext; | ||
| context: CompositionContext; | ||
| }; | ||
| ``` | ||
|
|
||
| The following changes were made: | ||
|
|
||
| - You are no longer passed the entire component | ||
| - Parameters are no longer added to the top level of the object | ||
| - Parameters are now of type `Record<string, ComponentParameter>` | ||
|
|
||
| ```jsx | ||
| type HeroParamters = { | ||
| title: string | ||
| } | ||
|
|
||
| type HeroProps = ComponentProps<HeroParameters> | ||
|
|
||
| const Hero = ({ title }: HeroProps) => { | ||
| return ( | ||
| <h1>{title}</h1> | ||
| ) | ||
| } | ||
| ``` | ||
|
|
||
| Should now be | ||
|
|
||
| ```jsx | ||
| type HeroParamters = { | ||
| title: ComponentParameter<string> | ||
| } | ||
|
|
||
| type HeroProps = ComponentProps<HeroParameters> | ||
|
|
||
| const Hero = ({ parameters: { title }}: HeroProps) => { | ||
| return ( | ||
| <h1>{title.value}</h1> | ||
| ) | ||
| } | ||
| ``` | ||
|
|
||
| This change was made to keep the full parameter intact and hopefully be able to use this data to implement conditional values. | ||
|
|
||
| ### UniformSlot | ||
|
|
||
| The slot definition type used to look like this: | ||
|
|
||
| ```jsx | ||
| export type SlotDefinition = { | ||
| name: string; | ||
| items: PropsWithChildren['children'][]; | ||
| }; | ||
| ``` | ||
|
|
||
| Now it looks like this: | ||
|
|
||
| ```jsx | ||
| export type SlotDefinition = { | ||
| name: string; | ||
| items: ({ | ||
| _id: string; | ||
| [CANVAS_PERSONALIZATION_PARAM]: VariantMatchCriteria | undefined; | ||
| component: ReactNode; | ||
| } | null)[]; | ||
| }; | ||
| ``` | ||
|
|
||
| Slots can be rendered with a single prop: | ||
|
|
||
| ```jsx | ||
| <UniformSlot slot={slots.header} /> | ||
| ``` | ||
|
|
||
| If you would like to extract all of the components that make up a slot, you can do the following: | ||
|
|
||
| ```jsx | ||
| const headerItems = slots.header.items.map((item) => item?.component); | ||
| ``` | ||
|
|
||
| If you would like to resolve the ComponentInstance data for the slot, you can use a component cache to resolve these. Currently this requires manual configuration as it wasnt working correctly when exported from the package: | ||
|
|
||
| Define your cache, in a file like `lib/cache.ts` | ||
|
|
||
| ```jsx | ||
| import { createCompositionCache } from "@uniformdev/next-app-router"; | ||
|
|
||
| export const compositionCache = createCompositionCache(); | ||
| ``` | ||
|
|
||
| Pass this cache to `UniformComposition` as a prop: | ||
|
|
||
| ```jsx | ||
| <UniformComposition | ||
| {...result} | ||
| resolveComponent={resolveComponent} | ||
| compositionCache={compositionCache} | ||
| /> | ||
| ``` | ||
|
|
||
| Then in your components, you can resolve component data including components that make up a slot: | ||
|
|
||
| ```jsx | ||
| import { | ||
| ComponentProps, | ||
| UniformSlot, | ||
| } from "@uniformdev/next-app-router/component"; | ||
|
|
||
| import { QuirkButton } from "./custom/QuirkButton"; | ||
| import { compositionCache } from "@/lib/cache"; | ||
|
|
||
| export type PageProps = unknown; | ||
| export type PageSlots = "content" | "header" | "footer"; | ||
|
|
||
| export const Page = ({ | ||
| slots, | ||
| context, | ||
| }: ComponentProps<PageProps, PageSlots>) => { | ||
| const headerItems = slots.header.items.map((item) => { | ||
| const resolved = compositionCache.getUniformComponent({ | ||
| componentId: item!._id, | ||
| compositionId: context._id, | ||
| }); | ||
|
|
||
| return resolved; | ||
| }); | ||
|
|
||
| return ( | ||
| <div>This is a page.</div> | ||
| ); | ||
| }; | ||
|
|
||
| ``` | ||
|
|
||
| ### Adapter Components | ||
|
|
||
| An adapter layer also exists to easy the transition from V1 to V2, use the built in `createAdapterResolveComponentFunction` function and pass the component mappings for your site. | ||
|
|
||
| ```jsx | ||
| import { createAdapterResolveComponentFunction } from '@uniformdev/next-app-router/compat'; | ||
|
|
||
| import * as mappings from './mappings'; | ||
|
|
||
| export const resolveComponent = createAdapterResolveComponentFunction({ mappings }); | ||
|
|
||
| ``` | ||
|
|
||
| Adjust your mappings to use `ResolveComponentResultWithType` and adding `mode: 'adapted'` to each definition that you are adapting. | ||
|
|
||
| ```tsx | ||
| import type { | ||
| ComponentProps, | ||
| ResolveComponentResultWithType, | ||
| } from '@uniformdev/next-app-router/compat'; | ||
|
|
||
| type PageComponentParameters = { | ||
| text: string; | ||
| }; | ||
|
|
||
| const PageComponent = ({ text }: ComponentProps<PageComponentParameters>) => { | ||
| return <div>{text}</div>; | ||
| }; | ||
|
|
||
| export const pageMapping: ResolveComponentResultWithType = { | ||
| type: 'page', | ||
| component: PageComponent, | ||
| mode: 'adapted', | ||
| }; | ||
| ``` | ||
|
|
||
| Use `UniformText` and `UniformSlot` components from the same `compat` export. | ||
|
|
||
| ### Clients | ||
|
|
||
| - `getDefaultRouteClient` is now `getRouteClient` | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.