Building blocksInstallFramework guides

Svelte / SvelteKit

SSR-safe booking page for SvelteKit.

The widget is client-only, so everything happens inside onMount — SSR never touches window.

src/routes/book/+page.svelte
<script lang="ts">
  import { onMount } from "svelte";

  export let apiKey = "mk_your_publishable_key";

  const CDN_URL = "https://widgets.medos.one/v2/unified.js";
  const CONTAINER_ID = "medos-booking-svelte";

  type MedosBookingApi = {
    init: (config: Record<string, unknown>) => void;
    open: (config?: Record<string, unknown>) => void;
    close: () => void;
  };

  const medos = () =>
    (window as unknown as { MedosBooking?: MedosBookingApi }).MedosBooking;

  /** Reuses an existing tag so client-side navigation doesn't re-download. */
  function load(): Promise<void> {
    if (medos()) return Promise.resolve();

    const existing = document.querySelector<HTMLScriptElement>(
      `script[src="${CDN_URL}"]`,
    );
    if (existing) {
      return new Promise((resolve) =>
        existing.addEventListener("load", () => resolve(), { once: true }),
      );
    }

    return new Promise((resolve, reject) => {
      const script = document.createElement("script");
      script.src = CDN_URL;
      script.async = true;
      script.onload = () => resolve();
      script.onerror = () => reject(new Error("Medos Booking failed to load"));
      document.head.appendChild(script);
    });
  }

  onMount(() => {
    let cancelled = false;

    load()
      .then(() => {
        if (cancelled) return;
        medos()?.init({
          apiKey,
          mode: "inline",
          containerId: CONTAINER_ID,
        });
      })
      .catch((err) => console.error(err));

    return () => {
      cancelled = true;
      // Inline mode has no close() — clear the container so navigating back
      // doesn't mount a second widget.
      const el = document.getElementById(CONTAINER_ID);
      if (el) el.innerHTML = "";
    };
  });
</script>

<h1>Book an appointment</h1>
<div id={CONTAINER_ID} style="min-height: 600px" />

Keep the same onMount block, drop containerId, and switch to mode: "modal". Then open from a button, guarded in case the bundle is still loading:

<button on:click={() => medos()?.open()}>Book appointment</button>

On this page