Greenwood v0.26.0

Published: July 26, 2022

What's New

After a lot of hard work, the Greenwood team is eager to share our first round of enhancements related to our SSR work. By fully leaning into Web Components as a standard API for server rendered pages, we have finally realized something we've been chasing since the early days of the project; all made possible through a new library we've started developing called Web Components Compiler (WCC)! 📣

WCC logo

Custom Elements as Pages

The most significant change in this release is how Greenwood handles server rendering by default. Instead of spinning up a (headless) browser with Puppeteer, WCC now provides the ability to deliver on what we think is a really nice and familiar developer experience for authoring server rendered content. We think custom elements fit right at home in providing a consistent and standards based solution for authoring pages, just as easily as they do for components.

Here is an example of what authoring an SSR page in Greenwood looks like now if using the new export default API.

// src/pages/artists.js
import fetch from "node-fetch";
import "../components/card.js";

export default class ArtistsPage extends HTMLElement {
  async connectedCallback() {
    const artists = await fetch("https://.../api/artists").then((resp) => resp.json());
    const html = artists
      .map((artist) => {
        const { name, imageUrl } = artist;

        return `
        <wc-card>
          <h2 slot="title">${name}</h2>
          <img slot="image" src="${imageUrl}" alt="Picture of ${name}"/>
        </wc-card>
      `;
      })
      .join("");

    this.innerHTML = `
      <h1>List of Artists: ${artists.length}</h1>
      ${html}
    `;
  }
}

Note: In this example, Greenwood will not ship any JS for this page. All the HTML is extracted at build / request time from the custom element. 💯

Since WCC is un-opinionated in how you author your custom elements, you will notice from the above snippet that there is no usage of Shadow DOM. This is intentional as this page content is intended for the Light DOM. The goal here is to allow users to opt-in as needed where it makes sense, because not everything needs the tight encapsulation of a Shadow Root.

However, the <wc-card></wc-card> totally can opt-in to (Declarative) Shadow DOM as you can see through its component definition, and its usage of <slot>s. It all works the same!

const template = document.createElement("template");

template.innerHTML = `
  <style>
    :host {
      display: block;
      width: 80%;
      margin: 50px auto!important;
      text-align: center;
    }

    [name="title"] {
      color: red;
    }

    ::slotted(img) {
      max-width: 500px;
    }

    hr {
      border-top: 1px solid var(--color-accent);
    }
  </style>

  <div class="card">
    <slot name="title">My default title</slot>
    <slot name="image"></slot>
  </div>

  <hr/>
`;

export default class Card extends HTMLElement {
  connectedCallback() {
    if (!this.shadowRoot) {
      this.attachShadow({ mode: "open" });
      this.shadowRoot.appendChild(template.content.cloneNode(true));
    }
  }
}

customElements.define("wc-card", Card);

WCC

In keeping with the spirit of Project Evergreen, the team wanted to keep things as close to the vest as possible. Although Greenwood does support Lit as an SSR solution, we wanted to make sure that it could be just as easy to author native HTMLElement custom elements, and refine that developer experience for the benefit of the community.

For those curious, let's take a quick peek under the hood to see how it works.

  1. Write a Web Component

    const template = document.createElement("template");
    
    template.innerHTML = `
      <style>
        .footer {
          color: white;
          background-color: #192a27;
        }
      </style>
    
      <footer class="footer">
        <h4>My Blog &copy; ${new Date().getFullYear()}</h4>
      </footer>
    `;
    
    class Footer extends HTMLElement {
      connectedCallback() {
        if (!this.shadowRoot) {
          this.attachShadow({ mode: "open" });
          this.shadowRoot.appendChild(template.content.cloneNode(true));
        }
      }
    }
    
    export default Footer;
    
    customElements.define("wcc-footer", Footer);
    
  2. Run it through the compiler

    import { renderToString } from "wc-compiler";
    
    const { html } = await renderToString(new URL("./path/to/component.js", import.meta.url));
    
    console.log(html);
    
  3. Get HTML!

    <wcc-footer>
      <template shadowrootmode="open">
        <style>
          .footer {
            color: white;
            background-color: #192a27;
          }
        </style>
    
        <footer class="footer">
          <h4>My Blog &copy; 2022</h4>
        </footer>
      </template>
    </wcc-footer>
    

Web Components Compiler is designed to make the writing and rendering of native web components as easy as possible, and has serverless and the edge in mind as first party runtimes, which we plan to support in Greenwood very soon. It is an opportunity to explore the web and ideate on shared goals and objectives. It also has no opinions on Light vs. Shadow DOM, recognizing that not one size fits all.

WCC also has no opinion on framework. We've even made a plugin for 11ty you can try! 🎈

And so now has come the time when Greenwood can transition off of Puppeteer and continue to live up to its ideal of becoming leaner over time and staying true to being your workbench for the web.

Puppeteer Plugin

🛑 This is a breaking change 🛑

OK, so by now with all this talk of WCC and internalized SSR support for web components, we should talk about Puppeteer. One thing the Greenwood team recognizes is that there are some things having access to an entire (headless) browser can provide, and in some cases, certain features still depend on it (for now).

So although WCC is now the default for SSR, Puppeteer is still available as a plugin that can be installed after you make the upgrade to v0.26.0. The upgrade should be quick and work the same as it did before. Just follow these steps.

  1. Install the Puppeteer renderer plugin

    $ npm install @greenwood/plugin-renderer-puppeteer --save-dev
    
  2. Add the plugin to your greenwood.config.js. You can also remove prerender: true.

    import { greenwoodPluginRendererPuppeteer } from "@greenwood/plugin-renderer-puppeteer";
    
    export default {
      plugins: [greenwoodPluginRendererPuppeteer()],
    };
    
  3. You can also delete the puppeteer package from your package.json.

That's it!

Learn More

That was a lot of info and a lot of new things to look forward to when building your next Greenwood project. We're very eager to continue exploring where WCC can go to really continue to enhance the authoring experience for native custom elements, and what we can be accomplished with Greenwood + WCC running at the edge!

For more information, check out these links:

Thanks for reading!