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
| 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
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 entrypriority— 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:
{
"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:
- Reads
settings/externalRegistryfrom Firestore - For each entry: dynamic-imports the bundle from
/admin/<kind>/<id>/bundle.js - Extracts
bundle.default(the manifest) - Caches in memory
When externals load
The boot order matters. From src/main.tsx:
import "./core/flexwegRuntime"— populateswindow.__FLEXWEG_RUNTIME__<App />renders →<CmsDataProvider>mountsCmsDataProvider's firstuseEffectcallsloadAllExternalEntries(), setsexternalsLoaded = true- The Firestore subscription
useEffectis gated onexternalsLoaded— soapplyPluginRegistrationruns 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:
- Calls
resetFilters()andresetActions()(the plugin registry itself) - Calls
resetBlocks(spareCoreBlocks: true) - Calls
resetDashboardCards() - 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.