Svelte’s onMount only ever runs in the browser — in SvelteKit that holds true even for server-rendered routes — so mounting @maildeno/editor is a straightforward bind:this plus onMount/onDestroy pair.
The editor component
<!-- Editor.svelte -->
<script lang="ts">
import { onMount, onDestroy } from "svelte";
import type { EditorHandle } from "@maildeno/editor/init";
let container: HTMLDivElement;
let handle: EditorHandle | undefined;
onMount(async () => {
const { init } = await import("@maildeno/editor/init");
handle = await init({
container,
capabilities: { export: ["html", "mjml", "json"] },
onSave: (payload: { templateId: string | null }) => {
console.log("Saved", payload);
const target = handle?.getHtml(); // handle?.getReactEmail() or handle?.getMjml()
console.log(target);
},
onSendTestEmail: async ({ to, subject, html }) => {
await fetch("/api/send-test", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ to, subject, html }),
});
},
});
});
onDestroy(() => handle?.destroy());
</script>
<div bind:this={container}/>
The dynamic import() inside onMount keeps @maildeno/editor/init — and its shadow-root/DOM usage — out of SvelteKit’s server-side module graph. In a client-only Svelte app (no SvelteKit) a static top-level import works fine too, since nothing evaluates it until onMount calls init().
SvelteKit route handler for test email
// src/routes/api/send-test/+server.ts
import { json } from "@sveltejs/kit";
import type { RequestHandler } from "./$types";
import { POSTMARK_SERVER_TOKEN } from "$env/static/private";
export const POST: RequestHandler = async ({ request }) => {
const { to, subject, html } = await request.json();
const res = await fetch("https://api.postmarkapp.com/email", {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"X-Postmark-Server-Token": POSTMARK_SERVER_TOKEN,
},
body: JSON.stringify({
From: "Your App <preview@yourapp.com>",
To: to,
Subject: subject,
HtmlBody: html,
MessageStream: "outbound",
}),
});
if (!res.ok) {
return json({ success: false, error: await res.text() }, { status: 502 });
}
const data = await res.json();
return json({ success: true, messageId: data.MessageID });
};
$env/static/private keeps the token server-only and fails the build if it’s referenced from client code — a useful guard rail beyond just "don’t put it in `.env.PUBLIC_*`".
Where to go next
-
Storage adapters — persist templates and images through a SvelteKit endpoint
-
Test email integration — the full send-test pattern, including other providers
-
@maildeno/editorreference — fullEditorHandleAPI