---
title: Element Static Content and Dashboard UI Plugins
description: "Beyond the `api/`, `lib/`, `spi/`, and `classpath/` trees covered in [Element Anatomy: A Technical Deep Dive](element-anatomy-a-technical-deep-dive) and [Packaging an Element with Maven](packaging-an-element-with-maven), an [Element](https://namazustudios.com/glossary/element/) can ship two additional content trees inside its `.elm` archive (or exploded directory): `static/` and `ui/`.…"
url: "https://namazustudios.com/docs/namazu-elements-core/element-static-content-and-dashboard-ui-plugins/"
updated: "2026-09-23"
category: Namazu Elements Core Features
---

# Element Static Content and Dashboard UI Plugins

Beyond the `api/`, `lib/`, `spi/`, and `classpath/` trees covered in [Element Anatomy: A Technical Deep Dive](element-anatomy-a-technical-deep-dive) and [Packaging an Element with Maven](packaging-an-element-with-maven), an [Element](https://namazustudios.com/glossary/element/) can ship two additional content trees inside its `.elm` archive (or exploded directory): `static/` and `ui/`. Both are served over HTTP automatically when the [Element](https://namazustudios.com/glossary/element/) is deployed, but they serve different purposes:

- `static/` - arbitrary static assets bundled with the [Element](https://namazustudios.com/glossary/element/) (a small SPA, JSON configuration, icons, downloadable files).
- `ui/` - dashboard UI plugin bundles, which extend the Elements admin console with custom pages.

This page documents how both trees get loaded and served at runtime. For how they're populated during a Maven build, see [Packaging an Element with Maven](packaging-an-element-with-maven).

## Loading Static Content

When an [Element](https://namazustudios.com/glossary/element/) is deployed, the loader reads its `static/` and `ui/` directories (if present) into an in-memory file listing before anything is served. This works identically whether the [Element](https://namazustudios.com/glossary/element/) is deployed as an exploded directory on the [Element](https://namazustudios.com/glossary/element/) path or as a packaged `.elm` archive; the archive is simply opened as a zip filesystem and walked the same way as a real directory. An [Element](https://namazustudios.com/glossary/element/) with neither directory simply serves nothing under either mount, which is why both trees are optional.

## URL Routing

Each content tree is mounted at its own URL prefix, computed independently at deploy time:

| Content tree | Default URL pattern | Override attribute |
| --- | --- | --- |
| `static/` | `/app/static/{prefix}/...` | `dev.getelements.Element.static.uri` |
| `ui/` | `/app/ui/{prefix}/...` | `dev.getelements.Element.ui.uri` |

`{prefix}` comes from the [Element](https://namazustudios.com/glossary/element/)'s `dev.getelements.elements.app.serve.prefix` attribute, and falls back to the [Element](https://namazustudios.com/glossary/element/)'s own `@ElementDefinition` [name](https://namazustudios.com/glossary/name/) if that attribute isn't set. Either mount point can be overridden per-[Element](https://namazustudios.com/glossary/element/) with the corresponding attribute above, in which case the override value is used verbatim as the context path instead of the computed `/app/{static|ui}/{prefix}` path.

Note

Before mounting a content tree, Elements checks the computed context path against its registry of reserved system paths (the [REST](https://namazustudios.com/glossary/rest/) API root, WebSocket root, and similar). If your override collides with a reserved path outright, deployment of that content tree is rejected; if it merely overlaps a catch-all route, the reserved system routes still take priority so they keep working.

## Serving Behavior

Once mounted, each tree is served by a small built-in static file servlet with the behavior you'd expect from a CDN-style file server:

- An index file is served for directory-style requests. The default is `index.html`, and it can be overridden per content tree with an attribute of the form `dev.getelements.{static|ui}.index` (use `static` or `ui` depending on which tree you're configuring).
- Custom error pages can be configured per HTTP status code with an attribute of the form `dev.getelements.{static|ui}.error.<code>`, e.g. `dev.getelements.static.error.404`.
- Conditional requests (`ETag` / `If-None-Match`, `Last-Modified`) and byte-range requests (`Range` / `If-Range`) are both supported, so large downloads and media files behave correctly with browsers and download managers.
- MIME types and any per-file headers are resolved once, at [Element](https://namazustudios.com/glossary/element/) load time, not per request.

## The Dashboard UI Plugin Convention

The `ui/` tree is just static content, but the Elements admin dashboard imposes a specific convention on top of it so it can discover and load your plugin automatically. There is no special-cased server code for this; the dashboard finds your plugin purely by fetching well-known files over HTTP from the URLs above.

Lay out `ui/` with one subdirectory per dashboard surface you want to extend:

```
ui/
  superuser/
    plugin.json
    plugin.bundle.js
  user/
    plugin.json
    plugin.bundle.js
```

Each subdirectory is served at `/app/ui/{prefix}/{segment}/`, where `{segment}` is `superuser` or `User`. The dashboard fetches `plugin.json` from each segment it finds and, if present, loads the referenced bundle. A minimal `plugin.json`:

```
{
  "schema": "1",
  "entries": [
    {
      "label": "Example Element",
      "icon": "Package",
      "bundlePath": "plugin.bundle.js",
      "route": "example-element"
    }
  ]
}
```

| Field | Description |
| --- | --- |
| `schema` | Manifest schema version. Currently `"1"`. |
| `entries[].label` | Text shown for this plugin in the dashboard sidebar. |
| `entries[].icon` | [name](https://namazustudios.com/glossary/name/) of a Lucide icon to display next to the label. |
| `entries[].bundlePath` | Path to the plugin's JavaScript bundle, relative to `plugin.json`. |
| `entries[].route` | Route segment used to reach the plugin at `/plugin/{route}` in the dashboard. |

`plugin.bundle.js` must be a self-contained, immediately-invoked script (no module loader or bundler runtime of its own) that registers a component against the route named in `plugin.json`:

```
(function () {
  var React = window.React;

  function ExamplePluginPage() {
    return React.createElement('div', null, 'Hello from my Element');
  }

  window.__elementsPlugins.register('example-element', ExamplePluginPage);
})();
```

The bundle reuses the dashboard's own `window.React` instance rather than bundling a copy of React itself, which keeps plugin bundles small and avoids duplicate-React errors. To discover plugins, the dashboard lists the Elements currently deployed, extracts each one's `/app/ui/{prefix}/` base path from its exposed URIs, fetches `plugin.json` for each known segment, and injects a `<script>` tag for the referenced bundle.

See [Building the Example Element: A Complete Walkthrough](element-example-complete-walkthrough) for a full working example, including the TypeScript/React source the example project builds into `plugin.bundle.js`.
