Skip to content

@ox-content/vite-plugin-svelte#

View Markdown

Svelte integration for Ox Content - embed Svelte 5 components in Markdown.

Installation#

vp install @ox-content/vite-plugin-svelte svelte @sveltejs/vite-plugin-svelte

Usage#

// vite.config.ts
import { defineConfig } from "vite";
import { svelte } from "@sveltejs/vite-plugin-svelte";
import { oxContentSvelte } from "@ox-content/vite-plugin-svelte";

export default defineConfig({
  plugins: [
    svelte(),
    oxContentSvelte({
      srcDir: "docs",
      // Auto-discover components with glob pattern
      components: "./src/components/*.svelte",
    }),
  ],
});

Options#

components#

  • Type: string | string[] | Record<string, string>

Components to register for use in Markdown. Supports:

// Single pattern
components: "./src/components/*.svelte";

// Multiple patterns
components: ["./src/components/*.svelte", "./src/ui/*.svelte"];

Component names are derived from file names in PascalCase:

  • counter.svelte → Counter
  • my-button.svelte → MyButton

Explicit Map#

components: {
  Counter: './src/components/Counter.svelte',
  Alert: './src/components/Alert.svelte',
}

runes#

  • Type: boolean
  • Default: true

Enable Svelte 5 Runes mode.

compiler#

  • Type: SvelteCompileFunction | { compile: SvelteCompileFunction }
  • Default: compile from svelte/compiler

Select the compiler used for Markdown and MDX modules generated by oxContentSvelte(). The configured Vite Svelte plugin still compiles ordinary .svelte files; this option keeps Ox Content-generated document modules on the same compiler path when you opt into an alternative compiler.

import { svelte } from "@rsvelte/vite-plugin-svelte";
import { compile } from "@rsvelte/vite-plugin-svelte-native";
import { oxContentSvelte } from "@ox-content/vite-plugin-svelte";

export default {
  plugins: [
    svelte(),
    oxContentSvelte({
      srcDir: "docs",
      compiler: compile,
    }),
  ],
};

Compiler warnings are forwarded to Vite, and generated JavaScript, sourcemaps, and diagnostics are normalized for both client and server document builds.

Using Components in Markdown#

Register shared components in the global components map, or import a component from the document that uses it.

# My Page

Here's an interactive counter:

<Counter initial={5} />

And an alert:

<Alert type="warning">
  This is a warning message!
</Alert>

On .mdx, a relative import is local to that file and overrides the global map when the names match:

import GtvChart from './gtv-chart/GtvChart.svelte'

<GtvChart title="ok" />

Optional renderIsland(name, props, filePath, slotHtml) can replace island inner HTML at transform time. Server-rendered islands are marked and adopted with Svelte 5 hydrate() on the client, while islands without SSR markup keep using mount(). The hook belongs on the Svelte adapter; @ox-content/vite-plugin does not import svelte/server.

Example Component (Svelte 5 Runes)#

<!-- src/components/Counter.svelte -->
<script lang="ts">
  interface Props {
    initial?: number;
  }

  let { initial = 0 }: Props = $props();
  let count = $state(initial);
</script>

<div class="counter">
  <button onclick={() => count--}>-</button>
  <span>{count}</span>
  <button onclick={() => count++}>+</button>
</div>

<style>
  .counter {
    display: inline-flex;
    gap: 8px;
    align-items: center;
  }
</style>

HTML-string custom hosts#

Custom hosts that call renderMarkdown() and place the resulting HTML inside their own Svelte page shell can use the Svelte adapter without importing each Markdown document as a Vite module. createSvelteHtmlHostRenderer() resolves document-local MDX imports, loads server modules, renders islands to HTML, and records browser module metadata for the island client. Svelte SSR head output, including <svelte:head> markup and injected scoped CSS, is returned as headHtml so the host can place it in the document <head> before client entries.

The lower-level framework-neutral contract is exported from @ox-content/vite-plugin/html-host, and the browser loader contract is exported from @ox-content/islands/html-host. The Svelte helpers keep the existing Svelte-specific names and virtual module id while delegating the shared discovery, metadata, lazy-loading, error, completion, and disposal behavior to that contract.

import { renderMarkdown, type MdxImport } from "@ox-content/vite-plugin";
import { createSvelteHtmlHostRenderer } from "@ox-content/vite-plugin-svelte";

const renderIslands = createSvelteHtmlHostRenderer({
  root: "/repo",
  srcDir: "content",
  loadModule: (moduleId) => viteDevServer.ssrLoadModule(moduleId),
});

const markdown = await renderMarkdown(source, "/repo/content/post.mdx", {
  srcDir: "content",
});
const rendered = await renderIslands(markdown.html, {
  documentPath: "/repo/content/post.mdx",
  imports: markdown.imports as MdxImport[],
  components: { Badge: "./src/components/Badge.svelte" },
});

The resolved client identity is written to each island as data-ox-module, with data-ox-export preserving named imports. Use the same module ids in the browser loader map so post-local components can share names across documents. Pass diagnostics: "collect" when a host wants structured diagnostics instead of thrown SvelteHtmlHostRenderError failures. The default server renderer owns its Svelte SSR runtime import so package-level ssr.noExternal keeps the renderer and compiled component aligned in Vite SSR. Hosts that pass renderComponent own runtime selection themselves and may load svelte/server through their host loader when that better matches their module graph.

Inside ctx.markdown.render({ renderHtml }), renderHtmlHostMarkdown() from @ox-content/vite-plugin/html-host can combine the Svelte island renderer with the Markdown context and return metadata.documentAssets for the final document shell:

const page = await ctx.markdown.render({
  source,
  documentPath: "content/post.mdx",
  renderHtml(markdown) {
    return renderHtmlHostMarkdown({
      context: markdown,
      renderIslands,
      documentAssets: { clientEntries: ["src/client.ts"] },
    });
  },
});

The browser client lives on a separate subpath: @ox-content/vite-plugin-svelte/html-host/client. It can hydrate existing SSR markup or mount fresh Svelte islands, and it keeps the runtime import lazy so custom hosts do not bundle Svelte DOM code into server-only paths.

import { initIslands } from "@ox-content/islands";
import {
  initSvelteHtmlHost,
  loadSvelteHtmlHostDomRuntime,
} from "@ox-content/vite-plugin-svelte/html-host/client";
import modules from "virtual:ox-content-svelte/html-host/modules";

initSvelteHtmlHost({
  initIslands,
  modules,
  loadRuntime: loadSvelteHtmlHostDomRuntime,
  mount: { mode: "hydrate" },
  options: { selector: ".ox-content [data-ox-island]" },
  onError(error) {
    console.error(error.message);
  },
});

For production custom hosts, generate the module map from the same selected document policy used by the site. createSvelteHtmlHostIslandRegistry() can read configured Ox Content collections, preserve published-only filtering, and serve virtual:ox-content-svelte/html-host/modules in both build and dev.

import { defineConfig } from "vite";
import { svelte } from "@sveltejs/vite-plugin-svelte";
import { oxContentCustomHost } from "@ox-content/vite-plugin";
import { createSvelteHtmlHostIslandRegistry } from "@ox-content/vite-plugin-svelte";

const oxContent = {
  srcDir: "content",
  collections: {
    blog: "blog/**/*.mdx",
  },
};

const svelteIslands = createSvelteHtmlHostIslandRegistry({
  oxContent,
  collectionDocuments: {
    select(document, context) {
      return context.command === "serve" || document.frontmatter.published === true;
    },
  },
});

export default defineConfig({
  appType: "custom",
  plugins: [
    svelteIslands.plugin,
    oxContentCustomHost({ host: "./src/site-host.ts", oxContent }),
    svelte(),
  ],
});

SSR stylesheets for custom hosts#

Svelte page and layout modules can be listed in oxContentCustomHost({ ssrStylesheets }) so the host can emit blocking styles for server-rendered output.

const ssr = ctx.assets.ssrStylesheets({
  modules: ["/src/Page.svelte"],
});
const content = await ctx.assets.stylesheetContent({
  stylesheets: ssr.stylesheets,
});
const assets = ctx.assets.document({
  islandStyles: ssr.stylesheets,
  inlineStyles: content.stylesheets.map((stylesheet) => ({
    key: `critical:${stylesheet.href}`,
    content: stylesheet.content,
  })),
});

Production builds use the configured Vite/Svelte pipeline and emit hashed CSS assets. Development traverses Svelte SSR roots and local Svelte children, deduplicates discovered style blocks per route, and returns dependency paths so custom-host responses are invalidated after component edits.

Virtual Modules#

  • virtual:ox-content-svelte/runtime - Svelte-specific runtime
  • virtual:ox-content-svelte/components - Registered components
  • virtual:ox-content-svelte/html-host/modules - Custom-host island module map
import { mount, unmount } from "virtual:ox-content-svelte/runtime";
import components from "virtual:ox-content-svelte/components";

HMR#

Components are hot-reloaded when modified. Svelte's built-in HMR is supported.

Svelte 5 Features#

This plugin is designed for Svelte 5 and supports:

  • Runes: $state, $derived, $effect, $props
  • Snippets: {#snippet} and {@render}
  • New event syntax: onclick instead of on:click

Last updated: