Server Rendering

Greenwood provides a couple of mechanisms for server-side rendering, building on top of our file-based routing convention.

To create a dynamic server route, just create a JavaScript file in the pages/ directory, and that's it!

src/
  pages/
    users.js

The above would serve content in a browser at the path /users/.

Usage

In your page file, Greenwood supports the following functions that you can export for providing server rendered content and frontmatter:

export default class MyPage extends HTMLElement {
  async connectedCallback() {
    this.innerHTML = "<!-- some HTML here -->";
  }
}

async function getBody(compilation, route) {
  return "<!-- some HTML here -->";
}

async function getLayout(compilation, route) {
  return "<!-- some HTML here -->";
}

async function getFrontmatter(compilation, route, label, id) {
  return {
    /* ... */
  };
}

export { getFrontmatter, getBody, getLayout };

None of these options will ship any JavaScript to the client.

API

Web (Server) Components

Everyone else gets to use their component model for authoring pages, so why not Web Components!? When using export default, Greenwood supports providing a custom element as the export for your page content, which Greenwood refers to as Web Server Components (WSCs) and uses WCC as the default renderer.

This is the recommended pattern for SSR in Greenwood:

import "../components/card/card.js"; // <wc-card></wc-card>

export default class UsersPage extends HTMLElement {
  async connectedCallback() {
    const users = await fetch("https://www.example.com/api/users").then((resp) => resp.json());
    const html = users
      .map((user) => {
        const { name, imageUrl } = user;
        return `
          <wc-card>
            <h2 slot="title">${name}</h2>
            <img slot="image" src="${imageUrl}" alt="${name}"/>
          </wc-card>
        `;
      })
      .join("");

    this.innerHTML = html;
  }
}

A couple of notes:

Body

To return just the body of the page, you can use the getBody API. You will get access the compilation, page specific data, and the incoming request.

In this example, we return a list of users from an API as HTML:

export async function getBody(compilation, page, request) {
  const users = await fetch("http://www.example.com/api/users").then((resp) => resp.json());
  const timestamp = new Date().getTime();
  const usersListItems = users.map((user) => {
    const { name, imageUrl } = user;

    return `
        <tr>
          <td>${name}</td>
          <td><img src="${imageUrl}"/></td>
        </tr>
      `;
  });

  return `
    <body>
      <h1>Hello from the server rendered users page! 👋</h1>
      <table>
        <tr>
          <th>Name</th>
          <th>Image</th>
        </tr>
        ${usersListItems.join("")}
      </table>
      <h6>Last Updated: ${timestamp}</h6>
    </body>
  `;
}

Layouts

For creating a layout dynamically, you can provide a getLayout function and return the HTML you need.

You can pull in data from Greenwood's compilation object as well as the specific route:

export async function getLayout(compilation, route) {
  return `
    <html>
      <head>
        <style>
          * {
            color: blue;
          }

          h1 {
            width: 50%;
            margin: 0 auto;
            text-align: center;
            color: red;
          }
        </style>
      </head>
      <body>
        <h1>This heading was rendered server side for route ${route}!</h1>
        <content-outlet></content-outlet>
      </body>
    </html>
  `;
}

Frontmatter

Any Greenwood supported frontmatter can be returned here. This is only run once when the server is started to populate pages metadata, which is helpful if you want your dynamic route to show up in a collection with other static pages. You can even define a layout and reuse all your existing layouts, even for server routes!

export async function getFrontmatter(compilation, route) {
  return {
    layout: "user",
    collection: "header",
    order: 1,
    title: `The ${route} page`,
    imports: ["/components/user.js"],
    data: {
      /* ... */
    },
  };
}

For defining custom dynamic based metadata, like for <meta> tags, use getLayout and define those tags right in your HTML.

Options

Prerender

To export server routes as just static HTML (no request time handling), you can export a prerender option from your page, set to true.

export const prerender = true;

So for example, /pages/artist.js would render out as /artists/index.html and would work with standard static hosting.

You can enable this for all pages using the prerender configuration option.

Isolation Mode

To execute an SSR page in its own isolated rendering context, you can export an isolation option from your page, set to true.

export const isolation = true;

For more information and how you can enable this for all pages, please see the isolation configuration docs.

Request Data

For request handling, Greenwood will pass a native Request object and a Greenwood compilation as "constructor props" to your Web Server Component's constructor function, or as the third parameter to the other SSR APIs. For async work, use an async connectedCallback.

export default class PostPage extends HTMLElement {
  constructor(request) {
    super();

    const params = new URLSearchParams(request.url.slice(request.url.indexOf("?")));
    this.postId = params.get("id");
  }

  async connectedCallback() {
    const { postId } = this;
    const post = await fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`).then((resp) =>
      resp.json(),
    );
    const { id, title, body } = post;

    this.innerHTML = `
      <h1>Fetched Post ID: ${id}</h1>
      <h2>${title}</h2>
      <p>${body}</p>
    `;
  }
}

Custom Imports

To use custom imports on the server side for prerendering or SSR use cases (ex. CSS, JSON), you will need to invoke Greenwood using NodeJS from the CLI and pass it the --loaders flag along with the path to Greenwood's provided loader function.

$ node --loader ./node_modules/@greenwood/cli/src/loader.js ./node_modules/@greenwood/cli/src/index.js <command>

Then you will be able to run this, or for any custom format you want using a plugin.

import sheet from "./styles.css" with { type: "css" };
import data from "./data.json" with { type: "json" };

console.log({ sheet, data });