@ox-content/vite-plugin-solid#
Solid integration for Ox Content - embed Solid components in Markdown.
Installation#
vp install @ox-content/vite-plugin-solid solid-js@next @solidjs/web@next @solidjs/vite-plugin
This 3.x beta adapter targets Solid 2 and @solidjs/vite-plugin. It does not
preserve the Solid 1 + vite-plugin-solid peer dependency path; keep the older
adapter release if your app has to stay on Solid 1.
Usage#
// vite.config.ts
import { defineConfig } from "vite";
import solid from "@solidjs/vite-plugin";
import { oxContentSolid } from "@ox-content/vite-plugin-solid";
export default defineConfig({
plugins: [
oxContentSolid({
srcDir: "docs",
// Auto-discover components with glob pattern
components: "./src/components/*.tsx",
}),
solid({ extensions: [".md", ".markdown", ".mdx"], compiler: "native" }),
],
});
Plugin Order and extensions#
Unlike the Vue, React, and Svelte integrations, this plugin has two setup rules
that are not optional. Both follow from the same fact: Solid's JSX is
compile-time only. There is no runtime element factory like React's
createElement or Vue's h to fall back on, so Markdown is emitted as Solid
JSX and @solidjs/vite-plugin is what turns it into DOM or SSR instructions.
oxContentSolid()must come beforesolid()in thepluginsarray. Both plugins areenforce: "pre", so array order decides which one sees the Markdown file first. Ifsolid()runs first, the Solid compiler sees raw Markdown instead of the generated JSX.solid()must be given the Markdown extensions. Usecompiler: "native"for the Solid 2 native OXC compiler path. Without the extensions, generated Markdown modules would be handed to the browser as uncompiled JSX.
Both mistakes are checked for you and reported with the fix — see
verifySolidPlugin.
Options#
components#
- Type:
string | string[] | Record<string, string>
Components to register for use in Markdown. Supports:
Glob Pattern (Recommended)#
// Single pattern
components: "./src/components/*.tsx";
// Multiple patterns
components: ["./src/components/*.tsx", "./src/ui/*.tsx"];
Component names are derived from file names in PascalCase:
counter.tsx→Countermy-button.tsx→MyButton
Explicit Map#
components: {
Counter: './src/components/Counter.tsx',
Alert: './src/components/Alert.tsx',
}
verifySolidPlugin#
- Type:
boolean - Default:
true
Fail fast on the two setup mistakes described above instead of letting them surface as an unrelated syntax error.
The plugin check runs when the config resolves; the extensions check runs the
first time a Markdown module comes out of the pipeline still uncompiled. Both
throw a message naming the fix.
Turn it off when Solid's JSX is compiled by something other than
@solidjs/vite-plugin.
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 start={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.tsx'
<GtvChart title="ok" />
Optional renderIsland(name, props, filePath) can replace island inner HTML
at transform time. The hook belongs on the Solid adapter; @ox-content/vite-plugin
does not import @solidjs/web for SSR.
Example Component#
// src/components/Counter.tsx
import { createSignal } from "solid-js";
export default function Counter(props: { start?: number }) {
const [count, setCount] = createSignal(props.start ?? 0);
return (
<div class="counter">
<button onClick={() => setCount(count() - 1)}>-</button>
<span>{count()}</span>
<button onClick={() => setCount(count() + 1)}>+</button>
</div>
);
}
Note that props is not destructured. Solid props are a reactive proxy, and
destructuring them reads every value once at setup time, which is what breaks
reactivity in components ported from React.
Virtual Modules#
virtual:ox-content-solid/components- Registered components
import components from "virtual:ox-content-solid/components";
There is no virtual:ox-content-solid/runtime counterpart to the Svelte
integration's: Solid mounts through render from @solidjs/web, which the
generated modules import directly.
Islands#
Markdown that uses a registered or document-imported component is emitted with
island markers and hydrated through @ox-content/islands, the same runtime the
other framework integrations use. Each island is mounted with render from
@solidjs/web and disposed when the Markdown component unmounts.
Markdown without any registered or document-imported component skips the island
runtime entirely and compiles to a single innerHTML binding.
HTML-string custom hosts#
Hosts that call renderMarkdown() and then place the returned HTML inside their
own Solid page shell can use the Solid adapter without importing each Markdown
document as a Vite module. renderSolidHtmlHost() resolves document-local MDX
imports, loads the matching server modules through a host-supplied loader, and
replaces island bodies with Solid SSR HTML.
import { renderSolidHtmlHost, type MdxImport } from "@ox-content/vite-plugin-solid";
const imports: MdxImport[] = [
{ source: "./Chart.tsx", specifiers: [{ imported: "default", local: "Chart", kind: "default" }] },
];
const rendered = await renderSolidHtmlHost({
html: markdown.html,
documentPath: "/repo/docs/report.mdx",
root: "/repo",
srcDir: "docs",
imports,
components: { Badge: "./src/components/Badge.tsx" },
loadModule: (moduleId) => viteDevServer.ssrLoadModule(moduleId),
resolveClientModule: (module) =>
module.source === "document" ? `./docs/${module.name}.tsx` : `./components/${module.name}.tsx`,
});
The module cache is scoped to one render call, so development edits should
trigger a fresh call after the host invalidates its page state.
resolveClientModule() writes the returned identity to each island as
data-ox-module, along with data-ox-export. Use the same keys in the browser
loader map so two documents can reuse a local component name without downstream
HTML replacement.
Diagnostics report missing components, module load failures, missing exports, SSR errors, and unsupported document-local import forms with document/component context. The supported document-local forms are the same ones the Markdown-module adapter understands: default imports and named imports with local bindings.
The browser client lives on a separate subpath so custom hosts can bundle it
without importing Vite, Node helpers, or native optional dependencies:
@ox-content/vite-plugin-solid/html-host/client. It is a fresh-mount bridge,
not Solid hydration. It reads the existing Ox Content island payload and authored
slot HTML, clears the target, and lets the caller mount with @solidjs/web.
When SSR produced HTML for a self-closing island, that rendered output is not
passed back as children.
For production custom hosts, generate the module map from the same site-selected document set instead of a broad source-directory glob. Keep the publication rule in your site code and share that selected list with both the custom host routes and the registry plugin.
// vite.config.ts
import { defineConfig } from "vite";
import solid from "@solidjs/vite-plugin";
import { oxContentCustomHost } from "@ox-content/vite-plugin";
import { createSolidHtmlHostIslandRegistry } from "@ox-content/vite-plugin-solid";
import { selectedDocuments } from "./src/site-content";
const solidIslands = createSolidHtmlHostIslandRegistry({
documents: async () =>
(await selectedDocuments()).map((document) => ({
documentPath: document.file,
source: document.source,
})),
});
export default defineConfig({
appType: "custom",
plugins: [
solidIslands.plugin,
oxContentCustomHost({ host: "./src/site-host.ts" }),
solid({ compiler: "native" }),
],
});
// src/site-host.ts
import { renderMarkdown, type MdxImport } from "@ox-content/vite-plugin";
import { renderSolidHtmlHost, toSolidHtmlHostClientModuleId } from "@ox-content/vite-plugin-solid";
import { selectedDocuments } from "./site-content";
export default {
async routes() {
return (await selectedDocuments()).map((document) => ({
path: document.url,
inputPath: document.file,
source: document.source,
async render(ctx) {
const markdown = await renderMarkdown(document.source, document.file, {
srcDir: "content",
});
const rendered = await renderSolidHtmlHost({
html: markdown.html,
imports: markdown.imports as MdxImport[],
documentPath: document.file,
root: ctx.root,
srcDir: "content",
loadModule: ctx.loadModule,
resolveClientModule: (module) =>
toSolidHtmlHostClientModuleId(module.serverModuleId, ctx.root),
});
return { html: rendered.html, source: document.source };
},
}));
},
};
The browser imports the generated virtual module. Its static dynamic-import roots are the selected island modules only, so Vite does not create entry chunks for unpublished documents that were omitted by your site policy. Dependencies reachable from those selected modules are still bundled normally.
import { initIslands } from "@ox-content/islands";
import { render } from "@solidjs/web";
import { initSolidHtmlHost } from "@ox-content/vite-plugin-solid/html-host/client";
import modules from "virtual:ox-content-solid/html-host/modules";
initSolidHtmlHost({
initIslands,
modules,
render({ component: Component, props, element, slotHtml }) {
const dispose = render(
() =>
slotHtml ? (
<Component {...props}>
<div innerHTML={slotHtml} />
</Component>
) : (
<Component {...props} />
),
element,
);
return dispose;
},
onError(error) {
console.error(error.message);
},
options: { selector: ".ox-content [data-ox-island]" },
});
initSolidHtmlHost() calls the initIslands() function supplied by the host and
returns its controller. If you already manage the island runtime yourself, use
createSolidHtmlHostLazyHydrate() from the same subpath and pass the returned
synchronous hydrate function to initIslands(). The adapter keeps pending lazy
imports cancellable: disposal before a module resolves prevents stale mount,
mounted cleanup runs exactly once, and unknown module, loader, runtime, export,
and render failures are reported to onError.
Island stylesheets for custom hosts#
Server-rendered islands often need their CSS before the client module mounts.
When you use oxContentCustomHost(), prefer the public host assets context so
the host never touches a Vite manifest or development module graph directly.
const styles = ctx.assets.stylesheets({
modules: rendered.clientModules.map((module) => module.moduleId),
});
const assets = ctx.assets.document({
islandStyles: styles.stylesheets,
clientEntries: ["src/main.ts"],
});
return {
html: `<!doctype html><html><head>${assets.headHtml}</head><body>${rendered.html}</body></html>`,
dependencies: styles.dependencies,
};
The assets context resolves direct and transitive island CSS before the client
module script, deduplicates through ctx.assets.document(), preserves dev CSS
query strings, and returns dependency paths that keep cached custom-host routes
fresh after stylesheet edits. Build mode uses the Vite manifest behind the same
method and returns emitted hashed hrefs for the same module identities. The
lower-level resolveSolidIslandStylesheets() helper remains available for
non-custom hosts that intentionally own the manifest or module graph.
HMR#
Components are hot-reloaded when modified. Markdown modules that use a changed component are invalidated alongside it.
Rust and N-API Codegen#
The Rust renderer can also emit Solid code directly from rendered Markdown HTML, without the Vite pipeline:
import { renderFrameworkComponentCode } from "@ox-content/napi";
renderFrameworkComponentCode("<p>Hello</p>", "solid", [], "component");
That path targets solid-js/h, Solid's hyperscript entry point, because it has
to produce code that runs without the JSX compiler. The Vite plugin emits JSX
instead, which compiles to faster, finer-grained output.