Registries

The admin maintains five runtime registries that hold what plugins / themes contribute. All five share a similar lifecycle: cleared on every pass, then re-populated.

The admin maintains five runtime registries that hold what plugins / themes contribute. All five share a similar lifecycle: cleared on every applyPluginRegistration() pass, then re-populated.

This page documents what each registry holds, when it's reset, and how to consume its contents.

The five registries

Registry What it holds Source
Plugin registry (filters + actions) Hook handlers src/core/pluginRegistry.ts
Block registry Editor blocks src/core/blockRegistry.ts
Dashboard card registry Dashboard cards src/core/dashboardCardRegistry.ts
Regeneration target registry Regen ▾ menu entries src/core/regenerationTargetRegistry.ts
External entries registry Installed external plugins / themes src/services/externalRegistry.ts

The first four reset together on every plugin-toggle / settings-change. The fifth is independent — it tracks what's installed on disk, separate from what's currently registered.

Plugin registry (filters + actions)

The most-used registry. Holds all filter and action handlers, indexed by hook name + priority.

API

TS
import { applyFilters, applyFiltersSync, doAction } from "@flexweg/cms-runtime";
// (Internal — not exported via the runtime API. Plugins consume via api.addFilter / api.addAction.)

The publisher / render code calls applyFilters and doAction to fire hooks. Plugin handlers register via pluginApi.addFilter / pluginApi.addAction.

Lifecycle

applyPluginRegistration(enabledFlags) {
  resetRegistry();  // clears filters + actions + blocks + cards + targets

  for (const muManifest of MU_PLUGINS) {
    muManifest.register(api);       // unconditional
  }
  for (const manifest of PLUGINS) {
    if (enabledFlags[manifest.id]) {
      manifest.register(api);
    }
  }
  for (const externalManifest of listExternalPlugins()) {
    if (enabledFlags[externalManifest.id]) {
      externalManifest.register(api);
    }
  }
  // Theme's register() is called separately when the theme activates
  getActiveTheme(activeThemeId).register?.(api);
}

So toggling any plugin enable/disable triggers a full re-registration. Cheap (no Firestore reads, just function calls).

Block registry

Holds editor blocks contributed by plugins, themes, and core.

Two-channel registration

Most blocks live in plugins / themes and clear on every pass. Core blocks are persistent:

TS
// src/core/coreBlocks.ts (side-effect imported from main.tsx)
registerCoreBlock(paragraphBlock);
registerCoreBlock(headingBlock);
// … etc

resetBlocks() spares core blocks — the toggle of an unrelated plugin can never accidentally strip the basics.

Consuming the registry

TS
import { listBlocks } from "@flexweg/cms-runtime";  // internal — used by the editor

const blocks = listBlocks();  // BlockManifest[]

The editor's inserter and inspector iterate this list to find blocks by category, by isActive predicate, etc.

Dashboard card registry

Holds cards contributed by plugins.

API

TS
import { listDashboardCards } from "@flexweg/cms-runtime";  // internal

const cards = listDashboardCards();  // DashboardCardManifest[]

The dashboard page snapshots this list once on mount and renders each card's component in priority order.

Lifecycle

Cleared by resetDashboardCards() on every applyPluginRegistration() pass. Re-registered as part of the same flow.

Regeneration target registry

Holds entries shown in the Themes → Regenerate site ▾ dropdown beyond the four core entries (All HTML / Home / Theme assets / Everything).

API

TS
api.registerRegenerationTarget({
  id: "my-plugin",
  labelKey: "regenerationTarget.label",
  descriptionKey: "regenerationTarget.description",
  priority: 200,
  run: async (ctx: PublishContext, log: PublishLogger) => {
    // Do the regeneration work
  },
});

Fields

  • id — unique identifier (typically the plugin id)
  • labelKey + descriptionKey — i18n keys for the dropdown entry
  • priority — sort order in the dropdown (lower runs first in Everything pass)
  • run(ctx, log) — the regeneration function. Receives the publish context and a logger.

The Everything entry runs every registered target in priority order plus the core entries. So registering a target makes it run both standalone (single-purpose dropdown click) AND as part of a broader Everything pass.

Built-in regen targets

Plugin id Priority What it does
flexweg-sitemaps flexweg-sitemaps 200 XSL + every yearly sitemap + index + News + robots.txt
flexweg-rss flexweg-rss 210 Every enabled feed + XSL stylesheet
flexweg-search flexweg-search 220 /search.js + /search-index.json
flexweg-archives flexweg-archives 230 Wipe /archives/ + rebuild
flexweg-favicon flexweg-favicon 240 site.webmanifest only

External entries registry

Tracks installed external plugins / themes. Separate from the runtime register flow because:

  • The on-disk manifest survives plugin toggles (you can disable a plugin without uninstalling)
  • The list is shared across boot passes (loaded once from Firestore at boot)
  • It's the source of truth for the Install plugin UI's currently-installed list

Storage

Lives at settings/externalRegistry in Firestore:

JSON
{
  "plugins": {
    "my-plugin": {
      "version": "1.0.0",
      "apiVersion": "1.0.0",
      "installedAt": 1735689600000
    }
  },
  "themes": {
    "my-theme": { /* same shape */ }
  }
}

API

TS
import { listExternalPlugins, listExternalThemes } from "@flexweg/cms-runtime";  // internal

const plugins = listExternalPlugins();  // PluginManifest[]
const themes = listExternalThemes();    // ThemeManifest[]

These lists are built by the boot loader (loadAllExternalEntries()) which:

  1. Reads settings/externalRegistry from Firestore
  2. For each entry: dynamic-imports the bundle from /admin/<kind>/<id>/bundle.js
  3. Extracts bundle.default (the manifest)
  4. Caches in memory

When externals load

The boot order matters. From src/main.tsx:

  1. import "./core/flexwegRuntime" — populates window.__FLEXWEG_RUNTIME__
  2. <App /> renders → <CmsDataProvider> mounts
  3. CmsDataProvider's first useEffect calls loadAllExternalEntries(), sets externalsLoaded = true
  4. The Firestore subscription useEffect is gated on externalsLoaded — so applyPluginRegistration runs ONCE with the complete plugin set (built-ins + externals)

If an external bundle fails to load (network error, parse error), it's skipped + logged. The boot doesn't abort.

Resetting the registries

resetRegistry() (in src/core/pluginRegistry.ts) is the master reset. It:

  1. Calls resetFilters() and resetActions() (the plugin registry itself)
  2. Calls resetBlocks(spareCoreBlocks: true)
  3. Calls resetDashboardCards()
  4. Calls resetRegenerationTargets()

So a single function call wipes everything plugins might have registered, in the right order, in one pass.

The external entries registry is not reset by resetRegistry() — it persists across plugin enable/disable cycles. Only the actual install / uninstall flow modifies it.

Idempotency

All registry add operations are idempotent: registering the same id twice replaces the previous entry, no error. So re-running register(api) multiple times produces the same final state.

This is important — the lifecycle calls register(api) multiple times during a session (every plugin toggle), and tests / hot-reload cycles can re-register without warning.

Continue