Greenwood v0.30.0

Published: Nov 2nd, 2024

What's New

In support of this latest release the Greenwood team would like to share some highlights with you, and top of the list for us is Import Attributes. Import Attributes are a powerful new web standard for loading non JavaScript files like CSS and JSON just by extending the standard ESM syntax. They work great with Web Components, or standalone. In addition, we want to showcase the HTML Web Components pattern, as well as introduce a new plugin to support CSS Modules ™️.

Please refer to the release notes for a complete changelog and overview of breaking changes.

Import Attributes

Although not implemented in all browsers (yet), Import Attributes are a Stage 4 TC39 proposal for extending ESM syntax to support additional module formats. CSS and JSON modules are already on the standards track, with HTML Modules possible in the future. For Greenwood, we have made support for CSS and JSON possible out of the box, with a polyfill config flag for providing fallback behavior in the interim.

// JSON is exported as an object
import data from "./data.json" with { type: "json" };

// CSS is exported as a Constructable StyleSheet
import sheet from "./styles" with { type: "css" };

For CSS, this works especially well with Shadow DOM based custom elements, as you can now author component styles in vanilla CSS files, and "adopt" them right into your Shadow roots. This same adoption technique can also be used to include any global "light DOM" styles, as you can adopt those too! So now you can finally share a global CSS theme file across both Light and Shadow DOM. Web standards ftw!

import themeSheet from "../theme.css" with { type: "css" };
import componentSheet from "./header.css" with { type: "css" };

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

template.innerHTML = `
  <header><!-- content goes here --></header>
`;

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

    this.shadowRoot.adoptedStyleSheets = [themeSheet, componentSheet];
  }
}

customElements.define("x-header", Header);

In the case of CSS Module Scripts, Greenwood will handle linking any CSS files used in an Import Attribute and also used in a <link> tag in your HTML (like our theme.css in the example above), ensuring a single source of truth for those contents across both your HTML and your JS.

HTML Web Components

As detailed in this excellent blog post, HTML Web Components are a strategy for leaning more into a less JavaScript dependent flavor of custom elements. Instead of (or in addition to) setting attributes, which would require JavaScript to do anything meaningful with from a content perspective, this options favors nesting the content as HTML. In this way, the custom element can be a nice styling wrapper or progressively enhanced experience on top. This also provides the benefit of playing nicely with CSS, which is global by default, as styling is not restricted by the encapsulation of the Shadow DOM. (think of it as a Light DOM <slot>, if you will.)

So instead of setting attributes:

<picture-frame url="/path/to/image.png" title="My Image"></picture-frame>

You would want to "pass" HTML as children instead:

<picture-frame>
  <h3>My Image<h3>
  <img src="/path/to/image.png" alt="My Image">
</picture-frame>

With a custom element definition like so:

// CSS can be referenced from HTML, utility libraries, etc
export default class PictureFrame extends HTMLElement {
  connectedCallback() {
    this.innerHTML = `
      <div class="picture-frame">
        ${this.innerHTML}
      </div>
    `;
  }
}

customElements.define("picture-frame", PictureFrame);

This pattern works great when combined with Greenwood's prerendering and static optimization settings for a no-runtime, JS based, SSG build pipeline. Or combine with our CSS Modules ™️ plugin. What's our CSS Modules plugin, you ask? Keep reading. 👇

CSS Modules

We are excited to share a new plugin we made to support CSS Modules ™️. NOT to be confused with CSS Module scripts, which we covered at the start of this post, this plugin aims to be a modest implementation of the specification. 🙂

With this plugin, create a standard CSS file that ends in .module.css:

/* header.module.css */
.container {
  display: flex;
  justify-content: space-between;
}

.navBarMenu {
  border: 1px solid #020202;
}

.navBarMenuItem {
  & a {
    text-decoration: none;
    color: #020202;
  }
}

@media screen and (min-width: 768px) {
  .container {
    padding: 10px 20px;
  }
}

And reference that in your Light DOM based custom element:

// header.js
import styles from "./header.module.css";

export default class Header extends HTMLElement {
  connectedCallback() {
    this.innerHTML = `
      <header class="${styles.container}">
        <ul class="${styles.navBarMenu}">
          <li class="${styles.navBarMenuItem}">
            <a href="/about/" title="Documentation">About</a>
          </li>
          <li class="${styles.navBarMenuItem}">
            <a href="/contact/" title="Guides">Contact</a>
          </li>
        </ul>
      </header>
    `;
  }
}

customElements.define("x-header", Header);

From there, Greenwood will scope your CSS class names by prefixing them with the filename and a hash, and will then inline that content into a <style> tag in your HTML. It will also strip out the reference to the module.css file from your calling file.

This plugin works great when combined with Greenwood's prerendering and static optimization settings for JS based static HTML generation featuring scoped styles with no JS overhead.

Honorable Mentions

The above highlights are by no means the only features delivered in this release, and so let's take a moment to check out some of the other capabilities and enhancements included in this release.

Init Refresh

We gave the initial scaffolding output when running our @greenwood/init command a refresh. It now comes with some useful starter code and links to the website for common documentation resources.

Greenwood Init Refresh

Try it out today!

$ npx @greenwood/init@latest my-app

Lit SSR Enhancements

In this release, we upgraded our Lit SSR plugin to support Lit v3 and addressed some bugs with our existing implementation. Checkout out the plugin docs for more info and check out our demo repo to see it all in action.

Content as Data

We took on a pretty significant overhaul of the feature set related to leveraging the content of your Greenwood project programmatically as data, for creating things like navigation menus, content lists, and more. As of this release, we have deprecated our "menu" feature and replaced it with a more useful and general purpose "Collections" feature (that acts the same in principal), introduced a data client, and made enhancements to page data and frontmatter. Check out the full docs to learn all about what's new.


As always, we're excited to see where the community can take Greenwood and are always available to chat on GitHub or Discord. See you for the next release! ✌️