Skip to main content

Tutorial with Next.js

Let's get started with Uniform Canvas and Next.js! This tutorial assumes you:

  • already have an account at uniform.app (free or otherwise)
  • have created a Project in Uniform Optimize to store your Canvas compositions
  • have installed a recent version of Node.js on your machine

Activate Canvas on your Uniform Project#

  1. Visit https://uniform.app and navigate to the project you wish to enable Canvas on
  2. Click Settings, then Integrations
  3. Locate the Canvas integration and add it to your project
  4. You will be taken to the Canvas Settings screen. Click the Component Library link at the bottom to proceed to the component library.

Create a Canvas component type#

In order to have something to render with Canvas, we need to define a component. This is very similar in concept to defining a React component's props type: what data does this component need to render, and which systems do we get the data from. To keep this as simple as possible we'll source content from a local text parameter, but keep in mind for later that the real power of Canvas is linking parameters to other data sources.

  1. Click (+) in the Component Library to add a new component. Name it 'Hello World' or anything you like.
  2. Make sure the Composition Component checkbox is set to ON, so that we can use this component as the root of a composition later.
  3. On the component editor, click (+) next to Parameters to add a new parameter
  4. Give the parameter a name such as 'Greeting', choose Text as the type, and give it a public ID that you will remember later such as 'greeting'. The public ID will be this parameter's name in the API and your code, so pick a nice variable name.
  5. Click Save and Close or type ctrl+shift+s to save and close the component

Create an instance of your Canvas component#

Now that we've defined a component type, we need to create an instance of that type - a Composition - that we can render in Next.

  1. Click Compositions in the left navigation, then (+) to add a new composition
  2. Pick the type we just added, and give it a name you will remember later such as Home.
  3. Enter some text in the greeting parameter (i.e. 'PC load letter? What does that mean?')
  4. Set the slug in the header to / (or some other value you like, but remember it for later)
  5. Click Save and publish or type ctrl+shift+s to save it
  6. Exit the Canvas editor by clicking < Compositions in the header, or using the back button

Create an API key to read your components#

  1. Click your team name (e.g. your@email.com's team) in the header to access your team
  2. Click Settings to access team settings
  3. Click API Keys. Create a new API key and give it any name, such as 'canvas-hello-world'
  4. Click Add permissions to a new project and choose the project you just activated Canvas on, then grant the following permissions:
    • Uniform Optimize -> Intent Manifest -> Read
    • Uniform Canvas -> Compositions -> Read Draft
    • Uniform Canvas -> Compositions -> Read Published
  5. Click Create API key
  6. Copy the API key and save it for later
  7. Copy the project ID and save it for later

Create the Next app#

In a console of your choice:

# 'canvas' can be any name you want herenpx create-next-app canvascd canvas
# The Canvas SDK is published in an npm package. yarn is also fine.npm install @uniformdev/canvas @uniformdev/canvas-react @uniformdev/cli

Fetching data from Canvas into Next.js#

In order to render the data from Canvas we need to fetch it into Next first. The Canvas client is a simple promise-based API client that can be used anywhere you like, such as during server-side rendering, static site generation, or even client-side rendering. For the purposes of this tutorial we will use static site generation to fetch the Canvas data.

  1. Run npm run dev/yarn dev to get your next.js app running locally

  2. Open your Next app's pages/index.js file

  3. Import the Canvas client:

    import { CanvasClient } from '@uniformdev/canvas';
  4. Add a getStaticProps function that will fetch the Canvas composition into a prop to your Next route

    export async function getStaticProps() {  // create the Canvas client  const client = new CanvasClient({    // if this weren't a tutorial, โ†™ should be in an environment variable :)    apiKey: 'your-api-key-here',    // if this weren't a tutorial, โ†™ should be in an environment variable :)    projectId: 'your-project-id-here',  });
      // fetch the composition from Canvas  const { composition } = await client.getCompositionBySlug({    // if you used something else as your slug, use that here instead    slug: '/',  });
      // set `composition` as a prop to the route  return {    props: {      composition,    },  };}
  5. Modify the Home component to receive the composition and render the data to the page (for now):

    export default function Home({ composition }) {  return (    <div className={styles.container}>      <pre>{JSON.stringify(composition, null, 2)}</pre>      {/* rest of the stock component here */}    </div>  );}
  6. You should now see the composition JSON rendered into your browser

Rendering the Canvas composition#

Now that we have the data for the composition, we need to render it. It is entirely possible to render straight from the raw data, but we have created some React components that make it simpler and more readable, enable automatic personalization and A/B testing, and handle mapping component types to their React implementations.

To render our very simple one-component composition, we use the <Composition /> component. This is the root of any rendering of Canvas compositions in React. Replace the Home component implementation (leave the getStaticProps) with the following:

import { Composition } from '@uniformdev/canvas-react';
export default function Home({ composition }) {  // if you didn't call your parameter 'greeting', use that name here instead  return <Composition data={composition}>{({ greeting }) => <p>{greeting}</p>}</Composition>;}

In this example we are using <Composition/>'s render props API to provide us with a simplified object to render our greeting parameter. This makes it easier to get at the root composition's parameters, but this is also completely valid if you don't like render props:

<Composition data={composition}>  <p>{composition.parameters.greeting?.value}</p></Composition>

Now you should see the contents of your parameter in a <p> on the Next app's home page. Congratulations, you successfully rendered your first Canvas composition! Now let's learn how to use the power of Canvas to aggregate data from other sources.

Adding data to Canvas#

We can add data from other content repositories to Canvas components, such as content management systems and commerce systems. This external data is referenced within the Canvas editor and stored as references to the external data, for example content IDs.

Once this data is linked in the Canvas editor, we need to fetch it from that system within our Next app so that we can use it. This is done using enhancers, which post-process the data from the Canvas Composition API after it is received to augment external system data into it.

Before you continue, read about enhancers and how they work.

The process for setting up prebuilt enhancers is specific to each system you want to pull data from. Pick a tool you use and try it out!

Headless CMS#

On-prem CMS#

Commerce#

Adding Slots#

Now that we've learned how Canvas models components and how to get external data into your components, let's talk about Slots. Slots are dynamic regions within a Canvas component that can contain other components. These enable developers to give marketers control over the layout of pieces of a page or screen; letting them add, remove, reorder, and personalize or test without needing to involve a developer.

Slots can be nested and added to any component, giving the content architect the capability to model complex layouts and give fine-grained control over what can be changed.

Let's add a slot to our project now.

tip

This tutorial assumes you have already completed the preceding basic tutorial.

  1. In the Uniform dashboard, go to your Component Library
  2. Open your existing component, click the Slots tab, and click (+) next to Slots - or type alt+shift+s. Call the slot Promos and leave everything else default.
  3. Save and close the existing component, or type ctrl+shift+s.
  4. Add a new component type called 'Promo' - let's add some promo cards to our composition
  5. Add a text parameter, 'name', to store a value
  6. Set the Title Parameter to name so that the value of the parameter is displayed in the editor
  7. Save the component by clicking the down arrow next to Save and close and choosing Save, or type ctrl+s to save without closing
  8. Click the Allow in a slot button. In the dialog, choose the existing component and the Promos slot we just added. It is not necessary to save after allowing the component in a slot.

Now we've added a named slot called 'promos' that we can add zero or more Promo components to. Let's add some to our composition.

  1. Click Compositions and open your composition
  2. You'll see the Promos slot now appearing along the parameters
  3. Click (+) in the Promos slot to add a new Promo there
  4. Click the added promo to expand it and set parameters. Note how the title parameter value shows up in the tree to aid in identifying the promo.
  5. Add a few promos if you wish, then save and publish the composition

Now it's time to implement the slot on the Next app. Rendering the slot is super easy:

  1. Open pages/index.js

  2. In the Home component within the <Composition /> component's body, add a <Slot />:

    // add Slot to importimport { Composition, Slot } from '@uniformdev/canvas-react';
    export default function Home({ composition }) {  return (    <Composition data={composition}>      {({ greeting }) => (        <article>          <p>{greeting}</p>          {/* add slot component */}          <Slot name="promos" />        </article>      )}    </Composition>  );}
  3. Nothing happens, other than some console warnings. The slot is configured, but it does not know how to render the promo components in the slot, so we need to teach the composition what to do with promos. We do this with a resolveRenderer function, which is passed the component from Canvas and returns the React component to render it with. The simplest resolveRenderer returns the same React component for any input:

    const resolveRenderer = (component) => {  const defaultComponent = ({ component }) => <p>This is a {component.type}</p>;
      return defaultComponent;};
    //...<Composition data={composition} resolveRenderer={resolveRenderer}>  ...</Composition>;

    If you add this resolve function then you will see This is a promo rendered for each promo you added to the composition.

  4. Let's teach the resolve function what to do specifically with promo Canvas components instead of any type of Canvas component. To do this, we need to check the type and return a different React component.

    // React component to render `promo` Canvas componentsfunction PromoComponent({ name }) {  return <p>Promo with name {name}</p>;}
    const resolveRenderer = (component) => {  // choose the component based on the Canvas component type  // (you can also use a Map, switch, next/dynamic, etc here)  if (component.type === 'promo') {    return PromoComponent;  }
      return null;};
tip

resolveComponent can be passed on either <Composition /> or <Slot />, enabling smaller resolve functions. The nearest ancestor resolve function is used if specified in multiple places.

tip

Want to use slots with TypeScript? Great! You can create discriminated unions to do type-checking of slot names, like this:

type HelloWorldSlots = 'promos' | 'and' | 'other' | 'slots';
<Slot<HelloWorldSlots> name="promos" />// will make TS error<Slot<HelloWorldSlots> name="bad" />

You can also type your resolveRenderer functions:

import { RenderComponentResolver } from '@uniformdev/canvas-react';
const resolveRenderer: RenderComponentResolver = (component) => {  // ...};

And also the Canvas component implementations:

import { CanvasComponentProps } from '@uniformdev/canvas-react';
type PromoComponentType = {  // 'name' parameter from Canvas  name: string;};
function PromoComponent({ name }: CanvasComponentProps<PromoComponentType>) {  return <p>Promo with name {name}</p>;}

Nesting Slots#

Slots can be nested, for example to create a hierarchy such as:

  • Page[content slot]
    • Two Column Layout[left slot]
      • Tab Set[tabs slot]
        • Tab

This enables modeling complex layout scenarios when it is desirable to cede control over most of the page layout to non-developers, for example in landing pages. Nested slots work exactly like root slots: you add a <Slot /> component to the rendering component that has the slot. e.g. adding a slot component to the PromoComponent, if the Promo Canvas component had slots.

Wrapping Slot Children#

In some cases it is desirable to render a HTML wrapper around every child component in a slot, without adding that wrapper to every component. For example when a component may be used in multiple locations, it might need special wrappers to meet style requirements. You can accomplish this using render props on the <Slot /> component, which defines a function that is called to wrap each child component:

// wraps all child Canvas components in the slot in <aside><Slot name="promos">{({ child, key }) => <aside key={key}>{child}</aside>}</Slot>

Adding Variants#

Sometimes it makes sense to have more than one visual rendering of the same data. Canvas allows you to do this with variants, which are named variations of a single component. A common use for variants might be several renditions of a call to action, or to provide visual A/B testing.

To create a variant, add it to the component definition in your component library under the Variants tab. Once the variant is created, you can choose the variant when editing a component of that type.

To implement a variant, you can either:

  • Conditionally render output within your rendering component based on the variant, which is appropriate if the variation is minor i.e.

    function ConditionalVariantDemo({ component } /* for TypeScript : ComponentProps*/) {  // note that `variant` will be undefined for the default variant  switch (component.variant) {    case 'LeftAlignImage':      // could also use a different component entirely here i.e. <LeftVariant {...props} />      return <p>Left variant</p>;    default:      return <p>Default variant</p>;  }}
  • Conditionally use a different component to render the variant within the resolveRenderer function, which is appropriate if the variation is major or you prefer this as the source of component selection, i.e.:

    const resolveRenderer = (component) => {  switch (component.type) {    case 'ComponentA':      if (component.variant === 'LeftAlignImage') {        return LeftAlignComponentA;      }
          return ComponentA;    // ...other components  }};
tip

Variants are useful top level constructs for creating visual difference for A/B tests, but for more granular control when a component has several knobs to configure, create parameters. Canvas provides parameter types such as checkboxes, numbers, and dropdowns that are perfect to expose configurable rendering options to authors.

Next preview mode#

Next.js supports preview mode, where it turns a static site into a temporarily serverless-side-rendered site in order to show real-time changes made from Canvas.

To setup preview mode:

  1. You will need to host your site on a platform that supports Next's preview mode, such as Vercel or Netlify.

  2. Create a Next API endpoint to allow enabling and disabling preview mode. It is a good practice to secure this endpoint with a shared secret to avoid unauthorised previewing.

    pages/api/preview.js
    const handler = async (req, res) => {  // NOTE: in production, get this from the next config API  const previewSecret = 'my-secret';
      if (!req.query.slug) {    return res.status(400).json({ message: 'Missing slug' });  }
      // raw string of the incoming slug  const slug = Array.isArray(req.query.slug) ? req.query.slug[0] : req.query.slug;
      // /api/preview?disable=true&slug=/return/to/this to turn off preview mode  if (req.query.disable) {    res.clearPreviewData();    res.redirect(slug);    return;  }
      if (req.query.secret !== previewSecret) {    return res.status(401).json({ message: 'Invalid token' });  }
      // enable preview mode and redirect to the slug to preview  res.setPreviewData({});  res.redirect(slug);};
    export default handler;
  3. In your Canvas Component Library, set the preview URL to https://localhost:3000/api/preview?secret=my-secret to configure preview on the Canvas editor side (substitute localhost:3000 with the URL to your Next site if needed)

  4. Edit a Composition. A new Preview button will appear now that you have set up the preview URL. Click it, and it will invoke your Next API to turn on preview mode. Nothing will change yet, because we have not told Next how to fetch preview data yet, but cookies will be set.

  5. Setup preview data fetching in getStaticProps

    pages/index.js
    import { CANVAS_DRAFT_STATE, CANVAS_PUBLISHED_STATE } from '@uniformdev/canvas';
    export const getStaticProps = async ({ preview }) => {  const canvasClient = new CanvasClient({ apiKey: 'api-key', projectId: 'project-id' });
      const { composition } = await canvasClient.getCompositionBySlug({    slug: '/',    // ADD: tells Canvas client to get preview layout data    state: preview ? CANVAS_DRAFT_STATE : CANVAS_PUBLISHED_STATE,  });
      // as an example here we are configuring the Contentful enhancer to fetch preview entries from Contentful,  // as well as preview layout from Canvas. This code creates a Contentful client, as outlined in the Contentful data tutorial.  const client = createClient({    space: 'space-id',    accessToken: 'delivery-token',  });
      // ADD: for preview Contentful data, we need a second client that tells it how to get preview entries  const previewClient = createClient({    space: 'space-id',    accessToken: 'preview-token',    host: 'preview.contentful.com',  });
      // ADD: the previewClient to the Contentful enhancer  const contentfulEnhancer = createContentfulEnhancer({ client, previewClient });
      await enhance({    composition,    enhancers: new EnhancerBuilder().parameterType(CANVAS_CONTENTFUL_PARAMETER_TYPES, contentfulEnhancer),    context: { preview },  });
      return {    props: {      composition,    },  };};
  6. You should now see preview content fetched when in preview mode, but it is not yet updating live - you have to refresh to see updates.

  7. Create a custom hook that tells Next to re-fetch static props when a change is detected to your Canvas content:

    hooks/useLivePreviewNextStaticProps.js
    import { useRouter } from 'next/router';import { useCallback } from 'react';import { useCompositionEventEffect } from '@uniformdev/canvas-react';
    export function useLivePreviewNextStaticProps(options) {  const router = useRouter();
      const effect = useCallback(() => {    // replacing the route with itself makes Next re-run getStaticProps    router.replace(router.asPath, undefined, { scroll: false });  }, [router]);
      // useCompositionEventEffect runs a callback when changes are made to the composition  return useCompositionEventEffect({    ...options,    enabled: router.isPreview,    effect,  });}
  8. Add the hook to the top of your home component:

    pages/index.js
    export default function Home({ composition }) {  useLivePreviewNextStaticProps({    compositionId: composition?._id,    projectId: 'your-uniform-project-id',  });
      // rest of component}
  9. The content will now reload when you save the composition in Canvas automatically.