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.
Collections
Section titled “Collections”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
categoryis 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. descriptionandchallengeare lists of plain paragraphs: a list of text fields, or a long text your mapping splits at blank lines.galleryandchallengeGalleryare 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.tssorts byposition). 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
idequal to itsslug(a client’s to itsname, assrc/content.config.tsdoes):getProject(slug)andgetPost(slug)look entries up by it. Services, testimonials and clients have no pages: a service’s or a testimonial’sslugis only an id. - A post’s
dateis an ISO date ("2025-04-16"). getOtherProjectsandgetOtherPostsfilter the full list, so they keep working when the loaders fetch from the CMS.
Rich text
Section titled “Rich text”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 blocksThere 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).
Images
Section titled “Images”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).