Node Types
Every page and component in a Meno project is a tree of nodes, and each node has a type that determines how it renders as .astro markup. Whether you build visually in the Meno editor or hand-edit the files, the same node types map to the same meno-astro dialect. This page is the catalog: for each type, what it looks like in markup and the rules that keep it round-tripping.
There are seven node types — node, component, slot, link, embed, list, and locale-list — plus dynamic tags and conditionals that wrap any node. Nothing is ever silently dropped: a type the codec does not recognize emits as a visible {/* meno:unknown */} comment rather than disappearing.
For prop values and templates that appear inside these nodes, see Component Props and Template Expressions.
node — HTML element
The most common node. It renders as a standard HTML element whose styles live in a class={style({…})} attribute, followed by any HTML attributes, then children.
<div class={style({ base: { display: "flex", gap: "12px" } })}>
<span class={style({ base: { color: "var(--text)" } })}>{text}</span>
</div>Styles always go inside style({...}), never a raw class="..." string. The argument is a Meno StyleObject — a flat object, or { base, tablet, mobile } for responsive breakpoints. Prop-bound values stay inline as { _mapping: true, prop, values }.
HTML attributes follow the class. A plain string uses the bare form; numbers and booleans use braces; templated strings become expressions:
<img class={style({ base: { objectFit: "cover" } })} src="/images/hero.webp" alt="" width={1200} height={600} />Void elements (img, br, input, …) self-close and ignore children. Text children are emitted raw when they stand alone (<h1>Hello</h1>) and as {"…"} expressions when they sit beside sibling nodes, so adjacent text stays distinct.
Editor metadata — interactiveStyles, label, generateElementClass — rides along in an optional second style() argument, renamed to interactive, label, and genClass:
<button class={style({ base: { color: "var(--text)" } }, {
interactive: [{ name: "onHover", postfix: ":hover", style: { base: { color: "var(--accent)" } } }]
})}>{text}</button>component — component instance
An instance of a reusable component renders as a Capitalized JSX tag with its props as attributes. The component name needs a matching local import in the frontmatter (import Heading from '../components/Heading.astro' on a page, or './Heading.astro' from another component).
<Heading size={1} text={i18n({ _i18n: true, en: "About", pl: "O nas" })} align="center" />
<Button isMarginTop={true} text="Services" link={{ href: "/services" }} />Each value type has a fixed encoding:
string →
text="Hi"number →
size={1}boolean →
isMarginTop={true}object / link →
link={{ href: "/services", target: "_blank" }}i18n value →
text={i18n({ _i18n: true, en: "…", pl: "…" })}
The component's own props are declared once in its resolveProps(Astro, {…}) block — that argument is the single source of truth, not a separate interface Props. See Component Props for the full list of prop types.
slot — child insertion point
A slot marks where a component instance's children are placed. It renders as a self-closing <slot />, or with fallback content when the node carries default children.
<slot />
<slot><p>Default content</p></slot>A component with no structure of its own emits a bare <slot /> as its entire body.
link — object link
A link renders through the runtime Link component, which localizes internal hrefs and applies the link reset. Imported from meno-astro/components.
<Link href="/pricing">Pricing</Link>The href resolves like an attribute. A plain string stays a string; an i18n href wraps in i18n({…}); a prop-bound href (a LinkMapping) wraps in href({…}):
<Link href={i18n({ _i18n: true, en: "/about", pl: "/o-nas" })}>About</Link>
<Link href={href({ _mapping: true, prop: "link" })} class={style({ base: { display: "flex" } })}>{text}</Link>embed — raw HTML / SVG
An embed passes raw HTML or SVG through untouched, via the runtime Embed component. A single-line payload inlines as a backtick template literal:
<Embed html={`<svg viewBox="0 0 24 24"><path d="M5 12h14" /></svg>`} />A multi-line payload is hoisted to a frontmatter const __embedN so the verbatim HTML is never re-indented, then referenced by name:
---
const __embed0 = `<svg width="515" height="84">
<path d="…" fill="#74B0FF" />
</svg>`;
---
<Embed html={__embed0} />When html is bound to a CMS field, it renders as <Embed html={i18n(cms.field)} />.
list — repeating items
A list repeats its children once per item. The source is either a component prop or a CMS collection. See Lists for the full picture.
A prop list maps over a prop with the list() helper. The loop variable defaults to item, and the index variable is always <itemAs>Index. limit and offset become the options object:
{ list(items, { limit: 6 }).map((item, itemIndex) => (
<div class={style({ base: { padding: "20px" } })}>
<h3>{item.title}</h3>
<p>{item.description}</p>
</div>
)) }A collection list hoists its query to a frontmatter const with getCollectionList(...), then maps over the result. The loop variable defaults to the singularized collection name ("products" → product); set itemAs to choose another. Make the templates in the body match the loop variable.
---
const productsList = await getCollectionList("products", { limit: 10 }, Astro);
---
{productsList.map((product, productIndex) => (
<div class={style({ base: { border: "1px solid var(--border)" } })}>
<h3>{i18n(product.title)}</h3>
<span>{`$${product.price}`}</span>
</div>
))}Collection items are raw entry data, so bare {{item.*}} chains emit wrapped in i18n(…) (identity for non-i18n values). Query options hoisted into getCollectionList include filter, sort, limit, offset, items, and excludeCurrentItem.
locale-list — language switcher
A locale-list renders the runtime LocaleList component, which generates a link for each configured locale. Boolean and display props are JSX attributes; each style sub-object is wrapped in style(…). See Internationalization.
<LocaleList displayType="nativeName" showFlag={true} style={style({ base: { display: "flex", gap: "8px" } })} itemStyle={style({ base: { textDecoration: "none" } })} activeItemStyle={style({ base: { fontWeight: "600" } })} />displayType accepts "code", "name", or "nativeName". The style sub-props are style, itemStyle, activeItemStyle, separatorStyle, and flagStyle; any interactiveStyles, label, or generateElementClass collect into a single meta={{…}} attribute.
Dynamic tags
When a node's tag itself contains a template — for example a heading whose level comes from a size prop (h{{size}}) — it cannot be a literal JSX tag. The codec hoists it to a frontmatter const Tag_N and references that:
---
const Tag_0 = `h${size}`;
---
<Tag_0 class={style({ base: { fontWeight: "500" } })}>{text}</Tag_0>The original tag string is restored on parse, so this round-trips like any other node.
Conditionals
Any node can be conditionally rendered. The condition wraps the node as {cond && ( … )}:
{visible && (
<div class={style({ base: { padding: "16px" } })}>A</div>
)}The condition takes three forms:
a template (
{{visible}}) →{visible && ( … )}a literal
false→{false && ( … )}a prop-bound
BooleanMapping→{when({…}) && ( … )}
Summary
Node type | Markup | Notes |
|---|---|---|
|
| Styles in |
|
| Capitalized tag, needs a local import |
|
|
|
|
|
|
|
| Multi-line hoists to |
|
| Prop list vs collection list |
|
| Style sub-props wrapped in |
Dynamic tag |
| For templated tag names |
Conditional |
| Wraps any node |
Nothing is dropped on the way through the codec. A node type it does not recognize emits as {/* meno:unknown "type" */}, so unsupported content stays visible in the source instead of vanishing on the next save.
See also Template Expressions for how {{…}} templates and i18n() wraps resolve, and Component Props for the prop types these nodes carry.