# How the Instatic Module Engine Defines and Renders Block Modules

> Discover how the Instatic module engine defines and renders block modules. Learn about its React canvas components, static HTML generation, and editor UI property schema.

- Repository: [CoreBunch/Instatic](https://github.com/CoreBunch/Instatic)
- Tags: internals
- Published: 2026-07-29

---

**The Instatic module engine treats every reusable UI block as a first-class component defined by a `ModuleDefinition<TProps>` type that specifies a React canvas component, a pure render function for static HTML generation, and a property schema for the editor UI.**

The Instatic module engine is the architectural backbone of the open-source **CoreBunch/Instatic** repository, providing a type-safe system for declaring, registering, and rendering block modules. This engine separates the interactive editing experience from the static publishing pipeline, ensuring that custom blocks behave consistently in both contexts. Understanding how the engine defines and renders block modules enables developers to extend the platform with reusable components that integrate seamlessly into the visual editor and generate optimized static output.

## Module Definition Architecture

Every block module begins with a type declaration in **[`src/core/module-engine/types.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/module-engine/types.ts)**. The engine consumes a **`ModuleDefinition<TProps>`** interface that strictly types the module's props and declares its runtime behavior.

### Core Definition Fields

A complete module definition requires these fields:

- **`id`**: A unique string identifier used for registration lookups and persistence.
- **`name`**: The human-readable label displayed in the editor palette.
- **`component`**: The React component rendered in the interactive canvas.
- **`render`**: A pure function that returns static markup for the publishing pipeline.
- **`schema`**: A **property schema** (`PropertyControl` union) that drives the editor’s property pane.

Optional fields include **`description`** for help text, **`icon`** (loaded from `pixel-art-icons`), and **`dependencies`** for declaring other required modules.

### Type-Safe Registration

The definition lives in the module’s index file and imports the `registry` singleton from **[`src/core/module-engine/registry.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/module-engine/registry.ts)**. The registry maintains a map of `moduleId → ModuleDefinition` and serves as the single source of truth for both the editor and server-side publisher.

```typescript
// src/modules/example/index.ts
import type { ModuleDefinition } from '@core/module-engine';
import { registry } from '@core/module-engine';
import ExampleEditor from './ExampleEditor';
import { renderExample } from './renderExample';
import { exampleSchema } from './schema';

const definition: ModuleDefinition<{ title: string; color: string }> = {
  id: 'example',
  name: 'Example Block',
  description: 'A simple illustrative block.',
  icon: () => import('pixel-art-icons/icons/star-solid.tsx'),
  component: ExampleEditor,
  render: renderExample,
  schema: exampleSchema,
};

registry.registerOrReplace(definition);

```

## Registering Modules with the Registry

During application bootstrap, each module’s index file executes **`registry.registerOrReplace(definition)`**. The registry implementation in **[`src/core/module-engine/registry.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/module-engine/registry.ts)** validates the definition and stores it in an internal map. This registration pattern ensures that modules are discoverable by the editor for canvas rendering and by the publisher for static site generation.

The registry’s singleton pattern guarantees that module definitions are immutable after registration, preventing runtime collisions and ensuring consistent behavior across the application lifecycle.

## Rendering Pipeline and Static Generation

When a page is published, the **publisher** (located in **[`src/core/publisher/publishPage.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/publishPage.ts)**) traverses the page tree. For every node whose type is a module, it invokes the module’s **`render`** function with the node’s current props.

### Render Output Structure

The `render` function returns a **`RenderOutput`** object defined in **[`src/core/module-engine/types.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/module-engine/types.ts)**:

- **`html`**: The static markup string inserted into the page.
- **`css`**: Optional styles emitted into the site-wide stylesheet.
- **`js`**: Optional JavaScript emitted as a separate file (never inlined).

```typescript
// src/modules/cta/renderCTA.ts
import type { RenderOutput } from '@core/module-engine/types';

export function renderCTA(props: {
  label: string;
  url: string;
  variant: 'primary' | 'secondary';
}): RenderOutput {
  const className = props.variant === 'primary' ? 'btn-primary' : 'btn-secondary';
  return {
    html: `<a href="${props.url}" class="${className}">${props.label}</a>`,
    css: `.btn-primary{background:#0d6efd;color:#fff}.btn-secondary{background:#6c757d;color:#fff}`
  };
}

```

### Publisher Integration

The publisher aggregates all JavaScript modules into a site-wide **module-JS map** (`buildPublishedSiteModuleJsMap`) and injects one `<script defer>` tag per module at the end of the HTML document. This architecture guarantees that the **runtime component** (`component`) and the **static render output** remain synchronized, while keeping the initial HTML payload free of inline scripts.

## Property Controls and Editor Integration

The **property schema** defined in **[`src/core/module-engine/propertySchema.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/module-engine/propertySchema.ts)** declares a discriminated union of `PropertyControl` objects (text inputs, selects, color pickers, etc.). The editor consumes this schema to auto-generate the property pane for any selected module instance.

When a content editor adjusts a value in the property pane, the engine updates the node’s props and triggers a re-render of the React component in the canvas. The same props are later passed to the `render` function during publishing, ensuring visual parity between the editing environment and the final static output.

## HTML Tag Badge Resolution

Modules can expose semantic metadata through the **HTML tag badge** system. The helper function **`resolveHtmlTagBadge`** in **[`src/core/module-engine/htmlTagBadge.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/module-engine/htmlTagBadge.ts)** centralizes the dispatch logic for badges (handling omitted values, static strings, or dynamic functions). This ensures the DOM/Layers panel displays consistent contextual information—such as labeling a module as an `<h1>` or `<section>`—without duplicating logic across the codebase.

## Practical Example: Building a Call-to-Action Module

Here is a complete implementation pattern for a "Call-to-Action" block that demonstrates the full lifecycle from definition to rendering:

```typescript
// src/modules/cta/index.ts
import type { ModuleDefinition } from '@core/module-engine';
import { registry } from '@core/module-engine';
import CTAEditor from './CTAEditor';
import { renderCTA } from './renderCTA';
import { ctaSchema } from './schema';

const ctaDefinition: ModuleDefinition<{
  label: string;
  url: string;
  variant: 'primary' | 'secondary';
}> = {
  id: 'cta',
  name: 'Call-to-Action',
  icon: () => import('pixel-art-icons/icons/target-solid.tsx'),
  component: CTAEditor,
  render: renderCTA,
  schema: ctaSchema,
};

registry.registerOrReplace(ctaDefinition);

```

When instantiated in a page, the module is represented as:

```json
{
  "type": "module",
  "moduleId": "cta",
  "props": {
    "label": "Join Now",
    "url": "/signup",
    "variant": "primary"
  }
}

```

The editor renders `CTAEditor` with live props controls, while the publisher calls `renderCTA` to generate the final static markup and styles.

## Summary

- **Module definitions** use the `ModuleDefinition<TProps>` interface in [`src/core/module-engine/types.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/module-engine/types.ts) to declare `id`, `name`, `component`, `render`, and `schema`.
- The **registry singleton** in [`src/core/module-engine/registry.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/module-engine/registry.ts) stores all modules via `registerOrReplace`, providing a centralized lookup for the editor and publisher.
- The **render function** returns a `RenderOutput` object containing `html`, optional `css`, and optional `js` for static site generation.
- **Property schemas** in [`src/core/module-engine/propertySchema.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/module-engine/propertySchema.ts) drive the editor UI, ensuring type-safe prop editing that mirrors the TypeScript interfaces.
- The **publisher** in [`src/core/publisher/publishPage.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/publishPage.ts) walks the page tree and aggregates module JavaScript into deferred script tags for optimal performance.

## Frequently Asked Questions

### What is the difference between the `component` and `render` properties in a module definition?

The **`component`** property is a React component that provides the interactive editing experience in the visual canvas, supporting user interactions and real-time previews. The **`render`** property is a pure function that returns static HTML, CSS, and JavaScript for the publishing pipeline, ensuring the final site output is optimized and server-renderable without React runtime overhead.

### How does the Instatic module engine handle JavaScript output during publishing?

When the publisher processes a module containing JavaScript, it extracts the `js` property from the `RenderOutput` object and aggregates all module scripts into a site-wide map. Instead of inlining scripts, the engine generates deferred `<script>` tags at the end of the HTML document, improving page load performance while maintaining modular separation of concerns.

### Where are module definitions stored at runtime?

Module definitions are stored in the **registry singleton** located in [`src/core/module-engine/registry.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/module-engine/registry.ts). This registry maintains a mapping of module IDs to their `ModuleDefinition` objects and acts as the single source of truth for both the client-side editor and the server-side publishing process.

### How do property controls connect to a module's TypeScript props?

The **`schema`** field in the module definition accepts a `PropertyControl` union type defined in [`src/core/module-engine/propertySchema.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/module-engine/propertySchema.ts). This schema describes the shape of editable properties (text inputs, selects, color pickers), and the editor uses this metadata to generate a property pane that enforces type safety, ensuring that values passed to the module's `component` and `render` functions always match the declared `TProps` generic.