diff --git a/.changeset/fuzzy-coins-march.md b/.changeset/fuzzy-coins-march.md new file mode 100644 index 0000000..e9a4497 --- /dev/null +++ b/.changeset/fuzzy-coins-march.md @@ -0,0 +1,5 @@ +--- +"@emdash-cms/cloudflare": patch +--- + +DIsplay an interstitial when loading playground diff --git a/demos/playground/wrangler.jsonc b/demos/playground/wrangler.jsonc index e30a16f..1478a6d 100644 --- a/demos/playground/wrangler.jsonc +++ b/demos/playground/wrangler.jsonc @@ -28,6 +28,11 @@ "zone_name": "emdashcms.com", "custom_domain": true, }, + { + "pattern": "www.emdashcms.com", + "zone_name": "emdashcms.com", + "custom_domain": true, + }, ], // No R2 -- media uploads are blocked in playground mode // No D1 -- database is inside the Durable Object diff --git a/packages/cloudflare/src/db/playground-loading.ts b/packages/cloudflare/src/db/playground-loading.ts new file mode 100644 index 0000000..eec0a20 --- /dev/null +++ b/packages/cloudflare/src/db/playground-loading.ts @@ -0,0 +1,260 @@ +/** + * Playground Loading Page + * + * Rendered when a user first hits /playground. Shows an animated loading state + * while the client-side JS calls /_playground/init to create the DO, run + * migrations, and apply the seed. Once init completes, redirects to the admin. + * + * No dependencies -- plain HTML with inline styles and a + +`; +} diff --git a/packages/cloudflare/src/db/playground-middleware.ts b/packages/cloudflare/src/db/playground-middleware.ts index e56b552..b4ce7e4 100644 --- a/packages/cloudflare/src/db/playground-middleware.ts +++ b/packages/cloudflare/src/db/playground-middleware.ts @@ -24,6 +24,7 @@ import type { EmDashPreviewDB } from "./do-class.js"; import { PreviewDODialect } from "./do-dialect.js"; import type { PreviewDBStub } from "./do-dialect.js"; import { isBlockedInPlayground } from "./do-playground-routes.js"; +import { renderPlaygroundLoadingPage } from "./playground-loading.js"; import { renderPlaygroundToolbar } from "./playground-toolbar.js"; /** Default TTL for playground data (1 hour) */ @@ -244,6 +245,9 @@ export const onRequest = defineMiddleware(async (context, next) => { const binding = getBindingName(); // --- Entry point: /playground --- + // Show a loading page immediately. The page calls /_playground/init via + // fetch to do the actual setup, then redirects to admin when ready. + // If the session is already initialized, skip the loading page. if (url.pathname === "/playground") { let token = cookies.get(COOKIE_NAME)?.value; if (!token) { @@ -256,19 +260,49 @@ export const onRequest = defineMiddleware(async (context, next) => { }); } + // Already initialized? Skip the loading page and go straight to admin. + if (initializedSessions.has(token)) { + return context.redirect("/_emdash/admin"); + } + + return new Response(renderPlaygroundLoadingPage(), { + status: 200, + headers: { "content-type": "text/html; charset=utf-8" }, + }); + } + + // --- Init endpoint: called by the loading page --- + if (url.pathname === "/_playground/init" && context.request.method === "POST") { + const token = cookies.get(COOKIE_NAME)?.value; + if (!token) { + return Response.json( + { error: { code: "NO_SESSION", message: "No playground session" } }, + { status: 400 }, + ); + } + + if (initializedSessions.has(token)) { + return Response.json({ ok: true }); + } + const stub = getStub(binding, token); const dialect = new PreviewDODialect({ getStub: () => stub }); // eslint-disable-next-line @typescript-eslint/no-explicit-any const db = new Kysely({ dialect }); - if (!initializedSessions.has(token)) { + try { await initializePlayground(db, token); initializedSessions.add(token); const fullStub = getFullStub(binding, token); await fullStub.setTtlAlarm(ttl); + return Response.json({ ok: true }); + } catch (error) { + console.error("Playground initialization failed:", error); + return Response.json( + { error: { code: "PLAYGROUND_INIT_ERROR", message: "Failed to initialize playground" } }, + { status: 500 }, + ); } - - return context.redirect("/_emdash/admin"); } // --- Reset endpoint ---