ReactPress 4.0 plugins extend server-side business logic (hooks, content rules, integrations) β WordPress-style extensibility without touching core. Themes = presentation; plugins = logic.
Built-in: SEO Β· auto-summary Β· batch WebP optimization Β· install from Admin or CLI
Modeled after WordPress: install/enable from Admin, configure via plugin.json, integrate with register() and hook subscriptions.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Two sources (how packages land in runtime) β
β local plugins/{id}/ β .reactpress/plugins/{id}/ β
β npm npm pack spec β .reactpress/plugins/{id}/ β (phase 2)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Three layers β
β plugins/ Registry β what can be installed β
β .reactpress/plugins/ Materialized β installed + dist β
β DB globalSetting Active β enabled list + per-plugin β
β config β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β activate
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Runtime HookService β
β require(server.module) β register(hooks, ctx) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Aligns with the three-layer directory model in themes/README.md to reduce cognitive load for theme/plugin authors.
| Type | Extends | Process | Examples |
|---|---|---|---|
| Theme | Visitor UI, SSR | Next.js (:3001) | Skins, article templates |
| Plugin | Business rules, hooks | NestJS API (:3002) | Auto summary, SEO |
| Webhook | Cross-system async notify | Server outbound HTTP | Slack, CI triggers |
Boundaries
- Plugins must not inject theme routes or modify Next build config.
- Themes must not write to the database directly β only via toolkit β API.
- Hooks (in-process, inbound, can mutate fields) vs Webhooks (outbound HTTP) are separate concepts.
Hook + webhook flow on article publish:
article.service
ββ applyFilters('article.beforePublish') β plugin sync rewrite summary, etc.
ββ persist
ββ doAction('article.afterPublish') β plugin sync side effects
ββ webhookService.dispatch('article.published') β external systems
{
"reactpress": {
"local": ["hello-world"]
}
}| Source | Metadata | Registry location |
|---|---|---|
| local | plugins/{id}/plugin.json |
reactpress.local array |
| npm | plugin.json inside package |
catalog anchor (phase 2, same rules as theme npm) |
To add a local plugin: id matches directory name and plugin.json id; add to local array.
Schema: plugin.manifest.schema.json. Parser: toolkit/src/plugin/extension/plugin.ts.
{
"$schema": "../plugin.manifest.schema.json",
"id": "my-plugin",
"name": "My Plugin",
"version": "1.0.0",
"description": "β¦",
"requires": ">=3.5.0",
"server": {
"module": "./dist/index.js",
"hooks": {
"subscribe": ["article.beforePublish"]
}
},
"settings": {
"schema": {
"type": "object",
"properties": {
"enabled": { "type": "boolean", "default": true }
}
}
},
"permissions": ["article:read"],
"capabilities": { "headless": true }
}| Field | Required | Description |
|---|---|---|
id |
β | kebab-case, matches directory name |
name / version |
β | Display name and semver |
requires |
Minimum ReactPress version | |
requiresPlugins |
Other plugin ids this depends on | |
server.module |
β* | Server entry (compiled JS) |
server.hooks.subscribe |
Declared hook names | |
admin.slots.subscribe |
Built-in Admin slot ids (enum) | |
admin.menu |
Standalone plugin settings page menu | |
settings.schema |
JSON Schema for config form | |
src/locales/{locale}.json |
Admin strings | |
permissions |
Admin capability declarations | |
capabilities |
Security capabilities (see below) |
* At least server.module, or one of admin.slots / admin.menu (Admin code convention: src/admin/index.ts).
| WordPress plugin header | ReactPress |
|---|---|
| Plugin Name | name |
| Version | version |
| Text Domain | id |
| Requires at least | requires |
| Requires Plugins | requiresPlugins |
Every plugin package uses the same layout: manifest and i18n at the root, all TypeScript under src/, split by runtime:
plugins/my-plugin/
βββ plugin.json # manifest (id, hooks, settings schema, admin slots)
βββ package.json
βββ tsconfig.json # extends ../tsconfig.base.json (server compile only)
βββ README.md
βββ src/
β βββ server/ # Node Hook logic β tsc β dist/
β β βββ index.ts # export function register(hooks, ctx)
β β βββ β¦
β βββ admin/ # optional React Admin UI (bundled by web via Vite)
β β βββ index.ts # export function registerAdmin(registry, ctx)
β β βββ β¦
β βββ locales/ # Admin UI strings (en.json, zh.json, β¦)
βββ dist/ # plugin.json β server.module (./dist/index.js)
βββ index.js
| Path | Runtime | Build |
|---|---|---|
src/server/ |
NestJS require() at API startup |
pnpm run build β tsc β dist/ |
src/admin/ |
Admin SPA (Vite import.meta.glob) |
Compiled when building web/ |
src/locales/ |
Admin i18n API | No build β served as JSON |
plugin.json |
Install / enable / config | No build |
Shared server compiler defaults live in tsconfig.base.json. Admin TSX is excluded from plugin tsc and compiled by the Admin Vite app instead.
Admin i18n lives in src/locales/{locale}.json; API GET /extension/plugins/:id/locales/:locale reads from the installed plugin tree (themes keep locales/ at package root).
Dependency rules
plugins/{id}/src/server β @fecommunity/reactpress-toolkit/plugin/server only
plugins/{id}/src/admin β toolkit/plugin/admin + toolkit/plugin/react
server core β HookService only, no direct plugin imports
web / themes β load plugin admin via convention src/admin/index.ts
Symmetric with server.hooks.subscribe: manifest declares slot enum, code convention defines entry path.
| Layer | Declaration | Notes |
|---|---|---|
plugin.json |
admin.slots.subscribe: ["article.editor.meta.afterSummary"] |
Built-in slot id enum drives Admin load |
| Plugin package | Fixed src/admin/index.ts β registerAdmin() |
No path in manifest |
| Core pages | <AdminSlot slot={AdminSlotIds.β¦} /> |
Host mount point |
Manifest example:
"admin": {
"slots": {
"subscribe": ["article.editor.meta.afterSummary"]
},
"menu": {
"title": "SEO Enhancement",
"path": "/plugins/seo/settings",
"permission": "extension:manage"
}
}Built-in slots (AdminSlotIds / JSON Schema enum):
| Slot id | Location |
|---|---|
article.editor.meta.afterSummary |
Article editor Β· below summary |
article.editor.sidebar.afterPublish |
Article editor Β· below publish box |
Core page mount:
import { AdminSlot, AdminSlotIds } from '@fecommunity/reactpress-toolkit/plugin/react';
<AdminSlot slot={AdminSlotIds.ARTICLE_EDITOR_META_AFTER_SUMMARY} context={slotContext} />;Plugin admin entry (plugins/{id}/src/admin/index.ts):
import {
AdminSlotIds,
type PluginAdminModule,
type PluginAdminRegistry,
type PluginAdminContext,
} from '@fecommunity/reactpress-toolkit/plugin/admin';
export function registerAdmin(registry: PluginAdminRegistry, ctx: PluginAdminContext): void {
registry.registerSlot(AdminSlotIds.ARTICLE_EDITOR_META_AFTER_SUMMARY, MyPanel, {
priority: 10,
});
}
export default { registerAdmin } satisfies PluginAdminModule;Slot component props: { context, pluginId, config }. context is defined by the host page (e.g. ArticleEditorAdminSlotContext with draft / patch / translate).
When enabled and manifest includes admin.slots or admin.menu, PluginAdminProvider loads plugins/{id}/src/admin/index by convention; enable/disable triggers re-bootstrap.
Admin dependency rules
plugins/{id}/src/admin β @fecommunity/reactpress-toolkit/plugin/admin + plugin/react + react/antd
Server entry contract:
import type { HookService, PluginContext } from '@fecommunity/reactpress-toolkit/plugin/server';
export function register(hooks: HookService, ctx: PluginContext): void {
hooks.addFilter(
'article.beforePublish',
async (article) => {
return article;
},
{ priority: 10, pluginId: ctx.id }
);
}
// optional: cleanup timers on deactivate
export function deactivate(): void {}| Action | Effect |
|---|---|
| Install | Materialize to .reactpress/plugins/; write installedPlugins[] |
| Enable | Write activePlugins[]; Server hot-loads server.module |
| Disable | Remove hooks; optionally call deactivate() |
| Uninstall | Must disable first; delete runtime directory |
Unlike themes: plugin enable/disable does not require API restart; theme enable requires Next restart (:3001).
Persistence (Setting.globalSetting.plugins):
{
installedPlugins: string[];
activePlugins: string[]; // ordered β affects load order
entries: Record<string, {
version: string;
source: 'local' | 'npm';
config?: Record<string, unknown>;
activatedAt?: string;
loadError?: string;
}>;
}| Hook | Type | Timing | Payload |
|---|---|---|---|
article.beforeCreate |
filter | Before create | Partial<Article> |
article.beforePublish |
filter | Before publish | Mutate fields (e.g. summary) |
article.afterPublish |
action | After publish | { article, isNew } |
comment.beforeCreate |
filter | Before comment | Can __hookReject |
comment.afterCreate |
action | After comment | { comment, createByAdmin } |
Reject a filter (toolkit createHookRejectValue / getHookReject):
import { createHookRejectValue } from '@fecommunity/reactpress-toolkit/plugin/server';
return createHookRejectValue(comment, { message: 'Comment rejected', statusCode: 400 });Same hook name sorted by priority (default 10); failed plugins write loadError without affecting others.
Standalone repo (recommended for third-party plugins): use the reactpress-plugin-starter template β hooks, Admin slots, settings page, and build scripts out of the box. Develop outside the monorepo, then install via npm (phase 2) or copy into plugins/{id}/.
In this monorepo:
- Copy
hello-world/βplugins/my-plugin/ - Edit
plugin.json,src/server/index.ts - Add to
plugins/package.jsonβlocal - Build and enable:
pnpm --filter @reactpress/plugin-my-plugin run build
# or
pnpm run build:plugins
pnpm dev # compiles local plugins on demand before start- Admin Plugins β Install β Enable
| Action | HTTP |
|---|---|
| List | GET /api/extension/plugins |
| State | GET /api/extension/plugins/state |
| Install | POST /api/extension/plugins/:id/install |
| Enable | POST /api/extension/plugins/:id/activate |
| Disable | POST /api/extension/plugins/:id/deactivate |
| Uninstall | DELETE /api/extension/plugins/:id |
| Config | PUT /api/extension/plugins/:id/config (auto reload after save) |
| Admin locales | GET /api/extension/plugins/:id/locales/:locale |
Plugins with settings.schema show a Settings link opening a WordPress-style config page (/plugins/{id}/settings).
reactpress plugin list
reactpress plugin install hello-world- Install/enable/config requires admin JWT +
extension:manage(web route layer). - Enabled plugins = trusted code:
require()in Node process, same model as WordPress; only admins can install/enable. server.modulepath constraint: relative paths inside plugin dir only (no.., no absolute paths); extensions limited to.js/.cjs/.mjs.- Config writes: server validates against manifest
settings.schema(Ajv); rejects dangerous keys like__proto__. - Materialize copy: production skips symlinks when copying plugin files.
- Plugin id: global kebab-case validation; invalid ids in
globalSetting.pluginsare ignored.
| capability | Meaning | Runtime enforcement |
|---|---|---|
headless |
Server only, no Admin UI | Documented convention |
network |
Outbound HTTP | Phase 2 |
filesystem |
Read/write outside project | Phase 2 |
database |
Register entities | Phase 2 |
Manifest permissions[] declares Admin UI capabilities; server API boundary is currently admin role.
| Scenario | Mount |
|---|---|
| Mutate fields before publish | article.beforePublish filter |
| Comment filtering | comment.beforeCreate filter |
| Post-publish analytics | article.afterPublish action |
| Server-only, no UI | server.module + headless: true |
Not suitable for plugins: visitor page components (use themes), large DB schema changes (core migration PR).
pnpm run build:plugins
pnpm --filter @reactpress/plugin-hello-world run build
pnpm --filter @reactpress/plugin-hello-world run typecheckFull pnpm build includes plugins after toolkit, before server.
| id | Name | Description |
|---|---|---|
hello-world |
Auto Summary | Generate summary from body/title when empty on publish |
seo |
SEO Enhancement | Slug, keywords, meta description with auto-fill |
image-optimizer |
Image Optimization | Analyze legacy assets, batch WebP variants |
| Module | Role |
|---|---|
server/src/modules/extension/plugin.service.ts |
Install, enable, config |
server/src/modules/extension/plugin-loader.service.ts |
Dynamic load + register() |
server/src/modules/hook/hook.service.ts |
Hook registry |
toolkit/src/plugin/server/ |
Plugin SDK |
toolkit/src/plugin/extension/plugin.ts |
Manifest parsing and state types |
cli/out/lib/plugin-build.js |
Dev on-demand compile |
web/src/modules/plugins/ |
Admin plugin list |
- npm catalog and
reactpress plugin add - Dynamic plugin Admin
admin.entryloading (custom React settings UI) reactpress plugin createCLI (scaffold: reactpress-plugin-starter), plugin marketplace
- reactpress-plugin-starter β standalone plugin template (hooks, Admin UI, settings)
- WordPress Plugin Handbook
- Root ARCHITECTURE.md β system architecture and extensibility
themes/README.mdβ theme registry and three-layer model