Astro
Next release
How midcode runs astro dev with one plugin added, what it edits in an .astro file, and how components, islands, pages and styles work.
This ships with the next release of midcode. The version you can download today (1.1.2) doesn’t have it yet.
midcode edits an Astro site in the markup of its .astro files, the part below the frontmatter: text, classes, attributes, new elements, their order, and the props a component is given where it’s used. The frontmatter and whatever sits inside { } stay exactly as you wrote them, except for what midcode adds there when you ask for it: the import of a component you drag in from the Assets tab, a stylesheet import in a site without Tailwind, and a variant prop when you give a component variants. An island (React, Preact, Solid, Svelte, Vue) is edited in its own file. midcode 1.1.2 and earlier edit only an Astro site’s JSX islands, and leave .astro files to view and comment.
At a glance
| Astro | |
|---|---|
| Detected by | astro in the dependencies of package.json |
| Runs with | Your project’s own astro dev, with your config and one Vite plugin added |
| Elements are marked by | That plugin, in memory, as each .astro file is loaded |
| Editing | Text, classes, attributes, tag; insert, move, duplicate, delete, wrap in a stack |
| Components | .astro components are recognised where they’re used, with their props as typed controls, and open on their own canvas with variants, states and variables |
| Pages | The files of src/pages. New pages and removing them |
| Styles | Your Tailwind 4 classes, or mid: utilities with midcode.css imported in each layout |
| Tried with | Astro 7 |
How midcode runs it
A project is Astro when astro is in its dependencies. midcode starts the Astro that’s in your node_modules:
astro dev --root <your project> --config <midcode's config> --port 4310 --host localhost --ignore-lockThe port is the first free one from 4310, and the process runs with the Node your shell has, or with midcode’s own when there is none. Astro’s telemetry is turned off for that process.
The config is a small file in midcode’s own data folder (~/Library/Application Support/midcode/injected/astro.config.mjs). It imports your astro.config and adds one plugin to its vite.plugins. Your config, your package.json and your lockfile aren’t changed, and the plugin only applies to the dev server, never to a build.
Astro compiles an .astro file before any plugin gets to transform it, so midcode hands the file over already marked, as it’s loaded. Take this component:
---
import Card from './Card.astro'
const { title } = Astro.props
const woods = ['Oak', 'Walnut']
---
<section class="hero">
<h1 class="title">{title}</h1>
<p class="lead">Furniture made to order.</p>
<ul>
{woods.map((wood) => <li class="wood">{wood}</li>)}
</ul>
<Card title="Oak" price={9} />
</section>What Astro compiles while midcode runs it has a mark on every element: data-mc="src/components/Hero.astro:6:1" on the <section> (the file, line and column where it’s written), and data-mci="Card|src/components/Hero.astro:12:3" on the <Card>. The elements at the top of each .astro file also get a data-mcu, read from Astro.props, which says where that component is used. Nothing is written to disk.
<html>, <head> and what’s inside it, <script> and <style> are never marked.
If the server doesn’t come up this way (the process stops, or nothing answers in 45 seconds), midcode runs your own dev script instead and says so: the site shows, without marks, and you can only view and comment.
What you can edit
The tools are the editor’s own: select, move and resize, text and media, the style panel, Insert. In the component above:
| Element | On the canvas |
|---|---|
<p class="lead"> | Everything: its text, its classes, its place |
<h1 class="title"> | Classes and attributes. Its text is {title}, which isn’t written there |
<li class="wood"> | Classes and attributes. An expression draws it, so it can’t be moved or removed |
<Card … /> | Its props, where it’s used |
Several elements can be removed, moved or wrapped in a stack (⇧A) at once, and ⌥-drag leaves the element where it is and puts a copy where you drop it. All of it stays within one file.
Text, classes and attributes
A text is replaced in the markup when everything inside its element is written text:
- <p class="lead">Furniture made to order.</p>+ <p class="lead">Furniture made to last.</p>A { or } you type is written as an entity, so Astro doesn’t read it as an expression. Text that comes from an expression is looked for as an exact string in the project’s .ts, .js, .json and .md files and edited there when midcode can tell which one it is; otherwise it’s edited in the code.
The style panel writes into a written class="…" attribute and creates it when there’s none. A class:list directive is an expression and is left alone. An attribute’s value is written as text (href="/about"), as a bare name when it’s a switch (disabled), or in braces when it’s a number or other code (width={640}).
Elements an expression draws
An element written inside { } (the <li> in the .map above, or what a condition returns) is one piece of that expression. You can restyle it, and every copy it draws changes with it. Moving or removing it would break the expression, so midcode refuses: “An expression draws this element ({…}). Move or remove it in the code.”
Components and props
An .astro component used in a page shows in Layers with its name and is selected as an instance. Its Props are typed controls, read from the interface Props (or type Props) in its frontmatter, with the defaults its Astro.props destructuring gives:
---
interface Props {
title: string
price?: number
featured?: boolean
}
const { title, price = 0, featured = false } = Astro.props
---A change is written where the component is used:
- <Card title="Oak" price={9} />+ <Card title="Walnut" price={12} featured />A value equal to the declared default removes the attribute. Astro’s own pass-through components get no instance mark: Fragment, ViewTransitions, ClientRouter, Content, Debug, Code, Prism.
Drag a component from the Assets tab onto the page and midcode writes the instance with its import, in the frontmatter (one is added at the top of the file when it has none), as a relative path with its extension: import Card from '../components/Card.astro'.
Variants and states
An .astro file outside your pages and layouts folders is a component. The Assets tab lists it, and it opens on its own canvas, where you add variants and states: see Components.
A state is written as classes (hover:, active:, focus-visible:). A variant is a variant prop: a variant?: 'primary' | 'ghost' member in interface Props, its default in the frontmatter’s destructuring (const { variant = 'primary' } = Astro.props), and data-variant={variant} with a group/<name> class on each element at the top of the markup. A component with no props yet gets the interface and the destructuring. Renaming or removing a variant follows the instances that set it (variant="ghost").
Islands
The same plugin marks the JSX of .tsx and .jsx files, .svelte files (read with your project’s Svelte compiler) and .vue templates. An island is edited in its own file, like in a project of that framework: see React with Vite, Svelte and Vue.
Pages
Pages are the files of src/pages: .astro, .md, .mdx, .html, .tsx and .jsx.
| File | Page |
|---|---|
src/pages/index.astro | The home page |
src/pages/about.astro | /about |
src/pages/blog/[slug].astro | /blog/[slug], a dynamic route |
For a dynamic route, the page menu in the top bar lists its real pages, found in the running site’s sitemap and links (Pages and navigation). Folders whose name starts with _ or a dot aren’t listed.
“New page” in the page menu writes src/pages/<path>.astro the way your home page is written, and “Remove” moves a page’s file to the Trash. When the home page imports a layout from a layouts folder and wraps its content in it, the new page does the same:
---
import Layout from '../layouts/Layout.astro'
---
<Layout>
<main class="mx-auto max-w-3xl px-6 py-24">
<h1 class="text-4xl font-semibold">About</h1>
<p class="mt-4">A new page, made in midcode.</p>
</main>
</Layout>If the home page gives its layout a title, the new page gets title="About". Without a layout, the new page is a whole document that imports the stylesheets the home page imports in its frontmatter. The class names are written when Tailwind is in your package.json.
A page written in Markdown shows on the canvas, but Markdown carries no marks. Edit that content in the CMS: a folder of .md or .mdx files with front matter is a collection there.
Site settings
The globe in the top bar opens Site settings. In an Astro site the title, the description, the language, search engines, the favicon and the social image are tags in the <head> of a layout: the first file of src/layouts, by name, that closes a <head>, or src/pages/index.astro when no layout does. Images are copied into public/.
What the layout takes from its props (<title>{title}</title>) shows as code and is left to your code. There’s one form for the whole site: pages have none of their own.
Styles
With Tailwind 4 (a stylesheet with @import "tailwindcss" or an @theme block), the panel writes your own utilities: see Tailwind CSS.
Without it, midcode writes its own prefixed utilities (mid:p-6, mid:md:flex) and keeps their plain CSS in src/midcode.css. Astro has no single entry file, so the first style edit imports the stylesheet in the frontmatter of every layout in src/layouts (or of every page in src/pages, when the site has no layouts), as one undoable step:
---+import '../midcode.css' import Header from '../components/Header.astro' ---A layout with no frontmatter gets one with that single line. Without Tailwind explains what’s in the file.
Limits
Insert’s interactive components are React (“Needs a React page”), and icons go in as inline SVG.
An element whose whole class is an expression (
class={classes}) is styled in the code: the panel works on a writtenclass="…".A tag with a dash in its name (a custom element like
<my-widget>) is read as a component and gets no mark.Elements move within one file.
Pages are looked for in
src/pages. With anothersrcDir, the list has the home page alone, and any path can be typed in the page menu.variables are written as
{title},src={image}and astyle={{ … }}object. A prop the frontmatter or the markup also reads in its own code isn’t renamed or removed from the canvas.Variants aren’t written for a component whose root is another component (“Its root is another component that doesn’t pass its props on ({…props}), so data-variant wouldn’t reach the page”), nor for one whose markup or frontmatter already reads
variant(“This component’s variants are written in code: edit them there or ask your agent”).In the released version there’s no free canvas. The next release brings the free canvas, for plain elements: a component or an element with an expression in it can’t float.
Tried with Astro 7: editing
.astrofiles, and a component used three times, once inside a.map. An Astro 7 site without Tailwind was tried too (a layout with no frontmatter gets one), and so were creating and removing a page, Site settings, and component mode (variants read, added, renamed and removed).