Skip to content
Docs
    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 through Astro.props.
    • Content files (content/pages/*.json and content/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 copy in 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.
    • \n is a line break.
    • {name} is a variable that the code fills in. It always renders as text.
    • To write a literal <, { or }, use &lt;, &#123; or &#125;. Write &amp; 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.

    1. 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.heading in a page means content/pages/about.json. copy.heading in a component means that component's own content file.

    2. 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.

    3. 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.

    4. 04

      Keep marked values well formed

      Close every mark you open, use only mark names that exist in src/marks.json (or strong, b, em, i), and keep every {name} variable the code passes in vars.

    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.