Guides

Hand-Editing & Round-Trip

A Meno project is a normal Astro codebase. Pages live in src/pages/*.astro, components in src/components/*.astro, and content collections in src/content/. You can open any of these files in your code editor, edit them by hand, and the changes show up in the visual editor — and vice versa. The two surfaces operate on the same files.

This guide explains how that sync works, the small set of rules your hand-edits must follow to stay in sync, and what does and does not survive a save.

Two editing surfaces, one file

Meno bridges the visual editor and the .astro source with a codec called meno-astro. When you open a file, the codec parses it into the editor's in-memory model; when you save in the editor, it serializes that model back to .astro. That cycle is a lossless round-trip: parse then emit returns the exact same file, byte for byte.

There is one condition. Your hand-edits must stay inside the dialect — a small, well-defined subset of Astro that the codec understands. Write in the dialect and the editor reads your edits perfectly and re-emits them unchanged. Write outside it (a raw Tailwind class="...", ad-hoc frontmatter logic) and the codec can't map it into the model, so the next visual save drops it.

The dialect is not a separate language. It is plain .astro with a handful of conventions: styles go through style(), translatable text through i18n(), props are declared in one resolveProps() call, and Meno's template bindings appear as ordinary JSX expressions. The sections below are those conventions, stated as golden rules.

The fastest way to learn the shape is to open a file the editor generated — src/components/ui/Button.astro is a good one — and mirror it. Everything here is visible in real project files.

The golden rules

1. Styles go in style({...}), never a raw class

Every visual style is the argument to a style() call, not a class string. The argument is a Meno StyleObject — either { base, tablet, mobile } for responsive breakpoints or a flat object — and it round-trips exactly.

  • <div class={style({ base: { display: "flex", gap: "12px" } })}>

  • <div class="flex gap-3">

A raw class string has no place in the model, so the editor can't read it and a save discards it. Prop-bound values stay inside the object as a mapping: { _mapping: true, prop: "variant", values: { primary: "var(--text)", secondary: "var(--bg)" } }. See Styling and Style Properties for the full StyleObject shape.

2. Translatable text goes in i18n({...})

A localized value is an i18n() call wrapping the { _i18n: true, en, pl, ... } shape — one key per locale.

  • <Heading text={i18n({ _i18n: true, en: "About", pl: "O nas" })} />

  • <Heading text="About" /> (a plain string is single-locale; fine when you don't translate it, but it won't carry per-locale values)

The i18n() resolver returns the right string for the active locale at render time and is identity for non-i18n values. See Internationalization.

3. Templates: {{expr}} in the model, {expr} in markup

Meno's dynamic bindings are {{expr}} in the model. In .astro markup they appear as ordinary JSX expressions, and the codec converts between the two.

  • <span>{item.title}</span> ⟶ model "{{item.title}}"

  • ✅ ` <span>{$${item.price}}</span> ⟶ model "${{item.price}}"`

A whole-string template becomes a bare expression; a mixed string becomes a backtick template literal. To reintroduce a binding by hand, write a {expr} with a bare identifier, member access, or ternary — the parser turns it back into {{expr}}. See Template Expressions.

4. Component props are JSX attributes

A component instance passes its props as JSX attributes, typed by their value form.

  • ✅ string: text="Hi" (or text={"a \"quoted\" value"} if it contains quotes or newlines)

  • ✅ number: size={1} · boolean: isMarginTop={true}

  • ✅ object / link: link={{ href: "/x", target: "_blank" }}

  • ✅ i18n: text={i18n({ _i18n: true, en: "Save", pl: "Zapisz" })}

  • size="1" (a number prop passed as a string)

Component tags are Capitalized and need a matching local import in the frontmatter — import Button from '../components/ui/Button.astro' on a page, './Button.astro' between sibling components. See Building Components and Component Props.

5. resolveProps(Astro, {...}) is the single source of truth for props

A component declares its props exactly once, as the argument to resolveProps. There is no separate interface Props — the destructured names and their TypeScript types are regenerated from that literal on every save. To change a prop, change the literal.

  • const { text, class: className } = resolveProps(Astro, { text: { type: "string", default: "Heading" } });

  • ❌ adding a interface Props { text: string } and expecting it to drive the model

Always keep class: className in the destructure so every instance can carry wrapper styles, and emit the call even when there are no props: const { class: className } = resolveProps(Astro, {});. Real components often split it across two statements (assign __props, then destructure) so a style() call can forward __propssrc/components/ui/Button.astro does exactly this.

6. Conditionals and lists are expression wrappers

A conditional node is a logical-and expression; a list is a .map().

  • ✅ conditional: {visible && ( <div>A</div> )} (model if: "{{visible}}"); {false && ( ... )} hides a node; a boolean mapping uses {when({ _mapping: true, prop: "icon", values: { arrow: true } }, __props) && ( ... )}

  • ✅ prop list: { list(items, { limit: 6 }).map((item, itemIndex) => ( ... )) }

  • ✅ collection list: a frontmatter const blogList = await getCollectionList("blog", { limit: 6 }, Astro) then { blogList.map((blog, blogIndex) => ( ... )) }

In a prop list the loop variable defaults to item; in a collection list it defaults to the singular of the source. The index is always <var>Index. If you author a collection list by hand, make the loop variable match the {{blog.title}} templates in the body, or set itemAs to name it. See Lists.

What round-trips and what doesn't

Anything written inside the dialect round-trips exactly. Edit a style() object, change a prop in resolveProps, add a <Button> with JSX attributes, wrap a node in {cond && ( ... )} — the editor reads it, and a later save re-emits it unchanged.

Content outside the dialect does not survive a save. The two to avoid:

  • A foreign class="px-4 flex" — Meno has no model for an arbitrary class string. Express styles through style({...}) instead.

  • Arbitrary frontmatter logic — extra const/import/function declarations beyond the conventions above aren't part of the model.

The practical rule is simple: if you can express something in the dialect, do; if you can't, leave that piece to a real Astro file outside the Meno-managed source rather than hand-writing it and hoping it persists. When in doubt, build the thing once in the visual editor, look at the .astro it produces, and copy that shape.

Determinism: don't hand-tune formatting

Every literal — style() objects, props, meta, i18n() values, list config — is printed by a deterministic serializer. A save always re-emits canonically:

  • stable key order (insertion order, with undefined values dropped)

  • JSON-style string escaping, identifier keys unquoted ("1": "67px" only when the key isn't a valid identifier)

  • 80-column wrapping: short values stay inline, longer ones expand to one entry per line

It also drops content-free defaults on load — an empty style, empty children, empty meta, an empty interface, and a lone single-element array child all normalize away. So a tablet: {} you add by hand is harmless but may disappear, and a one-element ["text"] collapses to the bare string "text".

The takeaway: don't fight the formatter. Hand-tuned indentation or key order gets rewritten the moment the editor saves. Match what generated files already look like and your edits and the editor's output stay byte-identical.

Practical tips

  • Mirror a generated file. Open one the editor produced (e.g. src/components/ui/Button.astro) and copy its structure. It is the most reliable spec.

  • Make small, targeted edits. Change one style() object, one prop, one piece of text. Small edits are easy to keep inside the grammar and easy to verify.

  • The editor watches your files. Save a .astro file on disk and the visual editor picks up the change automatically. You can keep both open and bounce between them.

  • Use an AI agent. Claude Code and other file-aware agents edit .astro files directly. Point them at the [/meno-astro skill](/docs/claude-code), which carries this same grammar as a copy-pasteable cheat-sheet, and they stay in the dialect for you.


For the node-by-node grammar, see Node Types. To learn the file skeletons from scratch, see Creating Pages and Building Components. For editing with an AI agent, see Claude Code. For the full runtime helper surface (style(), i18n(), list(), getCollectionList(), and the rest), see the meno-astro API.

Building the future of digital experiences, one website at a time

Product

Resources

Comparisons

© 2026 Meno. All rights reserved.