Skip to content
Sign inJoin waitlist

Editing the content — Tustel Astro

All the words of the site live in src/content/, and its photos in src/assets/images/. You edit plain JSON files (text in quotes, commas between entries), and the site updates while npm run dev is running. No CMS is needed. If you would rather manage content in Sanity, Payload, Strapi or Directus, see Using a headless CMS.

src/content/
site.json name, description, domain, menu, contact details, footer, and the parts several pages share
pages/home.json the copy of the home page
pages/about.json …and of every other page
collections/*.json projects, services, testimonials, posts, clients
routes.ts the URL of every page
schemas.ts the fields each kind of item has, and which are required

Not sure which file holds a piece of text? The page maps label every section of every page with its file.

src/content/site.json holds what appears on every page, and the sections that several pages share:

Field Shown where
site.name Browser title suffix (“About | Tustel”), share cards
site.owner The logo’s label for screen readers, in the header and the footer
site.title The browser title of the home page, and of any page without its own
site.description Search results and share cards of the home page, and of pages without their own description
site.url Set this to your domain before publishing. Canonical links, share links, sitemap.xml and robots.txt are built from it.
navigation The menu in the header (and the phone menu)
bookCall “Book a call”: the header button, and the first button in the Home hero, the photo band, “Let’s work together” and the About hero. It links to https://cal.com/: put your own booking page there (Cal.com, Calendly) or /contact.
viewWorks “View my works”: the second button in the same places, linking to the Portfolio
contact E-mail, phone, location and response time, with their labels, in “Let’s work together” (Home and the project pages)
socials The social links in the Home hero, the About hero and the footer
footer The note at the bottom of every page ({year} becomes the current year)
workTogether “Let’s work together” on Home and the project pages: label, title, text and photo
band The full-screen photo with a call to action on Home and About: title, text and photo
journal “From the journal” on Home and under every post: label, title and the “View all articles” link
facts The four counters on Home and About (“80+ Years Experience”, …)

The demo’s e-mail (hello@example.com), phone number and location are placeholders: replace all of them. The phone’s link is built from the number (+1 (415) 123-4567 calls tel:+14151234567), so write it the way people should read it. The Contact page shows only the form; your e-mail and phone appear in “Let’s work together”.

Links that start with https:// (the booking link, the social links) open in a new tab; mailto: and tel: links open the visitor’s mail app or phone.

A counter counts up by one every 5 ms from from to value when it scrolls into view, then shows suffix after it. This one counts from 40 to 120 and shows “120+”:

{ "value": "120", "from": "40", "suffix": "+", "label": "Projects Completed" }

Write whole numbers, in quotes like the demo’s. Keep the distance between from and value to a few hundred (a counter to 5000 from 0 would count for 25 seconds), and use "suffix": "" for no suffix.

Your name appears in several places: site.owner, the hero name in pages/home.json and pages/about.json, the footer note, the page descriptions (meta.description in about.json, portfolio.json and blog.json, contactPage.meta.description in contact.json), the descriptions of the two “About me” photos and the testimonial quotes (“Working with Alex…”). The logo draws it too (Making it look like your brand). Search src/content/ for “Alex” to find them all.

There is one file per page in src/content/pages/. Most hold the sections of that page and a meta entry:

{
"meta": {
"title": "About",
"description": "Alex Carter is a photographer and creative designer in California: portraits, brand photography, visual identities and websites, from idea to final detail."
},
"hero": { "…": "…" },
"process": { "…": "…" }
}

title becomes the browser tab (“About | Tustel”), and description the text in search results and share cards (80–160 characters). Change the text between the quotes, keeping the field names: the sections read them by name. Apostrophes are safe inside the double quotes, and the demo uses both the straight ’ and the typographic ’. A double quote inside the text needs a backslash before it (\"). JSON has no comments and no comma after the last entry of a list; if the site stops building after an edit, that is usually why.

File Page
home.json Home: the hero (the subtitle, the greeting, your name, your role, the three slideshow photos, and the card with your portrait, its label and the five specialities that scroll by), the line above the client logos, “About me” (two paragraphs, the “More about me” link, two photos), the portfolio heading with the words “Catalog” and “List”, the services heading and text, and the testimonials heading with the arrows’ labels for screen readers. It has no meta: its title and description are site.title and site.description.
about.json About: the hero (greeting, name, portrait, role, introduction) and “How I work” (label, title, text, photo and the four steps)
portfolio.json Portfolio (“Works” in the menu): label, title, text, and the text screen readers hear while more projects load
project.json The words shared by every project page: the four labels under the title (Client, Category, Year, Timeline), “Project Description”, “Project Details” and its four labels, “The Challenge” and “Other Projects”
blog.json Blog: label, title, text, and the text screen readers hear while more posts load
contact.json Contact: the page (contactPage: its meta, label, title and text) and the contact form (contactForm): fields, the note, the button texts and the title of each submission (Forms)
not-found.json The 404 page: its browser title (meta.title), “404”, the title, the text, the button and the photo

A post page has no copy file of its own: it shows the post, then “From the journal” (journal in site.json).

src/content/collections/ holds the lists the site builds pages and sections from:

File What it feeds
projects.json The Portfolio page, one page per project, the portfolio on Home (the first six on desktop, the first four on tablets and phones) and “Other Projects” at the end of a project page (the first six other projects)
services.json “What I Do” on Home (all of them, numbered 01, 02, … in the file’s order)
testimonials.json The testimonial slideshow on Home (all of them)
posts.json The Blog, one page per post, “From the journal” on Home (the first three) and under a post (the first three other posts)
clients.json The logo ticker on Home (all of them)

The order of the items in the file is the order on the site. The Blog doesn’t sort posts by date either: put a new post first to show it first. Portfolio and Blog show six items on desktop and four on tablets and phones, then more each time the visitor scrolls to the end of the list.

Copy an existing entry, paste it after the last one (mind the comma between entries), and change the values:

{
"slug": "lumen-studio",
"title": "Lumen Studio",
"category": "Brand Photography",
"summary": "A photography series for…",
"image": "/images/project-lumen-studio.jpg",
"client": "Lumen Studio",
"year": "2026",
"timeline": "4 Weeks",
"industry": "Hospitality",
"deliverables": "Brand Photography, Art Direction",
"scope": "Creative Direction, Post-production",
"description": ["First paragraph.", "Second paragraph."],
"gallery": ["/images/project-lumen-studio-1.jpg", "/images/project-lumen-studio-2.jpg", "/images/project-lumen-studio-3.jpg"],
"challenge": ["First paragraph.", "Second paragraph."],
"challengeGallery": ["/images/project-lumen-studio-4.jpg", "/images/project-lumen-studio-5.jpg", "/images/project-lumen-studio-6.jpg"]
}

Here’s what each field does:

  • slug is the URL: /portfolio/lumen-studio.
  • category is any words, shown on the cards and under the title.
  • summary is the text of the List view on Home, and the page’s search description.
  • image is the cover: the cards and the wide photo under the title.
  • client, category, year and timeline are the four details under the title; industry, deliverables, timeline and scope are “Project Details”.
  • description is “Project Description” and challenge “The Challenge”: one string per paragraph, plain text.
  • gallery holds the photos after the details, challengeGallery the photos after the challenge.

A project page shows the title with the client, category, year and timeline under it, then the cover, “Project Description” with its paragraphs, “Project Details” (industry, deliverables, timeline and scope), the first gallery, “The Challenge”, the second gallery, “Other Projects” and “Let’s work together”.

Each gallery has two columns: the first half of its photos on the left and the rest on the right (with three photos, two on the left and one on the right), every photo at its own proportions. The demo’s galleries have three or four photos.

Field What it is
slug The URL: /blog/<slug>
title The card and the top of the post
excerpt Under the title on the post page, and the search description
image The card (cropped to about 1.41 : 1) and the photo under the title, shown whole up to one screen tall
category Any words, shown on the card and above the title. The demo uses Photography, Development, Design and Branding.
date "2025-04-16", shown as “Apr 16, 2025”
body The text, as rich text (below)

A project’s search description is its summary, cut at 160 characters. A post’s is its excerpt; an excerpt shorter than 80 characters is followed by the start of the post’s first paragraph, cut at 160.

Add Fields
Service slug (an id: services have no page), name (the numbered row, “01 — Photography”), title and summary (shown for the chosen service) and image (shown 300 × 200)
Testimonial slug (an id), name, role ("Founder, Aurora Coffee"), photo (round, 68 px) and quote
Client name (also the logo’s text for screen readers) and logo: an SVG file in src/assets/images/ with a viewBox or a width and height, shown 25 px tall at its own proportions

On Home, pointing at a service on desktop (or tapping it on a tablet) shows its title, text and photo beside the list; on phones the chosen service opens under its name.

Delete the entry (and the comma before it, if it was the last one). Its page, its card and its sitemap entry disappear on the next build; no other item refers to it. Home leaves out its services list when there are no services, and its portfolio section when there are no projects.

src/content/schemas.ts is the reference: it lists every field, which are optional (.optional()), and a comment for the ones that are not obvious. npm run check reports a missing field (a misspelled name counts as missing) with the collection, the item and the field.

A post body is a list of blocks:

"body": [
{ "type": "heading", "text": "1. Aligning on Purpose and Personality" },
{ "type": "paragraph", "text": "A plain paragraph with an *italic* word." },
{ "type": "paragraph", "text": "The mistake:\nOvercomplicated menus leave users frustrated." },
{ "type": "list", "items": [
[{ "type": "paragraph", "text": "First item" }],
[{ "type": "paragraph", "text": "Second item" }]
] }
]

Headings have one size. Text is plain apart from two marks: words between asterisks are set in italics, and \n starts a new line inside a paragraph. There is no bold and there are no links inside the text. Each list item is itself a list of blocks, so an item can hold a nested list; add "ordered": true to a list for numbers instead of bullets. The demo’s posts use headings, paragraphs and bulleted lists.

  1. Put the file in src/assets/images/.
  2. Refer to it in the content as /images/<file>.

Astro resizes, compresses and converts the photos (to WebP) when the site builds, so each device gets a size that fits; SVG files are used as they are. Use photos at least as large as the demo’s, which the placeholders show:

Photos Demo size
The Home hero slideshow 2400 px wide, landscape (they fill the screen, cropped)
The photo band (Home, About) and the 404 photo 2400 × 3600, portrait (they fill the screen, cropped)
Project covers 2400 px wide, any proportion
Project gallery photos 1200 px wide, any proportion
Post photos 2400 px wide
“Let’s work together” (Home, project pages) 1200 × 1600, portrait
“How I work” on About 1200 × 1800, portrait
“About me” on Home 1000 × 750 and 600 × 399
Service photos 800 px wide (shown 300 × 200)
Your portrait (the Home hero card, the About hero) 300 × 300
Testimonial portraits 200 × 200

Photos in a fixed frame are cropped to fill it. Project covers, gallery photos and the photo at the top of a post are not: src/lib/image-size.ts reads the size of each file in src/assets/images/ when the site builds, so the project cards, the cover, the galleries and the post photo keep the photo’s own proportions and keep their space while it loads. Replacing one of these photos with one of another shape changes the height of its card or its place in the gallery: a portrait cover makes a tall card, and a row of cards lines up at the top. The cover on a project page and the photo of a post are at most one screen tall (cropped beyond). An image from another address (a CMS or a CDN) can’t be measured this way: its box falls back to 4:3 (3:2 for a post photo) unless you pass its size (Using a headless CMS).

A content path that points to a missing file stops the build with its name. Photos in the page copy written as { "src": …, "alt": … } have an alt: a short description for screen readers and search engines. Write one for each photo you change. A photo used in two places changes in both when you replace the file (your portrait is in the Home hero card and the About hero); to show different photos, give one of them another path in the content.

Files that browsers and social networks ask for by name live in public/:

File What it is
public/icon.svg, public/apple-icon.png The icon in the browser tab (it turns white in a dark browser theme) and on a phone’s home screen (180 × 180)
public/opengraph-image.png The picture shown when someone shares a link (2400 × 1260; 1200 × 630 or larger)

The share image’s description for screen readers is shareImageAlt in src/layouts/Layout.astro, and its size is written next to it (og:image:width and og:image:height): change both numbers if your image has another size. Every page uses this image when shared, except the project and post pages, which share their cover or photo.

Almost every word is content, including the form’s messages (contactForm in pages/contact.json). The few interface words that never change per site live in the code:

  • “Main” (the menu’s name for screen readers) in src/components/site/Bar.astro, and “Menu” (the phone menu button’s label) in src/design-system/components/navigation/Hamburger.astro;
  • the “/” between “Catalog” and “List” in src/components/sections/PortfolioSection.astro, and the “ — “ between a service’s number and its name in src/components/sections/ServicesSection.astro;
  • the dash between a post card’s date and category in src/design-system/components/cards/PostCard.astro;
  • the date format of posts (“Apr 16, 2025”) in src/lib/format.ts.

Search for the word and you will find it.

Terminal window
npm run check
npm run build

npm run check compares the collections with their schemas and the page copy with the sections that read it, and names the file and the field that is missing. A successful build means every page still renders and every image it shows exists. Neither checks that links written as text ("/contact") lead somewhere: click through the site once with npm run preview after big changes. See Going live for the rest of the checklist.