The most visible part of a domain-specific language is its syntax. It is also one of the easiest parts to overvalue.

An appealing example can make a language feel coherent before it has answered harder questions. What does each construct mean? Which forms are invalid? How does the language explain a mistake? Can a developer predict the generated output? Will today’s convenient shorthand become tomorrow’s ambiguous special case?

Feo is an early Rust-powered DSL for describing static websites. Building it has made me think less about how quickly I can produce an impressive demo and more about the contracts beneath one. The central work is not inventing punctuation. It is creating a mental model that can remain understandable as the language becomes more capable.

Why build a language for static sites?

Feo is not intended to be another general-purpose web framework. Its audience is narrower: developers who want static, deployable output; prefer a Rust-oriented toolchain; and do not want a browser runtime to be the default price of building a site.

Static sites are already well served by existing tools, so novelty alone would not justify a new one. Feo is useful to me as a way to explore a specific combination of decisions: layout as a first-class concern, structured separation among different kinds of work, plain content files, and optional behavior that does not redefine the whole project around client-side JavaScript.

The narrow scope is a constraint. Feo does not need to solve every application problem. It needs to make its chosen class of sites clear to describe and predictable to build.

That makes it a good developer-experience problem. A small language has nowhere to hide inconsistency. Every exception becomes part of the grammar, every hidden conversion affects the output, and every weak diagnostic interrupts the user’s understanding of the language.

Layout comes first

Web tools often organize their abstractions around components and leave page layout to a collection of styles attached afterward. Feo starts from a different question: what is the structure of this page?

The goal of a layout-first language is not to replace CSS. It is to make the major spatial relationships visible in the source. Rows, columns, regions, and component placement should form a structure a developer can read before tracing a set of unrelated class names. Styling can then refine that structure without being the only place where the page’s organization exists.

This choice creates useful pressure on the language. Layout constructs need clear nesting rules. Components need an understandable relationship to the regions that contain them. Responsive behavior cannot become an invisible collection of exceptions. If Feo cannot explain those relationships cleanly, attractive syntax will not rescue it.

Layout-first is therefore both a feature and a test of the mental model.

Separate files create separate responsibilities

The current design divides a site across four kinds of source:

  • .feo describes pages, layout, components, and references to content.
  • .fsty describes presentation.
  • .fisl contains optional interactive behavior.
  • TOML files contain content.

The file extensions are less important than the boundaries they represent. A content edit should not require navigating layout syntax. Adding a small interaction should not turn the entire page into a client-side application. Styling should have room to evolve without changing the meaning of the content it presents.

Separation has a cost. Developers must learn how the pieces connect, and a design split across files can become harder to follow if the references are indirect or inconsistent. Feo only benefits from these boundaries if the connections remain explicit. A referenced content value should be easy to locate. A style should have a clear target. An island of browser behavior should reveal where and why it is attached.

The goal is not separation for its own sake. It is to let each part answer one kind of question without obscuring the others.

The AST is the first real contract

It is tempting to begin a language by parsing a few examples and emitting HTML as soon as possible. That produces a satisfying milestone, but it can also fuse syntax, meaning, and output before any of them are stable.

I began Feo around an abstract syntax tree and tests. The AST forces a distinction between what the source looks like and what the language means. Two pieces of syntax may eventually produce the same node. One convenient syntax form may turn out to represent several incompatible ideas and need to be rejected. Those decisions are easier to see when the parser does not write output directly.

The AST also creates a boundary for the renderers. Feo’s HTML and CSS renderers already consume this structured representation instead of interpreting source text independently. A JavaScript renderer exists as an explicit boundary, but it currently produces no output; Feo’s native island behavior is still designed rather than implemented. Keeping that boundary visible without pretending it is finished reduces the number of places where the language’s meaning can drift later.

The current tests make parts of that contract visible. AST tests cover constraints such as valid heading levels, dotted content paths, and the import model. Renderer and pipeline tests lock in the relationship among parsed layouts, resolved content, HTML, and generated grid CSS. The dedicated parser, diagnostics, and JavaScript test files are still placeholders, so invalid-input behavior is not yet protected at the same depth. That gap is useful evidence about where the next work belongs.

This work is less dramatic than showing a finished page, but it is the work that determines whether later features can be added without turning the language into a pile of exceptions.

Parser structure affects language structure

A parser can become a mirror of every unresolved decision in the language. When recognition of a node, parsing of a value, validation of a rule, and construction of an error all happen in one place, it becomes difficult to tell whether a branch exists because the grammar requires it or because the implementation happened to grow that way.

Feo’s parser is now divided around those responsibilities. A lexer produces positioned tokens, an error module defines parse failures, and focused modules handle top-level grammar, nodes, and values. The exact boundaries may continue to change, but the purpose is stable. A parser should make the rules of the language easier to inspect, not bury them inside control flow.

This matters for future design. If adding a feature requires special cases across unrelated parser paths, that is evidence that the feature may not fit the existing mental model. Architecture does not decide the language, but it can expose where the language is becoming incoherent.

Diagnostics must grow with the grammar

Invalid programs are part of the language’s user experience. A grammar defines not only what can be written, but also the boundary developers will encounter when they write something else.

Feo’s parser currently returns an error message with a single byte position. That is a useful baseline, but it is not the diagnostic system I ultimately want. The dedicated diagnostics module and its test file are still empty, and precise source spans have not been implemented. A message about the right problem is still frustrating when it points to the wrong place or cannot identify the full construct involved.

That makes diagnostics a current design obligation rather than completed infrastructure. The eventual diagnostic should identify the construct the parser understood, the rule it could not satisfy, and a useful next action when one is available.

Ambiguity deserves special attention. A permissive parser can appear friendly because it accepts more input, but guessing creates unstable meaning. If the same source can reasonably be interpreted in two ways, rejecting it with a clear explanation is often better than choosing one silently. A strict rule can be learned. A hidden guess must be rediscovered.

This is also why diagnostics cannot be postponed until the grammar is “done.” The difficulty of explaining an error is feedback about the syntax itself. If a rule cannot produce a clear diagnostic, the rule may not yet be clear.

Tests protect behavior, not implementation

Language tools invite large refactors. Parser organization changes, AST types become more precise, and renderers gain new responsibilities. Tests are valuable because they allow that movement while preserving the behavior a developer depends on.

The important distinction is what the tests treat as public. A test that reproduces every internal parser step can make improvement harder. Feo’s existing suite focuses on observable behavior such as AST constraints, content lookup, renderer output, and complete parse-to-render pipelines. It does not yet test invalid grammar and diagnostics with the same care. Adding those cases should protect the language contract rather than the parser’s internal route to an answer.

That approach supports small, reversible development. A new grammar rule can be introduced with its accepted cases, rejected cases, and output behavior visible. If it creates too much complexity, it can be changed before other features depend on it. The test suite becomes a record of decisions rather than only a guard against crashes.

What I am deliberately not settling yet

Feo is still early, and several important decisions remain open. Components and component calls exist in the AST, but unresolved calls currently render as HTML comments rather than expanded output. Style imports and island imports can be represented, while .fsty and .fisl remain design work rather than implemented languages. The JavaScript renderer is still an empty stub. These are intentional boundaries, not completed features.

As they develop, component semantics need to stay simple as reuse becomes more powerful. Style composition needs enough flexibility for real sites without recreating the opacity I want the layout model to avoid. Optional islands need a clear boundary between static output and browser behavior.

The division between Feo and Ferrisle also needs practical testing. Feo should define the language. Ferrisle should coordinate building and serving a site. Project discovery, assets, content loading, development workflows, and output policy all put pressure on that boundary.

Leaving these questions open is intentional. Prematurely declaring every abstraction would create the appearance of completeness while making the wrong contracts harder to remove. The next useful step is the one that tests the model, not necessarily the one that makes the feature list longer.

Expressive enough, but still learnable

Every DSL eventually faces the same tension. Users want a concise way to express more ideas. Each shortcut, implicit rule, and extension point can make an individual example easier. Together, they can make the language harder to predict.

The answer is not to keep Feo artificially tiny. It is to require new expressiveness to fit the model already taught by the language—or to improve that model deliberately when it no longer fits. A feature should earn its syntax, its diagnostics, its tests, and its place in the generated output.

That is the larger developer-experience lesson Feo is teaching me. A language is an interface whose decisions accumulate. The pleasant syntax is valuable, but trust comes from the structure beneath it: constraints that remain consistent, diagnostics that make failure understandable, and boundaries that let the implementation evolve without changing the meaning of the user’s work.

Feo is not finished, and its syntax will change. The goal is to make those changes while preserving something more important than any particular notation: a language developers can learn, reason about, and eventually trust.