Build a Contentful App for easier slug management
April 6th, 2023
Background
Traditional monolith CMS's built a visual page tree for every page on the site based on different folder structures. They usually have folder icons, which indicate nesting. So there's no need to consider what the address of a web page will be. An "About" page, inside of a "Company" folder would build a folder structure of /company/about and no one really though about it. This is a great feature of monolith CMSs, but does not fit in the architecture of a headless CMS, such as Contentful. Below is a custom solution to provide a better editor experience.
Building apps is a great way to extend the functionality of Contentful in order to build things like custom fields, which we'll go over in this tutorial. One benefit of the app framework is that you can leverage Forma 36 design system so the fields you create will match the look and feel of Contentful's UI. Be sure to visit the Contentful App framework page, where you can find the API reference as well as examples of creating a custom app.
Video of the App that will be created.
Setup
To get started, run the npx create-contentful-app slug-prefix command in your projects root directory. This will start you off with all of the files needed to start developing your first app.
Then navigate to the app folder by running cd slug-prefix. Finally start the dev server by running npm run start to enable hot reloading.
1) Create your app definition
Go to the Apps tab under your organization settings
Click Create app. The "App details" page is displayed.
In the Name field, enter a custom name for your app. For our example app, we use Slug Prefix as a name.
In the Frontend field, enter URL where your app is hosted. Our example app is hosted locally for now, so the URL for it is http://localhost:3000.
Under the Locations area, select a the Entry field checkbox.
Click Save.
2) Install the Slug Prefix app to your space
In the Contentful web app, navigate to your space.
In the top pane, click Apps and select Custom apps.
Go to your newly created app, click on the actions menu icon and select Install.
Click the Environments field and select the checkbox for the environment that you would like the app to be installed in and click "Authorize Access". Your app is now installed to the selected environment.
3) Assigning your app to a content type
Go to the Content model tab and click the content type that you want to add the Slug Prefix field to.
On the Fields tab, click the Add field button.
Add a short text field, you can name it "Slug Prefix" if you like.
Then under the "Appearance" tab, select the newly created "Slug Prefix" App.
Click Save to apply changes to the content type.
Slug Prefix App code
Just show me the code already!
Bypass the tutorial and view to the full source of the Slug Prefix app.
No, I want to learn step by step.
Since the type of App we'll be running is a custom field, open the /src/locations/Field.tsx file. All of the changes we'll be making will be to just to this one file. At the top, add the following imports.
import React, { useState } from 'react';
import { Select } from '@contentful/f36-components';
import { FieldExtensionSDK } from '@contentful/app-sdk';
import { useSDK } from '@contentful/react-apps-toolkit';
import { SlugEditor } from '@contentful/field-editor-slug';
Select is a component from forma that matches the Contentful UI.
FieldExtensionSDK allows you access to the Contentful SDK so you can communicate with the Contentful web app.
useSDK hook returns an instance of the Contentful App SDK.
SlugEditor is an instance of Contentful's slug field.
Fit app to fill iframe
Below the imports, paste in the following code to start using the SDK.
const Field = () => {
const sdk = useSDK<FieldExtensionSDK>();
// Since this field is inside of an iframe, call contentful's iframe resizer to fit the contents of the extension to the iframe.
sdk.window.startAutoResizer();
// TODO: Add logic for handling the entry title and slug prefix values.
// Update the iframe height of the field.
sdk.window.updateHeight();
return (
<div>
{/* TODO: Add select box for the editor to choose from a list of slug prefixes. */}
{/* TODO: Add text input to display the slug prefix and title in URL format. */}
</div>
);
};
export default Field;
First we create a sdk variable so we can access data from Contentful on the entry later. Since apps render inside of an iframe, startAutoResizer() is used to allow the iframe's width and height to adjust to fit to the contents of the app. At the end of the code we call updateHeight() to update the size of the iframe.
Sprinkle on a little UI
Next, let's add the UI. Inside of the render method , add the <Select> from Forma, and then add the <SlugEditor> field from Contentful. In the end, it should look like the following...
return (
<div>
<Select
id="slugPrefix"
name="slugPrefix"
>
<Select.Option value=""></Select.Option>
</Select>
<br />
<SlugEditor field={sdk.field} baseSdk={sdk} />
</div>
);
Let's add options to the select box for whatever slug prefix values you want. Note that the value is what will be rendered in the slug field. In this case, we'll use "root" value to determine if we do not want a slug prefix. We'll add conditional code for that later.
<Select.Option value="root">Root</Select.Option>
<Select.Option value="blog">Blog Post</Select.Option>
<Select.Option value="recipe">Recipe</Select.Option>
Reference other fields on the entry
Next, paste in the code below under the // TODO: Add logic for handling the entry title and slug prefix values comment. This snippet will pull in the current entries display field, which is the field that is set to the entry's title. Also we read the current value of the field we are building.
const titleField = sdk.entry.fields[sdk.contentType.displayField];
const initSlug = sdk.field.getValue();
After that, we want to get the current slug prefix, which exists within the two slashes. Example: /slug/url would return slug.
const initPrefix = initSlug ? initSlug.substring(
initSlug.indexOf("/") + 1,
initSlug.lastIndexOf("/")
) : '';
Store slug and listen for changes
Here, we set the slug so that we can update it later, if needed.
const [slugPrefixValue, setSlugPrefixValue] = useState(initSlug ? initPrefix : 'root');
Next let's listen for changes to the slug prefix select box.
const handleOnChange = (event: { target: { value: React.SetStateAction<string>; }; }) => setSlugPrefixValue(event.target.value);
Back to the UI
Let's get back to the render method and add the following props to the <Select>. These will set the value of the select box, and add an event listener when it changes.
<Select...
value={slugPrefixValue} onChange={handleOnChange}
.../>
Last code update...
To help convert the entry title field into a slug format, we'll create a slugify function. Example: "Test page" becomes "test-page". You can add this after the const handleOnChange.
const slugify = (str: string) =>
str
.toLowerCase()
.trim()
.replace(/[^\w\s-]/g, '')
.replace(/[\s_-]+/g, '-')
.replace(/^-+|-+$/g, '');
Now we are ready to manage the state of the slug field whenever the display field changes. This is called when the display field (aka the entry title) field's value is changed. At this point, it will read the value of the slug prefix dropdown and append the display field after. This then set's the field's value to this format: /blog/test-page.
function updateSlugField(value: any) {
if (value === undefined) return;
const slugPrefix = slugPrefixValue === `root` ? `` : `/${slugPrefixValue}`;
const slugWithPrefix = `${slugPrefix}/${slugify(value)}`;
sdk.field.setValue(slugWithPrefix);
}
titleField.onValueChanged(updateSlugField);
Finally, let's set the value of the <SlugEditor> field to the value of the field, and also set the required baseSdk.
<SlugEditor field={sdk.field} baseSdk={sdk} />
Deploy!
When you are ready to take this live, run npm run build to create a production build, and then npm run upload to upload it to Contentful.
Hopefully you made it through and now have a basic understanding of some of the things you can do with Apps. If you have any questions, feel free to reach out.