Skip to main content

Headless CMS starter

With this starter kit you can use Contember platform as headless CMS and Next.js for frontend site.

You'll get:

  • Pages with 7 blocks (Hero section, Logos section, Content section, Features section, CTA section, Testimonial section, Contact section).
  • Blog and articles with powerful WYSIWYG editor.
  • Messages for saving forms from the frontend website.
  • Website settings (upload logotype, set up navigation).
  • ACL rules for administrators and public access for frontend website.
  • Basic Next.js website intentionally without any styling. See how you can easily query GraphQL API provided by Contember.

Thanks to Contember platform you can change anything. If you have any questions, we're happy to help in Discord.

Screenshot 2022-01-18 at 14 08 17

How to customise the starter kit

First, you should get to know our Quickstart. You can change our starter kit as you want to. Below, you can find some examples of possible customization.

Add new page content section

Pages are build from content sections. We've prepared 7 content sections for you (Hero section, Logos section, Content section, Features section, CTA section, Testimonial section and Contact section).

Let's say you want to add new section with text and image next to each other.

1. Change the model

Go to headless-cms/api/model/ContentBlocks.ts and add 'imageWithTextSection' option to ContentBlockType enum. We'll use image and content fields from ContentBlock to store our data.

headless-cms/api/model/ContentBlocks.ts
export const ContentBlockType = def.createEnum(
"heroSection",
"logosSection",
"contentSection",
"featureSection",
"ctaSection",
"testimonialSection",
"contactSection",
"imageWithTextSection"
)

2. Create and execute a migration

Run migration command to apply your changes.

npm run contember migrations:diff headless-cms add-image-with-text-section

Select "Yes and execute immediately" and wait for the migration to finish.

3. Add a new block to you application

Go to headless-cms/admin/components/ContentBlocks.tsx file and add <Block /> component into <BlockRepeater /> component. <Block /> should contain all necessary fields for your new section. We want to have an image with text, so we'll add a custom component <ContentField /> (you can learn more in /headless-cms/admin/components/ContentField.tsx) for text and <ImageUploadField /> component for the image.

headless-cms/admin/components/ContentBlocks.tsx
export const ContentBlocks = Component(
() => (
<BlockRepeater
field="blocks"
label={undefined}
discriminationField="type"
sortableBy="order"
addButtonText="Add content block"
>
...
<Block discriminateBy="imageWithTextSection" label="Image with text section">
<ContentField />
<ImageUploadField
label={locale["Image"]}
urlField="image.url"
widthField="image.width"
heightField="image.height"
>
<TextField field="image.alt" label={locale["Alternative text"]} />
</ImageUploadField>
</Block>
),
'ContentBlocks',
)

Now you can check your new section in your application and try adding this new block to a page.

4. Add new section to next.js frontend

Now, we need to render the block on frontend.

Go to headless-cms/head/components/blocks.tsx and add new react component called ImageWithText which will render content and image. Next, you'll need to add your new component to block map in Blocks component.

headless-cms/head/components/blocks.tsx
...
function ImageWithText({ content, image }) {
return (
<section>
{content?.parts &&
<RichTextRenderer blocks={content.parts} sourceField="json" />
}
<div>
{image &&
<div>
<img src={image.url} width={image.width} height={image.height} alt={image.alt} />
</div>
}
</div>
</section>
)
}

export default function Blocks({ blocks }) {
return blocks ? blocks.map((block) => {
const blocksElements = {
heroSection: <HeroSection {...block} key={block.id} />,
logosSection: <LogoSection {...block} key={block.id} />,
contentSection: <ContentSection {...block} key={block.id} />,
featureSection: <FeatureSection {...block} key={block.id} />,
ctaSection: <CtaSection {...block} key={block.id} />,
testimonialSection: <TestimonialSection {...block} key={block.id} />,
contactSection: <ContactSection {...block} key={block.id} />,
imageWithText: <ImageWithText {...block} key={block.id} />,
}

return blocksElements[block.type]
}) : null
}

That's all. Now you can use your new section in your website.

Reusable testimonials

You've decided that you want to edit all your testimonials in one place. You can easily do that, thanks to the Contember.

1. Change model

Create new entities called Testimonial and TestimonailAuthor in headless-cms/api/model/ folder.

headless-cms/api/model/Testimonial.ts
import { SchemaDefinition as def } from "@contember/schema-definition"
import { Content } from "./Content"
import { Image } from "./Image"

export class Testimonial {
content = def.oneHasOne(Content)
author = def.oneHasOne(TestimonailAuthor)
}

export class TestimonailAuthor {
name = def.stringColumn().notNull()
title = def.stringColumn()
image = def.manyHasOne(Image)
}

Go to headless-cms/api/model/ContentBlock.ts and remove TestimonialAuthor entity. Replace content and author fields with testimonial field.

headless-cms/api/model/ContentBlock.ts
...
import { Testimonial } from "./Testimonial"
...
export class ContentTestimonial {
order = def.intColumn().notNull()
testimonial = def.manyHasOne(Testimonial)
contentBlock = def.manyHasOne(ContentBlock, "testimonials").cascadeOnDelete()
}
...

Export new Testimonial entities in headless-cms/api/model/index.ts.

headless-cms/api/model/index.ts
export * from './Article'
export * from './Button'
export * from './ContactMessage'
export * from './Content'
export * from './ContentBlock'
export * from './Image'
export * from './Link'
export * from './Menu'
export * from './Page'
export * from './Seo'
export * from './Setting'
export * from './Testimonial'

To add newly created entities, you will need to update the public entry in the access control list.

headless-cms/api/acl.ts
...
const aclFactory = (model: Model.Schema): Acl.Schema => ({
roles: {
admin: {
variables: {},
stages: '*',
entities: PermissionsBuilder.create(model).allowAll().allowCustomPrimary()
.permissions,
s3: {
'**': {
upload: true,
read: true,
},
},
},
public: {
variables: {},
stages: '*',
s3: {
'**': {
upload: false,
read: true,
},
},
entities: {
...
Testimonial: {
predicates: {},
operations: readOnly(model, 'Testimonial', true),
},
...
},
},
},
})

2. Execute migrations

Run migration command to aplicate your changes.

npm run contember migrations:diff headless-cms separate-testimonials-from-content

Select "Yes and execute immediately" and wait for the migration to finish.

3. Add a page to your application

You'll need to add page where you can edit all your testimonials.

headless-cms/admin/pages/testimonals.tsx
import * as React from "react"
import { CreatePage, DataGridPage, EditPage, LinkButton, TextCell, HasOne, TextField } from "@contember/admin"
import { ContentField } from '../components/ContentField'
import { ImageField } from '../components/ImageField'

export const TestimonialList = (
<DataGridPage
entities="Testimonial"
itemsPerPage={50}
rendererProps={{
title: "Testimonials",
actions: <LinkButton to="testimonialCreate">Add testimonial</LinkButton>,
}}
>
<TextCell field="author.name" header="Author's name" />
</DataGridPage>
)

export const TestimonialCreate = (
<CreatePage
entity="Testimonial"
rendererProps={{ title: "Add testinomial" }}
redirectOnSuccess="testimonialEdit(id: $entity.id)"
>
<ContentField label="Testimonial" />
<HasOne field="author">
<TextField field="name" label="Author's name" />
<ImageField field="image" label="Author's photo" />
</HasOne>
</CreatePage>
)

export const TestimonialEdit = (
<EditPage entity="Testimonial(id=$id)" rendererProps={{ title: "Edit testimonial" }}>
<ContentField label="Testimonial" />
<HasOne field="author">
<TextField field="name" label="Author's name" />
<ImageField field="image" label="Author's photo" />
</HasOne>
</EditPage>
)

Add a new page to navigation in headless-cms/admin/components/Navigation.tsx.

headless-cms/admin/components/Navigation.tsx
export const Navigation = () => (
<Menu>
<Menu.Item>
<Menu.Item title={locale["Pages"]} to="pageList" />
<Menu.Item title={locale["Articles"]} to="articleList" />
<Menu.Item title={locale["Messages"]} to="messageList" />
<Menu.Item title={locale["Settings"]} to="settings" />
<Menu.Item title={locale["Navigation"]} to="navigationList" />
<Menu.Item title={locale["Testimonials"]} to="testimonialList" />
<Menu.Item title={locale["SEO"]}>
<Menu.Item title={locale["Articles"]} to="seoArticles" />
<Menu.Item title={locale["Pages"]} to="seoPages" />
</Menu.Item>
</Menu.Item>
</Menu>
)

Last but not least, replace <Repeater /> fields in the Testimonial Content block in headless-cms/admin/components/ContentBlocks.tsx.

headless-cms/admin/components/ContentBlocks.tsx
...
<Block
discriminateBy="testimonialSection"
label={<LabelWithIcon icon={<IconTestimonialSection />} label={locale["Testimonial section"]} />}
>
<TextField field="primaryText" label={locale["Headline"]} />
<ContentField />
<Repeater
field="testimonials"
label={locale["Testimonials"]}
sortableBy="order"
addButtonText="Add testimonial"
>
<SelectField field="testimonial" label={locale["Testimonial"]} options="Testimonial.author.name" />
</Repeater>
</Block>
...

That's all. Now you can edit your all your testimonals in one place.