Static site, enhanced by components

web.dev, the site for measuring and learning about modern web performance, is built as a static site generated from Markdown files using Eleventy. The content is fully readable without JavaScript, which keeps the site accessible to older browsers and users who disable scripts. JavaScript is only served to browsers that support type="module", including async/await and other evergreen features.

After the build process—which generates static HTML and bundles JavaScript with Rollup—the site runs on a custom Node.js server to handle redirects for invalid domains and parse a user's preferred language for an upcoming internationalization feature.

Eleventy and Markdown

Every page is written in Markdown with front matter that defines metadata like author(s), publish date, and tags. Eleventy exposes this as data to plugins and templates, along with the data cascade—information pulled from individual pages, layouts, and the folder hierarchy. Layouts can inherit from other layouts, letting different content types like posts and codelabs share one top-level HTML layout.

Eleventy’s programmatic collections power pagination and virtual pages. For example, a template with an expression for its permalink is re-rendered for every author, backed by a collection that produces pages like all of Addy’s posts.

One limitation: Eleventy is declarative, not imperative. You describe what you want, rather than how to build it, so it’s hard to run as part of a larger build tool—it can only be invoked via its command-line interface.

Nunjucks and shortcodes

Templating uses Nunjucks, which supports loops, conditionals, and shortcodes that generate HTML or invoke logic. The team has added about 20 shortcodes over time, most of which emit HTML including custom web components. An example:

{% Aside %}
See how Asides work in the web.dev codebase
{% endAside %}

It produces markup like:

<div class="aside color-state-info-text">
<p>See how Asides work in the web.dev codebase</p>
</div>

Shortcodes can also act as a metaprogramming layer: they accept arguments and can choose to return nothing, instead building up state or triggering behavior.

Progressive enhancement with components

The JavaScript bundle has two parts: bootstrap code for global state, Analytics, and single-page application (SPA) routing, plus code and CSS for Web Components that enhance the site.

The SPA model keeps global state about the user’s session—loading a new page would otherwise trigger calls to Firebase to check signed-in state. Different entrypoints load depending on the URL, using dynamic import() to reduce initial bytes.

Web Components fit a static site well: the browser manages the element lifecycle as the DOM changes, and older browsers simply ignore custom elements. Each component extends LitElement and implements callbacks like connectedCallback(), disconnectedCallback(), and attributeChangedCallback().

<web-url-chooser-container></web-url-chooser-container>
<web-lighthouse-scores-container></web-lighthouse-scores-container>

The Measure page relies most heavily on components. They follow the React-style Container Component model: a -container element connects to global state from unistore and renders a visual element that produces styled DOM nodes. These can also be referenced in regular Markdown source, so content teams can add functionality to any page.

A diagram that shows the relationship between global state and HTML elements that use it.
Global state and a Web Component

Beyond <web-sparkline-chart> and similar data visualizations, some components keep state across page navigation. Auditing a site on Measure and then leaving the page works; returning shows the task still running.