Skip to content
Sign inJoin waitlist

Using a headless CMS — Tustel Next.js

Connecting a headless CMS works the same for every Next.js template sold on Framebrick. The approach, rich text, images, keeping pages fresh, and step-by-step guides for Sanity, Payload, Strapi and Directus are in Use a headless CMS with a Next.js template. This page lists what is specific to Tustel.

Pages read them through src/lib/content.ts: getProjects / getProject / getOtherProjects, getPosts / getPost / getOtherPosts, getServices, getTestimonials and getClients. Mirror src/content/types.ts in the CMS:

Collection Key fields
Project slug, title, category, summary, image, client, year, timeline, industry, deliverables, scope, description (paragraphs), gallery (images), challenge (paragraphs), challengeGallery (images)
Service slug, name, title, summary, image
Testimonial slug, name, role, photo, quote
Post slug, title, excerpt, image, category, date, body (rich text)
Client name, logo
  • No collection refers to another. A project’s category is free text; a post’s is one of a few words (the demo uses Photography, Development, Design and Branding), so a choice field in the CMS works.
  • description and challenge are lists of plain paragraphs: a list of text fields, or a long text your mapping function splits at blank lines. gallery and challengeGallery are lists of images; each gallery puts the first half of its list in the left column and the rest in the right one.
  • Keep the collection’s order in the CMS (a sort field), because the site shows items in the order it receives them, not by date. Home shows the first six projects (four on tablets and phones), every service (numbered 01, 02, … in that order), every testimonial, the first three posts and every client; a project page lists the first six other projects, a post the first three other posts; Portfolio and Blog load six at a time on desktop and four on tablets and phones.
  • Services, testimonials and clients have no pages: a service’s or a testimonial’s slug is only an id.
  • getOtherProjects and getOtherPosts filter the full list, so they keep working when getProjects and getPosts fetch from the CMS.

Post bodies are blocks, not HTML:

{ type: "heading", text: "…" } // one heading size
{ type: "paragraph", text: "…" } // *italic* between asterisks, "\n" for a line break
{ type: "list", ordered?: true, items: [[ …blocks ], [ …blocks ]] } // each item is a list of blocks

There is no bold and there are no links inside the text. The demo’s posts use headings, paragraphs (with italics and line breaks) and bulleted lists. Convert your CMS’s rich text to these blocks in your mapping function, italics as *…* (how).

Upload images at least as large as the layout uses: 2400 px wide for project covers and post photos, 1200 px for gallery photos, 800 px for service photos and 200 × 200 for testimonial portraits. Client logos are SVG files shown 25 px tall at their own width. Using a CMS’s images in the site: Images.

Project cards, the project cover, the galleries and the photo at the top of a post keep each photo’s own proportions. For files in public/, src/lib/image-size.ts reads the size when the site builds; a CMS image is an address it can’t read, so those boxes fall back to 4:3 (3:2 for a post photo, 100 × 25 for a client logo) and crop the photo to fit. Every CMS stores an image’s width and height: return them from your mapping functions and let imageSize() answer with them for CMS addresses. It is called from src/lib/to-ui.ts and from the project and post pages (src/app/portfolio/[slug]/page.tsx, src/app/blog/[slug]/page.tsx).