# How to Define a New Module in the Instatic Module Engine

> Learn how to define a new module in the Instatic module engine. Create a folder, export a ModuleDefinition object, and register it with ModuleRegistry for the visual editor and publisher.

- Repository: [CoreBunch/Instatic](https://github.com/CoreBunch/Instatic)
- Tags: how-to-guide
- Published: 2026-08-01

---

**To define a new module in the Instatic module engine, create a folder under `src/modules/<module-id>/`, export a `ModuleDefinition` object conforming to the interface in [`src/core/plugin-sdk/modules.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugin-sdk/modules.ts), and register it with the `ModuleRegistry` to make it available in the visual editor and static publisher.**

In the CoreBunch/Instatic codebase, modules are reusable building blocks that users can drag onto pages in the visual editor. The module engine enforces type safety through TypeBox schemas and manages the full lifecycle from editor UI to runtime execution and static site generation.

## Module Architecture and File Structure

Every module resides in its own folder under `src/modules/`. The folder name becomes the module’s identifier (e.g., `base/text` or `custom/hero`).

Inside this folder, you must create an [`index.ts`](https://github.com/CoreBunch/Instatic/blob/main/index.ts) (or [`module.ts`](https://github.com/CoreBunch/Instatic/blob/main/module.ts)) file that exports a **module definition object**. This object implements the `ModuleDefinition` type imported from [`src/core/plugin-sdk/modules.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugin-sdk/modules.ts). The registry, publisher, and editor UI all reference this central type to ensure consistency across the stack.

## The ModuleDefinition Interface

The `ModuleDefinition` interface in [`src/core/plugin-sdk/modules.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugin-sdk/modules.ts) requires specific properties that control how your module behaves in the editor and final output:

- **`moduleId`** – A unique string identifier (e.g., `'base.helloWorld'`) used as the key in `ModuleRegistry`.
- **`title`** – The human-readable name displayed in the module picker UI.
- **`icon`** – A dynamic import pointing to a Pixel Art icon for the visual editor.
- **`propsSchema`** – A **TypeBox** schema defining configurable properties.
- **`defaultProps`** – Optional default values for the props defined in the schema.
- **`render`** – A function receiving the node's props and returning HTML (or a React component for the editor preview).
- **`css`** – Optional CSS string scoped to this module; deduplicated by `moduleId` during publishing.
- **`js`** – Optional client-side JavaScript executed once per page when this module is present.

### Props Schema and TypeBox Validation

Instatic uses **TypeBox** (`@sinclair/typebox`) to validate all untyped boundaries. Your `propsSchema` ensures that data persisted from the editor matches the expected types at runtime.

```typescript
import { Type } from '@sinclair/typebox';

const PropsSchema = Type.Object({
  heading: Type.String({ default: 'Hello' }),
  level: Type.Union([Type.Literal(1), Type.Literal(2), Type.Literal(3)])
});

```

The publisher and sandbox validate node data against this schema before rendering, preventing runtime type errors in the generated static site.

### Render Functions and Asset Injection

The `render` function receives the validated props and must return an HTML string:

```typescript
render: ({ props }) => {
  return `<h${props.level}>${props.heading}</h${props.level}>`;
}

```

If you provide a `css` property, the publisher in [`src/core/publisher/renderConfig.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/renderConfig.ts) collects these styles and deduplicates them by `moduleId` to avoid redundant CSS in the final output. Similarly, the `js` property defines client-side behavior that the sandbox VM in [`server/plugins/quickjs/bootstrap/src/modulePackRuntime.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/bootstrap/src/modulePackRuntime.ts) executes once per page when the module is present.

## Registering Your Module with the Registry

After defining your module, you must register it with the central `ModuleRegistry`. In [`src/core/plugin-sdk/modules.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugin-sdk/modules.ts), import your definition and call `registry.register()`:

```typescript
// src/core/plugin-sdk/modules.ts
import { helloWorldModule } from '../../modules/base/helloWorld';

export const registry = new ModuleRegistry();
registry.register(helloWorldModule);

```

The `ModuleRegistry` maintains a map of `moduleId → definition` and exposes `get(moduleId)` and `getAll()` methods. The admin UI in [`src/admin/pages/site/module-picker/moduleInserterModel.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/site/module-picker/moduleInserterModel.ts) calls `registry.getAll()` to populate the module picker, while the publisher uses `registry.get(node.moduleId)` to retrieve rendering logic during static site generation.

## Runtime Rendering and Deduplication

During the publishing phase, [`src/core/publisher/renderConfig.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/renderConfig.ts) walks the page node tree. For each node, it retrieves the corresponding `ModuleDefinition` from the registry. If the definition provides a `render` function, the publisher executes it; otherwise, it falls back to a generic `<div>` wrapper.

CSS and JS deduplication happen automatically based on the `moduleId`. The publisher ensures that a module’s CSS is injected only once per page regardless of how many instances of that module appear, and the sandbox VM loads the module’s JavaScript exactly once per `moduleId` when hydrating the page.

## Minimal HelloWorld Module Example

Here is a complete, runnable example of a minimal text module:

```typescript
// src/modules/base/helloWorld/index.ts
import { ModuleDefinition } from '@core/plugin-sdk/modules';
import { Type } from '@sinclair/typebox';

const PropsSchema = Type.Object({});

export const helloWorldModule: ModuleDefinition = {
  moduleId: 'base.helloWorld',
  title: 'Hello World',
  icon: () => import('pixel-art-icons/icons/clipboard.svg'),
  propsSchema: PropsSchema,
  defaultProps: {},
  
  render: ({ props }) => {
    return `<p>Hello World – enjoy Instatic!</p>`;
  },
  
  css: `
    .hello-world { color: var(--editor-surface-2); }
  `,
  
  js: `
    console.log('Hello World module loaded');
  `
};

```

```typescript
// src/core/plugin-sdk/modules.ts
import { helloWorldModule } from '../../modules/base/helloWorld';

export const registry = new ModuleRegistry();
registry.register(helloWorldModule);

```

Once registered, this module appears immediately in the visual editor’s module picker and renders correctly in both the sandbox preview and the final static HTML output.

## Summary

- **File Location**: Create a folder under `src/modules/<module-id>/` with an [`index.ts`](https://github.com/CoreBunch/Instatic/blob/main/index.ts) exporting a `ModuleDefinition`.
- **Type Safety**: Define `propsSchema` using TypeBox to validate editor input and prevent runtime errors.
- **Registration**: Import your module into [`src/core/plugin-sdk/modules.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugin-sdk/modules.ts) and call `registry.register()` to activate it.
- **Rendering**: Implement the `render` function to generate HTML; optionally provide `css` and `js` for styling and client-side behavior.
- **Deduplication**: The publisher in [`src/core/publisher/renderConfig.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/renderConfig.ts) automatically deduplicates CSS and JS per `moduleId` during static site generation.

## Frequently Asked Questions

### What file structure is required for a new Instatic module?

Create a folder under `src/modules/` using your desired module identifier as the folder name. Inside, place an [`index.ts`](https://github.com/CoreBunch/Instatic/blob/main/index.ts) (or [`module.ts`](https://github.com/CoreBunch/Instatic/blob/main/module.ts)) that exports a `ModuleDefinition` object conforming to the interface in [`src/core/plugin-sdk/modules.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugin-sdk/modules.ts). The folder name becomes the canonical path for imports and registration.

### How does the Instatic module engine validate module props?

The engine uses **TypeBox** schemas defined in the `propsSchema` property of your `ModuleDefinition`. When the publisher in [`src/core/publisher/renderConfig.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/renderConfig.ts) renders a page, it validates node data against this schema. This type-safe approach ensures that data persisted from the visual editor matches the expectations of your `render` function at build time.

### Where is module CSS and JavaScript deduplicated during publishing?

Deduplication occurs in [`src/core/publisher/renderConfig.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/renderConfig.ts). The publisher tracks which `moduleId` values have already contributed CSS and JS to the current page. Even if a module appears multiple times on the same page, its styles and scripts are injected only once, ensuring optimal bundle sizes in the static output.

### How does the visual editor discover available modules?

The editor UI queries the `ModuleRegistry` via `registry.getAll()` in [`src/admin/pages/site/module-picker/moduleInserterModel.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/site/module-picker/moduleInserterModel.ts). This model populates the module picker sidebar, using each definition’s `title` and `icon` properties to display available components. Registration in [`src/core/plugin-sdk/modules.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugin-sdk/modules.ts) is the only step required to make a module discoverable in the UI.