publish.headers is declared as a core filter next to publish.html (src/core/plugin-sdk/types/hooks.ts:52-64, context type at :80):
export interface CmsServerFilters {
'publish.html': string
'publish.headers': Record<string, string>
and documented as one (docs/features/plugin-system.md:544):
Filters: publish.html, publish.headers, content.entry.cells.
Nothing dispatches it. Every occurrence in the repo is a declaration, a comment, or that doc line:
| file:line |
what it is |
src/core/plugin-sdk/types/hooks.ts:54 |
filter value type |
src/core/plugin-sdk/types/hooks.ts:80 |
filter context type |
src/core/plugins/hookBus.ts:162 |
JSDoc example on applyFilter |
server/plugins/protocol/messages.ts:164 |
comment |
docs/features/plugin-system.md:544 |
docs |
The three production applyFilter calls are publish.html (server/publish/publishedHtmlPipeline.ts:61), content.entry.cells (server/publish/contentEvents.ts:79) and media.url.transform (server/publish/mediaPresentation.ts:60). Published-page responses assemble their headers directly (server/publish/publicRouter.ts:237,250,273,316,324,337,340) and never reach the bus. publishedHtmlPipeline.ts:9-23 documents its own stages and does not list it either.
Reproduce: register a handler and publish.
hooks.filter('publish.headers', (headers) => ({ ...headers, 'x-plugin': '1' }))
No error, no warning, no header. It fails silently in both directions: handleHooksFilter registers any name without validation (server/plugins/host/handlers/hooks.ts:32-43), and applyFilter early-returns when a name has no handlers (hookBus.ts:169-170).
Structurally this is a typing asymmetry rather than an oversight in one file. emit() is typed against the closed CoreHookEvent union (hookBus.ts:36-42), so a core event cannot be declared without something wiring it. applyFilter(name: string, …) (hookBus.ts:164-168) takes a raw string, so a filter can sit in the SDK types indefinitely with nothing on the host side to match it.
Either direction resolves it:
- dispatch it in
publicRouter.ts where the published response headers are assembled, passing the { siteId, pageId, slug } context the type already declares; or
- delete the two type members and the doc line, since the pre-release rule is no shims for surfaces that do not exist.
Happy to send a PR for whichever you prefer — I did not want to guess which, since implementing it makes a new public surface real.
Verified at 6b055cf7 (v0.0.16-3).
publish.headersis declared as a core filter next topublish.html(src/core/plugin-sdk/types/hooks.ts:52-64, context type at:80):and documented as one (
docs/features/plugin-system.md:544):Nothing dispatches it. Every occurrence in the repo is a declaration, a comment, or that doc line:
src/core/plugin-sdk/types/hooks.ts:54src/core/plugin-sdk/types/hooks.ts:80src/core/plugins/hookBus.ts:162applyFilterserver/plugins/protocol/messages.ts:164docs/features/plugin-system.md:544The three production
applyFiltercalls arepublish.html(server/publish/publishedHtmlPipeline.ts:61),content.entry.cells(server/publish/contentEvents.ts:79) andmedia.url.transform(server/publish/mediaPresentation.ts:60). Published-page responses assemble their headers directly (server/publish/publicRouter.ts:237,250,273,316,324,337,340) and never reach the bus.publishedHtmlPipeline.ts:9-23documents its own stages and does not list it either.Reproduce: register a handler and publish.
No error, no warning, no header. It fails silently in both directions:
handleHooksFilterregisters any name without validation (server/plugins/host/handlers/hooks.ts:32-43), andapplyFilterearly-returns when a name has no handlers (hookBus.ts:169-170).Structurally this is a typing asymmetry rather than an oversight in one file.
emit()is typed against the closedCoreHookEventunion (hookBus.ts:36-42), so a core event cannot be declared without something wiring it.applyFilter(name: string, …)(hookBus.ts:164-168) takes a raw string, so a filter can sit in the SDK types indefinitely with nothing on the host side to match it.Either direction resolves it:
publicRouter.tswhere the published response headers are assembled, passing the{ siteId, pageId, slug }context the type already declares; orHappy to send a PR for whichever you prefer — I did not want to guess which, since implementing it makes a new public surface real.
Verified at
6b055cf7(v0.0.16-3).