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.