12 Days of Contentful
← Go back

Day 2

Decorate your home

Information is scattered everywhere. No matter if you’re managing a simple blog or a marketing website for your company, you use tons of tools and services. For example, you might use Next.js to build the front end, Contentful to manage the content, Google Analytics to track the traffic, and Sprout Social to manage your social media efforts. The data is in silos which makes it difficult to tell a story.

Onboarding new members on these platforms can also become challenging, especially when these platforms govern the onboarding process. With the possibility of customizing the Home location of your Contentful space, you can solve these challenges! Using the App Framework, you can customize the Home screen to create dashboards, custom onboarding, or add learning materials that need to be visible to your users in the Space.

In the article, I’ll share how I customized the Home screen location of the Contentful Space we are using for the Advent Calendar.

Please note that this article is an overview and is not a detailed tutorial on creating an app with the App Framework. If you want to learn more about the App Framework, please go through this documentation.

Quick Overview

The app currently gives an overview of all the content - published and in the draft, on the Space’s home screen. The app also provides information about the number of articles published by an author! The following image shows the result.

Welcome screen

At the time of writing this article, we are not using any platform other than Contentful for the Advent Calendar. Hence, there is no need for data orchestration. However, if you want to bring data from other platforms, you can customize the app to do that!

If you want to jump to the code or try the app, follow the instructions mentioned in the GitHub repo and install the app. Try it out, and let me know what you think!

Bootstrapping the app

The Contentful App Framework provides all the necessary tools and libraries to create an app. Using the following command, I quickly bootstrapped the app repository.

npx create-contentful-app

After the dependencies got installed and the app was ready to run locally, I created an app on Contentful, selecting Home and App configuration as the location. The App definition of the app looks similar to the image below.

App definition

As the app ran on the local server during development, I configured the Frontend Host URL to the localhost server.

When I run the app locally using the following command and install it in Space, it takes me to the default configuration screen. In the next section, I’ll walk you through customizing the configuration screen.

npm run start

Customizing the App Configuration Location

This application uses GraphQL API to fetch the data. To use the GraphQL API, you need the Contentful Preview API Token. Once you generate that token, you need to configure it in the app. The App Configuration is where you can configure this token.

When a user installs the app, they get navigated to the App Configuration page, where they can enter this token. To add this functionality to the app, I updated the code in the src/locations/ConfigScreen.tsx file.

Using the below code, I added a form for the user to enter their API token.

return (
  <Flex
    flexDirection="column"
    className={css({ margin: '80px', maxWidth: '800px' })}
  >
    <Form>
      <FormControl>
        <FormControl.Label>Contentful Preview API Key</FormControl.Label>

        <TextInput
          value={parameters.apiKey}
          type="text"
          onChange={(e) =>
            setParameters({ ...parameters, apiKey: e.target.value })
          }
        />
      </FormControl>
    </Form>
  </Flex>
)

The value of the API token gets saved in the parameters of the app which allows me to refer to this value anywhere in the app. The final code of the ConfigScreen looks as follows:

import React, { useCallback, useState, useEffect } from 'react'
import { AppExtensionSDK } from '@contentful/app-sdk'
import { Form, FormControl, Flex, TextInput } from '@contentful/f36-components'
import { css } from 'emotion'
import { useSDK } from '@contentful/react-apps-toolkit'

export interface AppInstallationParameters {
  apiKey: string | undefined
}

const ConfigScreen = () => {
  const [parameters, setParameters] = useState<AppInstallationParameters>({
    apiKey: '',
  })

  const sdk = useSDK<AppExtensionSDK>()

  const onConfigure = useCallback(async () => {
    const currentState = await sdk.app.getCurrentState()

    return {
      parameters,

      targetState: currentState,
    }
  }, [parameters, sdk])

  useEffect(() => {
    sdk.app.onConfigure(() => onConfigure())
  }, [sdk, onConfigure])

  useEffect(() => {
    ;(async () => {
      const currentParameters: AppInstallationParameters | null =
        await sdk.app.getParameters()

      if (currentParameters) {
        setParameters(currentParameters)
      }

      sdk.app.setReady()
    })()
  }, [sdk])

  return (
    <Flex
      flexDirection="column"
      className={css({ margin: '80px', maxWidth: '800px' })}
    >
      <Form>
        <FormControl>
          <FormControl.Label>Contentful Preview API Key</FormControl.Label>

          <TextInput
            value={parameters.apiKey}
            type="text"
            onChange={(e) =>
              setParameters({ ...parameters, apiKey: e.target.value })
            }
          />
        </FormControl>
      </Form>
    </Flex>
  )
}

export default ConfigScreen

After saving this code, the app auto-refreshed and I was able to enter the Preview token of our space.

Customizing home screen

Customizing Home Screen

After configuring the API token and installing the app, there was one more step that I had to complete to view the custom home screen. If you’re creating an app to customize the home location, you need to update the appearance in Settings. Go to Settings > Home and select your app from the Appearance options. After doing so, I was able to view the home screen generated by the boilerplate.

For this app, I wanted to divide the home screen into two sections - the entries section that would list the entries based on their publish status and the author section that would show the list of authors. I broke down these two sections into different components.

Different sections of the home screen highlighted

Building the Entries component

The Entries component would fetch all the entries and render the output. It contains two sections - unpublished entries and published entries. Using the following query, I fetched all the entries (draft and published) from the GraphQL API.

{
  dailyEntryCollection(preview: true) {
    items {
      sys {
        id
        publishedVersion
      }
      title
      author {
        name
      }
    }
  }
}

Next, I filtered out the entries based on their status, using the below functions.

const unpublishedEntries = result.data.dailyEntryCollection.items.filter(
  (item: Entry) => !item.sys.publishedVersion
) as Entry[]

const publishedEntries = result.data.dailyEntryCollection.items.filter(
  (item: Entry) => item.sys.publishedVersion
) as Entry[]

Below is the code for the entire fetch function.

const fetchEntries = async (apiKey: string) => {
  const query = `
{
  dailyEntryCollection(preview: true) {
    items {
      sys {
        id
        publishedVersion
      }
      title
      author {
        name
      }
    }
  }
}
`

  const spaceId = sdk.ids.space
  const environmentId = sdk.ids.environment
  const url = URL + spaceId + '/environments/' + environmentId

  const response = await fetch(url, {
    method: 'POST',

    headers: {
      'Content-Type': 'application/json',

      Authorization: `Bearer ${apiKey}`,
    },

    body: JSON.stringify({ query }),
  })

  if (response.ok) {
    const result = await response.json()

    const unpublishedEntries = result.data.dailyEntryCollection.items.filter(
      (item: Entry) => !item.sys.publishedVersion
    ) as Entry[]

    const publishedEntries = result.data.dailyEntryCollection.items.filter(
      (item: Entry) => item.sys.publishedVersion
    ) as Entry[]

    return { unpublishedEntries, publishedEntries }
  }

  return null
}

The next step was to call the function whenever the component gets rendered and update the states to store the values. I used useEffect to handle this.

useEffect(() => {
  const { apiKey } = sdk.parameters.installation

  fetchEntries(apiKey).then((data) => {
    if (data) {
      const { unpublishedEntries, publishedEntries } = data

      setPublishedEntries(publishedEntries)

      setUnpublishedEntries(unpublishedEntries)
    }
  })
}, [])

Now that I had the data, I used the components from the Forma 36 design library to display it to the user.

return (
  <Flex flexDirection="column" flexGrow={1}>
    <Box margin="spacingS">
      <Heading>Unpublished</Heading>

      {unpublishedEntries &&
        unpublishedEntries.map((entry: Entry) => (
          <EntryCard
            key={entry.sys.id}
            status="draft"
            contentType="Daily Entry"
            title={entry.title}
            description={`by: ${entry.author.name}`}
            margin="spacingS"
          />
        ))}
    </Box>

    <Box margin="spacingS">
      <Heading>Published</Heading>

      {publishedEntries &&
        publishedEntries.map((entry: Entry) => (
          <EntryCard
            key={entry.sys.id}
            status="published"
            contentType="Daily Entry"
            title={entry.title}
            description={`by: ${entry.author.name}`}
            margin="spacingS"
          />
        ))}
    </Box>
  </Flex>
)

The finished code of the Entries component is as follows.

import React, { useEffect, useState } from 'react'
import { useSDK } from '@contentful/react-apps-toolkit'
import { Flex, EntryCard, Heading, Box } from '@contentful/f36-components'

const URL = 'https://graphql.contentful.com/content/v1/spaces/'

export interface Entry {
  sys: Sys
  title: string
  author: Author
}

export interface Author {
  name: string
}

export interface Sys {
  id: string
  publishedVersion: null
}

const Entries = () => {
  const sdk = useSDK()

  const [publishedEntries, setPublishedEntries] = useState<Entry[] | null>()

  const [unpublishedEntries, setUnpublishedEntries] = useState<Entry[] | null>()

  const fetchEntries = async (apiKey: string) => {
    const query = `
{
  dailyEntryCollection(preview: true) {
    items {
      sys {
        id

        publishedVersion
      }
      title
      author {
        name
      }
    }
  }
}
`

    const spaceId = sdk.ids.space
    const environmentId = sdk.ids.environment
    const url = URL + spaceId + '/environments/' + environmentId

    const response = await fetch(url, {
      method: 'POST',

      headers: {
        'Content-Type': 'application/json',

        Authorization: `Bearer ${apiKey}`,
      },

      body: JSON.stringify({ query }),
    })

    if (response.ok) {
      const result = await response.json()

      const unpublishedEntries = result.data.dailyEntryCollection.items.filter(
        (item: Entry) => !item.sys.publishedVersion
      ) as Entry[]

      const publishedEntries = result.data.dailyEntryCollection.items.filter(
        (item: Entry) => item.sys.publishedVersion
      ) as Entry[]

      return { unpublishedEntries, publishedEntries }
    }

    return null
  }

  useEffect(() => {
    const { apiKey } = sdk.parameters.installation

    fetchEntries(apiKey).then((data) => {
      if (data) {
        const { unpublishedEntries, publishedEntries } = data

        setPublishedEntries(publishedEntries)

        setUnpublishedEntries(unpublishedEntries)
      }
    })
  }, [])

  return (
    <Flex flexDirection="column" flexGrow={1}>
      <Box margin="spacingS">
        <Heading>Unpublished</Heading>

        {unpublishedEntries &&
          unpublishedEntries.map((entry: Entry) => (
            <EntryCard
              key={entry.sys.id}
              status="draft"
              contentType="Daily Entry"
              title={entry.title}
              description={`by: ${entry.author.name}`}
              margin="spacingS"
            />
          ))}
      </Box>

      <Box margin="spacingS">
        <Heading>Published</Heading>

        {publishedEntries &&
          publishedEntries.map((entry: Entry) => (
            <EntryCard
              key={entry.sys.id}
              status="published"
              contentType="Daily Entry"
              title={entry.title}
              description={`by: ${entry.author.name}`}
              margin="spacingS"
            />
          ))}
      </Box>
    </Flex>
  )
}

export default Entries

Import the Entries component in the Home component, and you could see the entires!

Building the Author component

Similar to the above Entries component, I used the GraphQL API to fetch the authors and their total number of published entries with the below query.

{
  authorCollection {
    items {
      name

      linkedFrom {
        dailyEntryCollection {
          total
        }
      }
    }
  }
}

Again, this data gets fetched when the component renders, and the value is provided to the author state. I then display the data as follows:

return (
  <Flex flexDirection="column" justifyContent="space-around">
    <Heading>Authors</Heading>

    {authors &&
      authors.map((entry: Author, i: number) => (
        <EntryCard
          key={i}
          contentType="Authors"
          title={entry.name}
          description={`published entries: ${entry.linkedFrom.dailyEntryCollection.total}`}
        />
      ))}
  </Flex>
)

The final code of the Author component is below:

import React, { useEffect, useState } from 'react'
import { useSDK } from '@contentful/react-apps-toolkit'
import { Flex, EntryCard, Heading, Box } from '@contentful/f36-components'

const URL = 'https://graphql.contentful.com/content/v1/spaces/'

export interface Author {
  name: string
  linkedFrom: LinkedFrom
}

export interface LinkedFrom {
  dailyEntryCollection: DailyEntryCollection
}

export interface DailyEntryCollection {
  total: number
}

const Authors = () => {
  const sdk = useSDK()

  const [authors, setAuthors] = useState<Author[] | null>()

  const fetchEntries = async (apiKey: string) => {
    const query = `
{
  authorCollection {
    items {
      name

      linkedFrom {
        dailyEntryCollection {
          total
        }
      }
    }
  }
}
`

    const spaceId = sdk.ids.space
    const environmentId = sdk.ids.environment
    const url = URL + spaceId + '/environments/' + environmentId

    const response = await fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',

        Authorization: `Bearer ${apiKey}`,
      },
      body: JSON.stringify({ query }),
    })

    if (response.ok) {
      const result = await response.json()

      return result.data.authorCollection.items as Author[]
    }

    return []
  }

  useEffect(() => {
    const { apiKey } = sdk.parameters.installation

    fetchEntries(apiKey).then((data: Author[]) => {
      if (data) {
        setAuthors(data)
      }
    })
  }, [])

  return (
    <Flex flexDirection="column" justifyContent="space-around">
      <Heading>Authors</Heading>

      {authors &&
        authors.map((entry: Author, i: number) => (
          <EntryCard
            key={i}
            contentType="Authors"
            title={entry.name}
            description={`published entries: ${entry.linkedFrom.dailyEntryCollection.total}`}
          />
        ))}
    </Flex>
  )
}

export default Authors

Now that I have both the Entries component and the Author component, I imported them to the Home component to display them on the Home screen. The finished code for the home component looked as follows

import React from 'react'
import { Text, Flex } from '@contentful/f36-components'
import tokens from '@contentful/f36-tokens'
import { HomeExtensionSDK } from '@contentful/app-sdk'
import { useSDK } from '@contentful/react-apps-toolkit'
import Entries from '../components/Entries'
import Authors from '../components/Authors'

const Home = () => {
  const sdk = useSDK<HomeExtensionSDK>()

  return (
    <Flex flexDirection="column" alignItems="center" fullWidth>
      <Flex
        justifyContent="center"
        padding="spacing3Xl"
        fullWidth
        style={{ backgroundColor: tokens.gray700 }}
      >
        <Flex flexDirection="column" gap="spacingXl" style={{ width: '900px' }}>
          <Text
            fontColor="colorWhite"
            fontSize="fontSize4Xl"
            fontWeight="fontWeightDemiBold"
          >
            👋 Welcome back, {sdk.user.firstName}!
          </Text>
        </Flex>
      </Flex>

      <Flex
        style={{ width: '900px' }}
        flexDirection="row"
        marginTop="spacing3Xl"
      >
        <Entries />

        <Authors />
      </Flex>
    </Flex>
  )
}

export default Home

I now have a complete app that gives an overview of the content in the space! However, this app was running locally on my machine, and my team didn’t have access. To make the app available to everyone, I had to deploy it. In the next section, I’ll share how I deployed the app.

Deploy the app to Contentful

Now that my app was ready, I had to deploy it for others to use it too. You can deploy the app either on Contentful itself or external platforms like Netlify, Vercel, etc. In this section, I’ll share how I deployed it on Contentful.

The first thing I had to do was compile and build the app using the npm run build command. This command creates a build directory that gets used by Contentful. Next, I executed the npm run upload command.

On execution, the upload command asks for the following details:

If everything goes well, your app will get successfully uploaded and ready to be used by your team!

What’s next?

In this article, I gave an overview of how to customize the Home screen of a Contentful Space using the App Framework. I also shared the steps to deploy the app on Contentful.

We barely scratched the surface of the endless possibilities! You can customize the app to show data from external sources and create dashboards or onboarding flows. If you’re curious and want to dive deeper, go through the documentation. You can also get inspiration from the apps created by the community on the Developer Showcase and share your apps too!

If at any point you have questions, feel free to reach out! I would be more than happy to help you create your app!