Skip to content

Plugin Architecture — Boundaries & Ownership

Defines what belongs to the host plugin (ui/) vs what comes from the lib (@middag-io/react).

This guide uses MIDDAG Account (WordPress) as reference, but the same boundaries apply to any Inertia host.

Plugin Structure (scaffolded by create-middag-ui)

ui/src/
├── app/                     # PLUGIN — App bootstrap & registration
│   ├── register-{product}.ts #  Selective block/shell/layout registration
│   └── page-resolver.tsx    #   Contract: prefix + direct page resolution
├── adapters/                # PLUGIN — Inertia mock adapters (dev only)
├── mock/                    # PLUGIN — Dev mock data & navigation
│   ├── navigation.ts        #   Sidebar structure for dev server
│   └── data.ts              #   Synthetic props for direct pages
├── components/
│   └── {product}/           # PLUGIN — Product-specific components
├── lib/                     # PLUGIN — Utilities & hooks
├── pages/                   # PLUGIN — Page components
│   └── mock-contracts/      #   Static PageContract for dev mock
├── contracts.ts             # PLUGIN — Type re-exports from lib
├── entry-wp.tsx             # PLUGIN — WP production entry (or entry-{host}.tsx)
├── main.tsx                 # PLUGIN — Dev server entry
├── app.tsx                  # PLUGIN — Dev mock app (BrowserRouter + routes)
├── tailwind.css             # PLUGIN — Tailwind v4 @theme config
├── theme.css                # PLUGIN — Host isolation + token overrides
└── theme-*.css              # PLUGIN — Optional theme variants

Boundaries

PLUGIN (host developers own this)

Everything in ui/src/ that is specific to the product.

AreaWhatRule
app/Bootstrap & registrationRegister only blocks/shells the plugin uses
components/{product}/Product-specific componentsCustom UI for the product
lib/Utils & hooksShared utilities for the plugin
pages/Direct pagesPages using usePage() from Inertia
mock/Dev data & navigationMock data files, not inline in app.tsx
*.cssThemes & TailwindTokens, host isolation, theme variants
entry-*.tsxHost entryBoot Inertia in production
main.tsx / app.tsxDev entryMock app with BrowserRouter

LIB (comes from @middag-io/react — don't copy)

WhatHow to use
ShellsregisterShell("product", ProductShell)
LayoutsregisterLayout("stack", StackLayout)
BlocksregisterBlock("dense_table", DenseTableBlock)
ContractPage<ContractPage contract={...} />
Registriesimport { registerBlock } from "@middag-io/react"
ProvidersI18nProvider, AuthProvider, FlashProvider, etc.
Theme tokensimport "@middag-io/react/style.css"
ReUI componentsimport { Button } from "@middag-io/react/reui/button"
Mock utilitiesimport { MockPageProvider } from "@middag-io/react/mock"

Rules

  1. Never copy lib components — import from @middag-io/react or @middag-io/react/reui/*
  2. Product-specific componentscomponents/{product}/ (not blocks/)
  3. Mock datamock/data.ts (not inline in app.tsx)
  4. Navigationmock/navigation.ts (not inline in app.tsx)
  5. CSS isolationtheme.css handles #middag-app { all: initial } for WP
  6. Selective registrationregister-{product}.ts (not registerDefaults() in production for bundle size)

Data flow

Host (PHP/Moodle) → Inertia props → entry-{host}.tsx → page-resolver → ContractPage or DirectPage
  • Contract pages: Host sends { contract: PageContract }ContractPage renders
  • Direct pages: Host sends custom props → React component consumes via usePage()
  • Dev mode: app.tsx provides mock data via MockPageProvider

Scaffold template

When create-middag-ui scaffolds a new project, it should generate:

src/
├── mock/
│   ├── navigation.ts    # buildNavigation() — sidebar for dev
│   └── data.ts          # sharedProps + mock page props
├── pages/
│   └── mock-contracts/  # Static PageContract examples
├── app.tsx              # Slim: imports from mock/*, defines routes only
├── main.tsx             # Dev entry
└── entry-wp.tsx         # Host entry (or entry-moodle.tsx)

app.tsx should contain only route definitions and wrappers — no inline data or navigation.

Released under the proprietary license.