TL;DR: Next already knows how to compile
.tsx/.jsx. Add the@knighted/jsx/loaderonly when you need the tagged-template runtime during SSR (for example, to pre-render DOM snippets, mix DOM + React output, or reuse server utilities that rely onjsx/reactJsx).
After running npm run build (which outputs dist/), alias the package and register the loader as a post-loader so it runs after SWC:
// next.config.mjs
import path from 'node:path'
const distDir = path.resolve(process.cwd(), 'dist')
export default {
webpack(config) {
config.resolve.alias = {
...(config.resolve.alias ?? {}),
'@knighted/jsx': path.join(distDir, 'index.js'),
'@knighted/jsx/react': path.join(distDir, 'react/index.js'),
}
config.module.rules.push({
test: /\.[jt]sx?$/,
enforce: 'post',
use: [{ loader: path.join(distDir, 'loader/jsx.js') }],
})
return config
},
}With the loader in place you can generate DOM fragments on the server and hydrate React components on the client using the same helpers:
// pages/index.tsx (or an app/ route)
import { jsx } from '@knighted/jsx'
import { reactJsx } from '@knighted/jsx/react'
const buildDomShell = () =>
jsx`
<section data-kind="dom-runtime">
<p>Rendered on the server</p>
</section>
`
export async function getServerSideProps() {
return { props: { domHtml: buildDomShell().outerHTML } }
}
export default function Page({ domHtml }: { domHtml: string }) {
return reactJsx`
<main>
<button type="button">React badge</button>
<div dangerouslySetInnerHTML={${{ __html: domHtml }}}></div>
</main>
`
}Tip: The same pattern works in
app/routes, API handlers, and server actions anywhere you want DOM output produced by the runtime.
This repo includes test/fixtures/next-app, which wires everything together. Build it locally to see the integration end to end:
npx next build test/fixtures/next-appOr run the automated test, which builds the app and checks the emitted HTML:
npx vitest run test/next-fixture.test.tsThat fixture doubles as a template when you want to copy the setup into your own project.