Skip to content

Sites a server renders

Next release

How midcode edits a site whose HTML a server builds from templates (PHP, Django, Rails, Hugo and others) by finding each element of the page in your template files.

View as Markdown

This ships with the next release of midcode. The version you can download today (1.1.2) doesn’t have it yet.

A PHP, Django, Rails, Hugo, Jekyll or Eleventy site can be edited on the canvas: text, classes, attributes, the tag, new elements, their order. The edit is written into the template file. What midcode can’t edit is what the server works out while it renders: a value printed from a variable, content that comes from a database, HTML a helper builds.

This page explains how that works, because it’s different from a React or Vue project, and it decides what is and isn’t editable. Each stack has its own page with the command midcode runs and what was tried: Laravel, WordPress, PHP and Twig, Django, Flask and FastAPI, Ruby on Rails, Hugo, Jekyll and Eleventy.

At a glance

Sites a server renders
Runs withThe stack’s own command on a port midcode picks, or the command and address you give it
Elements are marked bymidcode, after each page loads, by matching the page against the project’s template files
EditingText, classes, attributes and tag; insert, move, duplicate and remove. Only what is written in a template
ComponentsNot recognised. A partial or an include is edited in its own file
PagesRead from the stack’s routes or files where midcode knows how; otherwise you type the path. New pages in Hugo, Jekyll, Eleventy and Laravel
StylesTailwind 4 classes, or mid: utilities with a midcode.css linked from your layouts
Tried withPlain PHP, Twig, Django, Flask, FastAPI, Rails 8, Hugo, Jekyll 4, Eleventy, Express with EJS, WordPress

Why these pages arrive without marks

In a Next.js or Vite project, midcode adds a step to the dev server that marks every element with the file, line and column where it’s written. That’s how a click on the canvas knows which line to change (How midcode works).

A site that a server renders has no such step. PHP, Python, Ruby or a site generator turns templates into finished HTML, and midcode has no place to hook into that. The page reaches the canvas with nothing that says where each element came from.

So midcode puts the marks on afterwards. Each time a page loads on the canvas, it looks up every element of that page in the project’s templates and, where it finds it, gives it the same mark (file:line:col). From then on the canvas treats it like any marked element. Nothing is added to your project to do this: the marks exist only in the page shown inside midcode.

This is how midcode edits every stack it starts with the stack’s own command, and any project with a dev script whose pages come from templates, such as Express with EJS (Any other stack). An Angular app is matched the same way, against its components’ templates. Two stacks get real marks instead: Laravel’s Blade views, marked as Blade compiles them, and Shopify themes, where midcode uploads a marked copy of the theme.

How midcode finds an element

When a page loads, midcode notes for each element its tag, classes, id, its own text, a few attributes and what it sits inside. Then it reads every template in the project and lists the elements written there, each with what is fixed and what is computed.

Take this template:

templates/home.html
{% extends "base.html" %}
{% block content %}
<section class="hero">
  <h1>Fresh bread, every morning</h1>
  <p class="lead">{{ tagline }}</p>
  <ul class="menu">
    {% for item in menu %}
    <li class="menu-item">{{ item.name }}</li>
    {% endfor %}
  </ul>
</section>
{% endblock %}
  • The <h1> holds only words, and no other template has an <h1> with those words. It’s found at once, and its text can be edited.

  • The <section class="hero"> isn’t written inside the layout’s <main>, where the page shows it. midcode looks for it across the project: it’s the <section> with that class that contains the <h1> already found.

  • The <p class="lead"> and the <ul class="menu"> are found among the children written inside that <section>. The paragraph’s classes can be edited. Its text can’t: it’s {{ tagline }}.

  • Every <li> the loop draws points at the same line. Select the second one and the right panel says “Item 2 of a list: editing the code changes all of them.”

What has to match

A written element can be an element of the page only if everything fixed about it is there:

  • The tag is the same.

  • The id, when the template writes one plainly.

  • Every class written plainly. Extra classes on the page are fine (a script or the server added them). A class glued to something computed, like btn-{{ size }}, is ignored.

  • Telling attributes written plainly: alt, title, name, type, placeholder, for, role, aria-label, value, method, rel and target have to be equal. href, src and action count in favour when they’re equal and don’t rule the element out when they differ, because servers rewrite addresses.

  • The text, when the element holds nothing but written text. Whitespace doesn’t matter, and curled quotes and joined dashes compare equal to straight ones. When the text is mixed with computed parts or with other elements, each written run of four characters or more has to appear.

Where it looks

  1. First, the elements that can’t be anything else: one place in the whole project fits, and by more than the tag (an id, the text, a couple of classes).

  2. Then top-down. An element inside a parent that was found is looked for among the children written inside that parent in the template. Several alike, one after another, are taken in order: the first <div class="card"> of the page is the first one written, the second is the second. When the one before has something computed in it, midcode takes it for a loop and assigns the same written element again.

  3. An element written in another file than its parent (the page a layout wraps, an include, a partial) is looked for in the whole project. It counts only if exactly one place fits, or if it’s the one that holds the elements already found inside it. A bare <body>, <main>, <header>, <footer>, <nav> or <aside> is matched when the project writes that tag once.

When it gives up

When two places fit equally and nothing settles it, the element gets no mark. midcode never guesses: an unmarked element can’t be edited by mistake. You can still select it and leave a comment on it; the Code section of the right panel says it isn’t in your project’s files. Its parent usually is.

Which templates are read

FilesRead asComputed, never edited
.blade.phpBlade{{ }}, {!! !!}, @directives, @php … @endphp, <?php ?>
.twig .njk .nunjucks .jinja .jinja2 .j2 .liquid .hbs .handlebars .mustache .gohtml .tmplBraces: Twig, Nunjucks, Jinja, Liquid, Handlebars, Go templates{{ }} and {% %}. {# #} is a comment
.html .htmHTML that may hold the same braces (Django, Jinja, Hugo and Eleventy name theirs so){{ }} and {% %}
.erb .ejs .eex .heex .leexERB, EJS<% %> and <%= %>
.php .phtmlPHP<?php ?> and <?= ?>
.md .markdownMarkdown, only on sites built from it (Markdown content)Anything but plain words

An attribute’s value is split into what’s written and what’s computed: in class="card {{ extra }}", card can be changed and {{ extra }} is left as it is.

Haml, Slim, Pug and Razor (.cshtml) aren’t read. Elements written in them aren’t found.

Templates are looked for in the whole project, except in folders that hold what’s installed or built: node_modules, vendor, a Python virtual environment, storage, tmp, log, dist, build, out, _site, staticfiles, wp-admin, wp-includes, and any folder whose name starts with a dot. In a Rails or Laravel app and in a Hugo site, public/ is skipped too. A template bigger than 400 KB is skipped.

What you can edit

  • Text: double-click it (Text, images and video). It has to be written in the template, as the whole content of its element.

  • Classes and styles: everything in the style panel. The written part of class is changed; computed parts stay.

  • Attributes written plainly: a link’s href, an image’s src and alt, a form field’s settings.

  • The tag of a text element (a heading, a paragraph, a <span>), with Tag under Typography in the right panel.

  • Structure: insert from the Insert panel, drag to move inside the same file, duplicate, delete. With several selected, ⌫ removes them, ⇧A wraps them in a stack and dragging one moves them all. ⌥-drag leaves the element where it is and puts a copy where you drop it, in the same file.

Changing a heading and the padding of its section, in a project without Tailwind:

templates/home.html
-<section class="hero">-  <h1>Fresh bread, every morning</h1>+<section class="hero mid:p-6">+  <h1>Bread, pastries and coffee</h1>

Every edit is one step of ⌘Z and shows in Publish, like in any other project.

Text you type is escaped for the language: a {{ typed into a Blade view is written @{{, and in a Twig, Jinja or Liquid file as &#123;{, so it stays text.

What can’t be edited from the canvas

  • Computed output. {{ post.title }}, <?= $title ?>, <%= @post.title %>: the words on the page aren’t in the template. Before giving up, midcode looks for those exact words in the project’s .json, .js, .ts and .md files (a data file, a locale, front matter) and changes them there when they’re written in one place (Text, images and video). It doesn’t search .php, .py, .rb, .yml or .toml files, nor other templates. When nothing is found it says “This text isn’t written there: it comes from a variable or prop.” Change it where the value is written.

  • Content from a database: a WordPress post’s body, a product’s description. It isn’t in any file.

  • HTML a helper builds: Rails’ link_to and image_tag, Django’s {{ form.as_p }}, a WordPress menu. No template writes those tags, so they get no mark.

  • Elements a condition splits: a tag opened in one branch of an if and closed in another can’t be moved or removed from the canvas. Its classes and text still can.

  • Components that need an import: the interactive components of the Insert panel are React components. In a template they’re refused (“This component needs a React page”).

Pages that redraw themselves

A page that changes after it loaded is watched: htmx swapping a fragment, Turbo navigating, a Livewire update, a client-side router. When elements arrive without a mark, or a marked one loses its mark, midcode matches the whole page again, a few times a second at most. An element that isn’t found this time keeps the mark it had. A text you’re typing into right now is left alone.

After an edit

midcode writes the template, and the server renders it again on the next request. If the site reloads its own pages (Hugo and Eleventy do), that’s all. If not, midcode reloads the page in every breakpoint a moment after the edit. A site that rebuilds, like Jekyll, may not be done yet: when the page comes back the same as before the edit, midcode loads it again a little later, up to two more times.

A change you make outside midcode shows when the page loads again: press Reload breakpoints in the top bar. Templates are read again whenever they change, so the marks stay right.

Styles

With Tailwind 4 in the project, midcode writes Tailwind’s own classes (Tailwind CSS). Without it, midcode writes its mid: utilities and keeps their CSS in a file called midcode.css (Without Tailwind).

In these sites midcode.css goes in the folder the stack serves as it is, and every layout links it. A layout is any template that closes a <head>. The link is added on your first style edit, as one undoable step:

layouts/_default/baseof.html
     <link rel="stylesheet" href="/style.css">+    <link rel="stylesheet" href="/midcode.css">   </head>
StackWhere midcode.css goesHow layouts link it
Hugo, Zolastatic//midcode.css
JekyllThe project’s root folder/midcode.css
Eleventy that copies public/ to its rootpublic//midcode.css
Rails, Laravel without Vitepublic//midcode.css
PHPNext to index.php/midcode.css
FlaskThe app’s static//static/midcode.css
DjangoThe folder in STATICFILES_DIRSUnder STATIC_URL
A WordPress themeThe theme’s folderget_theme_file_uri()

midcode starts one command. If your CSS is compiled by a watcher of its own (Tailwind’s CLI, a Procfile.dev), run it yourself, or a new Tailwind class won’t have its CSS.

Where midcode can’t tell which folder is served (FastAPI, a whole WordPress install, a Django project without STATICFILES_DIRS), it writes midcode.css at the top of the project (in src/ when there is one) and says it couldn’t tell where to import it. Add the <link> to your layout yourself.

New pages and Site settings

midcode adds and removes pages where a page is a file it knows how to write: content/about.md in Hugo, about.md in Jekyll, a template beside the home page in Eleventy, and a Blade view with its Route::view line in Laravel. Each stack’s page shows the file. In Django, Flask, FastAPI, Rails, WordPress and plain PHP a page is code (a route, a view, a controller), so the page menu has no plus there: write it yourself, or ask your agent.

Site settings, the globe in the top bar, writes the title, the description, the language, search engines, the favicon and the social image as tags in the site’s <head>. That’s the first layout (a template that closes a <head>), in alphabetical order of its path. What the template computes there ({{ .Site.Title }}, @yield('title'), <%= content_for(:title) %>) is shown as code and isn’t written. The images are copied to the folder midcode.css goes in (the table above). Where midcode can’t tell which folder that is and the project has no public/ folder, it asks you to put the image where the site serves it and link it in the code. There’s one form for the whole site. WordPress has no Site settings: it keeps them in its database. Tried with Hugo, Jekyll and Laravel.

Limits

  • Matching is finding, not knowing. On a page built from many near-identical fragments in different files, some elements stay unmarked.

  • If the project’s package.json lists Next.js, Vite, Astro, Nuxt, SvelteKit, React Router or Remix, midcode opens it as that kind of project and doesn’t match templates. Laravel with Vite is the exception.

  • Inside an inline <svg>, only the <svg> itself is matched.

  • The stylesheet link is written for layouts in .html, .php, .erb, .ejs, .twig, .njk, Jinja, Handlebars and Go template files. A layout in another kind of file (.phtml, .heex, a .liquid layout outside a Shopify theme) needs the link added by hand.

  • A page is read up to 8,000 elements.

  • Elements move inside one file. Moving one from a partial into the page that includes it is done in the code.

  • The free canvas is for Next.js projects. Component mode and variables aren’t available in these templates.

  • With more than one layout, Site settings writes the first by path, which may not be the one your pages use. The note under the form’s images names the file it writes.

  • Replacing an image from the panel copies the new file into public/. That’s where Laravel, Rails and many PHP sites serve files from. It hasn’t been tried on stacks that serve them from another folder (Hugo’s static/, Django’s and Flask’s static files, a WordPress theme): there, set the image’s src by hand.

  • Phoenix (.heex is read as ERB; its <.components> and {@values} stay as text), ASP.NET, Zola and MkDocs start with their own command but haven’t been tried. See Any other stack.

Troubleshooting

Nothing on the page can be edited. The Code section of the right panel says “midcode can’t edit this project’s code yet” when midcode found no template it reads, looking up to five folders deep. Check the table of template files above. A .php file counts only if it has markup in it.

One element has no file and line, and its neighbours do. It’s built by a helper, comes from a database, or is written in two places midcode can’t tell apart. In that last case, give it an id or a class of its own in the template, and it will be found on the next load.

The site doesn’t start. Open the Dev server log in the top bar to see what the command printed. Change how it runs on the error card lets you set your own command and address, saved in .midcode/server.json (Any other stack). Matching works the same however the site is served, including a site that already runs somewhere else.