Building the page editor first
Giant Context is being built as an AI that makes websites. None of that comes first. What comes first is a tool a person uses with no model in the room, a page editor for building a page by hand. Everything I build from here starts on top of it.
The editor exists because I wanted Elementor without WordPress and nothing existing was close enough. Elementor is WordPress's drag-and-drop page builder, and it is how most of the web without a developer gets made. The React equivalents were not mature enough to build a product on, and I was never going back to PHP, so the stack I had written down needed something real to prove itself on. That became the editor Elementor would have been if it were born in React and owned end to end.
Sections, columns, blocks
A page is not a list of blocks. It is three levels: a page is sections, a section holds columns, a column holds blocks. Container blocks nest further through their own data. That extra structure is what makes a real layout possible, because a two-column split with different widths on a phone is a thing a column knows about and a flat list cannot express.
The whole content model
type PageContent = Section[];
type Section = { id: string; type: "section"; settings: SectionSettings; // containerWidth, gap, inset columns: Column[]; styles?: BlockAdvancedStyles;};
type Column = { id: string; width: ResponsiveValue<number>; // grid columns out of 12, per breakpoint blocks: Block[];};
type Block<T> = { id: string; type: BlockType; data: T; styles?: BlockAdvancedStyles;};Three things in there are load-bearing and were decided on day one.
Every section, column and block carries a generated id, and everything in the system addresses them by that id rather than by position. Move a block and no other reference breaks. It also means a tree that arrives without ids renders perfectly and can never be edited again, so ids get backfilled at the write boundary rather than trusted.
A column's width is not a number, it is a value per breakpoint, and resolution cascades downward: mobile falls back to tablet, tablet falls back to desktop. Set desktop only and the layout is inherited all the way down. Set mobile and it wins on phones alone.
And a block's data is its own typed shape. Variants are fields inside that data, not separate block types. A code block that renders as a terminal is the same block type with variant set to terminal, which keeps the palette from becoming a list of near-duplicates.
It is structured on purpose, because structure is what you can build tools on. Every capability the builder has, and everything I hang on it later, rests on a page being a typed tree and not freeform markup.
Why not an HTML builder
The obvious alternative was to skip all of this. Let a person arrange things and emit HTML, the way most visual editors do underneath. It is less machinery and the output is universal.
The deciding factor was that the styling is structured too. The component library I build on describes a style as an object with typed keys rather than a string of CSS. Padding is a number. A colour is a token that resolves against the theme. A breakpoint is a key on that object. So a block's data is typed, its styles are typed, and a page is one typed object from the top of the tree to the last margin.
Everything else in this codebase is declared once and generated outward, the API description producing the clients and the validation and the forms. A model filling typed fields works inside that. It can be checked against a schema, rejected when it is wrong, and corrected in one field without touching the rest. A model emitting HTML is emitting a string, and a string is the one thing in this system nothing can verify. Structured input is also, in theory, what a model is better at, though that is a belief I hold rather than something I have measured.
The practical half is less noble. Prototyping in this library is fast, and I was one person trying to get a real editor standing in weeks.
Two costs are already visible. A component library is not the lightest thing to put underneath a public website, and there are approaches built for exactly that which I did not take. And a builder made of blocks may quietly push every site built with it toward the same shape, which is the failure mode of every template system ever made and not obviously one I have escaped.
Typed all the way down, on the theory that a machine will write into it better than it would write markup. That theory is untested.
What a block is
A block type is a directory with three files and one exported object. The object is the only thing that gets registered.
One block, defined
Blocks/Divider/├── index.ts # the definition, the only registered artifact├── view.tsx # how it renders└── settings.tsx # how it is edited
type BlockDefinition<T> = { type: BlockType; label: string; // i18n key icon: string; // "lucide:minus" defaultData: T; // what a new one starts as View: LazyComponent<BlockViewProps<T>>; Settings: LazyComponent<BlockSettingsProps<T>>;};Adding a block is adding a directory and one line in a registry map. There are around forty of them so far and I add one most weeks, which is the part of this work that never finishes.
The settings panel is a hand-written React component, not generated from a schema. Everywhere else in this codebase the opposite is true, forms and validation and clients all fall out of one declaration, so writing these by hand looks like a lapse.
A schema can tell you a field is a colour. It cannot tell you that this particular colour control belongs under Appearance, below the background controls, disabled when the background type is an image, and labelled differently on a hero than on a divider. Generated panels are how you get a form with a preview pane instead of a page editor. What is shared is the vocabulary rather than the generation: a library of inputs, a colour selector, a media picker, a spacing control, a responsive slider, an icon picker, that every panel composes from.
The cost is already being paid. Forty panels have no automated way to stay consistent with each other, and the conventions live in whichever panel I copied last rather than in a document.
What makes a page editor hard
A page editor sounds simple and is not. The parts that make it usable are the parts that are expensive to build, which is why the React page builders that existed were not good enough to adopt.
The one that matters most is invisible when it works. What renders in the editing canvas is not a preview of the page. It is the page. The canvas looks up the block type in the same registry the public site uses and renders the same view component, and the only thing that differs between the two is a boolean the component receives telling it whether it is being edited. There is no second renderer, no preview mode with its own quirks, and no class of bug where a page looks right while you are building it and wrong once it ships, because there is nowhere for the two to disagree.
Blocks reorder by dragging, and not with move-up and move-down buttons. A proper sortable tree with drop indicators showing where a block will land before you release it, the placeholder shifting and the layout reflowing under the cursor, drop targets for a column, for the gap between two blocks, for inside a container, and for the empty space below the last section that creates a new one. That is the interaction a person coming from Elementor expects and rarely gets from an editor someone rolled themselves, because doing it well in a browser is genuinely hard.
Every block has a settings panel of labelled accordion sections, so a block with a dozen options is a set of named groups rather than an unreadable stack of controls. Undo and redo hold fifty steps. Nothing persists until you press Save, which sounds obvious and means every edit in a session is one atomic thing you can walk away from.
And there have to be enough blocks to build a real page. Not a starter set of five. Building the editor took weeks. Building blocks is the work that has no end, and the palette is already searchable because scrolling it stopped being practical.
Building a page
Building a page in it is what Elementor should have been. A person starts with a blank page and adds a block, a hero. The heading is typed straight into it, going bold or linked as it is formatted, because the editor knows it is editing a heading and not a paragraph of markup. An image block goes in below. It lands in the wrong spot. When it is dragged, a line shows where it will go, moving with it over the other blocks. It drops where the line was.
The image is selected and its settings open to the side, a stack of accordions. Open one and its controls appear, the rest stay shut. Each control changes the one selected block, live, in front of the person building it.
Previewing a phone in a desktop window
The canvas has a device toggle. Pick mobile and the frame narrows to a phone width so you can lay out the mobile version. But the browser is not on a phone. It is a desktop window with a narrow box drawn inside it, and every CSS media query in the page is still answering questions about the window, not the box.
So blocks in the editor cannot use media queries to decide what breakpoint they are at. They read the chosen breakpoint from context instead, and the context says which device you are pretending to be. The same component on the live site has no such context and falls back to the real thing.
That is two paths through the same code, which is exactly what the shared-renderer decision was meant to avoid, and it is the price of a device preview that is not an iframe. There are two sets of numbers as a result. The canvas frame widths, 1440, 768 and 390, are what the preview draws. The breakpoint thresholds, 768 and 1024, are where the real layout actually changes. They are related and they are not the same list, and keeping them in one file each is the only thing stopping them drifting.
Built for a person first
None of this was built for an AI. It was built for a person, because when it was built there was no AI, and a person had to be able to make a page.
When the model arrives, it will not get its own private way to make pages. It will write into the same blocks a person does, dropping a hero block a person can open and rewrite, revising a page a person laid out. One editor, one definition of what a page is, because the editor was there first and the model will be taught to speak its language. The common alternative, an AI that emits HTML a person then has to wrangle, is the worse one.
The builder is also the last gate before anything ships, and it is where the person stays in charge. Even once the AI can draft a whole page, that draft is a starting point and not the finished one. A person opens the builder to make the small changes, the fine-tuning, the adjustments a model will not think to make, because the page is theirs and they can see what it should be. The AI will do the heavy work of generating a page. What it finally becomes stays with the person.
The same holds downstream, and further than a page. A header, a footer, a layout, a dialog, a sidebar, an email, a form, a help article, a documentation page. Every one of those is the same tree of sections and blocks in a different table, edited in the same canvas with a different palette. Whatever gets built on top works on these same blocks. There is no second content system beside the first. There is the builder, and then there is whatever gets built to run through it.
Why the editor is the harder half
The editor is the part that will end up least visible. The pitch will be the AI. What the AI will stand on is a hand-built page editor that came first, and the reason a person is never stuck with a page they cannot change.
Anyone can call the same models I will. The editor underneath is the slow part: the typed tree, the drag targets, the forty settings panels, the one renderer serving both surfaces. Whoever skips it ships HTML a person then has to wrangle, which is the thing I set out to stop doing.
Building something like this
I'm Jesse. I build platforms end to end, and I'm open to work. If this is the kind of engineering you need, get in touch.