Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/public-form-ssr-routes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"emdash": patch
"@emdash-cms/plugin-forms": patch
---

Fixes public form embeds during SSR by allowing frontend plugin components to call public plugin routes without self-fetching.
4 changes: 4 additions & 0 deletions packages/core/src/astro/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import {
runWithContext,
} from "../request-context.js";
import type { EmDashConfig } from "./integration/runtime.js";
import { createPublicPluginApiRouteHandler } from "./public-plugin-api-routes.js";
import type { EmDashHandlers } from "./types.js";

// Cached runtime instance (persists across requests within worker)
Expand Down Expand Up @@ -368,8 +369,10 @@ export const onRequest = defineMiddleware(async (context, next) => {
try {
const runtime = await getRuntime(config, initSubTimings);
setupVerified = true;
const handlePublicPluginApiRoute = createPublicPluginApiRouteHandler(runtime);
// eslint-disable-next-line typescript/no-unsafe-type-assertion -- partial object; getPageRuntime() only checks for the page-contribution methods
locals.emdash = {
handlePublicPluginApiRoute,
collectPageMetadata: runtime.collectPageMetadata.bind(runtime),
collectPageFragments: runtime.collectPageFragments.bind(runtime),
getPublicMediaUrl: createPublicMediaUrlResolver(runtime.storage),
Expand Down Expand Up @@ -496,6 +499,7 @@ export const onRequest = defineMiddleware(async (context, next) => {

// Plugin routes
handlePluginApiRoute: runtime.handlePluginApiRoute.bind(runtime),
handlePublicPluginApiRoute: createPublicPluginApiRouteHandler(runtime),
getPluginRouteMeta: runtime.getPluginRouteMeta.bind(runtime),

// Media provider methods
Expand Down
41 changes: 41 additions & 0 deletions packages/core/src/astro/public-plugin-api-routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { HandlerResponse } from "./types.js";

export type PublicPluginApiRouteHandler = (
pluginId: string,
method: string,
path: string,
request: Request,
) => Promise<HandlerResponse>;

interface PublicPluginApiRouteRuntime {
getPluginRouteMeta(pluginId: string, path: string): { public: boolean } | null;
handlePluginApiRoute(
pluginId: string,
method: string,
path: string,
request: Request,
): Promise<HandlerResponse>;
}

function pluginRouteNotFound(): HandlerResponse {
return {
success: false,
error: {
code: "NOT_FOUND",
message: "Plugin route not found",
},
};
}

export function createPublicPluginApiRouteHandler(
runtime: PublicPluginApiRouteRuntime,
): PublicPluginApiRouteHandler {
return async (pluginId, method, path, request) => {
const meta = runtime.getPluginRouteMeta(pluginId, path);
if (meta?.public !== true) {
return pluginRouteNotFound();
}

return runtime.handlePluginApiRoute(pluginId, method, path, request);
};
}
8 changes: 8 additions & 0 deletions packages/core/src/astro/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,14 @@ export interface EmDashHandlers {
request: Request,
) => Promise<HandlerResponse>;

// Public-only plugin API route handler for SSR page components.
handlePublicPluginApiRoute: (
pluginId: string,
method: string,
path: string,
request: Request,
) => Promise<HandlerResponse>;

// Plugin route metadata (for auth decisions before dispatch)
getPluginRouteMeta: (pluginId: string, path: string) => { public: boolean } | null;

Expand Down
23 changes: 23 additions & 0 deletions packages/core/src/plugin-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@
* Import as: `import { apiFetch, parseApiResponse, isRecord } from "emdash/plugin-utils";`
*/

import type { EmDashHandlers } from "./astro/types.js";

export type PublicPluginApiRouteHandler = EmDashHandlers["handlePublicPluginApiRoute"];

export interface PublicPluginRuntimeLocals {
emdash?: {
handlePublicPluginApiRoute?: PublicPluginApiRouteHandler;
};
}

/**
* Fetch wrapper that adds the `X-EmDash-Request` CSRF protection header.
*
Expand All @@ -20,6 +30,19 @@ export function apiFetch(input: string | URL | Request, init?: RequestInit): Pro
return fetch(input, { ...init, headers });
}

/**
* Get the public-only plugin route dispatcher exposed to SSR page components.
*
* This intentionally reads `handlePublicPluginApiRoute`, not the raw
* `handlePluginApiRoute` used by core's authenticated plugin API route.
*/
export function getPublicPluginApiRouteHandler(
locals: PublicPluginRuntimeLocals | null | undefined,
): PublicPluginApiRouteHandler | undefined {
const handler = locals?.emdash?.handlePublicPluginApiRoute;
return typeof handler === "function" ? handler : undefined;
}

/**
* Parse an API response, unwrapping the `{ data: T }` envelope.
*
Expand Down
Loading
Loading