Layouts

Shared wrapper templates using TSX and components.

Right now our 11ty "page" is a JS render function. But most people use site/index.md with frontmatter that points to a "layout". Let's do that in this step. Since this is something 11ty will "see" (in the frontmatter), let's use the 11ty convention of _layouts for the directory:

mkdir _layouts

Let's rename index.11ty.tsx to _layouts/MainLayout.11ty.tsx. Then, change it to render a title from frontmatter and the Markdown contents, while keeping the use of our <Heading> component:

import { ViewProps } from "../eleventy";
import { Heading } from "../components/Heading";

export function MainLayout({ content, title }: ViewProps): JSX.Element {
	return (
		<html lang="en">
			<head>
				<title>{title}</title>
			</head>
			<body>
				<Heading name={title} />
				{content}
			</body>
		</html>
	);
}

export const render = MainLayout;

This will control the layout of all Markdown files in our site.

We made a slight change to eleventy.ts to collect the title:

export type ViewProps = {
	content: string;
	title: string;
};

Our tsconfig.json needs a slight addition, to look in this new _layouts directory:

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

Same for vitest.config.js:

import { defineConfig } from "vitest/config";

export default defineConfig({
	esbuild: {
		jsx: "transform",
		jsxInject: "import { jsx } from 'jsx-async-runtime/jsx-runtime'",
		jsxFactory: "jsx",
		jsxImportSource: "jsx-async-runtime",
	},
	test: {
		environment: "happy-dom",
		include: [
			"./components/**/*.test.tsx",
			"./site/**/*.test.tsx",
			"./_layouts/**/*.test.tsx",
		],
	},
});

Let's now move over to our test. Move site/index.test.tsx to the layout, finishing with _layouts/MainLayout.test.tsx. Then update it as follows:

import { expect, test } from "vitest";
import { renderToString } from "jsx-async-runtime";
import { MainLayout } from "./MainLayout.11ty";
import { screen } from "@testing-library/dom";
import { ViewProps } from "../eleventy";

test("render MainLayout", async () => {
	const viewProps: ViewProps = {
		content: "<p>This is <em>the body</em></p>",
		title: "My Site",
	};
	const result = MainLayout(viewProps);
	document.body.innerHTML = await renderToString(result);
	expect(screen.getByText(`Hello My Site`)).toBeTruthy();
	expect(screen.getByText(`the body`)).toBeTruthy();
});

We test the layout by passing its "slot" content in via component props. As you can see, we did a test for the <em> content.

One last configuration change. Let's update eleventy.config.ts to point at _layouts:

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",
			layouts: "../_layouts",
			output: "_site",
		},
	};
}

With that in place, we can use Markdown in our site...pointed at a layout...which points at a component. Here's site/index.md:

---
title: My Site
layout: MainLayout.11ty.tsx
---

This is a _very_ nice site.

When we re-run our build, we get the output we expect:

<!doctype html>
<html lang="en">
	<head>
		<title>My Site</title>
	</head>
	<body>
		<h1>Hello My Site</h1>
		<p>This is a <em>very</em> nice site.</p>
	</body>
</html>

Our site builds, our tests pass. We have component-driven development and the tooling that TS/TSX/testing brings. What more could we want?

Well...sitting in Vitest and working with actual builds using actual Eleventy -- the data cascade, from Markdown files on disk. That...may be the topic of another tutorial.

Conclusion

That's a whirlwind tour of adding TypeScript and TSX to your 11ty tooling. More than that, though: embracing component-driven development and testing to stay in the flow, especially when combined with the debugger.

Give it a try and let us know what you think in the comments below.