Greenwood v0.34.0

Published: July 13th, 2026

Brackets with database logo

What's New

The team is excited to bring you a new Greenwood release and we would like to share with you a few of the key features available in this release. These changes include dynamic routing capabilities for SSR and SSG use cases, improvements to WCC (including a brand new website (opens in a new window) ✨), and a round of enhancements to Greenwood's Lit SSR integration. Along with this set of enhancements, we addressed some bug fixes and continued our effort to improve ecosystem compatibility.

Please refer to the release notes (opens in a new window) for the complete changelog and overview of breaking changes.

Dynamic Routing

Greenwood now supports dynamic routing for SSR pages, API routes and SSG pre-rendering; allowing a single file to serve multiple routes or emit multiple static HTML files. This is achieved by wrapping the file name in brackets, e.g. [slug].js, with the value available in the params object of the constructor props or context passed in.

The below example would serve all routes matching /blog/<slug>/:

import { getPostBySlug } from '../../services/posts.js';

export default class BlogPostPage extends HTMLElement {
  #slug;

  constructor({ params }) {
    super();
    this.#slug = params.slug;
  }

  async connectedCallback() {
    const post = await getPostBySlug(this.#slug);

    this.innerHTML = `
      <body>
        <h1>${post.title}</h1>
        <p>${post.content}</p>
      </body>
    `;
  }
}

Building on top of Dynamic Routing, you can opt to generate all your dynamic SSR routes as static HTML instead, using getStaticPaths and optionally getStaticParams. For getStaticPaths, an array of objects must be returned with at least one property matching the value inside the brackets. getStaticParams can be used to augment the properties within the params object passed in via constructor props.

We can take the following example, which will generate N number of static HTML files for each item returned by getStaticPaths:

import { getProducts } from '../../services/products.js'

export async function getStaticPaths() {
  const products = await getProducts();

  return products.map((product) => {
    return {
      // return as many other params as you want
      params: {
        id: product.id,
        name: product.name,
      },
    };
  });
}

export async function getStaticParams({ params }) {
  const products = await getProducts();
  const product = products.find((product) => product.id === params.id);

  return { product };
}

export default class ProductPage extends HTMLElement {
  #product;

  constructor({ params }) {
    super();
    this.#product = params.product;
  }

  connectedCallback() {
    this.innerHTML = `
      <body>
        <h2>${this.#product.name}</h2>
        <p>${this.#product.description}</p>
      </body>
    `;
  }
}

Check out some example repos of this feature in action:

WCC Enhancements

Greenwood has had a plugin that makes it easy to leverage WCC's experimental JSX syntax (opens in a new window) when authoring native Web Components and SSR pages, and now includes support for the new and (very!) experimental capability of using TC39 Signals for reactivity (opens in a new window).

After installing the plugin, you can author native custom elements with a render function that returns JSX. Very useful for templating, with support for basic reactivity.

export default class Counter extends HTMLElement {
  count;

  constructor() {
    super();
    this.count = 0;
  }

  connectedCallback() {
    if (!this.shadowRoot) {
      this.attachShadow({ mode: 'open' }); // if using Declarative Shadow DOM
      this.render(); // this is required
    }
  }

  increment() {
    this.count += 1;
    this.render();
  }

  decrement() {
    this.count -= 1;
    this.render();
  }

  render() {
    const { count } = this;

    return (
      <div style="color:red">
        <button onclick={(this.count -= 1)}> - </button>
        <span>
          You have clicked <span class="red">{count}</span> times
        </span>
        <button onclick={this.increment}> - </button>
        <button onclick={this.decrement}> + </button>
      </div>
    );
  }
}

customElements.define('x-counter', Counter);

When enabling the inferredObservability option in the plugin's configuration object and in your custom element definition file, TC39 Signals will be available for reactivity.

// include this line
export const inferredObservability = true;

export default class Counter extends HTMLElement {
  constructor() {
    super();
    this.count = new Signal.State(0);
    this.parity = new Signal.Computed(() => (this.count.get() % 2 === 0 ? 'even' : 'odd'));
  }

  connectedCallback() {
    if (!this.shadowRoot) {
      this.attachShadow({
        mode: 'open',
      });
      // this call is still needed
      this.render();
    }
  }

  increment() {
    this.count.set(this.count.get() + 1);
  }

  decrement() {
    this.count.set(this.count.get() - 1);
  }

  render() {
    const { count, parity } = this;

    return (
      <div>
        <button onclick={this.increment}>Increment (+)</button>
        <button onclick={this.decrement}>Decrement (-)</button>

        <span>
          The count is {count.get()} ({parity.get()})
        </span>
      </div>
    );
  }
}

customElements.define('x-counter', Counter);

Check out our example repo (opens in a new window) to see a demonstration of using this plugin.

Lit SSR Enhancements

In this release, Greenwood has made a number of enhancements to better leverage some of Lit's new SSR capabilities.

Note: This plugin is still limited by known caveats (opens in a new window) with Lit's own SSR support, in particular around async work.

CSS Module Scripts are now possible in SSR contexts! 🥳

import { LitElement, html } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import sheet from './card.css' with { type: 'css' };

@customElement('app-card')
export class Card extends LitElement {

  @property()
  accessor title: string;

  @property()
  accessor thumbnail: string;

  static styles = [sheet];

  constructor() {
    super();

    this.title;
    this.thumbnail;
  }

  render() {
    const { title, thumbnail } = this;

    return html`
      <div>
        <h3>${title}</h3>
        <img src="${thumbnail}" alt="${title}" loading="lazy" width="100%">
      </div>
    `;
  }
}

And Greenwood's own convention for constructor props are now supported, in conjunction with the new Dynamic Routing feature.

import { LitElement, html } from "lit";

export default class ProductDetailsPage extends LitElement {
  static get properties() {
    return {
      id: { type: Number },
    };
  }

  render() {
    const { id } = this;

    return html`
      <h1>Product Details Page</h1>
      <p>Product ID: ${id}</p>
    `;
  }
}

See this example repo (opens in a new window) to see a demonstration of all these capabilities.

Honorable Mentions

In addition to the above key features, we would also like to share some additional noteworthy items included as part of this release:


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