In addition to supporting static and Single Page application project types, you can also use Greenwood to author routes completely in JavaScript and host these on a server.
👉 To run a Greenwood project with SSR routes for production, just use the
serve
command.
File based routing also applies to server routes. Just create JavaScript file in the pages/ directory and that's it!
src/
pages/
users.js
greenwood.config.js
The above would serve content in a browser at /users/
.
In your page .js file, Greenwood supports the following functions you can export
for providing server rendered configuration and content:
default
: Use a custom element to render your page content. Will take precedence over getBody
. Will also automatically track your custom element dependencies, in place of having to define frontmatter imports in getFrontmatter
.getFrontmatter
: Static frontmatter, useful in conjunction with content as data or otherwise static configuration / metadata.getBody
: Effectively anything that you could put into a <content-outlet></content-outlet>
.getLayout
: Effectively the same as a page layout.async function getFrontmatter(compilation, route, label, id) {
return { /* ... */ };
}
async function getBody(compilation, route) {
return '<!-- some HTML here -->';
}
async function getLayout(compilation, route) {
return '<!-- some HTML here -->';
}
export default class MyPage extends HTMLElement {
constructor() { }
async connectedCallback() {
this.innerHTML = '<!-- some HTML here -->';
}
}
export {
getFrontmatter,
getBody,
getLayout
};
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). It uses WCC by default which also includes support for rendering Declarative Shadow DOM.
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:
async
operations for data loading in connectedCallback
.<layout>
tag. However, for any interactive elements within your page, Definitely use Declarative Shadow DOM!Any Greenwood supported frontmatter can be returned here. This is only run once when the server is started to populate the graph, which is helpful if you want your dynamic route to show up in the metadata for your 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: `${compilation.config.title} - ${route}`,
imports: [
'/components/user.js'
],
data: {
/* ... */
}
};
}
For defining custom dynamic based metadata, like for
<meta>
tags, usegetLayout
and define those tags right in your HTML.
So for example, /pages/artist.js
would render out as /artists/index.html
and would not require the serve task. So if you need more flexibility in how you create your pages, but still want to just serve it statically, you can!
For just returning content, you can use getBody
. For example, return a list of users from an API as the HTML you need.
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>Fetched at: ${timestamp}</h6>
</body>
`;
}
For creating a layout dynamically, you can use getLayout
and return the HTML you need.
export async function getLayout(compilation, route) {
return `
<html>
<head>
<meta name="description" content="${compilation.config.title} - ${route} (this route was generated server side!!!)">
<style>
* {
color: blue;
}
h1 {
width: 50%;
margin: 0 auto;
text-align: center;
color: red;
}
</style>
</head>
<body>
<h1>This heading was rendered server side!</h1>
<content-outlet></content-outlet>
</body>
</html>
`;
}
For request time data fetching, Greenwood will pass a native Request
object and a Greenwood compilation params as "constructor props" to your Web Server Component's constructor
. 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>
`;
}
}
To export server routes as just static HTML, you can export a prerender
option from your page set to true
.
export const prerender = true;
You can enable this for all pages using the prerender configuration option.
To execute an SSR page in its own request context when running greenwood serve
, 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.
To enable custom imports on the server side for prerendering or SSR use cases, you will need to invoke Greenwood using node
on the CLI and pass it the --loaders
flag.
$ node --loader ./node_modules/@greenwood/cli/src/loader.js ./node_modules/.bin/greenwood <command>
Then you will be able to run this code with NodeJS, 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 });
Notes
node
One of the great things about Greenwood is that you can seamlessly move from completely static to server rendered, without giving up either one! 💯
Given the following workspace of just pages
src/
pages/
index.md
about.md
Greenwood would output the following static build output
public/
about
index.html
index.html
Now, add a dynamic route and run serve
...
src/
pages/
index.md
about.md
user.js
Greenwood will now build and serve all the static content from the pages/ directory as before BUT will also start a server that will now fulfill requests to the newly added server rendered pages too. Neat!
Greenwood provides the ability to prerender your project and Web Components. So what is the difference between that and rendering? In the context of Greenwood, rendering is the process of generating the initial HTML as you would when running on a server. Prerendering is the ability to execute exclusively browser code in a browser and capture that result as static HTML.
So what does that mean, exactly? Basically, you can think of them as being complimentary, where in you might have server side routes that pull content server side (getBody
), but can be composed of static HTML layouts (in your src/layouts directory) that can have client side code (Web Components) with <script>
tags that could be run after through a headless browser.
The hope with Greenwood is that user's can choose the best blend of server rendering and browser prerendering that fits their projects best because running in a browser unlocks more client side capabilities that will (likely) never be available in a server context, like:
window
/ document
objectsSo server rendering, when constraints are understood, can be a lot a faster to execute compared to a headless browser. However, with good caching strategies, the cost of rendering HTML once with either technique, when amortized over all the subsequent requests and responses, usually ends up being negligible in the long run.
So we hope users find a workflow that works best for them and see Greenwood as more of a knob or spectrum, rather than a toggle. This blog post also provides a lot of good information on the various rendering strategies implemented these days. ⚙️