11ty Views

Interfacing the 11ty system and components.

We're doing TSX for our index.11ty.tsx 11ty template. But it doesn't really feel like a component, per-se. It feels more like a view. It mediates between incoming 11ty data and the logic for that "page".

Let's change index.11ty.tsx to be a "view":

  • The render function grabs the 11ty-specific data from the (large) 11ty API
  • A component for that page receives what it needs from the outside world in 11ty

Why this split? Testing! It's not that fun mediating with the (untyped) 11ty "bag-o-data".

We'll start with some pseudo-typing for 11ty. Make a file eleventy.ts at the root:

export type ViewProps = {
	page: {
		filePathStem: string;
	};
};

11ty has a lot more than this. Your site might have even more (custom collections, etc.) We'll keep it simple for now.

We'll change our index page at site/index.11ty.tsx to have a render function which does the mediation:

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

export type IndexProps = {
	filePathStem: string;
};

export function Index({ filePathStem }: IndexProps): JSX.Element {
	return <Heading name={filePathStem} />;
}

export function render({ page }: ViewProps): JSX.Element {
	return <Index filePathStem={page.filePathStem} />;
}

As you can see, the render function does little more than pass the massaged-data from 11ty into the component you're really interested in: this page. That is, the Index component.

Our tests in index.test.tsx can then model this. You first focus on writing something that gets passed in specific data, using tests to be more productive. Then, when done, write a test for the render function, which needs the 11ty data. We can see that split here:

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

test("renders Index component", async () => {
	const result = renderToString(<Index filePathStem="/index" />);
	document.body.innerHTML = await renderToString(result);
	expect(screen.getByText(`Hello /index`)).toBeTruthy();
});
test("render index view", async () => {
	const viewProps: ViewProps = {
		page: { filePathStem: "/index" },
	};
	// Let's do this as a call, rather than TSX, to remind
	// ourselves that this is a view, not a "component".
	const result = render(viewProps);
	document.body.innerHTML = await renderToString(result);
	expect(screen.getByText(`Hello /index`)).toBeTruthy();
});

When your view needs lots of data from across parts of the 11ty surface area, this split becomes more convenient.
Moreover, when you have a chain of layouts to format Markdown data, this mediation is more important.

Speaking of layouts... In the next step, we'll add one to our project.