Prerender with Eleventy
Eleventy can write a lit-ui-router app as a static site. Eleventy owns the files, the layout around each page and the host rules. prerender() from lit-ui-router-ssr owns everything routing decides: which paths are pages, which redirect, what the 404 page holds, and the server render of each page. The client then adopts the served page instead of rendering it again.
The example is the Hello Solar System tutorial app, built this way. Its files:
src/app.tsholds the components, the states,configure(router)andpage(router), the template both halves render.src/route-table.tsis the route table: the same names and urls as plain data, and the paths to emit.src/prerender.tsruns oneprerender()over those paths and returns the pages and rules.site/is Eleventy's input: a template per output kind, and the layout.src/client.tsboots the client withbootstrap().eleventy.config.jsbuilds both halves with Vite and runsrender.js, which callssrc/prerender.ts, before each Eleventy build.build.jsrecords the base path and starts Eleventy.
Who writes what
prerender() runs before each Eleventy build, and its result becomes global data. Its write collects each page body in memory instead of writing files, and rules: 'none' skips the _redirects file, leaving the redirect lines on the result:
const result = await prerender({
mounts,
router,
paths,
outDir: '',
rules: 'none',
dehydrate: {},
renderShell: async (_verdict, { path }) => {
await settle(router, path);
return page(router);
},
write: (file, body) => {
bodies.set(file, body);
},
});Each page in result.pages names the file it belongs at, such as planets/3/index.html or 404.html, and its verdict carries the status. From there Eleventy does the writing:
- A paginated template writes one page per entry in
result.pages, at itsfile, wrapped in the layout. - A second template writes
_redirectsfromresult.rules. A third writes a page per redirect that sends the browser on, for hosts that don't read_redirects. - The layout owns the document: the
<head>, the client script, and a<base>for a site served under a path prefix.
Vite builds the two halves Eleventy can't compile itself: the server module that renders the pages, and the client bundle Eleventy copies through. Under eleventy --serve, a save under src/ runs both builds and the render again. The render runs in a fresh Node process each time, because importing the rebuilt module again would define the app's custom elements a second time.
The page, then the client
dehydrate: {} puts each page's resolved data into its hydration signature, so the client boots on the served data instead of running the resolves again. bootstrap() then adopts the served <ui-view> and its routed component, keeping the server's nodes.
Links bind href=${srefHref(...)} rather than uiSref. The server renders attribute parts and skips element parts, so a uiSref link would be served as an <a> with no href until the client boots.
<app-root> renders into a shadow root, so the render lists LitElementRenderer beside the package's own renderers. The default leaves it out, and <app-root> would render empty.
Details
Lit's Eleventy plugin.@lit-labs/eleventy-plugin-lit renders the custom elements in each finished page after Eleventy writes it. On a page prerender() already rendered, it renders <app-root> again with no router and puts that empty render ahead of the routed one, and the browser shows the empty one. Keep the routed pages out of the plugin's reach.
One render at a time. prerender() renders pages one after another, so a build waits on every page's resolves in turn. See One render at a time.
Trailing slashes. Each page lands at <path>/index.html, which hosts serve at the slashed url. The browser router and the route table both relax strict mode, as Static hosts add a trailing slash describes.
With Vite alone. Prerender with Vite does the same from vite build, with no Eleventy.