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 requiredNot sure which file holds a piece of text? The page maps label every section of every page with its file.
Your details first
Section titled “Your details first”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.
Page copy
Section titled “Page copy”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).
Collections
Section titled “Collections”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.
Adding a project
Section titled “Adding a project”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:
slugis the URL:/portfolio/lumen-studio.categoryis any words, shown on the cards and under the title.summaryis the text of the List view on Home, and the page’s search description.imageis the cover: the cards and the wide photo under the title.client,category,yearandtimelineare the four details under the title;industry,deliverables,timelineandscopeare “Project Details”.descriptionis “Project Description” andchallenge“The Challenge”: one string per paragraph, plain text.galleryholds the photos after the details,challengeGallerythe 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.
Adding a post
Section titled “Adding a post”| 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.
Adding a service, testimonial or client
Section titled “Adding a service, testimonial or client”| 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.
Removing an item
Section titled “Removing an item”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.
What each field means
Section titled “What each field means”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.
Rich text: posts
Section titled “Rich text: posts”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.
Images
Section titled “Images”- Put the file in
src/assets/images/. - 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.
Text that is not in content/
Section titled “Text that is not in content/”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) insrc/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 insrc/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.
After editing
Section titled “After editing”npm run checknpm run buildnpm 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.