Menu
Site code reference
Pages and components
How a Starjay site splits structure, presentation and wording across page files, component files and content files, and how to change text correctly.
A Starjay site keeps three things apart:
- Page files (
src/pages/*.astro) hold structure: which sections appear, in what order, and what each one receives. - Component files (
src/components/{sections,layout,ui}/*.astro) hold presentation: markup, classes and responsive behavior. They take their content throughAstro.props. - Content files (
content/pages/*.jsonandcontent/components/**/*.json) hold the visible wording.
Where the wording goes
After every AI change, whether from Starjay's AI or your own, Starjay moves the visible text out of the .astro files that change touched and into content files. It then rewrites the code to read the text from there:
- Text a page passes to a component goes into the page's content file,
content/pages/{route}.json. The page passes it on as a prop:heading={content.ourStory.heading}. - Text written directly in a page's own markup goes under
copyin the same file:{content.copy.questions}. - A component's default text and its own template text go into
content/components/{category}/{Name}.json. The component imports that file and uses the values as prop defaults:heading = copy.heading.
The content file mirrors the source path: src/pages/index.astro uses content/pages/index.json, src/pages/blog/[slug].astro uses content/pages/blog/[slug].json, and src/components/sections/Hero.astro uses content/components/sections/Hero.json.
Not every string moves. Starjay only moves text it can prove renders exactly the same afterwards, so some text stays in code: link addresses, image alt text, text inside expressions, and anything it can't move safely.
This happens as part of the change, so stepping back through versions or rejecting the session undoes it along with everything else.
Styled phrases use named marks
When a sentence mixes plain text with styled words, line breaks or simple variables, it's stored as one marked value and rendered by the <Text> component:
<name>…</name>applies a mark. Marks can nest but not overlap.\nis a line break.{name}is a variable that the code fills in. It always renders as text.- To write a literal
<,{or}, use<,{or}. Write&only where a literal&would otherwise start one of those escapes.
strong, b, em and i work everywhere. Every other mark is defined once in src/marks.json as an element and a class. Changing a mark's class there restyles every place that uses it. Names that Starjay creates combine a readable stem with a short code, such as textAccent3k9f2a. The friendly names you see on the formatting toolbar are labels for these marks. In code, always use the mark's name, never its label.
A value that names a mark missing from src/marks.json shows that phrase as plain text. A malformed value (an unclosed mark, say) shows the whole value as plain text. Neither breaks the page.
Worked example
An About page with one section. The page passes its section's text from its content file and has one line of its own copy:
---
// src/pages/about.astro
import Base from '../layouts/Base.astro'
import StoryIntro from '../components/sections/StoryIntro.astro'
import content from '../../content/pages/about.json'
// An address returned by browse_assets, used as is
const teamPhoto = '/_images/team-at-work~0c9e5b1a-6d2f-4e8a-9b47-2f1d8c3a7e60/public'
---
<Base>
<StoryIntro
heading={content.ourStory.heading}
body={content.ourStory.body}
imageUrl={teamPhoto}
imageAlt="Four people sketching around a workshop table"
/>
<p class="mx-auto max-w-3xl px-6 py-12 text-ink-700">{content.copy.questionsWriteToUs}</p>
</Base>
// content/pages/about.json
{
"ourStory": {
"heading": "Our story",
"body": "We started in a garage in 2019, and we still answer every email ourselves."
},
"copy": {
"questionsWriteToUs": "Questions? Write to us any time."
}
}
The component declares its props, takes its defaults from its own content file, and renders marked text through <Text>:
---
// src/components/sections/StoryIntro.astro
import copy from '../../../content/components/sections/StoryIntro.json'
import Text from '@starjay/design-system/components/text/Text.astro'
interface Props {
heading?: string
body?: string
imageUrl?: string
imageAlt?: string
}
const { heading = copy.heading, body = copy.body, imageUrl, imageAlt = '' } = Astro.props as Props
---
<section data-component="StoryIntro" class="w-full bg-surface-50 py-24">
<div class="mx-auto grid max-w-5xl gap-12 px-6 md:grid-cols-2">
<div class="flex flex-col gap-6">
<p class="text-sm uppercase tracking-wide text-ink-500">
<Text value={copy.tagline} marks={['textAccent3k9f2a']} />
</p>
<h2 class="font-display text-4xl text-ink-900">{heading}</h2>
<p class="text-lg text-ink-700">{body}</p>
<p class="text-ink-700"><Text value={copy.signOff} /></p>
</div>
{imageUrl && <img src={imageUrl} alt={imageAlt} class="w-full rounded-lg object-cover" loading="lazy" decoding="async" />}
</div>
</section>
// content/components/sections/StoryIntro.json
{
"heading": "Our story",
"body": "Tell visitors how you started.",
"tagline": "Built for <textAccent3k9f2a>small teams</textAccent3k9f2a>",
"signOff": "See you soon,\nThe <strong>Example Studio</strong> team"
}
The page passes heading and body, so the page's values show. The tagline and sign-off are the component's own text: they're the same on every page that uses StoryIntro.
// src/marks.json
{
"textAccent3k9f2a": { "tag": "span", "class": "text-accent-600" }
}
<Text> takes value (the marked string), marks (the custom marks this field offers on the formatting toolbar; it doesn't change what renders) and vars (values for {name} placeholders). It renders the text with no wrapper element.
Change text correctly
Edit the content file, not the code, whenever the text has already been moved there. write_file and patch_file both accept content/pages/**.json and content/components/**.json.
- 01
Find where the text is read
Open the page or component and look at the prop or expression that renders the text.
content.ourStory.headingin a page meanscontent/pages/about.json.copy.headingin a component means that component's own content file. - 02
Pick the right file for the scope you want
A value in a page's content file changes that page only. A value in a component's content file is a default: it changes every page that uses the component without passing that prop. Layout components such as the header and footer usually take all their text from defaults, so editing their content file changes every page.
- 03
Change the value, keep the key
Edit only the string. The code reads each value by its key, so renaming or removing a key breaks the page. The file must stay valid JSON; a write that isn't valid JSON is refused.
- 04
Keep marked values well formed
Close every mark you open, use only mark names that exist in
src/marks.json(orstrong,b,em,i), and keep every{name}variable the code passes invars.
To add new text, write it directly in the .astro file as an ordinary string or element text. When your change ends, Starjay moves it into the right content file for you. You can also add the key to the content file yourself and read it in code.
To add a new style for part of a sentence, add a mark to src/marks.json and use it in the value, or write the styled element in the markup and let Starjay convert it.