@tuqulore-inc/eleventy-plugin-preact
This page covers the internal design of @tuqulore-inc/eleventy-plugin-preact, the plugin that treats JSX, TSX, and MDX as Eleventy templates. For the API reference and installation, see the package README. For day-to-day writing conventions, see Preset Conventions / Templates.
What it renders on the server
The plugin registers .jsx, .tsx, and .mdx as Eleventy template formats and renders all of them through Preact's server-side renderer. Templates are loaded via Node.js custom loaders, so the files themselves behave like normal ES modules. .ts is not registered as a template format, but the same loader handles it so templates and components can import .ts utilities on the SSR side.
The plugin stops here. Everything client-side — bundling, is-land wiring, hydration — is the job of eleventy-plugin-preact-island. This plugin only produces HTML.
Why Preact
The boilerplate uses the same UI runtime for SSR and Partial Hydration. React, Preact, Solid, and Svelte were all on the table; Preact wins for the following reasons.
- The bundle is small enough to serve from a CDN like esm.sh. Browsers resolve
preactthrough an import map, keeping the client bundle minimal. - Its React-compatible API means JSX and
useState/useEffectwork as expected. - SSR is handled by
preact-render-to-stringand needs no runtime server — everything runs inside the Eleventy build. @11ty/is-land's init hook composes cleanly with Preact, so the Partial Hydration layer stays thin.
If you need a different runtime, the plan is to fork the preset rather than support multiple runtimes side by side. A single preset does not try to be neutral about which UI library it renders with.
Why MDX
If pages were written in Markdown only, dropping a dynamic component (an Island) would require another template language and another file. If pages were written in JSX only, headings and lists would become verbose.
MDX lets you write roughly 80% Markdown and 20% JSX in one file. Docs, articles, and landing pages all fit the same authoring model — that is the trade-off this plugin optimizes for.
A Rust transform layer
Once Preact and MDX were set on the UI side, the remaining decision was what to put in the transform layer that Node loads them through. This plugin uses oxc-transform for JSX/TSX/TS transpilation and Sätteri for MDX compilation. Both are written in Rust and exposed to Node.js as native bindings via napi.
JSX/TSX/TS used to go through @babel/core with @babel/preset-react. Babel carries a heavy dependency tree, and its throughput does not scale well in an SSR loader that transforms one file at a time. oxc-transform never crosses a process boundary — it is a plain function call — and finishes the same workload in a fraction of the time. The plugin pins the JSX runtime to runtime: "automatic", importSource: "preact" and always passes those options, which is also why the Eleventy-recommended tsx wrapper (esbuild underneath) was not chosen. tsx reads the user's tsconfig.json, and depending on allowJs it can transpile .jsx and .tsx under different JSX runtimes in the same project. Fixing the options in the plugin removes that inconsistency by construction.
MDX used to go through @mdx-js/node-loader into @mdx-js/mdx with a stack of remark plugins for GFM, frontmatter, and heading anchors. The unified pipeline walks the syntax tree once per plugin, so latency grows with every plugin added. Sätteri builds the tree once on the Rust side and only hands JS plugins the nodes that pass a tag-name filter. With the same feature set, MDX compilation ends up an order of magnitude faster, and remark-gfm / remark-frontmatter / remark-mdx-frontmatter / rehype-slug / rehype-autolink-headings disappear from the dependency graph. GFM and YAML frontmatter come built in; the plugin only ships a thin HAST plugin that assigns heading ids.
The upshot is that .tsx and .ts now work on the SSR side, so the earlier asymmetry — .client.tsx bundled for the client but unreadable during SSR — is gone. .jsx, .tsx, .ts, and .mdx all go through the same loader chain. MDX page data now flows through the built-in YAML frontmatter that Sätteri already parses, while .jsx / .tsx keep the export const data = { ... } shape because JSX parsing rules do not allow a --- fence. Details live in Preset Conventions / Templates.
The eleventy singleton
The plugin exposes a singleton, valid during SSR, that any template or partial can import to read page data and global data without prop drilling.
See Preset Conventions / Data Access for the shape of the singleton and how to use it. Internally, the plugin swaps the singleton per template render, so parallel async rendering stays consistent.
Layout chaining
Eleventy's Layout Chaining is handled transparently by the MDX loader. The parent layout receives the child's rendered HTML through eleventy.content and injects it via dangerouslySetInnerHTML.
The writing side of layout chaining, page data, and the split between MDX and JSX are all covered in Preset Conventions / Templates.
Using it without the preset
@tuqulore-inc/eleventy-plugin-preact is installable on its own. If you have an existing Eleventy project and only want JSX/MDX SSR, this is a valid path.
Note that Partial Hydration is not included in that setup — using <Island> will fail to resolve. To combine both, either register @tuqulore-inc/eleventy-plugin-preact-island alongside this one, or use the preset. See the package README for details.