Web Component & Shadow DOM Support
Relevant source files
The following files were used as context for generating this wiki page:
Rendertron provides rendering for pages that use Web Components. Since headless Chrome's serialization of native Shadow DOM can be ineffective for SEO bots, Rendertron's strategy is to enforce the use of the Shady DOM polyfill. This ensures that content within a component's shadow root is properly serialized into the final HTML output.
This is achieved by injecting polyfill-forcing scripts into the page context before the page's own scripts execute. Users can trigger this behavior by passing a specific query parameter in the render request, which is often handled automatically by the official Express.js middleware.
Sources: README.md:100-108, src/renderer.ts:90-92
Polyfill Injection Mechanism
The core of Web Component support lies within the Renderer class, which uses Puppeteer to control the headless browser. Before navigating to the target URL, Rendertron injects several scripts into the page's context using page.evaluateOnNewDocument. This ensures that the polyfills are activated before any of the page's application code runs.
Sources: src/renderer.ts:88-92
Injected Polyfill Configuration
The following scripts are injected to force Shady DOM and Shady CSS to be used, even if the browser has native support for Shadow DOM:
// src/renderer.ts:90-92
page.evaluateOnNewDocument('customElements.forcePolyfill = true');
page.evaluateOnNewDocument('ShadyDOM = {force: true}');
page.evaluateOnNewDocument('ShadyCSS = {shimcssproperties: true}');
customElements.forcePolyfill = true: Forces the custom elements polyfill to be used.ShadyDOM = {force: true}: This is the key instruction that enables the Shady DOM polyfill, which makes shadow roots inspectable and serializable as regular DOM.ShadyCSS = {shimcssproperties: true}: Ensures that CSS Custom Properties are correctly shimmed for styling within the polyfilled shadow DOM.
Sources: src/renderer.ts:90-92
Rendering Flow
The diagram below illustrates the sequence of events when a request is made to render a page with Web Components.
This diagram shows how the middleware adds the wc-inject-shadydom parameter, which signals to Rendertron to perform the polyfill injection before rendering the page.
Sources: middleware/README.md:28-29, src/renderer.ts:88-92, middleware/src/test/middleware-test.ts:83-88
Configuration and Usage
To enable Web Component rendering, the service needs to be instructed to inject the Shady DOM polyfills. This is typically done via a URL query parameter.
| Parameter | Description | Example |
|---|---|---|
wc-inject-shadydom | When set to true (or present), Rendertron forces the Shady DOM polyfills to be loaded and enabled. This is necessary for pages using Web Components v1 with webcomponents-lite.js or webcomponents-loader.js. | /render/https://my-site.com/page?wc-inject-shadydom=true |
injectShadyDom | A boolean option in the Express middleware that, when true, automatically adds the wc-inject-shadydom=true parameter to requests proxied to the Rendertron service. | rendertron.makeMiddleware({ proxyUrl: '...', injectShadyDom: true }) |
Sources: README.md:106-108, middleware/README.md:28-29, src/test/app-test.ts:58-61
The following flowchart details the logic within Rendertron when it receives a render request.
Note: The wc-inject-shadydom parameter is checked by the middleware, not the core renderer. The renderer injects the polyfill scripts for every request, relying on the target page's polyfill loader (webcomponents-loader.js) to use them.
Sources: src/renderer.ts:88-103, middleware/src/test/middleware-test.ts:83-88
Testing Scenarios
The project includes several tests to validate the Web Component rendering functionality. These tests confirm that content within a custom element's shadow root is present in the serialized output when polyfills are correctly applied.
| Test File | Description | Expected Outcome |
|---|---|---|
shadow-dom-polyfill-all.html | Uses webcomponents-lite.js, which includes all polyfills. | The text "shadow-root-text" is successfully rendered and found in the output. |
shadow-dom-polyfill-loader.html | Uses webcomponents-loader.js, which dynamically loads necessary polyfills. | The text "shadow-root-text" is successfully rendered. |
shadow-dom-no-polyfill.html | A page with a custom element and shadow DOM but no polyfill script. | This test is marked as failing, indicating that without polyfills, the shadow DOM content is not correctly serialized. |
Sources: src/test/app-test.ts:50-76, test-resources/shadow-dom-polyfill-all.html, test-resources/shadow-dom-polyfill-loader.html, test-resources/shadow-dom-no-polyfill.html
Here is an example test case from app-test.ts:
// src/test/app-test.ts:64-70
test('renders shadow DOM - webcomponents-lite.js polyfill', async (t) => {
const res = await server.get(`/render/${
testBase}shadow-dom-polyfill-all.html?wc-inject-shadydom=true`);
t.is(res.status, 200);
t.true(res.text.indexOf('shadow-root-text') !== -1);
});
This test verifies that a page using webcomponents-lite.js is correctly rendered when the wc-inject-shadydom flag is active.
Sources: src/test/app-test.ts:64-70
Summary
Rendertron's support for Web Components is robust but opinionated. It mandates the use of Shady DOM polyfills to guarantee that dynamically generated content inside shadow roots is captured in the final static HTML. This is accomplished by preemptively injecting scripts that force the polyfills to activate. For developers, this means ensuring their PWA includes a Web Components polyfill loader and that Rendertron is configured (usually via middleware) to enable the injection logic.