Skip to content
Sign inJoin waitlist

Using a headless CMS — Tustel Astro

Connecting a headless CMS works the same for every Framebrick Astro template. Loaders and schemas, rich text, images, rebuilding on publish, and step-by-step guides for Sanity, Payload, Strapi and Directus are in Use a headless CMS with an Astro template. This page lists what is specific to Tustel.

src/content.config.ts defines them, src/content/schemas.ts checks every entry when the site builds, and pages read them through src/lib/content.ts: getProjects / getProject / getOtherProjects, getPosts / getPost / getOtherPosts, getServices, getTestimonials and getClients. To fetch a collection from a CMS, replace its loader and keep its schema, or rewrite those functions; pages, sections and components stay unchanged. Mirror the schemas in the CMS:

Collection Key fields
projects slug, title, category, summary, image, client, year, timeline, industry, deliverables, scope, description (paragraphs), gallery (images), challenge (paragraphs), challengeGallery (images)
services slug, name, title, summary, image
testimonials slug, name, role, photo, quote
posts slug, title, excerpt, image, category, date, body (rich text)
clients 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 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) and return entries in that order with their position, because the site shows items in the order it receives them, not by date (src/lib/content.ts sorts by position). 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.
  • Keep each entry’s id equal to its slug (a client’s to its name, as src/content.config.ts does): getProject(slug) and getPost(slug) look entries up by it. Services, testimonials and clients have no pages: a service’s or a testimonial’s slug is only an id.
  • A post’s date is an ISO date ("2025-04-16").
  • getOtherProjects and getOtherPosts filter the full list, so they keep working when the loaders 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, 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.

A CMS gives full image URLs: the image components (FillImage and SizedImage in src/design-system/components/media/) and the share images take either a /images/<file> path or a URL (isRemote() and imageUrl() in src/design-system/utils/image.ts). Allow the CMS’s image host in astro.config.mjs (image.domains) so Astro resizes its photos too.

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 src/assets/images/, 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 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/pages/portfolio/[slug].astro, src/pages/blog/[slug].astro).