Skip to content
Sign inJoin waitlist

Editing the content — Tustel Next.js

All the words and images of the site live in src/content/. You edit plain TypeScript files (quotes around text, commas between items), 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.ts name, description, domain, menu, contact details, footer, and the parts several pages share
pages/home.ts the copy of the home page
pages/about.ts …and of every other page
collections/*.ts projects, services, testimonials, posts, clients
routes.ts the URL of every page
types.ts the fields each kind of item has

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

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

Export 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:

{ value: "120", from: "40", suffix: "+", label: "Projects Completed" }, // counts 40 → 120, shows "120+"

Write whole numbers. 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.ts and pages/about.ts, the footer note, the page descriptions (meta.description in about.ts, portfolio.ts, blog.ts and contact.ts), 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 export an object with the sections of that page and a meta entry:

export const about = {
meta: {
title: "About", // browser tab: "About | Tustel"
description: "Alex Carter is a photographer and…", // search results and share cards (80–160 characters)
},
hero: { … },
process: { … },
};

Change the text between the quotes, keeping the field names: the sections read them by name. Apostrophes are safe inside double quotes, and the demo uses both the straight ’ and the typographic ’. A double quote inside a double-quoted string needs a backslash before it.

File Page
home.ts 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.ts About: the hero (greeting, name, portrait, role, introduction) and “How I work” (label, title, text, photo and the four steps)
portfolio.ts Portfolio (“Works” in the menu): label, title, text, and the text screen readers hear while more projects load
project.ts 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.ts Blog: label, title, text, and the text screen readers hear while more posts load
contact.ts Contact: label, title and text, and the contact form (contactForm): fields, the note, the button texts and the title of each submission (Forms)
not-found.ts The 404 page: its browser 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.ts).

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

File What it feeds
projects.ts 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.ts “What I Do” on Home (all of them, numbered 01, 02, … in the file’s order)
testimonials.ts The testimonial slideshow on Home (all of them)
posts.ts The Blog, one page per post, “From the journal” on Home (the first three) and under a post (the first three other posts)
clients.ts 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 below, and change the values:

{
slug: "lumen-studio", // the URL: /portfolio/lumen-studio
title: "Lumen Studio",
category: "Brand Photography", // any words: on the cards and under the title
summary: "A photography series for…", // the List view on Home and the page's search description
image: "/images/project-lumen-studio.jpg", // the cover: the cards and the wide photo under the title
client: "Lumen Studio",
year: "2026",
timeline: "4 Weeks", // under the title and in "Project Details"
industry: "Hospitality",
deliverables: "Brand Photography, Art Direction",
scope: "Creative Direction, Post-production",
description: ["First paragraph.", "Second paragraph."], // "Project Description"
gallery: ["/images/project-lumen-studio-1.jpg", …], // the photos after the details
challenge: ["First paragraph.", "Second paragraph."], // "The Challenge"
challengeGallery: ["/images/project-lumen-studio-5.jpg", …], // 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”. The paragraphs are plain text, one string each.

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 public/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. 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/types.ts is the reference: it lists every field, which are optional (?), and a comment for the ones that are not obvious. Your editor shows the same information while you type.

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." }, // \n: a line break
{ type: "list", items: [ // bullets; ordered: true for numbers
[{ 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. The demo’s posts use headings, paragraphs and bulleted lists.

  1. Put the file in public/images/.
  2. Refer to it in the content as /images/<file> (the path starts at public).

Sizes and formats are handled for you: pages use next/image, which serves a scaled, modern format per device. 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 public/ 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).

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.

Two things sit outside the content because browsers and social networks ask for them by name:

File What it is
src/app/icon.svg, src/app/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 shareImage.alt in src/lib/meta.ts, next to its size: change width and height there 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.ts). The few interface words that never change per site live in the code:

  • “Main” (the menu’s name for screen readers) and “Menu” (the phone menu button’s label) in src/components/site/Header.tsx;
  • the “/” between “Catalog” and “List” in src/components/sections/PortfolioSection.tsx, and the “ — “ between a service’s number and its name in src/components/sections/ServicesSection.tsx;
  • the dash between a post card’s date and category in src/design-system/components/cards/PostCard.tsx;
  • 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 build

A successful build means every content file has the fields it needs and every page still renders. It doesn’t check that the images exist or that links written as text ("/contact") lead somewhere: click through the site once with npm start after big changes. See Going live for the rest of the checklist.