Templating in TSX

Use TSX (JSX in TypeScript) to generate HTML.

Templates in static sites are often based on JSX - an extension to JavaScript that adds HTML support to the syntax.

I get it -- JSX generates strong emotions. But you can't beat the tooling support. Using TSX (the TypeScript flavor of JSX) as a template language for 11ty is really sweet for two reasons: great tooling support and the isolation of component-driven development.

The first thing to understand: esbuild has JSX support. But it doesn't actually do JSX processing. It expects to be pointed at a JSX renderer. Most people associate that with React, Preact, or other cumbersome experiences.

But there are actually standalone JSX processors that can run in Node during build. (Or even in the browser, post-load.) We're going to use jsx-async-runtime which gives us:

  • Support TS typing
  • Supported, active
  • Lets us eliminate import h via tsconfig jsImportSource
  • It's async (unlike Preact's renderer) to allow 11ty Image

First, we install the package as a dependency:

  "dependencies": {
    "@11ty/eleventy": "3.0.0-alpha.4",
    "jsx-async-runtime": "^0.1.8"
  },

We now need to tell our tsx package (really: esbuild) how to handle TSX. You can pass command line arguments. Or, you can infer from tsconfig.json, which we'll do:

{
	"compilerOptions": {
		"module": "ESNext",
		"target": "ESNext",
		"moduleResolution": "Node",
		"skipLibCheck": true,
		"jsx": "react-jsx",
		"jsxImportSource": "jsx-async-runtime"
	},
	"exclude": ["node_modules", "_site"]
}

These two new compiler options are important:

These are confusing and brittle, especially the second part.

Let's rename our file to site/index.11ty.tsx and return JSX.Element instead of a string:

export function render(): JSX.Element {
	return <h1>Hello TSX</h1>;
}

Several interesting points. Obviously, the return clearly indicates a return type of JSX.Element.

Even better: what's missing. In our previous work with 11ty and TSX, we had to preface each file with a specific declaration of the h function via import:

import h, { JSX } from "vhtml";

This was annoying. Not the least of which: h wasn't even used in the file and showed up as an unused import. This then required extra typing to suppress the warning. With jsx-async-runtime, we don't need to import h or JSX.

If we try to build now, it will...fail. 11ty templates are supposed to return a string, not a JSX element. Let's fix that in eleventy.config.ts by using eleventyConfig.addTransform:

import { renderToString } from "jsx-async-runtime";

export default function (eleventyConfig: any) {
	eleventyConfig.addExtension(["11ty.jsx", "11ty.ts", "11ty.tsx"], {
		key: "11ty.js",
	});

	eleventyConfig.addTransform("tsx", async (content: any) => {
		const result = await renderToString(content);
		return `<!doctype html>\n${result}`;
	});

	return {
		dir: {
			input: "site",
			output: "_site",
		},
	};
}

Now when you build, you'll get this in _site/index.html:

<!DOCTYPE html>
<h1>Hello TSX</h1>

To recap what we did here:

  • Add JSX/TSX handling to tsconfig.json
  • Changed our one page/template to TSX
  • Taught 11ty to render .tsx templates from JSX.Element to a string

In the next step, we'll put our site to the test. Literally! We'll add tests to validate our components.