From 4be3e2ef96b44ee9138d768d5eecae0f98dc0088 Mon Sep 17 00:00:00 2001 From: EuanTop Date: Tue, 28 Jul 2026 14:26:19 +0800 Subject: [PATCH 1/3] fix(publish): expose public path to publish filters --- docs/features/plugin-system.md | 7 ++- docs/features/publisher.md | 2 +- server/handlers/cms/data/preview.ts | 1 + server/plugins/protocol/messages.ts | 2 +- server/publish/publicRenderer.ts | 50 +++++++++++++------ server/publish/publishedHtmlPipeline.ts | 1 + server/publish/republish.ts | 18 +++---- .../publish-html-filter-context.test.ts | 2 +- .../server/cmsTemplateRoutes.test.ts | 9 ++++ src/__tests__/server/publicRendering.test.ts | 39 ++++++++++++--- src/core/plugin-sdk/types/hooks.ts | 17 +++++-- src/core/plugins/hookBus.ts | 2 +- 12 files changed, 109 insertions(+), 41 deletions(-) diff --git a/docs/features/plugin-system.md b/docs/features/plugin-system.md index 7bd0a856f..1f827ac4b 100644 --- a/docs/features/plugin-system.md +++ b/docs/features/plugin-system.md @@ -536,13 +536,18 @@ Plugin storage is per-plugin, per-collection. The collection name must match a ` ```js api.cms.hooks.on('publish.after', async (event) => { /* … */ }) -api.cms.hooks.filter('publish.html', async (html) => html + '') +api.cms.hooks.filter('publish.html', async (html, { path }) => { + const canonical = new URL(path, 'https://example.com').href + return html.replace('', ``) +}) const name = await api.cms.hooks.emit('sync.done', { /* … */ }) // name === 'plugin..sync.done' ``` **Host-emitted events** (the reserved core list, `CORE_HOOK_EVENTS` in `src/core/plugins/hookBus.ts`): `publish.before`, `publish.after`, `content.entry.created`, `content.entry.updated`, `content.entry.deleted`, `settings.changed`. **Filters**: `publish.html`, `publish.headers`, `content.entry.cells`. +`publish.html` and `publish.headers` handlers receive `{ pluginId, siteId, pageId, slug, path }`. `slug` identifies the rendered page or template document; for an entry route it remains the entry template's slug. `path` is the emitted page's public URL pathname, so it differs per entry (for example `/posts/hello-world`) and is `/` for the home page. + **Plugin emits are namespaced.** The host rewrites every `emit('', …)` to `plugin..` (a name already in your own namespace passes through unchanged), so event provenance is unforgeable — a plugin cannot fire `content.entry.created` or any other core event at other listeners, and emitting a name in *another* plugin's namespace (`plugin..*`) is rejected with an error. `emit` resolves to the canonical namespaced name. Cross-plugin eventing still works: subscribing is unrestricted, so a plugin listens to another plugin's events by their full namespaced name, e.g. `api.cms.hooks.on('plugin.acme.analytics.page-view', …)`. ### Loop sources — requires `loops.register` diff --git a/docs/features/publisher.md b/docs/features/publisher.md index 340879c1e..027f4d9cd 100644 --- a/docs/features/publisher.md +++ b/docs/features/publisher.md @@ -390,7 +390,7 @@ applyPublishedHtmlPipeline(renderedOutput, db) ├─→ Splice in declarative tags from plugin manifests' `frontend.assets[]` ├─→ Stamp form page tokens onto CMS-native
tags (`stampFormPageTokens`) ├─→ Inject per-module published JS: one `