Skip to main content

Registries

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

RegistryWhat it holdsSource
Plugin registry (filters + actions)Hook handlerssrc/core/pluginRegistry.ts
Block registryEditor blockssrc/core/blockRegistry.ts
Dashboard card registryDashboard cardssrc/core/dashboardCardRegistry.ts
Regeneration target registryRegen ▾ menu entriessrc/core/regenerationTargetRegistry.ts
External entries registryInstalled external plugins / themessrc/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

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:

// 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

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

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

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

PluginidPriorityWhat it does
flexweg-sitemapsflexweg-sitemaps200XSL + every yearly sitemap + index + News + robots.txt
flexweg-rssflexweg-rss210Every enabled feed + XSL stylesheet
flexweg-searchflexweg-search220/search.js + /search-index.json
flexweg-archivesflexweg-archives230Wipe /archives/ + rebuild
flexweg-faviconflexweg-favicon240site.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:

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

API

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