Web Standards

Throughout our docs we make heavy use of, and reference to, some of the following Web APIs, either indirectly or as part of the core surface area of Greenwood itself. This section is a general introduction to them with relevant links and resources.

Import Attributes

Building upon ECMAScript Modules, Greenwood supports Import Attributes on the client and on the server seamlessly, supporting both CSS and JSON modules out of the box.

// returns a Constructable StyleSheet
// https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet/CSSStyleSheet
import sheet from "./styles.css" with { type: "css" };

console.log({ sheet });
// returns an object
import data from "./data.json" with { type: "json" };

console.log({ data });

⚠️ Although Import Attributes are not baseline yet, Greenwood supports polyfilling them with a configuration flag.

Web Components

Web Components are a collection of standard Web APIs that can be mixed and matched to create your own encapsulated styles and behaviors:

A simple example putting it all together might look like this:

import sheet from "./card.css" with { type: "css" };

// create a template element
// to be populated with dynamic HTML
const template = document.createElement("template");

export default class Card extends HTMLElement {
  connectedCallback() {
    // this block can be SSR'd and thus wont need to be re-run on the client
    if (!this.shadowRoot) {
      const thumbnail = this.getAttribute("thumbnail");
      const title = this.getAttribute("title");

      template.innerHTML = `
        <div class="card">
          <h3>${title}</h3>
          <img src="${thumbnail}" alt="${title}" loading="lazy" width="100%">
        </div>
      `;

      // attach our template to our Shadow root
      this.attachShadow({ mode: "open" });
      this.shadowRoot.appendChild(template.content.cloneNode(true));
    }

    // adopt our CSS Module Script
    this.shadowRoot.adoptedStyleSheets = [sheet];
  }
}

// defining the HTML tag that will invoke this definition
customElements.define("x-card", Card);

And would be used like this:

<x-card title="My Title" thumbnail="/path/to/image.png"></x-card>

Greenwood promotes Web Components not only as a great way to add sprinkles of JavaScript to an otherwise static site, but also for static templating through prerendering with all the power and expressiveness of JavaScript as well as completely full-stack web components.

Fetch (and Friends)

Fetch is a web standard for making HTTP requests and is supported both on the client and the server. It also brings along "companion" APIs like Request, Response, and Headers.

This suite of APIs is featured prominently in our API Route examples:

// a standard request object is passed in
// and a standard response object should be returned
export async function handler(request) {
  console.log("endpoint visited", request.url);

  return new Response("...", {
    headers: new Headers({
      /* ... */
    }),
  });
}

Import Maps

During local development, Greenwood loads all assets from your browser unbundled, serving the content right off disk. Import maps allow bare specifiers typically found when referencing packages from npm, to work natively in the browser. When installing a package as a dependency in your package.json, Greenwood will walk your dependencies and all their dependencies, to build up a map to be injected into the <head> of your HTML.

This is a sample of an import map that would be generated after having installed the lit package:

<html>
  <head>
    <script type="importmap">
      {
        "imports": {
          "lit": "/node_modules/lit/index.js",
          "lit-html": "/node_modules/lit-html/lit-html.js",
          "lit-element": "/node_modules/lit-element/index.js",
          "...": "..."
        }
      }
    </script>
  </head>
  <body>
    <!-- ... -->
  </body>
</html>

URL

The URL constructor provides an elegant way for referencing static assets on the client and on the server, and it works great when combined with URLSearchParams for easily interacting with search params in a request.

Below is an example used in an API Route handler:

export async function handler(request) {
  const params = new URLSearchParams(request.url.slice(request.url.indexOf("?")));
  const name = params.has("name") ? params.get("name") : "World";
  const msg = `Hello, ${name}! `;

  return new Response(JSON.stringify({ msg }), {
    headers: new Headers({
      "Content-Type": "application/json",
    }),
  });
}

FormData

FormData is a very useful Web API that works great both on the client and the server, when dealing with forms.

In the browser, it can be used to easily gather the inputs of a <form> tag for communicating with a backend API:

<html>
  <head>
    <script>
      window.addEventListener("DOMContentLoaded", () => {
        window.document.querySelector("form").addEventListener("submit", async (e) => {
          e.preventDefault();

          // with FormData we can pass the whole <form> to the constructor
          // and send a URL Encoded request to the API backend
          const formData = new FormData(e.currentTarget);
          const term = formData.get("term");
          const response = await fetch("/api/search", {
            method: "POST",
            body: new URLSearchParams({ term }).toString(),
            headers: new Headers({
              "content-type": "application/x-www-form-urlencoded",
            }),
          });
          // ...
        });
      });
    </script>
  </head>

  <body>
    <h1>Search Page</h1>

    <form>
      <label for="term">
        <input type="search" name="term" placeholder="a product..." required />
      </label>
      <button type="submit">Search</button>
    </form>
  </body>
</html>

On the server, we can use the same API to collect the inputs from that form request:

// src/pages/api/search.js
// we pull in WCC here to generate HTML fragments for us
import { getProductsBySearchTerm } from "../../db/client.js";

export async function handler(request) {
  // use the web standard FormData to get the incoming form submission
  const formData = await request.formData();
  const term = formData.has("term") ? formData.get("term") : "";
  const products = await getProductsBySearchTerm(term);

  // ...
}